diff --git a/.eslintrc.cjs b/.eslintrc.cjs index af744db3e1..f52da3fa5d 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -674,7 +674,7 @@ module.exports = { 'no-this-before-super': [2], 'no-throw-literal': [2], 'no-undef-init': [2], - 'no-undef': [0], + 'no-undef': [2], // it is still needed by eslint & IDE to prompt undefined names in real time 'no-undefined': [0], 'no-underscore-dangle': [0], 'no-unexpected-multiline': [2], diff --git a/.gitignore b/.gitignore index 86e6e4fefd..619cb1cabb 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,10 @@ _test # IntelliJ .idea + +# IntelliJ Gateway +.uuid + # Goland's output filename can not be set manually /go_build_* /gitea_* diff --git a/MAINTAINERS b/MAINTAINERS index ad02ecc755..f0caae4d22 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -31,7 +31,6 @@ Gary Kim (@gary-kim) Guillermo Prandi (@guillep2k) Mura Li (@typeless) 6543 <6543@obermui.de> (@6543) -jaqra (@jaqra) David Svantesson (@davidsvantesson) a1012112796 <1012112796@qq.com> (@a1012112796) Karl Heinz Marbaise (@khmarbaise) @@ -63,3 +62,4 @@ Yu Liu <1240335630@qq.com> (@HEREYUA) Kemal Zebari (@kemzeb) Rowan Bohde (@bohde) hiifong (@hiifong) +metiftikci (@metiftikci) diff --git a/Makefile b/Makefile index 32ea823e1e..5524cfb08c 100644 --- a/Makefile +++ b/Makefile @@ -806,22 +806,22 @@ $(DIST_DIRS): .PHONY: release-windows release-windows: | $(DIST_DIRS) - CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION) . + CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo $(TAGS)' -ldflags '-s -w -linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION) . ifeq (,$(findstring gogit,$(TAGS))) - CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo gogit $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION)-gogit . + CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo gogit $(TAGS)' -ldflags '-s -w -linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION)-gogit . endif .PHONY: release-linux release-linux: | $(DIST_DIRS) - CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets '$(LINUX_ARCHS)' -out gitea-$(VERSION) . + CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w -linkmode external -extldflags "-static" $(LDFLAGS)' -targets '$(LINUX_ARCHS)' -out gitea-$(VERSION) . .PHONY: release-darwin release-darwin: | $(DIST_DIRS) - CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '$(LDFLAGS)' -targets 'darwin-10.12/amd64,darwin-10.12/arm64' -out gitea-$(VERSION) . + CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w $(LDFLAGS)' -targets 'darwin-10.12/amd64,darwin-10.12/arm64' -out gitea-$(VERSION) . .PHONY: release-freebsd release-freebsd: | $(DIST_DIRS) - CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '$(LDFLAGS)' -targets 'freebsd/amd64' -out gitea-$(VERSION) . + CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w $(LDFLAGS)' -targets 'freebsd/amd64' -out gitea-$(VERSION) . .PHONY: release-copy release-copy: | $(DIST_DIRS) diff --git a/assets/go-licenses.json b/assets/go-licenses.json index 64c3b8b51c..8c155ceadb 100644 --- a/assets/go-licenses.json +++ b/assets/go-licenses.json @@ -104,16 +104,6 @@ "path": "github.com/Azure/go-ntlmssp/LICENSE", "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2016 Microsoft\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/ClickHouse/ch-go", - "path": "github.com/ClickHouse/ch-go/LICENSE", - "licenseText": "Copyright 2016-2023 ClickHouse, Inc.\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 2016-2023 ClickHouse, Inc.\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/ClickHouse/clickhouse-go/v2", - "path": "github.com/ClickHouse/clickhouse-go/v2/LICENSE", - "licenseText": "Copyright 2016-2023 ClickHouse, Inc.\n\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 2016-2023 ClickHouse, Inc.\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/DataDog/zstd", "path": "github.com/DataDog/zstd/LICENSE", @@ -179,16 +169,6 @@ "path": "github.com/aws/aws-sdk-go-v2/service/codecommit/LICENSE.txt", "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/aws/aws-sdk-go", - "path": "github.com/aws/aws-sdk-go/LICENSE.txt", - "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/aws/aws-sdk-go/internal/sync/singleflight", - "path": "github.com/aws/aws-sdk-go/internal/sync/singleflight/LICENSE", - "licenseText": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" - }, { "name": "github.com/aws/smithy-go", "path": "github.com/aws/smithy-go/LICENSE", @@ -519,16 +499,6 @@ "path": "github.com/go-enry/go-enry/v2/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." }, - { - "name": "github.com/go-faster/city", - "path": "github.com/go-faster/city/LICENSE", - "licenseText": "MIT License\n\nCopyright (c) 2018 tenfy\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/go-faster/errors", - "path": "github.com/go-faster/errors/LICENSE", - "licenseText": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" - }, { "name": "github.com/go-fed/httpsig", "path": "github.com/go-fed/httpsig/LICENSE", @@ -569,11 +539,6 @@ "path": "github.com/go-sql-driver/mysql/LICENSE", "licenseText": "Mozilla Public License Version 2.0\n==================================\n\n1. Definitions\n--------------\n\n1.1. \"Contributor\"\n means each individual or legal entity that creates, contributes to\n the creation of, or owns Covered Software.\n\n1.2. \"Contributor Version\"\n means the combination of the Contributions of others (if any) used\n by a Contributor and that particular Contributor's Contribution.\n\n1.3. \"Contribution\"\n means Covered Software of a particular Contributor.\n\n1.4. \"Covered Software\"\n means Source Code Form to which the initial Contributor has attached\n the notice in Exhibit A, the Executable Form of such Source Code\n Form, and Modifications of such Source Code Form, in each case\n including portions thereof.\n\n1.5. \"Incompatible With Secondary Licenses\"\n means\n\n (a) that the initial Contributor has attached the notice described\n in Exhibit B to the Covered Software; or\n\n (b) that the Covered Software was made available under the terms of\n version 1.1 or earlier of the License, but not also under the\n terms of a Secondary License.\n\n1.6. \"Executable Form\"\n means any form of the work other than Source Code Form.\n\n1.7. \"Larger Work\"\n means a work that combines Covered Software with other material, in \n a separate file or files, that is not Covered Software.\n\n1.8. \"License\"\n means this document.\n\n1.9. \"Licensable\"\n means having the right to grant, to the maximum extent possible,\n whether at the time of the initial grant or subsequently, any and\n all of the rights conveyed by this License.\n\n1.10. \"Modifications\"\n means any of the following:\n\n (a) any file in Source Code Form that results from an addition to,\n deletion from, or modification of the contents of Covered\n Software; or\n\n (b) any new file in Source Code Form that contains any Covered\n Software.\n\n1.11. \"Patent Claims\" of a Contributor\n means any patent claim(s), including without limitation, method,\n process, and apparatus claims, in any patent Licensable by such\n Contributor that would be infringed, but for the grant of the\n License, by the making, using, selling, offering for sale, having\n made, import, or transfer of either its Contributions or its\n Contributor Version.\n\n1.12. \"Secondary License\"\n means either the GNU General Public License, Version 2.0, the GNU\n Lesser General Public License, Version 2.1, the GNU Affero General\n Public License, Version 3.0, or any later versions of those\n licenses.\n\n1.13. \"Source Code Form\"\n means the form of the work preferred for making modifications.\n\n1.14. \"You\" (or \"Your\")\n means an individual or a legal entity exercising rights under this\n License. For legal entities, \"You\" includes any entity that\n controls, is controlled by, or is under common control with You. For\n purposes of this definition, \"control\" means (a) the power, direct\n or indirect, to cause the direction or management of such entity,\n whether by contract or otherwise, or (b) ownership of more than\n fifty percent (50%) of the outstanding shares or beneficial\n ownership of such entity.\n\n2. License Grants and Conditions\n--------------------------------\n\n2.1. Grants\n\nEach Contributor hereby grants You a world-wide, royalty-free,\nnon-exclusive license:\n\n(a) under intellectual property rights (other than patent or trademark)\n Licensable by such Contributor to use, reproduce, make available,\n modify, display, perform, distribute, and otherwise exploit its\n Contributions, either on an unmodified basis, with Modifications, or\n as part of a Larger Work; and\n\n(b) under Patent Claims of such Contributor to make, use, sell, offer\n for sale, have made, import, and otherwise transfer either its\n Contributions or its Contributor Version.\n\n2.2. Effective Date\n\nThe licenses granted in Section 2.1 with respect to any Contribution\nbecome effective for each Contribution on the date the Contributor first\ndistributes such Contribution.\n\n2.3. Limitations on Grant Scope\n\nThe licenses granted in this Section 2 are the only rights granted under\nthis License. No additional rights or licenses will be implied from the\ndistribution or licensing of Covered Software under this License.\nNotwithstanding Section 2.1(b) above, no patent license is granted by a\nContributor:\n\n(a) for any code that a Contributor has removed from Covered Software;\n or\n\n(b) for infringements caused by: (i) Your and any other third party's\n modifications of Covered Software, or (ii) the combination of its\n Contributions with other software (except as part of its Contributor\n Version); or\n\n(c) under Patent Claims infringed by Covered Software in the absence of\n its Contributions.\n\nThis License does not grant any rights in the trademarks, service marks,\nor logos of any Contributor (except as may be necessary to comply with\nthe notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\nNo Contributor makes additional grants as a result of Your choice to\ndistribute the Covered Software under a subsequent version of this\nLicense (see Section 10.2) or under the terms of a Secondary License (if\npermitted under the terms of Section 3.3).\n\n2.5. Representation\n\nEach Contributor represents that the Contributor believes its\nContributions are its original creation(s) or it has sufficient rights\nto grant the rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\nThis License is not intended to limit any rights You have under\napplicable copyright doctrines of fair use, fair dealing, or other\nequivalents.\n\n2.7. Conditions\n\nSections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted\nin Section 2.1.\n\n3. Responsibilities\n-------------------\n\n3.1. Distribution of Source Form\n\nAll distribution of Covered Software in Source Code Form, including any\nModifications that You create or to which You contribute, must be under\nthe terms of this License. You must inform recipients that the Source\nCode Form of the Covered Software is governed by the terms of this\nLicense, and how they can obtain a copy of this License. You may not\nattempt to alter or restrict the recipients' rights in the Source Code\nForm.\n\n3.2. Distribution of Executable Form\n\nIf You distribute Covered Software in Executable Form then:\n\n(a) such Covered Software must also be made available in Source Code\n Form, as described in Section 3.1, and You must inform recipients of\n the Executable Form how they can obtain a copy of such Source Code\n Form by reasonable means in a timely manner, at a charge no more\n than the cost of distribution to the recipient; and\n\n(b) You may distribute such Executable Form under the terms of this\n License, or sublicense it under different terms, provided that the\n license for the Executable Form does not attempt to limit or alter\n the recipients' rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\nYou may create and distribute a Larger Work under terms of Your choice,\nprovided that You also comply with the requirements of this License for\nthe Covered Software. If the Larger Work is a combination of Covered\nSoftware with a work governed by one or more Secondary Licenses, and the\nCovered Software is not Incompatible With Secondary Licenses, this\nLicense permits You to additionally distribute such Covered Software\nunder the terms of such Secondary License(s), so that the recipient of\nthe Larger Work may, at their option, further distribute the Covered\nSoftware under the terms of either this License or such Secondary\nLicense(s).\n\n3.4. Notices\n\nYou may not remove or alter the substance of any license notices\n(including copyright notices, patent notices, disclaimers of warranty,\nor limitations of liability) contained within the Source Code Form of\nthe Covered Software, except that You may alter any license notices to\nthe extent required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\nYou may choose to offer, and to charge a fee for, warranty, support,\nindemnity or liability obligations to one or more recipients of Covered\nSoftware. However, You may do so only on Your own behalf, and not on\nbehalf of any Contributor. You must make it absolutely clear that any\nsuch warranty, support, indemnity, or liability obligation is offered by\nYou alone, and You hereby agree to indemnify every Contributor for any\nliability incurred by such Contributor as a result of warranty, support,\nindemnity or liability terms You offer. You may include additional\ndisclaimers of warranty and limitations of liability specific to any\njurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n---------------------------------------------------\n\nIf it is impossible for You to comply with any of the terms of this\nLicense with respect to some or all of the Covered Software due to\nstatute, judicial order, or regulation then You must: (a) comply with\nthe terms of this License to the maximum extent possible; and (b)\ndescribe the limitations and the code they affect. Such description must\nbe placed in a text file included with all distributions of the Covered\nSoftware under this License. Except to the extent prohibited by statute\nor regulation, such description must be sufficiently detailed for a\nrecipient of ordinary skill to be able to understand it.\n\n5. Termination\n--------------\n\n5.1. The rights granted under this License will terminate automatically\nif You fail to comply with any of its terms. However, if You become\ncompliant, then the rights granted under this License from a particular\nContributor are reinstated (a) provisionally, unless and until such\nContributor explicitly and finally terminates Your grants, and (b) on an\nongoing basis, if such Contributor fails to notify You of the\nnon-compliance by some reasonable means prior to 60 days after You have\ncome back into compliance. Moreover, Your grants from a particular\nContributor are reinstated on an ongoing basis if such Contributor\nnotifies You of the non-compliance by some reasonable means, this is the\nfirst time You have received notice of non-compliance with this License\nfrom such Contributor, and You become compliant prior to 30 days after\nYour receipt of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\ninfringement claim (excluding declaratory judgment actions,\ncounter-claims, and cross-claims) alleging that a Contributor Version\ndirectly or indirectly infringes any patent, then the rights granted to\nYou by any and all Contributors for the Covered Software under Section\n2.1 of this License shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all\nend user license agreements (excluding distributors and resellers) which\nhave been validly granted by You or Your distributors under this License\nprior to termination shall survive termination.\n\n************************************************************************\n* *\n* 6. Disclaimer of Warranty *\n* ------------------------- *\n* *\n* Covered Software is provided under this License on an \"as is\" *\n* basis, without warranty of any kind, either expressed, implied, or *\n* statutory, including, without limitation, warranties that the *\n* Covered Software is free of defects, merchantable, fit for a *\n* particular purpose or non-infringing. The entire risk as to the *\n* quality and performance of the Covered Software is with You. *\n* Should any Covered Software prove defective in any respect, You *\n* (not any Contributor) assume the cost of any necessary servicing, *\n* repair, or correction. This disclaimer of warranty constitutes an *\n* essential part of this License. No use of any Covered Software is *\n* authorized under this License except under this disclaimer. *\n* *\n************************************************************************\n\n************************************************************************\n* *\n* 7. Limitation of Liability *\n* -------------------------- *\n* *\n* Under no circumstances and under no legal theory, whether tort *\n* (including negligence), contract, or otherwise, shall any *\n* Contributor, or anyone who distributes Covered Software as *\n* permitted above, be liable to You for any direct, indirect, *\n* special, incidental, or consequential damages of any character *\n* including, without limitation, damages for lost profits, loss of *\n* goodwill, work stoppage, computer failure or malfunction, or any *\n* and all other commercial damages or losses, even if such party *\n* shall have been informed of the possibility of such damages. This *\n* limitation of liability shall not apply to liability for death or *\n* personal injury resulting from such party's negligence to the *\n* extent applicable law prohibits such limitation. Some *\n* jurisdictions do not allow the exclusion or limitation of *\n* incidental or consequential damages, so this exclusion and *\n* limitation may not apply to You. *\n* *\n************************************************************************\n\n8. Litigation\n-------------\n\nAny litigation relating to this License may be brought only in the\ncourts of a jurisdiction where the defendant maintains its principal\nplace of business and such litigation shall be governed by laws of that\njurisdiction, without reference to its conflict-of-law provisions.\nNothing in this Section shall prevent a party's ability to bring\ncross-claims or counter-claims.\n\n9. Miscellaneous\n----------------\n\nThis License represents the complete agreement concerning the subject\nmatter hereof. If any provision of this License is held to be\nunenforceable, such provision shall be reformed only to the extent\nnecessary to make it enforceable. Any law or regulation which provides\nthat the language of a contract shall be construed against the drafter\nshall not be used to construe this License against a Contributor.\n\n10. Versions of the License\n---------------------------\n\n10.1. New Versions\n\nMozilla Foundation is the license steward. Except as provided in Section\n10.3, no one other than the license steward has the right to modify or\npublish new versions of this License. Each version will be given a\ndistinguishing version number.\n\n10.2. Effect of New Versions\n\nYou may distribute the Covered Software under the terms of the version\nof the License under which You originally received the Covered Software,\nor under the terms of any subsequent version published by the license\nsteward.\n\n10.3. Modified Versions\n\nIf you create software not governed by this License, and you want to\ncreate a new license for such software, you may create and use a\nmodified version of this License if you rename the license and remove\nany references to the name of the license steward (except to note that\nsuch modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary\nLicenses\n\nIf You choose to distribute Source Code Form that is Incompatible With\nSecondary Licenses under the terms of this version of the License, the\nnotice described in Exhibit B of this License must be attached.\n\nExhibit A - Source Code Form License Notice\n-------------------------------------------\n\n This Source Code Form is subject to the terms of the Mozilla Public\n License, v. 2.0. If a copy of the MPL was not distributed with this\n file, You can obtain one at http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular\nfile, then You may include the notice in a location (such as a LICENSE\nfile in a relevant directory) where a recipient would be likely to look\nfor such a notice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - \"Incompatible With Secondary Licenses\" Notice\n---------------------------------------------------------\n\n This Source Code Form is \"Incompatible With Secondary Licenses\", as\n defined by the Mozilla Public License, v. 2.0.\n" }, - { - "name": "github.com/go-testfixtures/testfixtures/v3", - "path": "github.com/go-testfixtures/testfixtures/v3/LICENSE", - "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2016 Andrey Nering\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/go-webauthn/webauthn", "path": "github.com/go-webauthn/webauthn/LICENSE", @@ -964,11 +929,6 @@ "path": "github.com/opencontainers/image-spec/specs-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 Copyright 2016 The Linux Foundation.\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/paulmach/orb", - "path": "github.com/paulmach/orb/LICENSE.md", - "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2017 Paul Mach\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject 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, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n" - }, { "name": "github.com/pierrec/lz4/v4", "path": "github.com/pierrec/lz4/v4/LICENSE", @@ -1059,21 +1019,11 @@ "path": "github.com/sassoftware/go-rpmutils/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/segmentio/asm", - "path": "github.com/segmentio/asm/LICENSE", - "licenseText": "MIT License\n\nCopyright (c) 2021 Segment\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/sergi/go-diff/diffmatchpatch", "path": "github.com/sergi/go-diff/diffmatchpatch/LICENSE", "licenseText": "Copyright (c) 2012-2016 The go-diff Authors. All rights reserved.\n\nPermission is hereby granted, free of charge, to any person obtaining a\ncopy of this software and associated documentation files (the \"Software\"),\nto deal in the Software without restriction, including without limitation\nthe rights to use, copy, modify, merge, publish, distribute, sublicense,\nand/or sell copies of the Software, and to permit persons to whom the\nSoftware is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included\nin all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS\nOR IMPLIED, 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\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\nDEALINGS IN THE SOFTWARE.\n\n" }, - { - "name": "github.com/shopspring/decimal", - "path": "github.com/shopspring/decimal/LICENSE", - "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2015 Spring, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n\n- Based on https://github.com/oguzbilgic/fpd, which has the following license:\n\"\"\"\nThe MIT License (MIT)\n\nCopyright (c) 2013 Oguz Bilgic\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject 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, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\"\"\"\n" - }, { "name": "github.com/sirupsen/logrus", "path": "github.com/sirupsen/logrus/LICENSE", @@ -1184,16 +1134,6 @@ "path": "go.etcd.io/bbolt/LICENSE", "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2013 Ben Johnson\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject 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, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n" }, - { - "name": "go.opentelemetry.io/otel", - "path": "go.opentelemetry.io/otel/LICENSE", - "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" - }, - { - "name": "go.opentelemetry.io/otel/trace", - "path": "go.opentelemetry.io/otel/trace/LICENSE", - "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" - }, { "name": "go.uber.org/atomic", "path": "go.uber.org/atomic/LICENSE.txt", diff --git a/cmd/admin_user_create.go b/cmd/admin_user_create.go index 106d14b25a..bf8cbc7c4c 100644 --- a/cmd/admin_user_create.go +++ b/cmd/admin_user_create.go @@ -69,6 +69,10 @@ var microcmdUserCreate = &cli.Command{ } func runCreateUser(c *cli.Context) error { + // this command highly depends on the many setting options (create org, visibility, etc.), so it must have a full setting load first + // duplicate setting loading should be safe at the moment, but it should be refactored & improved in the future. + setting.LoadSettings() + if err := argsSet(c, "email"); err != nil { return err } diff --git a/cmd/main.go b/cmd/main.go index fd648946ef..7251bd09a3 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -165,6 +165,7 @@ func NewMainApp(appVer AppVersion) *cli.App { app.Commands = append(app.Commands, subCmdWithConfig...) app.Commands = append(app.Commands, subCmdStandalone...) + setting.InitGiteaEnvVars() return app } diff --git a/cmd/main_test.go b/cmd/main_test.go index c182b44019..3ec584d323 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -6,7 +6,6 @@ package cmd import ( "fmt" "io" - "os" "path/filepath" "strings" "testing" @@ -113,37 +112,17 @@ func TestCliCmd(t *testing.T) { _, _ = fmt.Fprint(ctx.App.Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf)) return nil }) - var envBackup []string - for _, s := range os.Environ() { - if strings.HasPrefix(s, "GITEA_") && strings.Contains(s, "=") { - envBackup = append(envBackup, s) - } - } - clearGiteaEnv := func() { - for _, s := range os.Environ() { - if strings.HasPrefix(s, "GITEA_") { - _ = os.Unsetenv(s) - } - } - } - defer func() { - clearGiteaEnv() - for _, s := range envBackup { - k, v, _ := strings.Cut(s, "=") - _ = os.Setenv(k, v) - } - }() - for _, c := range cases { - clearGiteaEnv() - for k, v := range c.env { - _ = os.Setenv(k, v) - } - args := strings.Split(c.cmd, " ") // for test only, "split" is good enough - r, err := runTestApp(app, args...) - assert.NoError(t, err, c.cmd) - assert.NotEmpty(t, c.exp, c.cmd) - assert.Contains(t, r.Stdout, c.exp, c.cmd) + t.Run(c.cmd, func(t *testing.T) { + for k, v := range c.env { + t.Setenv(k, v) + } + args := strings.Split(c.cmd, " ") // for test only, "split" is good enough + r, err := runTestApp(app, args...) + assert.NoError(t, err, c.cmd) + assert.NotEmpty(t, c.exp, c.cmd) + assert.Contains(t, r.Stdout, c.exp, c.cmd) + }) } } diff --git a/cmd/migrate.go b/cmd/migrate.go index 4e4dd45af3..459805a76d 100644 --- a/cmd/migrate.go +++ b/cmd/migrate.go @@ -18,7 +18,7 @@ import ( var CmdMigrate = &cli.Command{ Name: "migrate", Usage: "Migrate the database", - Description: "This is a command for migrating the database, so that you can run gitea admin create-user before starting the server.", + Description: `This is a command for migrating the database, so that you can run "gitea admin create user" before starting the server.`, Action: runMigrate, } diff --git a/cmd/web.go b/cmd/web.go index ef8a7426c1..f8217758e5 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -12,6 +12,7 @@ import ( "path/filepath" "strconv" "strings" + "time" _ "net/http/pprof" // Used for debugging if enabled and a web server is running @@ -115,6 +116,16 @@ func showWebStartupMessage(msg string) { log.Info("* CustomPath: %s", setting.CustomPath) log.Info("* ConfigFile: %s", setting.CustomConf) log.Info("%s", msg) // show startup message + + if setting.CORSConfig.Enabled { + log.Info("CORS Service Enabled") + } + if setting.DefaultUILocation != time.Local { + log.Info("Default UI Location is %v", setting.DefaultUILocation.String()) + } + if setting.MailService != nil { + log.Info("Mail Service Enabled: RegisterEmailConfirm=%v, Service.EnableNotifyMail=%v", setting.Service.RegisterEmailConfirm, setting.Service.EnableNotifyMail) + } } func serveInstall(ctx *cli.Context) error { diff --git a/cmd/web_acme.go b/cmd/web_acme.go index 90e4a02764..2fe14c1f54 100644 --- a/cmd/web_acme.go +++ b/cmd/web_acme.go @@ -54,7 +54,7 @@ func runACME(listenAddr string, m http.Handler) error { altTLSALPNPort = p } - magic := certmagic.NewDefault() + magic := &certmagic.Default magic.Storage = &certmagic.FileStorage{Path: setting.AcmeLiveDirectory} // Try to use private CA root if provided, otherwise defaults to system's trust var certPool *x509.CertPool diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index d819b55d28..eacc732c22 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1485,6 +1485,10 @@ LEVEL = Info ;REPO_INDEXER_EXCLUDE = ;; ;MAX_FILE_SIZE = 1048576 +;; +;; Bleve engine has performance problems with fuzzy search, so we limit the fuzziness to 0 by default to disable it. +;; If you'd like to enable it, you can set it to a value between 0 and 2. +;TYPE_BLEVE_MAX_FUZZINESS = 0 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/go.mod b/go.mod index 9b99face84..dc80d2ca2b 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,6 @@ require ( github.com/PuerkitoBio/goquery v1.10.0 github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.7.3 github.com/alecthomas/chroma/v2 v2.14.0 - github.com/aws/aws-sdk-go v1.55.5 github.com/aws/aws-sdk-go-v2/credentials v1.17.42 github.com/aws/aws-sdk-go-v2/service/codecommit v1.27.3 github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb @@ -61,7 +60,6 @@ require ( github.com/go-redsync/redsync/v4 v4.13.0 github.com/go-sql-driver/mysql v1.8.1 github.com/go-swagger/go-swagger v0.31.0 - github.com/go-testfixtures/testfixtures/v3 v3.11.0 github.com/go-webauthn/webauthn v0.11.2 github.com/gobwas/glob v0.2.3 github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f @@ -145,8 +143,6 @@ require ( filippo.io/edwards25519 v1.1.0 // indirect git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect - github.com/ClickHouse/ch-go v0.63.1 // indirect - github.com/ClickHouse/clickhouse-go/v2 v2.24.0 // indirect github.com/DataDog/zstd v1.5.6 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.3.0 // indirect @@ -204,8 +200,6 @@ require ( github.com/go-ap/errors v0.0.0-20240910140019-1e9d33cc1568 // indirect github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect github.com/go-enry/go-oniguruma v1.2.1 // indirect - github.com/go-faster/city v1.0.1 // indirect - github.com/go-faster/errors v0.7.1 // indirect github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-ini/ini v1.67.0 // indirect @@ -270,7 +264,6 @@ require ( github.com/oklog/ulid v1.3.1 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/onsi/ginkgo v1.16.5 // indirect - github.com/paulmach/orb v0.11.1 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect @@ -285,7 +278,6 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.6.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/segmentio/asm v1.2.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 @@ -310,8 +302,6 @@ require ( github.com/zeebo/blake3 v0.2.4 // indirect go.etcd.io/bbolt v1.3.11 // indirect go.mongodb.org/mongo-driver v1.17.1 // indirect - go.opentelemetry.io/otel v1.31.0 // indirect - go.opentelemetry.io/otel/trace v1.31.0 // 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 diff --git a/go.sum b/go.sum index 93b2f9fa99..b5e64321b5 100644 --- a/go.sum +++ b/go.sum @@ -59,10 +59,6 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzS github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/ClickHouse/ch-go v0.63.1 h1:s2JyZvWLTCSAGdtjMBBmAgQQHMco6pawLJMOXi0FODM= -github.com/ClickHouse/ch-go v0.63.1/go.mod h1:I1kJJCL3WJcBMGe1m+HVK0+nREaG+JOYYBWjrDrF3R0= -github.com/ClickHouse/clickhouse-go/v2 v2.24.0 h1:L/n/pVVpk95KtkHOiKuSnO7cu2ckeW4gICbbOh5qs74= -github.com/ClickHouse/clickhouse-go/v2 v2.24.0/go.mod h1:iDTViXk2Fgvf1jn2dbJd1ys+fBkdD1UMRnXlwmhijhQ= github.com/DataDog/zstd v1.5.6 h1:LbEglqepa/ipmmQJUDnSsfvA8e8IStVcGaFWDuxvGOY= github.com/DataDog/zstd v1.5.6/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/Julusian/godocdown v0.0.0-20170816220326-6d19f8ff2df8/go.mod h1:INZr5t32rG59/5xeltqoCJoNY7e5x/3xoY9WSWVWg74= @@ -109,8 +105,6 @@ 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 v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU= -github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/aws/aws-sdk-go-v2 v1.32.3 h1:T0dRlFBKcdaUPGNtkBSwHZxrtis8CQU17UpNBZYd0wk= github.com/aws/aws-sdk-go-v2 v1.32.3/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo= github.com/aws/aws-sdk-go-v2/credentials v1.17.42 h1:sBP0RPjBU4neGpIYyx8mkU2QqLPl5u9cmdTWVzIpHkM= @@ -237,8 +231,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0= github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE= -github.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+8Y+8shw= -github.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21 h1:PdsjTl0Cg+ZJgOx/CFV5NNgO1ThTreqdgKYiDCMHJwA= @@ -317,10 +309,6 @@ github.com/go-enry/go-enry/v2 v2.9.1 h1:G9iDteJ/Mc0F4Di5NeQknf83R2OkRbwY9cAYmcqV github.com/go-enry/go-enry/v2 v2.9.1/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8= github.com/go-enry/go-oniguruma v1.2.1 h1:k8aAMuJfMrqm/56SG2lV9Cfti6tC4x8673aHCcBk+eo= github.com/go-enry/go-oniguruma v1.2.1/go.mod h1:bWDhYP+S6xZQgiRL7wlTScFYBe023B6ilRZbCAD5Hf4= -github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw= -github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw= -github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg= -github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo= github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e h1:oRq/fiirun5HqlEWMLIcDmLpIELlG4iGbd0s8iqgPi8= github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= @@ -372,8 +360,6 @@ github.com/go-swagger/go-swagger v0.31.0/go.mod h1:WSigRRWEig8zV6t6Sm8Y+EmUjlzA/ 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-testfixtures/testfixtures/v3 v3.11.0 h1:XxQr8AnPORcZkyNd7go5UNLPD3dULN8ixYISlzrlfEQ= -github.com/go-testfixtures/testfixtures/v3 v3.11.0/go.mod h1:THmudHF1Ixq++J2/UodcJpxUphfyEd77m83TvDtryqE= 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.15 h1:eG1OhggBJTkDE8gUeOlGRbRe8E/PSVG26YG4AyFbwkU= @@ -385,7 +371,6 @@ github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6Wezm github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs= 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= @@ -410,7 +395,6 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -497,22 +481,6 @@ github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= -github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= -github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= -github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= -github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= -github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= -github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw= -github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= -github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA= -github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQyktQ5+f3dMVZfwD2KWJUgm7M0gdL9NGr8KA= github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= @@ -533,10 +501,6 @@ github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bB github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= github.com/jhillyerd/enmime v1.3.0 h1:LV5kzfLidiOr8qRGIpYYmUZCnhrPbcFAnAFUnWn99rw= github.com/jhillyerd/enmime v1.3.0/go.mod h1:6c6jg5HdRRV2FtvVL69LjiX1M8oE0xDX9VEhV3oy4gs= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -550,11 +514,8 @@ github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4 github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4 h1:cTxwSmnaqLoo+4tLukHoB9iqHOu3LmLhRmgUxZo6Vp4= github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4/go.mod h1:ghbZscTyKdM07+Fw3KSi0hcJm+AlEUWj8QLlPtijN/M= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= @@ -629,7 +590,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 h1:j2kD3MT1z4PXCiUllUJF9mWUESr9TWKS7iEKsQ/IipM= github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM= github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= @@ -670,9 +630,6 @@ github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3I 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/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= -github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU= -github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= -github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY= 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= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= @@ -735,8 +692,6 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6Ng 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= github.com/sassoftware/go-rpmutils v0.4.0/go.mod h1:3goNWi7PGAT3/dlql2lv3+MSN5jNYPjT5mVcQcIsYzI= -github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= -github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= @@ -783,7 +738,6 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -797,7 +751,6 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= -github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ= github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM= @@ -823,9 +776,6 @@ github.com/xanzy/go-gitlab v0.112.0 h1:6Z0cqEooCvBMfBIHw+CgO4AKGRV8na/9781xOb0+D github.com/xanzy/go-gitlab v0.112.0/go.mod h1:wKNKh3GkYDMOsGmnfuX+ITCmDuSDWFO0G+C4AygL9RY= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= -github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= -github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= -github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -842,9 +792,7 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yohcop/openid-go v1.0.1 h1:DPRd3iPO5F6O5zX2e62XpVAbPT6wV51cuucH0z9g3js= github.com/yohcop/openid-go v1.0.1/go.mod h1:b/AvD03P0KHj4yuihb+VtLD6bYYgsy0zqBzPCRjkCNs= -github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -863,13 +811,8 @@ github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= -go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHyIM= go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4= -go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= -go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= -go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= -go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= 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= @@ -941,7 +884,6 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= @@ -1022,10 +964,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm 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= golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200928182047-19e03678916f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 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= @@ -1046,8 +986,6 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/models/activities/repo_activity.go b/models/activities/repo_activity.go index 3ffad035b7..3ccdbd47d3 100644 --- a/models/activities/repo_activity.go +++ b/models/activities/repo_activity.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" + "xorm.io/builder" "xorm.io/xorm" ) @@ -337,8 +338,10 @@ func newlyCreatedIssues(ctx context.Context, repoID int64, fromTime time.Time) * func activeIssues(ctx context.Context, repoID int64, fromTime time.Time) *xorm.Session { sess := db.GetEngine(ctx).Where("issue.repo_id = ?", repoID). And("issue.is_pull = ?", false). - And("issue.created_unix >= ?", fromTime.Unix()). - Or("issue.closed_unix >= ?", fromTime.Unix()) + And(builder.Or( + builder.Gte{"issue.created_unix": fromTime.Unix()}, + builder.Gte{"issue.closed_unix": fromTime.Unix()}, + )) return sess } diff --git a/models/activities/user_heatmap_test.go b/models/activities/user_heatmap_test.go index a039fd3613..380045d3c5 100644 --- a/models/activities/user_heatmap_test.go +++ b/models/activities/user_heatmap_test.go @@ -64,11 +64,9 @@ func TestGetUserHeatmapDataByUser(t *testing.T) { for _, tc := range testCases { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: tc.userID}) - doer := &user_model.User{ID: tc.doerID} - _, err := unittest.LoadBeanIfExists(doer) - assert.NoError(t, err) - if tc.doerID == 0 { - doer = nil + var doer *user_model.User + if tc.doerID != 0 { + doer = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: tc.doerID}) } // get the action for comparison diff --git a/models/auth/webauthn_test.go b/models/auth/webauthn_test.go index f1cf398adf..654427e974 100644 --- a/models/auth/webauthn_test.go +++ b/models/auth/webauthn_test.go @@ -44,7 +44,7 @@ func TestWebAuthnCredential_UpdateSignCount(t *testing.T) { cred := unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1}) cred.SignCount = 1 assert.NoError(t, cred.UpdateSignCount(db.DefaultContext)) - unittest.AssertExistsIf(t, true, &auth_model.WebAuthnCredential{ID: 1, SignCount: 1}) + unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1, SignCount: 1}) } func TestWebAuthnCredential_UpdateLargeCounter(t *testing.T) { @@ -52,7 +52,7 @@ func TestWebAuthnCredential_UpdateLargeCounter(t *testing.T) { cred := unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1}) cred.SignCount = 0xffffffff assert.NoError(t, cred.UpdateSignCount(db.DefaultContext)) - unittest.AssertExistsIf(t, true, &auth_model.WebAuthnCredential{ID: 1, SignCount: 0xffffffff}) + unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1, SignCount: 0xffffffff}) } func TestCreateCredential(t *testing.T) { @@ -63,5 +63,5 @@ func TestCreateCredential(t *testing.T) { assert.Equal(t, "WebAuthn Created Credential", res.Name) assert.Equal(t, []byte("Test"), res.CredentialID) - unittest.AssertExistsIf(t, true, &auth_model.WebAuthnCredential{Name: "WebAuthn Created Credential", UserID: 1}) + unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{Name: "WebAuthn Created Credential", UserID: 1}) } diff --git a/models/db/name.go b/models/db/name.go index 55c9dffb6a..5f98edbb28 100644 --- a/models/db/name.go +++ b/models/db/name.go @@ -5,20 +5,13 @@ package db import ( "fmt" - "regexp" "strings" "unicode/utf8" "code.gitea.io/gitea/modules/util" ) -var ( - // ErrNameEmpty name is empty error - ErrNameEmpty = util.SilentWrap{Message: "name is empty", Err: util.ErrInvalidArgument} - - // AlphaDashDotPattern characters prohibited in a username (anything except A-Za-z0-9_.-) - AlphaDashDotPattern = regexp.MustCompile(`[^\w-\.]`) -) +var ErrNameEmpty = util.SilentWrap{Message: "name is empty", Err: util.ErrInvalidArgument} // ErrNameReserved represents a "reserved name" error. type ErrNameReserved struct { @@ -82,20 +75,20 @@ func (err ErrNameCharsNotAllowed) Unwrap() error { // IsUsableName checks if name is reserved or pattern of name is not allowed // based on given reserved names and patterns. -// Names are exact match, patterns can be prefix or suffix match with placeholder '*'. -func IsUsableName(names, patterns []string, name string) error { +// Names are exact match, patterns can be a prefix or suffix match with placeholder '*'. +func IsUsableName(reservedNames, reservedPatterns []string, name string) error { name = strings.TrimSpace(strings.ToLower(name)) if utf8.RuneCountInString(name) == 0 { return ErrNameEmpty } - for i := range names { - if name == names[i] { + for i := range reservedNames { + if name == reservedNames[i] { return ErrNameReserved{name} } } - for _, pat := range patterns { + for _, pat := range reservedPatterns { if pat[0] == '*' && strings.HasSuffix(name, pat[1:]) || (pat[len(pat)-1] == '*' && strings.HasPrefix(name, pat[:len(pat)-1])) { return ErrNamePatternNotAllowed{pat} diff --git a/models/fixtures/action_run_job.yml b/models/fixtures/action_run_job.yml index 9b6f5b9a88..8837e6ec2d 100644 --- a/models/fixtures/action_run_job.yml +++ b/models/fixtures/action_run_job.yml @@ -64,7 +64,7 @@ name: job2 attempt: 1 job_id: job2 - needs: [job1] + needs: '["job1"]' task_id: 51 status: 5 started: 1683636528 diff --git a/models/fixtures/protected_tag.yml b/models/fixtures/protected_tag.yml index dbec52c0c2..1944e7bd84 100644 --- a/models/fixtures/protected_tag.yml +++ b/models/fixtures/protected_tag.yml @@ -2,23 +2,23 @@ id: 1 repo_id: 4 name_pattern: /v.+/ - allowlist_user_i_ds: [] - allowlist_team_i_ds: [] + allowlist_user_i_ds: "[]" + allowlist_team_i_ds: "[]" created_unix: 1715596037 updated_unix: 1715596037 - id: 2 repo_id: 1 name_pattern: v-* - allowlist_user_i_ds: [] - allowlist_team_i_ds: [] + allowlist_user_i_ds: "[]" + allowlist_team_i_ds: "[]" created_unix: 1715596037 updated_unix: 1715596037 - id: 3 repo_id: 1 name_pattern: v-1.1 - allowlist_user_i_ds: [2] - allowlist_team_i_ds: [] + allowlist_user_i_ds: "[2]" + allowlist_team_i_ds: "[]" created_unix: 1715596037 updated_unix: 1715596037 diff --git a/models/issues/comment.go b/models/issues/comment.go index e4537aa872..a7ec8f57fc 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -197,6 +197,20 @@ func (t CommentType) HasMailReplySupport() bool { return false } +func (t CommentType) CountedAsConversation() bool { + for _, ct := range ConversationCountedCommentType() { + if t == ct { + return true + } + } + return false +} + +// ConversationCountedCommentType returns the comment types that are counted as a conversation +func ConversationCountedCommentType() []CommentType { + return []CommentType{CommentTypeComment, CommentTypeReview} +} + // RoleInRepo presents the user's participation in the repo type RoleInRepo string @@ -592,26 +606,26 @@ func (c *Comment) LoadAttachments(ctx context.Context) error { return nil } -// UpdateAttachments update attachments by UUIDs for the comment -func (c *Comment) UpdateAttachments(ctx context.Context, uuids []string) error { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err +// UpdateCommentAttachments update attachments by UUIDs for the comment +func UpdateCommentAttachments(ctx context.Context, c *Comment, uuids []string) error { + if len(uuids) == 0 { + return nil } - defer committer.Close() - - attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, uuids) - if err != nil { - return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", uuids, err) - } - for i := 0; i < len(attachments); i++ { - attachments[i].IssueID = c.IssueID - attachments[i].CommentID = c.ID - if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil { - return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err) + return db.WithTx(ctx, func(ctx context.Context) error { + attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, uuids) + if err != nil { + return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", uuids, err) } - } - return committer.Commit() + for i := 0; i < len(attachments); i++ { + attachments[i].IssueID = c.IssueID + attachments[i].CommentID = c.ID + if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil { + return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err) + } + } + c.Attachments = attachments + return nil + }) } // LoadAssigneeUserAndTeam if comment.Type is CommentTypeAssignees, then load assignees @@ -878,7 +892,7 @@ func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment // Check comment type. switch opts.Type { case CommentTypeCode: - if err = updateAttachments(ctx, opts, comment); err != nil { + if err = UpdateCommentAttachments(ctx, comment, opts.Attachments); err != nil { return err } if comment.ReviewID != 0 { @@ -893,12 +907,12 @@ func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment } fallthrough case CommentTypeComment: - if _, err = db.Exec(ctx, "UPDATE `issue` SET num_comments=num_comments+1 WHERE id=?", opts.Issue.ID); err != nil { + if err := UpdateIssueNumComments(ctx, opts.Issue.ID); err != nil { return err } fallthrough case CommentTypeReview: - if err = updateAttachments(ctx, opts, comment); err != nil { + if err = UpdateCommentAttachments(ctx, comment, opts.Attachments); err != nil { return err } case CommentTypeReopen, CommentTypeClose: @@ -910,23 +924,6 @@ func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment return UpdateIssueCols(ctx, opts.Issue, "updated_unix") } -func updateAttachments(ctx context.Context, opts *CreateCommentOptions, comment *Comment) error { - attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, opts.Attachments) - if err != nil { - return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", opts.Attachments, err) - } - for i := range attachments { - attachments[i].IssueID = opts.Issue.ID - attachments[i].CommentID = comment.ID - // No assign value could be 0, so ignore AllCols(). - if _, err = db.GetEngine(ctx).ID(attachments[i].ID).Update(attachments[i]); err != nil { - return fmt.Errorf("update attachment [%d]: %w", attachments[i].ID, err) - } - } - comment.Attachments = attachments - return nil -} - func createDeadlineComment(ctx context.Context, doer *user_model.User, issue *Issue, newDeadlineUnix timeutil.TimeStamp) (*Comment, error) { var content string var commentType CommentType @@ -1182,8 +1179,8 @@ func DeleteComment(ctx context.Context, comment *Comment) error { return err } - if comment.Type == CommentTypeComment { - if _, err := e.ID(comment.IssueID).Decr("num_comments").Update(new(Issue)); err != nil { + if comment.Type.CountedAsConversation() { + if err := UpdateIssueNumComments(ctx, comment.IssueID); err != nil { return err } } @@ -1300,6 +1297,21 @@ func (c *Comment) HasOriginalAuthor() bool { return c.OriginalAuthor != "" && c.OriginalAuthorID != 0 } +func UpdateIssueNumCommentsBuilder(issueID int64) *builder.Builder { + subQuery := builder.Select("COUNT(*)").From("`comment`").Where( + builder.Eq{"issue_id": issueID}.And( + builder.In("`type`", ConversationCountedCommentType()), + )) + + return builder.Update(builder.Eq{"num_comments": subQuery}). + From("`issue`").Where(builder.Eq{"id": issueID}) +} + +func UpdateIssueNumComments(ctx context.Context, issueID int64) error { + _, err := db.GetEngine(ctx).Exec(UpdateIssueNumCommentsBuilder(issueID)) + return err +} + // InsertIssueComments inserts many comments of issues. func InsertIssueComments(ctx context.Context, comments []*Comment) error { if len(comments) == 0 { @@ -1332,8 +1344,7 @@ func InsertIssueComments(ctx context.Context, comments []*Comment) error { } for _, issueID := range issueIDs { - if _, err := db.Exec(ctx, "UPDATE issue set num_comments = (SELECT count(*) FROM comment WHERE issue_id = ? AND `type`=?) WHERE id = ?", - issueID, CommentTypeComment, issueID); err != nil { + if err := UpdateIssueNumComments(ctx, issueID); err != nil { return err } } diff --git a/models/issues/comment_test.go b/models/issues/comment_test.go index d81f33f953..ae0bc3ce17 100644 --- a/models/issues/comment_test.go +++ b/models/issues/comment_test.go @@ -45,6 +45,24 @@ func TestCreateComment(t *testing.T) { unittest.AssertInt64InRange(t, now, then, int64(updatedIssue.UpdatedUnix)) } +func Test_UpdateCommentAttachment(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1}) + attachment := repo_model.Attachment{ + Name: "test.txt", + } + assert.NoError(t, db.Insert(db.DefaultContext, &attachment)) + + err := issues_model.UpdateCommentAttachments(db.DefaultContext, comment, []string{attachment.UUID}) + assert.NoError(t, err) + + attachment2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: attachment.ID}) + assert.EqualValues(t, attachment.Name, attachment2.Name) + assert.EqualValues(t, comment.ID, attachment2.CommentID) + assert.EqualValues(t, comment.IssueID, attachment2.IssueID) +} + func TestFetchCodeComments(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) @@ -97,3 +115,12 @@ func TestMigrate_InsertIssueComments(t *testing.T) { unittest.CheckConsistencyFor(t, &issues_model.Issue{}) } + +func Test_UpdateIssueNumComments(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) + + assert.NoError(t, issues_model.UpdateIssueNumComments(db.DefaultContext, issue2.ID)) + issue2 = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) + assert.EqualValues(t, 1, issue2.NumComments) +} diff --git a/models/issues/issue_update.go b/models/issues/issue_update.go index 2a6f3603bc..479834045c 100644 --- a/models/issues/issue_update.go +++ b/models/issues/issue_update.go @@ -34,7 +34,7 @@ func UpdateIssueCols(ctx context.Context, issue *Issue, cols ...string) error { return nil } -func changeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isClosed, isMergePull bool) (*Comment, error) { +func ChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isClosed, isMergePull bool) (*Comment, error) { // Reload the issue currentIssue, err := GetIssueByID(ctx, issue.ID) if err != nil { @@ -134,7 +134,7 @@ func CloseIssue(ctx context.Context, issue *Issue, doer *user_model.User) (*Comm } defer committer.Close() - comment, err := changeIssueStatus(ctx, issue, doer, true, false) + comment, err := ChangeIssueStatus(ctx, issue, doer, true, false) if err != nil { return nil, err } @@ -159,7 +159,7 @@ func ReopenIssue(ctx context.Context, issue *Issue, doer *user_model.User) (*Com } defer committer.Close() - comment, err := changeIssueStatus(ctx, issue, doer, false, false) + comment, err := ChangeIssueStatus(ctx, issue, doer, false, false) if err != nil { return nil, err } @@ -405,19 +405,10 @@ func NewIssueWithIndex(ctx context.Context, doer *user_model.User, opts NewIssue return err } - if len(opts.Attachments) > 0 { - attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, opts.Attachments) - if err != nil { - return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", opts.Attachments, err) - } - - for i := 0; i < len(attachments); i++ { - attachments[i].IssueID = opts.Issue.ID - if _, err = e.ID(attachments[i].ID).Update(attachments[i]); err != nil { - return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err) - } - } + if err := UpdateIssueAttachments(ctx, opts.Issue.ID, opts.Attachments); err != nil { + return err } + if err = opts.Issue.LoadAttributes(ctx); err != nil { return err } diff --git a/models/issues/issue_user_test.go b/models/issues/issue_user_test.go index ce47adb53a..7c21aa15ee 100644 --- a/models/issues/issue_user_test.go +++ b/models/issues/issue_user_test.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/models/unittest" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_NewIssueUsers(t *testing.T) { @@ -27,9 +28,8 @@ func Test_NewIssueUsers(t *testing.T) { } // artificially insert new issue - unittest.AssertSuccessfulInsert(t, newIssue) - - assert.NoError(t, issues_model.NewIssueUsers(db.DefaultContext, repo, newIssue)) + require.NoError(t, db.Insert(db.DefaultContext, newIssue)) + require.NoError(t, issues_model.NewIssueUsers(db.DefaultContext, repo, newIssue)) // issue_user table should now have entries for new issue unittest.AssertExistsAndLoadBean(t, &issues_model.IssueUser{IssueID: newIssue.ID, UID: newIssue.PosterID}) diff --git a/models/issues/label_test.go b/models/issues/label_test.go index 1d4b6f4684..185fa11bbc 100644 --- a/models/issues/label_test.go +++ b/models/issues/label_test.go @@ -387,7 +387,7 @@ func TestDeleteIssueLabel(t *testing.T) { expectedNumIssues := label.NumIssues expectedNumClosedIssues := label.NumClosedIssues - if unittest.BeanExists(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: labelID}) { + if unittest.GetBean(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: labelID}) != nil { expectedNumIssues-- if issue.IsClosed { expectedNumClosedIssues-- diff --git a/models/issues/pull.go b/models/issues/pull.go index efb24e8984..8c43eb19dd 100644 --- a/models/issues/pull.go +++ b/models/issues/pull.go @@ -499,65 +499,6 @@ func (pr *PullRequest) IsFromFork() bool { return pr.HeadRepoID != pr.BaseRepoID } -// SetMerged sets a pull request to merged and closes the corresponding issue -func (pr *PullRequest) SetMerged(ctx context.Context) (bool, error) { - if pr.HasMerged { - return false, fmt.Errorf("PullRequest[%d] already merged", pr.Index) - } - if pr.MergedCommitID == "" || pr.MergedUnix == 0 || pr.Merger == nil { - return false, fmt.Errorf("Unable to merge PullRequest[%d], some required fields are empty", pr.Index) - } - - pr.HasMerged = true - sess := db.GetEngine(ctx) - - if _, err := sess.Exec("UPDATE `issue` SET `repo_id` = `repo_id` WHERE `id` = ?", pr.IssueID); err != nil { - return false, err - } - - if _, err := sess.Exec("UPDATE `pull_request` SET `issue_id` = `issue_id` WHERE `id` = ?", pr.ID); err != nil { - return false, err - } - - pr.Issue = nil - if err := pr.LoadIssue(ctx); err != nil { - return false, err - } - - if tmpPr, err := GetPullRequestByID(ctx, pr.ID); err != nil { - return false, err - } else if tmpPr.HasMerged { - if pr.Issue.IsClosed { - return false, nil - } - return false, fmt.Errorf("PullRequest[%d] already merged but it's associated issue [%d] is not closed", pr.Index, pr.IssueID) - } else if pr.Issue.IsClosed { - return false, fmt.Errorf("PullRequest[%d] already closed", pr.Index) - } - - if err := pr.Issue.LoadRepo(ctx); err != nil { - return false, err - } - - if err := pr.Issue.Repo.LoadOwner(ctx); err != nil { - return false, err - } - - if _, err := changeIssueStatus(ctx, pr.Issue, pr.Merger, true, true); err != nil { - return false, fmt.Errorf("Issue.changeStatus: %w", err) - } - - // reset the conflicted files as there cannot be any if we're merged - pr.ConflictedFiles = []string{} - - // We need to save all of the data used to compute this merge as it may have already been changed by TestPatch. FIXME: need to set some state to prevent TestPatch from running whilst we are merging. - if _, err := sess.Where("id = ?", pr.ID).Cols("has_merged, status, merge_base, merged_commit_id, merger_id, merged_unix, conflicted_files").Update(pr); err != nil { - return false, fmt.Errorf("Failed to update pr[%d]: %w", pr.ID, err) - } - - return true, nil -} - // NewPullRequest creates new pull request with labels for repository. func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *Issue, labelIDs []int64, uuids []string, pr *PullRequest) (err error) { ctx, committer, err := db.TxContext(ctx) diff --git a/models/issues/review.go b/models/issues/review.go index 8b345e5fd8..3e787273be 100644 --- a/models/issues/review.go +++ b/models/issues/review.go @@ -639,6 +639,10 @@ func InsertReviews(ctx context.Context, reviews []*Review) error { return err } } + + if err := UpdateIssueNumComments(ctx, review.IssueID); err != nil { + return err + } } return committer.Commit() diff --git a/models/migrations/base/tests.go b/models/migrations/base/tests.go index 2eb85cd8a7..fe6de9c517 100644 --- a/models/migrations/base/tests.go +++ b/models/migrations/base/tests.go @@ -76,7 +76,7 @@ func PrepareTestEnv(t *testing.T, skip int, syncModels ...any) (*xorm.Engine, fu t.Errorf("error whilst initializing fixtures from %s: %v", fixturesDir, err) return x, deferFn } - if err := unittest.LoadFixtures(x); err != nil { + if err := unittest.LoadFixtures(); err != nil { t.Errorf("error whilst loading fixtures from %s: %v", fixturesDir, err) return x, deferFn } diff --git a/models/organization/org_user_test.go b/models/organization/org_user_test.go index 55abb63203..c5110b2a34 100644 --- a/models/organization/org_user_test.go +++ b/models/organization/org_user_test.go @@ -131,7 +131,7 @@ func TestAddOrgUser(t *testing.T) { testSuccess := func(orgID, userID int64, isPublic bool) { org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: orgID}) expectedNumMembers := org.NumMembers - if !unittest.BeanExists(t, &organization.OrgUser{OrgID: orgID, UID: userID}) { + if unittest.GetBean(t, &organization.OrgUser{OrgID: orgID, UID: userID}) == nil { expectedNumMembers++ } assert.NoError(t, organization.AddOrgUser(db.DefaultContext, orgID, userID)) diff --git a/models/packages/package.go b/models/packages/package.go index c12f345f0e..31e1277a6e 100644 --- a/models/packages/package.go +++ b/models/packages/package.go @@ -248,6 +248,18 @@ func GetPackageByID(ctx context.Context, packageID int64) (*Package, error) { return p, nil } +// UpdatePackageNameByID updates the package's name, it is only for internal usage, for example: rename some legacy packages +func UpdatePackageNameByID(ctx context.Context, ownerID int64, packageType Type, packageID int64, name string) error { + var cond builder.Cond = builder.Eq{ + "package.id": packageID, + "package.owner_id": ownerID, + "package.type": packageType, + "package.is_internal": false, + } + _, err := db.GetEngine(ctx).Where(cond).Update(&Package{Name: name, LowerName: strings.ToLower(name)}) + return err +} + // GetPackageByName gets a package by name func GetPackageByName(ctx context.Context, ownerID int64, packageType Type, name string) (*Package, error) { var cond builder.Cond = builder.Eq{ diff --git a/models/project/project.go b/models/project/project.go index 87e679e1b7..edeb0b4742 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -126,6 +126,14 @@ func (p *Project) LoadRepo(ctx context.Context) (err error) { return err } +func ProjectLinkForOrg(org *user_model.User, projectID int64) string { //nolint + return fmt.Sprintf("%s/-/projects/%d", org.HomeLink(), projectID) +} + +func ProjectLinkForRepo(repo *repo_model.Repository, projectID int64) string { //nolint + return fmt.Sprintf("%s/projects/%d", repo.Link(), projectID) +} + // Link returns the project's relative URL. func (p *Project) Link(ctx context.Context) string { if p.OwnerID > 0 { @@ -134,7 +142,7 @@ func (p *Project) Link(ctx context.Context) string { log.Error("LoadOwner: %v", err) return "" } - return fmt.Sprintf("%s/-/projects/%d", p.Owner.HomeLink(), p.ID) + return ProjectLinkForOrg(p.Owner, p.ID) } if p.RepoID > 0 { err := p.LoadRepo(ctx) @@ -142,7 +150,7 @@ func (p *Project) Link(ctx context.Context) string { log.Error("LoadRepo: %v", err) return "" } - return fmt.Sprintf("%s/projects/%d", p.Repo.Link(), p.ID) + return ProjectLinkForRepo(p.Repo, p.ID) } return "" } diff --git a/models/repo.go b/models/repo.go index 3e9c52fdd9..9bc67079a9 100644 --- a/models/repo.go +++ b/models/repo.go @@ -16,6 +16,8 @@ import ( "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" + + "xorm.io/builder" ) // Init initialize model @@ -24,7 +26,7 @@ func Init(ctx context.Context) error { } type repoChecker struct { - querySQL func(ctx context.Context) ([]map[string][]byte, error) + querySQL func(ctx context.Context) ([]int64, error) correctSQL func(ctx context.Context, id int64) error desc string } @@ -35,8 +37,7 @@ func repoStatsCheck(ctx context.Context, checker *repoChecker) { log.Error("Select %s: %v", checker.desc, err) return } - for _, result := range results { - id, _ := strconv.ParseInt(string(result["id"]), 10, 64) + for _, id := range results { select { case <-ctx.Done(): log.Warn("CheckRepoStats: Cancelled before checking %s for with id=%d", checker.desc, id) @@ -51,21 +52,23 @@ func repoStatsCheck(ctx context.Context, checker *repoChecker) { } } -func StatsCorrectSQL(ctx context.Context, sql string, id int64) error { - _, err := db.GetEngine(ctx).Exec(sql, id, id) +func StatsCorrectSQL(ctx context.Context, sql any, ids ...any) error { + args := []any{sql} + args = append(args, ids...) + _, err := db.GetEngine(ctx).Exec(args...) return err } func repoStatsCorrectNumWatches(ctx context.Context, id int64) error { - return StatsCorrectSQL(ctx, "UPDATE `repository` SET num_watches=(SELECT COUNT(*) FROM `watch` WHERE repo_id=? AND mode<>2) WHERE id=?", id) + return StatsCorrectSQL(ctx, "UPDATE `repository` SET num_watches=(SELECT COUNT(*) FROM `watch` WHERE repo_id=? AND mode<>2) WHERE id=?", id, id) } func repoStatsCorrectNumStars(ctx context.Context, id int64) error { - return StatsCorrectSQL(ctx, "UPDATE `repository` SET num_stars=(SELECT COUNT(*) FROM `star` WHERE repo_id=?) WHERE id=?", id) + return StatsCorrectSQL(ctx, "UPDATE `repository` SET num_stars=(SELECT COUNT(*) FROM `star` WHERE repo_id=?) WHERE id=?", id, id) } func labelStatsCorrectNumIssues(ctx context.Context, id int64) error { - return StatsCorrectSQL(ctx, "UPDATE `label` SET num_issues=(SELECT COUNT(*) FROM `issue_label` WHERE label_id=?) WHERE id=?", id) + return StatsCorrectSQL(ctx, "UPDATE `label` SET num_issues=(SELECT COUNT(*) FROM `issue_label` WHERE label_id=?) WHERE id=?", id, id) } func labelStatsCorrectNumIssuesRepo(ctx context.Context, id int64) error { @@ -102,11 +105,11 @@ func milestoneStatsCorrectNumIssuesRepo(ctx context.Context, id int64) error { } func userStatsCorrectNumRepos(ctx context.Context, id int64) error { - return StatsCorrectSQL(ctx, "UPDATE `user` SET num_repos=(SELECT COUNT(*) FROM `repository` WHERE owner_id=?) WHERE id=?", id) + return StatsCorrectSQL(ctx, "UPDATE `user` SET num_repos=(SELECT COUNT(*) FROM `repository` WHERE owner_id=?) WHERE id=?", id, id) } func repoStatsCorrectIssueNumComments(ctx context.Context, id int64) error { - return StatsCorrectSQL(ctx, "UPDATE `issue` SET num_comments=(SELECT COUNT(*) FROM `comment` WHERE issue_id=? AND type=0) WHERE id=?", id) + return StatsCorrectSQL(ctx, issues_model.UpdateIssueNumCommentsBuilder(id)) } func repoStatsCorrectNumIssues(ctx context.Context, id int64) error { @@ -125,9 +128,12 @@ func repoStatsCorrectNumClosedPulls(ctx context.Context, id int64) error { return repo_model.UpdateRepoIssueNumbers(ctx, id, true, true) } -func statsQuery(args ...any) func(context.Context) ([]map[string][]byte, error) { - return func(ctx context.Context) ([]map[string][]byte, error) { - return db.GetEngine(ctx).Query(args...) +// statsQuery returns a function that queries the database for a list of IDs +// sql could be a string or a *builder.Builder +func statsQuery(sql any, args ...any) func(context.Context) ([]int64, error) { + return func(ctx context.Context) ([]int64, error) { + var ids []int64 + return ids, db.GetEngine(ctx).SQL(sql, args...).Find(&ids) } } @@ -198,7 +204,16 @@ func CheckRepoStats(ctx context.Context) error { }, // Issue.NumComments { - statsQuery("SELECT `issue`.id FROM `issue` WHERE `issue`.num_comments!=(SELECT COUNT(*) FROM `comment` WHERE issue_id=`issue`.id AND type=0)"), + statsQuery(builder.Select("`issue`.id").From("`issue`").Where( + builder.Neq{ + "`issue`.num_comments": builder.Select("COUNT(*)").From("`comment`").Where( + builder.Expr("issue_id = `issue`.id").And( + builder.In("type", issues_model.ConversationCountedCommentType()), + ), + ), + }, + ), + ), repoStatsCorrectIssueNumComments, "issue count 'num_comments'", }, diff --git a/models/repo/repo.go b/models/repo/repo.go index 2d9b9de88d..5ef4d470c3 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -11,6 +11,7 @@ import ( "net" "net/url" "path/filepath" + "regexp" "strconv" "strings" @@ -60,13 +61,15 @@ func (err ErrRepoIsArchived) Error() string { } var ( - reservedRepoNames = []string{".", "..", "-"} - reservedRepoPatterns = []string{"*.git", "*.wiki", "*.rss", "*.atom"} + validRepoNamePattern = regexp.MustCompile(`[-.\w]+`) + invalidRepoNamePattern = regexp.MustCompile(`[.]{2,}`) + reservedRepoNames = []string{".", "..", "-"} + reservedRepoPatterns = []string{"*.git", "*.wiki", "*.rss", "*.atom"} ) -// IsUsableRepoName returns true when repository is usable +// IsUsableRepoName returns true when name is usable func IsUsableRepoName(name string) error { - if db.AlphaDashDotPattern.MatchString(name) { + if !validRepoNamePattern.MatchString(name) || invalidRepoNamePattern.MatchString(name) { // Note: usually this error is normally caught up earlier in the UI return db.ErrNameCharsNotAllowed{Name: name} } diff --git a/models/repo/repo_test.go b/models/repo/repo_test.go index 6d88d170da..001f8ecd84 100644 --- a/models/repo/repo_test.go +++ b/models/repo/repo_test.go @@ -217,3 +217,15 @@ func TestComposeSSHCloneURL(t *testing.T) { setting.SSH.Port = 123 assert.Equal(t, "ssh://git@[::1]:123/user/repo.git", ComposeSSHCloneURL("user", "repo")) } + +func TestIsUsableRepoName(t *testing.T) { + assert.NoError(t, IsUsableRepoName("a")) + assert.NoError(t, IsUsableRepoName("-1_.")) + assert.NoError(t, IsUsableRepoName(".profile")) + + assert.Error(t, IsUsableRepoName("-")) + assert.Error(t, IsUsableRepoName("🌞")) + assert.Error(t, IsUsableRepoName("the..repo")) + assert.Error(t, IsUsableRepoName("foo.wiki")) + assert.Error(t, IsUsableRepoName("foo.git")) +} diff --git a/models/repo_test.go b/models/repo_test.go index 2a8a4a743e..bcf62237f0 100644 --- a/models/repo_test.go +++ b/models/repo_test.go @@ -7,6 +7,7 @@ import ( "testing" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unittest" "github.com/stretchr/testify/assert" @@ -22,3 +23,16 @@ func TestDoctorUserStarNum(t *testing.T) { assert.NoError(t, DoctorUserStarNum(db.DefaultContext)) } + +func Test_repoStatsCorrectIssueNumComments(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) + assert.NotNil(t, issue2) + assert.EqualValues(t, 0, issue2.NumComments) // the fixture data is wrong, but we don't fix it here + + assert.NoError(t, repoStatsCorrectIssueNumComments(db.DefaultContext, 2)) + // reload the issue + issue2 = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) + assert.EqualValues(t, 1, issue2.NumComments) +} diff --git a/models/unittest/fixtures.go b/models/unittest/fixtures.go index 4dde5410d6..fb2d2d0085 100644 --- a/models/unittest/fixtures.go +++ b/models/unittest/fixtures.go @@ -1,97 +1,33 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -//nolint:forbidigo package unittest import ( "fmt" - "os" - "time" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/auth/password/hash" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" - "github.com/go-testfixtures/testfixtures/v3" "xorm.io/xorm" "xorm.io/xorm/schemas" ) -var fixturesLoader *testfixtures.Loader +type FixturesLoader interface { + Load() error +} + +var fixturesLoader FixturesLoader // GetXORMEngine gets the XORM engine -func GetXORMEngine(engine ...*xorm.Engine) (x *xorm.Engine) { - if len(engine) == 1 { - return engine[0] - } +func GetXORMEngine() (x *xorm.Engine) { return db.GetEngine(db.DefaultContext).(*xorm.Engine) } -// InitFixtures initialize test fixtures for a test database -func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) { - e := GetXORMEngine(engine...) - var fixtureOptionFiles func(*testfixtures.Loader) error - if opts.Dir != "" { - fixtureOptionFiles = testfixtures.Directory(opts.Dir) - } else { - fixtureOptionFiles = testfixtures.Files(opts.Files...) - } - dialect := "unknown" - switch e.Dialect().URI().DBType { - case schemas.POSTGRES: - dialect = "postgres" - case schemas.MYSQL: - dialect = "mysql" - case schemas.MSSQL: - dialect = "mssql" - case schemas.SQLITE: - dialect = "sqlite3" - default: - fmt.Println("Unsupported RDBMS for integration tests") - os.Exit(1) - } - loaderOptions := []func(loader *testfixtures.Loader) error{ - testfixtures.Database(e.DB().DB), - testfixtures.Dialect(dialect), - testfixtures.DangerousSkipTestDatabaseCheck(), - fixtureOptionFiles, - } - - if e.Dialect().URI().DBType == schemas.POSTGRES { - loaderOptions = append(loaderOptions, testfixtures.SkipResetSequences()) - } - - fixturesLoader, err = testfixtures.New(loaderOptions...) - if err != nil { - return err - } - - // register the dummy hash algorithm function used in the test fixtures - _ = hash.Register("dummy", hash.NewDummyHasher) - - setting.PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy") - - return err -} - -// LoadFixtures load fixtures for a test database -func LoadFixtures(engine ...*xorm.Engine) error { - e := GetXORMEngine(engine...) - var err error - // (doubt) database transaction conflicts could occur and result in ROLLBACK? just try for a few times. - for i := 0; i < 5; i++ { - if err = fixturesLoader.Load(); err == nil { - break - } - time.Sleep(200 * time.Millisecond) - } - if err != nil { - fmt.Printf("LoadFixtures failed after retries: %v\n", err) - } - // Now if we're running postgres we need to tell it to update the sequences - if e.Dialect().URI().DBType == schemas.POSTGRES { - results, err := e.QueryString(`SELECT 'SELECT SETVAL(' || +func loadFixtureResetSeqPgsql(e *xorm.Engine) error { + results, err := e.QueryString(`SELECT 'SELECT SETVAL(' || quote_literal(quote_ident(PGT.schemaname) || '.' || quote_ident(S.relname)) || ', COALESCE(MAX(' ||quote_ident(C.attname)|| '), 1) ) FROM ' || quote_ident(PGT.schemaname)|| '.'||quote_ident(T.relname)|| ';' @@ -107,22 +43,42 @@ func LoadFixtures(engine ...*xorm.Engine) error { AND D.refobjsubid = C.attnum AND T.relname = PGT.tablename ORDER BY S.relname;`) - if err != nil { - fmt.Printf("Failed to generate sequence update: %v\n", err) - return err - } - for _, r := range results { - for _, value := range r { - _, err = e.Exec(value) - if err != nil { - fmt.Printf("Failed to update sequence: %s Error: %v\n", value, err) - return err - } + if err != nil { + return fmt.Errorf("failed to generate sequence update: %w", err) + } + for _, r := range results { + for _, value := range r { + _, err = e.Exec(value) + if err != nil { + return fmt.Errorf("failed to update sequence: %s, error: %w", value, err) } } } + return nil +} + +// InitFixtures initialize test fixtures for a test database +func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) { + xormEngine := util.IfZero(util.OptionalArg(engine), GetXORMEngine()) + fixturesLoader, err = NewFixturesLoader(xormEngine, opts) + // fixturesLoader = NewFixturesLoaderVendor(xormEngine, opts) + + // register the dummy hash algorithm function used in the test fixtures _ = hash.Register("dummy", hash.NewDummyHasher) setting.PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy") - return err } + +// LoadFixtures load fixtures for a test database +func LoadFixtures() error { + if err := fixturesLoader.Load(); err != nil { + return err + } + // Now if we're running postgres we need to tell it to update the sequences + if GetXORMEngine().Dialect().URI().DBType == schemas.POSTGRES { + if err := loadFixtureResetSeqPgsql(GetXORMEngine()); err != nil { + return err + } + } + return nil +} diff --git a/models/unittest/fixtures_loader.go b/models/unittest/fixtures_loader.go new file mode 100644 index 0000000000..14686caf63 --- /dev/null +++ b/models/unittest/fixtures_loader.go @@ -0,0 +1,201 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package unittest + +import ( + "database/sql" + "encoding/hex" + "fmt" + "os" + "path/filepath" + "slices" + "strings" + + "gopkg.in/yaml.v3" + "xorm.io/xorm" + "xorm.io/xorm/schemas" +) + +type fixtureItem struct { + tableName string + tableNameQuoted string + sqlInserts []string + sqlInsertArgs [][]any + + mssqlHasIdentityColumn bool +} + +type fixturesLoaderInternal struct { + db *sql.DB + dbType schemas.DBType + files []string + fixtures map[string]*fixtureItem + quoteObject func(string) string + paramPlaceholder func(idx int) string +} + +func (f *fixturesLoaderInternal) mssqlTableHasIdentityColumn(db *sql.DB, tableName string) (bool, error) { + row := db.QueryRow(`SELECT COUNT(*) FROM sys.identity_columns WHERE OBJECT_ID = OBJECT_ID(?)`, tableName) + var count int + if err := row.Scan(&count); err != nil { + return false, err + } + return count > 0, nil +} + +func (f *fixturesLoaderInternal) preprocessFixtureRow(row []map[string]any) (err error) { + for _, m := range row { + for k, v := range m { + if s, ok := v.(string); ok { + if strings.HasPrefix(s, "0x") { + if m[k], err = hex.DecodeString(s[2:]); err != nil { + return err + } + } + } + } + } + return nil +} + +func (f *fixturesLoaderInternal) prepareFixtureItem(file string) (_ *fixtureItem, err error) { + fixture := &fixtureItem{} + fixture.tableName, _, _ = strings.Cut(filepath.Base(file), ".") + fixture.tableNameQuoted = f.quoteObject(fixture.tableName) + + if f.dbType == schemas.MSSQL { + fixture.mssqlHasIdentityColumn, err = f.mssqlTableHasIdentityColumn(f.db, fixture.tableName) + if err != nil { + return nil, err + } + } + + data, err := os.ReadFile(file) + if err != nil { + return nil, fmt.Errorf("failed to read file %q: %w", file, err) + } + + var rows []map[string]any + if err = yaml.Unmarshal(data, &rows); err != nil { + return nil, fmt.Errorf("failed to unmarshal yaml data from %q: %w", file, err) + } + if err = f.preprocessFixtureRow(rows); err != nil { + return nil, fmt.Errorf("failed to preprocess fixture rows from %q: %w", file, err) + } + + var sqlBuf []byte + var sqlArguments []any + for _, row := range rows { + sqlBuf = append(sqlBuf, fmt.Sprintf("INSERT INTO %s (", fixture.tableNameQuoted)...) + for k, v := range row { + sqlBuf = append(sqlBuf, f.quoteObject(k)...) + sqlBuf = append(sqlBuf, ","...) + sqlArguments = append(sqlArguments, v) + } + sqlBuf = sqlBuf[:len(sqlBuf)-1] + sqlBuf = append(sqlBuf, ") VALUES ("...) + paramIdx := 1 + for range row { + sqlBuf = append(sqlBuf, f.paramPlaceholder(paramIdx)...) + sqlBuf = append(sqlBuf, ',') + paramIdx++ + } + sqlBuf[len(sqlBuf)-1] = ')' + fixture.sqlInserts = append(fixture.sqlInserts, string(sqlBuf)) + fixture.sqlInsertArgs = append(fixture.sqlInsertArgs, slices.Clone(sqlArguments)) + sqlBuf = sqlBuf[:0] + sqlArguments = sqlArguments[:0] + } + return fixture, nil +} + +func (f *fixturesLoaderInternal) loadFixtures(tx *sql.Tx, file string) (err error) { + fixture := f.fixtures[file] + if fixture == nil { + if fixture, err = f.prepareFixtureItem(file); err != nil { + return err + } + f.fixtures[file] = fixture + } + + _, err = tx.Exec(fmt.Sprintf("DELETE FROM %s", fixture.tableNameQuoted)) // sqlite3 doesn't support truncate + if err != nil { + return err + } + + if fixture.mssqlHasIdentityColumn { + _, err = tx.Exec(fmt.Sprintf("SET IDENTITY_INSERT %s ON", fixture.tableNameQuoted)) + if err != nil { + return err + } + defer func() { _, err = tx.Exec(fmt.Sprintf("SET IDENTITY_INSERT %s OFF", fixture.tableNameQuoted)) }() + } + for i := range fixture.sqlInserts { + _, err = tx.Exec(fixture.sqlInserts[i], fixture.sqlInsertArgs[i]...) + } + if err != nil { + return err + } + return nil +} + +func (f *fixturesLoaderInternal) Load() error { + tx, err := f.db.Begin() + if err != nil { + return err + } + defer func() { _ = tx.Rollback() }() + + for _, file := range f.files { + if err := f.loadFixtures(tx, file); err != nil { + return fmt.Errorf("failed to load fixtures from %s: %w", file, err) + } + } + return tx.Commit() +} + +func FixturesFileFullPaths(dir string, files []string) ([]string, error) { + if files != nil && len(files) == 0 { + return nil, nil // load nothing + } + files = slices.Clone(files) + if len(files) == 0 { + entries, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + for _, e := range entries { + files = append(files, e.Name()) + } + } + for i, file := range files { + if !filepath.IsAbs(file) { + files[i] = filepath.Join(dir, file) + } + } + return files, nil +} + +func NewFixturesLoader(x *xorm.Engine, opts FixturesOptions) (FixturesLoader, error) { + files, err := FixturesFileFullPaths(opts.Dir, opts.Files) + if err != nil { + return nil, fmt.Errorf("failed to get fixtures files: %w", err) + } + f := &fixturesLoaderInternal{db: x.DB().DB, dbType: x.Dialect().URI().DBType, files: files, fixtures: map[string]*fixtureItem{}} + switch f.dbType { + case schemas.SQLITE: + f.quoteObject = func(s string) string { return fmt.Sprintf(`"%s"`, s) } + f.paramPlaceholder = func(idx int) string { return "?" } + case schemas.POSTGRES: + f.quoteObject = func(s string) string { return fmt.Sprintf(`"%s"`, s) } + f.paramPlaceholder = func(idx int) string { return fmt.Sprintf(`$%d`, idx) } + case schemas.MYSQL: + f.quoteObject = func(s string) string { return fmt.Sprintf("`%s`", s) } + f.paramPlaceholder = func(idx int) string { return "?" } + case schemas.MSSQL: + f.quoteObject = func(s string) string { return fmt.Sprintf("[%s]", s) } + f.paramPlaceholder = func(idx int) string { return "?" } + } + return f, nil +} diff --git a/models/unittest/fixtures_test.go b/models/unittest/fixtures_test.go new file mode 100644 index 0000000000..a4c55f4e55 --- /dev/null +++ b/models/unittest/fixtures_test.go @@ -0,0 +1,114 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package unittest_test + +import ( + "path/filepath" + "testing" + + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/require" + "xorm.io/xorm" +) + +var NewFixturesLoaderVendor = func(e *xorm.Engine, opts unittest.FixturesOptions) (unittest.FixturesLoader, error) { + return nil, nil +} + +/* +// the old code is kept here in case we are still interested in benchmarking the two implementations +func init() { + NewFixturesLoaderVendor = func(e *xorm.Engine, opts unittest.FixturesOptions) (unittest.FixturesLoader, error) { + return NewFixturesLoaderVendorGoTestfixtures(e, opts) + } +} + +func NewFixturesLoaderVendorGoTestfixtures(e *xorm.Engine, opts unittest.FixturesOptions) (*testfixtures.Loader, error) { + files, err := unittest.FixturesFileFullPaths(opts.Dir, opts.Files) + if err != nil { + return nil, fmt.Errorf("failed to get fixtures files: %w", err) + } + var dialect string + switch e.Dialect().URI().DBType { + case schemas.POSTGRES: + dialect = "postgres" + case schemas.MYSQL: + dialect = "mysql" + case schemas.MSSQL: + dialect = "mssql" + case schemas.SQLITE: + dialect = "sqlite3" + default: + return nil, fmt.Errorf("unsupported RDBMS for integration tests: %q", e.Dialect().URI().DBType) + } + loaderOptions := []func(loader *testfixtures.Loader) error{ + testfixtures.Database(e.DB().DB), + testfixtures.Dialect(dialect), + testfixtures.DangerousSkipTestDatabaseCheck(), + testfixtures.Files(files...), + } + if e.Dialect().URI().DBType == schemas.POSTGRES { + loaderOptions = append(loaderOptions, testfixtures.SkipResetSequences()) + } + return testfixtures.New(loaderOptions...) +} +*/ + +func prepareTestFixturesLoaders(t testing.TB) unittest.FixturesOptions { + _ = user_model.User{} + opts := unittest.FixturesOptions{Dir: filepath.Join(test.SetupGiteaRoot(), "models", "fixtures"), Files: []string{ + "user.yml", + }} + require.NoError(t, unittest.CreateTestEngine(opts)) + return opts +} + +func TestFixturesLoader(t *testing.T) { + opts := prepareTestFixturesLoaders(t) + loaderInternal, err := unittest.NewFixturesLoader(unittest.GetXORMEngine(), opts) + require.NoError(t, err) + loaderVendor, err := NewFixturesLoaderVendor(unittest.GetXORMEngine(), opts) + require.NoError(t, err) + t.Run("Internal", func(t *testing.T) { + require.NoError(t, loaderInternal.Load()) + require.NoError(t, loaderInternal.Load()) + }) + t.Run("Vendor", func(t *testing.T) { + if loaderVendor == nil { + t.Skip() + } + require.NoError(t, loaderVendor.Load()) + require.NoError(t, loaderVendor.Load()) + }) +} + +func BenchmarkFixturesLoader(b *testing.B) { + opts := prepareTestFixturesLoaders(b) + require.NoError(b, unittest.CreateTestEngine(opts)) + loaderInternal, err := unittest.NewFixturesLoader(unittest.GetXORMEngine(), opts) + require.NoError(b, err) + loaderVendor, err := NewFixturesLoaderVendor(unittest.GetXORMEngine(), opts) + require.NoError(b, err) + + // BenchmarkFixturesLoader/Vendor + // BenchmarkFixturesLoader/Vendor-12 1696 719416 ns/op + // BenchmarkFixturesLoader/Internal + // BenchmarkFixturesLoader/Internal-12 1746 670457 ns/op + b.Run("Internal", func(b *testing.B) { + for i := 0; i < b.N; i++ { + require.NoError(b, loaderInternal.Load()) + } + }) + b.Run("Vendor", func(b *testing.B) { + if loaderVendor == nil { + b.Skip() + } + for i := 0; i < b.N; i++ { + require.NoError(b, loaderVendor.Load()) + } + }) +} diff --git a/models/unittest/fscopy.go b/models/unittest/fscopy.go index 4d7ee2151d..690089bbc5 100644 --- a/models/unittest/fscopy.go +++ b/models/unittest/fscopy.go @@ -67,7 +67,7 @@ func SyncDirs(srcPath, destPath string) error { } // find and delete all untracked files - destFiles, err := util.StatDir(destPath, true) + destFiles, err := util.ListDirRecursively(destPath, &util.ListDirOptions{IncludeDir: true}) if err != nil { return err } @@ -86,13 +86,13 @@ func SyncDirs(srcPath, destPath string) error { } // sync src files to dest - srcFiles, err := util.StatDir(srcPath, true) + srcFiles, err := util.ListDirRecursively(srcPath, &util.ListDirOptions{IncludeDir: true}) if err != nil { return err } for _, srcFile := range srcFiles { destFilePath := filepath.Join(destPath, srcFile) - // util.StatDir appends a slash to the directory name + // util.ListDirRecursively appends a slash to the directory name if strings.HasSuffix(srcFile, "/") { err = os.MkdirAll(destFilePath, os.ModePerm) } else { diff --git a/models/unittest/reflection.go b/models/unittest/reflection.go index 141fc66b99..bc96a05973 100644 --- a/models/unittest/reflection.go +++ b/models/unittest/reflection.go @@ -4,7 +4,7 @@ package unittest import ( - "log" + "fmt" "reflect" ) @@ -14,7 +14,7 @@ func fieldByName(v reflect.Value, field string) reflect.Value { } f := v.FieldByName(field) if !f.IsValid() { - log.Panicf("can not read %s for %v", field, v) + panic(fmt.Errorf("can not read %s for %v", field, v)) } return f } diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go index 12f3c25676..0dcff166cc 100644 --- a/models/unittest/testdb.go +++ b/models/unittest/testdb.go @@ -28,16 +28,7 @@ import ( "xorm.io/xorm/names" ) -// giteaRoot a path to the gitea root -var ( - giteaRoot string - fixturesDir string -) - -// FixturesDir returns the fixture directory -func FixturesDir() string { - return fixturesDir -} +var giteaRoot string func fatalTestError(fmtStr string, args ...any) { _, _ = fmt.Fprintf(os.Stderr, fmtStr, args...) @@ -68,6 +59,7 @@ func InitSettings() { _ = hash.Register("dummy", hash.NewDummyHasher) setting.PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy") + setting.InitGiteaEnvVarsForTesting() } // TestOptions represents test options @@ -79,39 +71,14 @@ type TestOptions struct { // MainTest a reusable TestMain(..) function for unit tests that need to use a // test database. Creates the test database, and sets necessary settings. -func MainTest(m *testing.M, testOpts ...*TestOptions) { - searchDir, _ := os.Getwd() - for searchDir != "" { - if _, err := os.Stat(filepath.Join(searchDir, "go.mod")); err == nil { - break // The "go.mod" should be the one for Gitea repository - } - if dir := filepath.Dir(searchDir); dir == searchDir { - searchDir = "" // reaches the root of filesystem - } else { - searchDir = dir - } - } - if searchDir == "" { - panic("The tests should run in a Gitea repository, there should be a 'go.mod' in the root") - } - - giteaRoot = searchDir +func MainTest(m *testing.M, testOptsArg ...*TestOptions) { + testOpts := util.OptionalArg(testOptsArg, &TestOptions{}) + giteaRoot = test.SetupGiteaRoot() setting.CustomPath = filepath.Join(giteaRoot, "custom") InitSettings() - fixturesDir = filepath.Join(giteaRoot, "models", "fixtures") - var opts FixturesOptions - if len(testOpts) == 0 || len(testOpts[0].FixtureFiles) == 0 { - opts.Dir = fixturesDir - } else { - for _, f := range testOpts[0].FixtureFiles { - if len(f) != 0 { - opts.Files = append(opts.Files, filepath.Join(fixturesDir, f)) - } - } - } - - if err := CreateTestEngine(opts); err != nil { + fixturesOpts := FixturesOptions{Dir: filepath.Join(giteaRoot, "models", "fixtures"), Files: testOpts.FixtureFiles} + if err := CreateTestEngine(fixturesOpts); err != nil { fatalTestError("Error creating test engine: %v\n", err) } @@ -172,16 +139,16 @@ func MainTest(m *testing.M, testOpts ...*TestOptions) { fatalTestError("git.Init: %v\n", err) } - if len(testOpts) > 0 && testOpts[0].SetUp != nil { - if err := testOpts[0].SetUp(); err != nil { + if testOpts.SetUp != nil { + if err := testOpts.SetUp(); err != nil { fatalTestError("set up failed: %v\n", err) } } exitStatus := m.Run() - if len(testOpts) > 0 && testOpts[0].TearDown != nil { - if err := testOpts[0].TearDown(); err != nil { + if testOpts.TearDown != nil { + if err := testOpts.TearDown(); err != nil { fatalTestError("tear down failed: %v\n", err) } } diff --git a/models/unittest/unit_tests.go b/models/unittest/unit_tests.go index 4ac858e04e..1c5595aef8 100644 --- a/models/unittest/unit_tests.go +++ b/models/unittest/unit_tests.go @@ -4,13 +4,17 @@ package unittest import ( + "fmt" "math" + "os" + "strings" "code.gitea.io/gitea/models/db" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "xorm.io/builder" + "xorm.io/xorm" ) // Code in this file is mainly used by unittest.CheckConsistencyFor, which is not in the unit test for various reasons. @@ -51,22 +55,23 @@ func whereOrderConditions(e db.Engine, conditions []any) db.Engine { return e.OrderBy(orderBy) } -// LoadBeanIfExists loads beans from fixture database if exist -func LoadBeanIfExists(bean any, conditions ...any) (bool, error) { +func getBeanIfExists(bean any, conditions ...any) (bool, error) { e := db.GetEngine(db.DefaultContext) return whereOrderConditions(e, conditions).Get(bean) } -// BeanExists for testing, check if a bean exists -func BeanExists(t assert.TestingT, bean any, conditions ...any) bool { - exists, err := LoadBeanIfExists(bean, conditions...) - assert.NoError(t, err) - return exists +func GetBean[T any](t require.TestingT, bean T, conditions ...any) (ret T) { + exists, err := getBeanIfExists(bean, conditions...) + require.NoError(t, err) + if exists { + return bean + } + return ret } // AssertExistsAndLoadBean assert that a bean exists and load it from the test database func AssertExistsAndLoadBean[T any](t require.TestingT, bean T, conditions ...any) T { - exists, err := LoadBeanIfExists(bean, conditions...) + exists, err := getBeanIfExists(bean, conditions...) require.NoError(t, err) require.True(t, exists, "Expected to find %+v (of type %T, with conditions %+v), but did not", @@ -112,25 +117,11 @@ func GetCount(t assert.TestingT, bean any, conditions ...any) int { // AssertNotExistsBean assert that a bean does not exist in the test database func AssertNotExistsBean(t assert.TestingT, bean any, conditions ...any) { - exists, err := LoadBeanIfExists(bean, conditions...) + exists, err := getBeanIfExists(bean, conditions...) assert.NoError(t, err) assert.False(t, exists) } -// AssertExistsIf asserts that a bean exists or does not exist, depending on -// what is expected. -func AssertExistsIf(t assert.TestingT, expected bool, bean any, conditions ...any) { - exists, err := LoadBeanIfExists(bean, conditions...) - assert.NoError(t, err) - assert.Equal(t, expected, exists) -} - -// AssertSuccessfulInsert assert that beans is successfully inserted -func AssertSuccessfulInsert(t assert.TestingT, beans ...any) { - err := db.Insert(db.DefaultContext, beans...) - assert.NoError(t, err) -} - // AssertCount assert the count of a bean func AssertCount(t assert.TestingT, bean, expected any) bool { return assert.EqualValues(t, expected, GetCount(t, bean)) @@ -155,3 +146,39 @@ func AssertCountByCond(t assert.TestingT, tableName string, cond builder.Cond, e return assert.EqualValues(t, expected, GetCountByCond(t, tableName, cond), "Failed consistency test, the counted bean (of table %s) was %+v", tableName, cond) } + +// DumpQueryResult dumps the result of a query for debugging purpose +func DumpQueryResult(t require.TestingT, sqlOrBean any, sqlArgs ...any) { + x := db.GetEngine(db.DefaultContext).(*xorm.Engine) + goDB := x.DB().DB + sql, ok := sqlOrBean.(string) + if !ok { + sql = fmt.Sprintf("SELECT * FROM %s", db.TableName(sqlOrBean)) + } else if !strings.Contains(sql, " ") { + sql = fmt.Sprintf("SELECT * FROM %s", sql) + } + rows, err := goDB.Query(sql, sqlArgs...) + require.NoError(t, err) + defer rows.Close() + columns, err := rows.Columns() + require.NoError(t, err) + + _, _ = fmt.Fprintf(os.Stdout, "====== DumpQueryResult: %s ======\n", sql) + idx := 0 + for rows.Next() { + row := make([]any, len(columns)) + rowPointers := make([]any, len(columns)) + for i := range row { + rowPointers[i] = &row[i] + } + require.NoError(t, rows.Scan(rowPointers...)) + _, _ = fmt.Fprintf(os.Stdout, "- # row[%d]\n", idx) + for i, col := range columns { + _, _ = fmt.Fprintf(os.Stdout, " %s: %v\n", col, row[i]) + } + idx++ + } + if idx == 0 { + _, _ = fmt.Fprintf(os.Stdout, "(no result, columns: %s)\n", strings.Join(columns, ", ")) + } +} diff --git a/models/user/email_address.go b/models/user/email_address.go index 5c04909ed7..74ba5f617a 100644 --- a/models/user/email_address.go +++ b/models/user/email_address.go @@ -357,8 +357,8 @@ func VerifyActiveEmailCode(ctx context.Context, code, email string) *EmailAddres if user := GetVerifyUser(ctx, code); user != nil { // time limit code prefix := code[:base.TimeLimitCodeLength] - data := fmt.Sprintf("%d%s%s%s%s", user.ID, email, user.LowerName, user.Passwd, user.Rands) - + opts := &TimeLimitCodeOptions{Purpose: TimeLimitCodeActivateEmail, NewEmail: email} + data := makeTimeLimitCodeHashData(opts, user) if base.VerifyTimeLimitCode(time.Now(), data, setting.Service.ActiveCodeLives, prefix) { emailAddress := &EmailAddress{UID: user.ID, Email: email} if has, _ := db.GetEngine(ctx).Get(emailAddress); has { @@ -486,10 +486,10 @@ func ActivateUserEmail(ctx context.Context, userID int64, email string, activate // Activate/deactivate a user's primary email address and account if addr.IsPrimary { - user, exist, err := db.Get[User](ctx, builder.Eq{"id": userID, "email": email}) + user, exist, err := db.Get[User](ctx, builder.Eq{"id": userID}) if err != nil { return err - } else if !exist { + } else if !exist || !strings.EqualFold(user.Email, email) { return fmt.Errorf("no user with ID: %d and Email: %s", userID, email) } diff --git a/models/user/search.go b/models/user/search.go index 6af3389237..85915f4020 100644 --- a/models/user/search.go +++ b/models/user/search.go @@ -39,8 +39,6 @@ type SearchUserOptions struct { IsTwoFactorEnabled optional.Option[bool] IsProhibitLogin optional.Option[bool] IncludeReserved bool - - ExtraParamStrings map[string]string } func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Session { diff --git a/models/user/user.go b/models/user/user.go index cf08d26498..19879fbcc7 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -181,7 +181,8 @@ func (u *User) BeforeUpdate() { u.MaxRepoCreation = -1 } - // Organization does not need email + // FIXME: this email doesn't need to be in lowercase, because the emails are mainly managed by the email table with lower_email field + // This trick could be removed in new releases to display the user inputed email as-is. u.Email = strings.ToLower(u.Email) if !u.IsOrganization() { if len(u.AvatarEmail) == 0 { @@ -310,17 +311,6 @@ func (u *User) OrganisationLink() string { return setting.AppSubURL + "/org/" + url.PathEscape(u.Name) } -// GenerateEmailActivateCode generates an activate code based on user information and given e-mail. -func (u *User) GenerateEmailActivateCode(email string) string { - code := base.CreateTimeLimitCode( - fmt.Sprintf("%d%s%s%s%s", u.ID, email, u.LowerName, u.Passwd, u.Rands), - setting.Service.ActiveCodeLives, time.Now(), nil) - - // Add tail hex username - code += hex.EncodeToString([]byte(u.LowerName)) - return code -} - // GetUserFollowers returns range of user's followers. func GetUserFollowers(ctx context.Context, u, viewer *User, listOptions db.ListOptions) ([]*User, int64, error) { sess := db.GetEngine(ctx). @@ -863,12 +853,38 @@ func GetVerifyUser(ctx context.Context, code string) (user *User) { return nil } -// VerifyUserActiveCode verifies active code when active account -func VerifyUserActiveCode(ctx context.Context, code string) (user *User) { +type TimeLimitCodePurpose string + +const ( + TimeLimitCodeActivateAccount TimeLimitCodePurpose = "activate_account" + TimeLimitCodeActivateEmail TimeLimitCodePurpose = "activate_email" + TimeLimitCodeResetPassword TimeLimitCodePurpose = "reset_password" +) + +type TimeLimitCodeOptions struct { + Purpose TimeLimitCodePurpose + NewEmail string +} + +func makeTimeLimitCodeHashData(opts *TimeLimitCodeOptions, u *User) string { + return fmt.Sprintf("%s|%d|%s|%s|%s|%s", opts.Purpose, u.ID, strings.ToLower(util.IfZero(opts.NewEmail, u.Email)), u.LowerName, u.Passwd, u.Rands) +} + +// GenerateUserTimeLimitCode generates a time-limit code based on user information and given e-mail. +// TODO: need to use cache or db to store it to make sure a code can only be consumed once +func GenerateUserTimeLimitCode(opts *TimeLimitCodeOptions, u *User) string { + data := makeTimeLimitCodeHashData(opts, u) + code := base.CreateTimeLimitCode(data, setting.Service.ActiveCodeLives, time.Now(), nil) + code += hex.EncodeToString([]byte(u.LowerName)) // Add tail hex username + return code +} + +// VerifyUserTimeLimitCode verifies the time-limit code +func VerifyUserTimeLimitCode(ctx context.Context, opts *TimeLimitCodeOptions, code string) (user *User) { if user = GetVerifyUser(ctx, code); user != nil { // time limit code prefix := code[:base.TimeLimitCodeLength] - data := fmt.Sprintf("%d%s%s%s%s", user.ID, user.Email, user.LowerName, user.Passwd, user.Rands) + data := makeTimeLimitCodeHashData(opts, user) if base.VerifyTimeLimitCode(time.Now(), data, setting.Service.ActiveCodeLives, prefix) { return user } diff --git a/modules/assetfs/layered.go b/modules/assetfs/layered.go index 9678d23ad6..4f3811ba2b 100644 --- a/modules/assetfs/layered.go +++ b/modules/assetfs/layered.go @@ -103,7 +103,7 @@ func (l *LayeredFS) ReadLayeredFile(elems ...string) ([]byte, string, error) { } func shouldInclude(info fs.FileInfo, fileMode ...bool) bool { - if util.CommonSkip(info.Name()) { + if util.IsCommonHiddenFileName(info.Name()) { return false } if len(fileMode) == 0 { diff --git a/modules/git/batch_reader.go b/modules/git/batch_reader.go index 532dbad989..33e54fe75c 100644 --- a/modules/git/batch_reader.go +++ b/modules/git/batch_reader.go @@ -242,7 +242,7 @@ func BinToHex(objectFormat ObjectFormat, sha, out []byte) []byte { return out } -// ParseTreeLine reads an entry from a tree in a cat-file --batch stream +// ParseCatFileTreeLine reads an entry from a tree in a cat-file --batch stream // This carefully avoids allocations - except where fnameBuf is too small. // It is recommended therefore to pass in an fnameBuf large enough to avoid almost all allocations // @@ -250,7 +250,7 @@ func BinToHex(objectFormat ObjectFormat, sha, out []byte) []byte { // SP NUL // // We don't attempt to convert the raw HASH to save a lot of time -func ParseTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBuf, shaBuf []byte) (mode, fname, sha []byte, n int, err error) { +func ParseCatFileTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBuf, shaBuf []byte) (mode, fname, sha []byte, n int, err error) { var readBytes []byte // Read the Mode & fname @@ -260,7 +260,7 @@ func ParseTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBu } idx := bytes.IndexByte(readBytes, ' ') if idx < 0 { - log.Debug("missing space in readBytes ParseTreeLine: %s", readBytes) + log.Debug("missing space in readBytes ParseCatFileTreeLine: %s", readBytes) return mode, fname, sha, n, &ErrNotExist{} } diff --git a/modules/git/command.go b/modules/git/command.go index b231c3beea..2584e3cc57 100644 --- a/modules/git/command.go +++ b/modules/git/command.go @@ -236,10 +236,16 @@ type RunOpts struct { } func commonBaseEnvs() []string { - // at the moment, do not set "GIT_CONFIG_NOSYSTEM", users may have put some configs like "receive.certNonceSeed" in it envs := []string{ - "HOME=" + HomeDir(), // make Gitea use internal git config only, to prevent conflicts with user's git config - "GIT_NO_REPLACE_OBJECTS=1", // ignore replace references (https://git-scm.com/docs/git-replace) + // Make Gitea use internal git config only, to prevent conflicts with user's git config + // It's better to use GIT_CONFIG_GLOBAL, but it requires git >= 2.32, so we still use HOME at the moment. + "HOME=" + HomeDir(), + // Avoid using system git config, it would cause problems (eg: use macOS osxkeychain to show a modal dialog, auto installing lfs hooks) + // This might be a breaking change in 1.24, because some users said that they have put some configs like "receive.certNonceSeed" in "/etc/gitconfig" + // For these users, they need to migrate the necessary configs to Gitea's git config file manually. + "GIT_CONFIG_NOSYSTEM=1", + // Ignore replace references (https://git-scm.com/docs/git-replace) + "GIT_NO_REPLACE_OBJECTS=1", } // some environment variables should be passed to git command diff --git a/modules/git/parse.go b/modules/git/parse.go new file mode 100644 index 0000000000..eb26632cc0 --- /dev/null +++ b/modules/git/parse.go @@ -0,0 +1,78 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "bytes" + "fmt" + "strconv" + "strings" + + "code.gitea.io/gitea/modules/optional" +) + +var sepSpace = []byte{' '} + +type LsTreeEntry struct { + ID ObjectID + EntryMode EntryMode + Name string + Size optional.Option[int64] +} + +func parseLsTreeLine(line []byte) (*LsTreeEntry, error) { + // expect line to be of the form: + // \t + // \t + + var err error + posTab := bytes.IndexByte(line, '\t') + if posTab == -1 { + return nil, fmt.Errorf("invalid ls-tree output (no tab): %q", line) + } + + entry := new(LsTreeEntry) + + entryAttrs := line[:posTab] + entryName := line[posTab+1:] + + entryMode, entryAttrs, _ := bytes.Cut(entryAttrs, sepSpace) + _ /* entryType */, entryAttrs, _ = bytes.Cut(entryAttrs, sepSpace) // the type is not used, the mode is enough to determine the type + entryObjectID, entryAttrs, _ := bytes.Cut(entryAttrs, sepSpace) + if len(entryAttrs) > 0 { + entrySize := entryAttrs // the last field is the space-padded-size + size, _ := strconv.ParseInt(strings.TrimSpace(string(entrySize)), 10, 64) + entry.Size = optional.Some(size) + } + + switch string(entryMode) { + case "100644": + entry.EntryMode = EntryModeBlob + case "100755": + entry.EntryMode = EntryModeExec + case "120000": + entry.EntryMode = EntryModeSymlink + case "160000": + entry.EntryMode = EntryModeCommit + case "040000", "040755": // git uses 040000 for tree object, but some users may get 040755 for unknown reasons + entry.EntryMode = EntryModeTree + default: + return nil, fmt.Errorf("unknown type: %v", string(entryMode)) + } + + entry.ID, err = NewIDFromString(string(entryObjectID)) + if err != nil { + return nil, fmt.Errorf("invalid ls-tree output (invalid object id): %q, err: %w", line, err) + } + + if len(entryName) > 0 && entryName[0] == '"' { + entry.Name, err = strconv.Unquote(string(entryName)) + if err != nil { + return nil, fmt.Errorf("invalid ls-tree output (invalid name): %q, err: %w", line, err) + } + } else { + entry.Name = string(entryName) + } + return entry, nil +} diff --git a/modules/git/parse_nogogit.go b/modules/git/parse_nogogit.go index 546b38be37..676bb3c76c 100644 --- a/modules/git/parse_nogogit.go +++ b/modules/git/parse_nogogit.go @@ -10,8 +10,6 @@ import ( "bytes" "fmt" "io" - "strconv" - "strings" "code.gitea.io/gitea/modules/log" ) @@ -21,71 +19,30 @@ func ParseTreeEntries(data []byte) ([]*TreeEntry, error) { return parseTreeEntries(data, nil) } -var sepSpace = []byte{' '} - +// parseTreeEntries FIXME this function's design is not right, it should make the caller read all data into memory func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) { - var err error entries := make([]*TreeEntry, 0, bytes.Count(data, []byte{'\n'})+1) for pos := 0; pos < len(data); { - // expect line to be of the form: - // \t - // \t posEnd := bytes.IndexByte(data[pos:], '\n') if posEnd == -1 { posEnd = len(data) } else { posEnd += pos } + line := data[pos:posEnd] - posTab := bytes.IndexByte(line, '\t') - if posTab == -1 { - return nil, fmt.Errorf("invalid ls-tree output (no tab): %q", line) - } - - entry := new(TreeEntry) - entry.ptree = ptree - - entryAttrs := line[:posTab] - entryName := line[posTab+1:] - - entryMode, entryAttrs, _ := bytes.Cut(entryAttrs, sepSpace) - _ /* entryType */, entryAttrs, _ = bytes.Cut(entryAttrs, sepSpace) // the type is not used, the mode is enough to determine the type - entryObjectID, entryAttrs, _ := bytes.Cut(entryAttrs, sepSpace) - if len(entryAttrs) > 0 { - entrySize := entryAttrs // the last field is the space-padded-size - entry.size, _ = strconv.ParseInt(strings.TrimSpace(string(entrySize)), 10, 64) - entry.sized = true - } - - switch string(entryMode) { - case "100644": - entry.entryMode = EntryModeBlob - case "100755": - entry.entryMode = EntryModeExec - case "120000": - entry.entryMode = EntryModeSymlink - case "160000": - entry.entryMode = EntryModeCommit - case "040000", "040755": // git uses 040000 for tree object, but some users may get 040755 for unknown reasons - entry.entryMode = EntryModeTree - default: - return nil, fmt.Errorf("unknown type: %v", string(entryMode)) - } - - entry.ID, err = NewIDFromString(string(entryObjectID)) + lsTreeLine, err := parseLsTreeLine(line) if err != nil { - return nil, fmt.Errorf("invalid ls-tree output (invalid object id): %q, err: %w", line, err) + return nil, err } - - if len(entryName) > 0 && entryName[0] == '"' { - entry.name, err = strconv.Unquote(string(entryName)) - if err != nil { - return nil, fmt.Errorf("invalid ls-tree output (invalid name): %q, err: %w", line, err) - } - } else { - entry.name = string(entryName) + entry := &TreeEntry{ + ptree: ptree, + ID: lsTreeLine.ID, + entryMode: lsTreeLine.EntryMode, + name: lsTreeLine.Name, + size: lsTreeLine.Size.Value(), + sized: lsTreeLine.Size.Has(), } - pos = posEnd + 1 entries = append(entries, entry) } @@ -100,7 +57,7 @@ func catBatchParseTreeEntries(objectFormat ObjectFormat, ptree *Tree, rd *bufio. loop: for sz > 0 { - mode, fname, sha, count, err := ParseTreeLine(objectFormat, rd, modeBuf, fnameBuf, shaBuf) + mode, fname, sha, count, err := ParseCatFileTreeLine(objectFormat, rd, modeBuf, fnameBuf, shaBuf) if err != nil { if err == io.EOF { break loop diff --git a/modules/git/pipeline/lfs_nogogit.go b/modules/git/pipeline/lfs_nogogit.go index b22805c132..92e35c5a10 100644 --- a/modules/git/pipeline/lfs_nogogit.go +++ b/modules/git/pipeline/lfs_nogogit.go @@ -114,7 +114,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err case "tree": var n int64 for n < size { - mode, fname, binObjectID, count, err := git.ParseTreeLine(objectID.Type(), batchReader, modeBuf, fnameBuf, workingShaBuf) + mode, fname, binObjectID, count, err := git.ParseCatFileTreeLine(objectID.Type(), batchReader, modeBuf, fnameBuf, workingShaBuf) if err != nil { return nil, err } diff --git a/modules/git/repo_archive.go b/modules/git/repo_archive.go index 1bf1aa41b9..2b45a50f19 100644 --- a/modules/git/repo_archive.go +++ b/modules/git/repo_archive.go @@ -8,7 +8,6 @@ import ( "context" "fmt" "io" - "os" "path/filepath" "strings" ) @@ -63,15 +62,11 @@ func (repo *Repository) CreateArchive(ctx context.Context, format ArchiveType, t cmd.AddOptionFormat("--format=%s", format.String()) cmd.AddDynamicArguments(commitID) - // Avoid LFS hooks getting installed because of /etc/gitconfig, which can break pull requests. - env := append(os.Environ(), "GIT_CONFIG_NOSYSTEM=1") - var stderr strings.Builder err := cmd.Run(&RunOpts{ Dir: repo.Path, Stdout: target, Stderr: &stderr, - Env: env, }) if err != nil { return ConcatenateError(err, stderr.String()) diff --git a/modules/git/submodule.go b/modules/git/submodule.go new file mode 100644 index 0000000000..017b644052 --- /dev/null +++ b/modules/git/submodule.go @@ -0,0 +1,66 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "bufio" + "context" + "fmt" + "os" + + "code.gitea.io/gitea/modules/log" +) + +type TemplateSubmoduleCommit struct { + Path string + Commit string +} + +// GetTemplateSubmoduleCommits returns a list of submodules paths and their commits from a repository +// This function is only for generating new repos based on existing template, the template couldn't be too large. +func GetTemplateSubmoduleCommits(ctx context.Context, repoPath string) (submoduleCommits []TemplateSubmoduleCommit, _ error) { + stdoutReader, stdoutWriter, err := os.Pipe() + if err != nil { + return nil, err + } + opts := &RunOpts{ + Dir: repoPath, + Stdout: stdoutWriter, + PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error { + _ = stdoutWriter.Close() + defer stdoutReader.Close() + + scanner := bufio.NewScanner(stdoutReader) + for scanner.Scan() { + entry, err := parseLsTreeLine(scanner.Bytes()) + if err != nil { + cancel() + return err + } + if entry.EntryMode == EntryModeCommit { + submoduleCommits = append(submoduleCommits, TemplateSubmoduleCommit{Path: entry.Name, Commit: entry.ID.String()}) + } + } + return scanner.Err() + }, + } + err = NewCommand(ctx, "ls-tree", "-r", "--", "HEAD").Run(opts) + if err != nil { + return nil, fmt.Errorf("GetTemplateSubmoduleCommits: error running git ls-tree: %v", err) + } + return submoduleCommits, nil +} + +// AddTemplateSubmoduleIndexes Adds the given submodules to the git index. +// It is only for generating new repos based on existing template, requires the .gitmodules file to be already present in the work dir. +func AddTemplateSubmoduleIndexes(ctx context.Context, repoPath string, submodules []TemplateSubmoduleCommit) error { + for _, submodule := range submodules { + cmd := NewCommand(ctx, "update-index", "--add", "--cacheinfo", "160000").AddDynamicArguments(submodule.Commit, submodule.Path) + if stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath}); err != nil { + log.Error("Unable to add %s as submodule to repo %s: stdout %s\nError: %v", submodule.Path, repoPath, stdout, err) + return err + } + } + return nil +} diff --git a/modules/git/submodule_test.go b/modules/git/submodule_test.go new file mode 100644 index 0000000000..d53946a27d --- /dev/null +++ b/modules/git/submodule_test.go @@ -0,0 +1,48 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetTemplateSubmoduleCommits(t *testing.T) { + testRepoPath := filepath.Join(testReposDir, "repo4_submodules") + submodules, err := GetTemplateSubmoduleCommits(DefaultContext, testRepoPath) + require.NoError(t, err) + + assert.Len(t, submodules, 2) + + assert.EqualValues(t, "<°)))><", submodules[0].Path) + assert.EqualValues(t, "d2932de67963f23d43e1c7ecf20173e92ee6c43c", submodules[0].Commit) + + assert.EqualValues(t, "libtest", submodules[1].Path) + assert.EqualValues(t, "1234567890123456789012345678901234567890", submodules[1].Commit) +} + +func TestAddTemplateSubmoduleIndexes(t *testing.T) { + ctx := context.Background() + tmpDir := t.TempDir() + var err error + _, _, err = NewCommand(ctx, "init").RunStdString(&RunOpts{Dir: tmpDir}) + require.NoError(t, err) + _ = os.Mkdir(filepath.Join(tmpDir, "new-dir"), 0o755) + err = AddTemplateSubmoduleIndexes(ctx, tmpDir, []TemplateSubmoduleCommit{{Path: "new-dir", Commit: "1234567890123456789012345678901234567890"}}) + require.NoError(t, err) + _, _, err = NewCommand(ctx, "add", "--all").RunStdString(&RunOpts{Dir: tmpDir}) + require.NoError(t, err) + _, _, err = NewCommand(ctx, "-c", "user.name=a", "-c", "user.email=b", "commit", "-m=test").RunStdString(&RunOpts{Dir: tmpDir}) + require.NoError(t, err) + submodules, err := GetTemplateSubmoduleCommits(DefaultContext, tmpDir) + require.NoError(t, err) + assert.Len(t, submodules, 1) + assert.EqualValues(t, "new-dir", submodules[0].Path) + assert.EqualValues(t, "1234567890123456789012345678901234567890", submodules[0].Commit) +} diff --git a/modules/git/tests/repos/repo4_submodules/HEAD b/modules/git/tests/repos/repo4_submodules/HEAD new file mode 100644 index 0000000000..cb089cd89a --- /dev/null +++ b/modules/git/tests/repos/repo4_submodules/HEAD @@ -0,0 +1 @@ +ref: refs/heads/master diff --git a/modules/git/tests/repos/repo4_submodules/config b/modules/git/tests/repos/repo4_submodules/config new file mode 100644 index 0000000000..07d359d07c --- /dev/null +++ b/modules/git/tests/repos/repo4_submodules/config @@ -0,0 +1,4 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = true diff --git a/modules/git/tests/repos/repo4_submodules/objects/97/c3d30df0e6492348292600920a6482feaebb74 b/modules/git/tests/repos/repo4_submodules/objects/97/c3d30df0e6492348292600920a6482feaebb74 new file mode 100644 index 0000000000..7596090b49 Binary files /dev/null and b/modules/git/tests/repos/repo4_submodules/objects/97/c3d30df0e6492348292600920a6482feaebb74 differ diff --git a/modules/git/tests/repos/repo4_submodules/objects/c7/e064ed49b44523cba8a5dfbc37d2ce1bb41d34 b/modules/git/tests/repos/repo4_submodules/objects/c7/e064ed49b44523cba8a5dfbc37d2ce1bb41d34 new file mode 100644 index 0000000000..e3a13c156d Binary files /dev/null and b/modules/git/tests/repos/repo4_submodules/objects/c7/e064ed49b44523cba8a5dfbc37d2ce1bb41d34 differ diff --git a/modules/git/tests/repos/repo4_submodules/objects/e1/e59caba97193d48862d6809912043871f37437 b/modules/git/tests/repos/repo4_submodules/objects/e1/e59caba97193d48862d6809912043871f37437 new file mode 100644 index 0000000000..a8d6e5c17c --- /dev/null +++ b/modules/git/tests/repos/repo4_submodules/objects/e1/e59caba97193d48862d6809912043871f37437 @@ -0,0 +1,2 @@ +x[ +Â0EýÎ*æ_é$MÑ5tifBk IÅ•¹7æk~ÞÃ9ܘ—åÜ ü¦ð.jÖÈ ÅOÚ äÉ"zÂ`ß#IirF…µÍ¹ÀØ$%¹Âçò|4)°¯?t¼É=”Ë:K¦ï­#[$D¿¯û¿^˜…¡®Ó’y½HU/f?G \ No newline at end of file diff --git a/modules/git/tests/repos/repo4_submodules/refs/heads/master b/modules/git/tests/repos/repo4_submodules/refs/heads/master new file mode 100644 index 0000000000..102bc34da8 --- /dev/null +++ b/modules/git/tests/repos/repo4_submodules/refs/heads/master @@ -0,0 +1 @@ +e1e59caba97193d48862d6809912043871f37437 diff --git a/modules/git/tree.go b/modules/git/tree.go index 1da4a9fa5d..5a644f6c87 100644 --- a/modules/git/tree.go +++ b/modules/git/tree.go @@ -17,7 +17,7 @@ func NewTree(repo *Repository, id ObjectID) *Tree { } } -// SubTree get a sub tree by the sub dir path +// SubTree get a subtree by the sub dir path func (t *Tree) SubTree(rpath string) (*Tree, error) { if len(rpath) == 0 { return t, nil @@ -62,3 +62,14 @@ func (repo *Repository) LsTree(ref string, filenames ...string) ([]string, error return filelist, err } + +// GetTreePathLatestCommit returns the latest commit of a tree path +func (repo *Repository) GetTreePathLatestCommit(refName, treePath string) (*Commit, error) { + stdout, _, err := NewCommand(repo.Ctx, "rev-list", "-1"). + AddDynamicArguments(refName).AddDashesAndList(treePath). + RunStdString(&RunOpts{Dir: repo.Path}) + if err != nil { + return nil, err + } + return repo.GetCommit(strings.TrimSpace(stdout)) +} diff --git a/modules/git/tree_blob_nogogit.go b/modules/git/tree_blob_nogogit.go index 92d3d107a7..b7bcf40edd 100644 --- a/modules/git/tree_blob_nogogit.go +++ b/modules/git/tree_blob_nogogit.go @@ -17,7 +17,6 @@ func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) { ptree: t, ID: t.ID, name: "", - fullName: "", entryMode: EntryModeTree, }, nil } diff --git a/modules/git/tree_entry_nogogit.go b/modules/git/tree_entry_nogogit.go index 1c3bcd197a..81fb638d56 100644 --- a/modules/git/tree_entry_nogogit.go +++ b/modules/git/tree_entry_nogogit.go @@ -9,23 +9,17 @@ import "code.gitea.io/gitea/modules/log" // TreeEntry the leaf in the git tree type TreeEntry struct { - ID ObjectID - + ID ObjectID ptree *Tree entryMode EntryMode name string - - size int64 - sized bool - fullName string + size int64 + sized bool } // Name returns the name of the entry func (te *TreeEntry) Name() string { - if te.fullName != "" { - return te.fullName - } return te.name } diff --git a/modules/git/tree_test.go b/modules/git/tree_test.go index 6d2b5c84d5..5fee64b038 100644 --- a/modules/git/tree_test.go +++ b/modules/git/tree_test.go @@ -25,3 +25,18 @@ func TestSubTree_Issue29101(t *testing.T) { assert.True(t, IsErrNotExist(err)) } } + +func Test_GetTreePathLatestCommit(t *testing.T) { + repo, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "repo6_blame")) + assert.NoError(t, err) + defer repo.Close() + + commitID, err := repo.GetBranchCommitID("master") + assert.NoError(t, err) + assert.EqualValues(t, "544d8f7a3b15927cddf2299b4b562d6ebd71b6a7", commitID) + + commit, err := repo.GetTreePathLatestCommit("master", "blame.txt") + assert.NoError(t, err) + assert.NotNil(t, commit) + assert.EqualValues(t, "45fb6cbc12f970b04eacd5cd4165edd11c8d7376", commit.ID.String()) +} diff --git a/modules/gitrepo/gitrepo.go b/modules/gitrepo/gitrepo.go index 831b9d7bb7..540b724489 100644 --- a/modules/gitrepo/gitrepo.go +++ b/modules/gitrepo/gitrepo.go @@ -43,19 +43,20 @@ type contextKey struct { } // RepositoryFromContextOrOpen attempts to get the repository from the context or just opens it +// The caller must call "defer gitRepo.Close()" func RepositoryFromContextOrOpen(ctx context.Context, repo Repository) (*git.Repository, io.Closer, error) { - ds := reqctx.GetRequestDataStore(ctx) - if ds != nil { - gitRepo, err := RepositoryFromRequestContextOrOpen(ctx, ds, repo) + reqCtx := reqctx.FromContext(ctx) + if reqCtx != nil { + gitRepo, err := RepositoryFromRequestContextOrOpen(reqCtx, repo) return gitRepo, util.NopCloser{}, err } gitRepo, err := OpenRepository(ctx, repo) return gitRepo, gitRepo, err } -// RepositoryFromRequestContextOrOpen opens the repository at the given relative path in the provided request context -// The repo will be automatically closed when the request context is done -func RepositoryFromRequestContextOrOpen(ctx context.Context, ds reqctx.RequestDataStore, repo Repository) (*git.Repository, error) { +// RepositoryFromRequestContextOrOpen opens the repository at the given relative path in the provided request context. +// Caller shouldn't close the git repo manually, the git repo will be automatically closed when the request context is done. +func RepositoryFromRequestContextOrOpen(ctx reqctx.RequestContext, repo Repository) (*git.Repository, error) { ck := contextKey{repoPath: repoPath(repo)} if gitRepo, ok := ctx.Value(ck).(*git.Repository); ok { return gitRepo, nil @@ -64,7 +65,7 @@ func RepositoryFromRequestContextOrOpen(ctx context.Context, ds reqctx.RequestDa if err != nil { return nil, err } - ds.AddCloser(gitRepo) - ds.SetContextValue(ck, gitRepo) + ctx.AddCloser(gitRepo) + ctx.SetContextValue(ck, gitRepo) return gitRepo, nil } diff --git a/modules/indexer/code/indexer.go b/modules/indexer/code/indexer.go index c1ab26569c..728b37fab6 100644 --- a/modules/indexer/code/indexer.go +++ b/modules/indexer/code/indexer.go @@ -123,13 +123,12 @@ func Init() { for _, indexerData := range items { log.Trace("IndexerData Process Repo: %d", indexerData.RepoID) if err := index(ctx, indexer, indexerData.RepoID); err != nil { - unhandled = append(unhandled, indexerData) if !setting.IsInTesting { log.Error("Codes indexer handler: index error for repo %v: %v", indexerData.RepoID, err) } } } - return unhandled + return nil // do not re-queue the failed items, otherwise some broken repo will block the queue } indexerQueue = queue.CreateUniqueQueue(ctx, "code_indexer", handler) diff --git a/modules/indexer/code/indexer_test.go b/modules/indexer/code/indexer_test.go index d04088531a..f358bbe785 100644 --- a/modules/indexer/code/indexer_test.go +++ b/modules/indexer/code/indexer_test.go @@ -15,6 +15,8 @@ import ( "code.gitea.io/gitea/modules/indexer/code/bleve" "code.gitea.io/gitea/modules/indexer/code/elasticsearch" "code.gitea.io/gitea/modules/indexer/code/internal" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" _ "code.gitea.io/gitea/models" _ "code.gitea.io/gitea/models/actions" @@ -279,7 +281,7 @@ func testIndexer(name string, t *testing.T, indexer internal.Indexer) { func TestBleveIndexAndSearch(t *testing.T) { unittest.PrepareTestEnv(t) - + defer test.MockVariableValue(&setting.Indexer.TypeBleveMaxFuzzniess, 2)() dir := t.TempDir() idx := bleve.NewIndexer(dir) diff --git a/modules/indexer/internal/bleve/util.go b/modules/indexer/internal/bleve/util.go index a0c3dc4ad4..b6daa9e14b 100644 --- a/modules/indexer/internal/bleve/util.go +++ b/modules/indexer/internal/bleve/util.go @@ -9,6 +9,7 @@ import ( "unicode" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "github.com/blevesearch/bleve/v2" @@ -54,9 +55,9 @@ func openIndexer(path string, latestVersion int) (bleve.Index, int, error) { return index, 0, nil } -// This method test the GuessFuzzinessByKeyword method. The fuzziness is based on the levenshtein distance and determines how many chars -// may be different on two string and they still be considered equivalent. -// Given a phrasse, its shortest word determines its fuzziness. If a phrase uses CJK (eg: `갃갃갃` `啊啊啊`), the fuzziness is zero. +// GuessFuzzinessByKeyword guesses fuzziness based on the levenshtein distance and determines how many chars +// may be different on two string, and they still be considered equivalent. +// Given a phrase, its shortest word determines its fuzziness. If a phrase uses CJK (eg: `갃갃갃` `啊啊啊`), the fuzziness is zero. func GuessFuzzinessByKeyword(s string) int { tokenizer := unicode_tokenizer.NewUnicodeTokenizer() tokens := tokenizer.Tokenize([]byte(s)) @@ -85,5 +86,5 @@ func guessFuzzinessByKeyword(s string) int { return 0 } } - return min(maxFuzziness, len(s)/4) + return min(min(setting.Indexer.TypeBleveMaxFuzzniess, maxFuzziness), len(s)/4) } diff --git a/modules/indexer/internal/bleve/util_test.go b/modules/indexer/internal/bleve/util_test.go index 8f7844464e..1a7e4db0f4 100644 --- a/modules/indexer/internal/bleve/util_test.go +++ b/modules/indexer/internal/bleve/util_test.go @@ -7,10 +7,15 @@ import ( "fmt" "testing" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + "github.com/stretchr/testify/assert" ) func TestBleveGuessFuzzinessByKeyword(t *testing.T) { + defer test.MockVariableValue(&setting.Indexer.TypeBleveMaxFuzzniess, 2)() + scenarios := []struct { Input string Fuzziness int // See util.go for the definition of fuzziness in this particular context @@ -46,7 +51,7 @@ func TestBleveGuessFuzzinessByKeyword(t *testing.T) { } for _, scenario := range scenarios { - t.Run(fmt.Sprintf("ensure fuzziness of '%s' is '%d'", scenario.Input, scenario.Fuzziness), func(t *testing.T) { + t.Run(fmt.Sprintf("Fuziniess:%s=%d", scenario.Input, scenario.Fuzziness), func(t *testing.T) { assert.Equal(t, scenario.Fuzziness, GuessFuzzinessByKeyword(scenario.Input)) }) } diff --git a/modules/markup/sanitizer_default.go b/modules/markup/sanitizer_default.go index 5eeafe940a..14161eb533 100644 --- a/modules/markup/sanitizer_default.go +++ b/modules/markup/sanitizer_default.go @@ -48,7 +48,7 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy { policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(unchecked|checked|indeterminate)$`)).OnElements("li") // Allow 'color' and 'background-color' properties for the style attribute on text elements. - policy.AllowStyles("color", "background-color").OnElements("span", "p") + policy.AllowStyles("color", "background-color").OnElements("div", "span", "p", "tr", "th", "td") policy.AllowAttrs("src", "autoplay", "controls").OnElements("video") diff --git a/modules/repository/init.go b/modules/repository/init.go index 5f500c5233..24602ae090 100644 --- a/modules/repository/init.go +++ b/modules/repository/init.go @@ -81,7 +81,7 @@ func LoadRepoConfig() error { if isDir, err := util.IsDir(customPath); err != nil { return fmt.Errorf("failed to check custom %s dir: %w", t, err) } else if isDir { - if typeFiles[i].custom, err = util.StatDir(customPath); err != nil { + if typeFiles[i].custom, err = util.ListDirRecursively(customPath, &util.ListDirOptions{SkipCommonHiddenNames: true}); err != nil { return fmt.Errorf("failed to list custom %s files: %w", t, err) } } diff --git a/modules/reqctx/datastore.go b/modules/reqctx/datastore.go index 66361a4587..94232450f3 100644 --- a/modules/reqctx/datastore.go +++ b/modules/reqctx/datastore.go @@ -88,6 +88,21 @@ func (r *requestDataStore) cleanUp() { } } +type RequestContext interface { + context.Context + RequestDataStore +} + +func FromContext(ctx context.Context) RequestContext { + // here we must use the current ctx and the underlying store + // the current ctx guarantees that the ctx deadline/cancellation/values are respected + // the underlying store guarantees that the request-specific data is available + if store := GetRequestDataStore(ctx); store != nil { + return &requestContext{Context: ctx, RequestDataStore: store} + } + return nil +} + func GetRequestDataStore(ctx context.Context) RequestDataStore { if req, ok := ctx.Value(RequestDataStoreKey).(*requestDataStore); ok { return req @@ -97,11 +112,11 @@ func GetRequestDataStore(ctx context.Context) RequestDataStore { type requestContext struct { context.Context - dataStore *requestDataStore + RequestDataStore } func (c *requestContext) Value(key any) any { - if v := c.dataStore.GetContextValue(key); v != nil { + if v := c.GetContextValue(key); v != nil { return v } return c.Context.Value(key) @@ -109,9 +124,10 @@ func (c *requestContext) Value(key any) any { func NewRequestContext(parentCtx context.Context, profDesc string) (_ context.Context, finished func()) { ctx, _, processFinished := process.GetManager().AddTypedContext(parentCtx, profDesc, process.RequestProcessType, true) - reqCtx := &requestContext{Context: ctx, dataStore: &requestDataStore{values: make(map[any]any)}} + store := &requestDataStore{values: make(map[any]any)} + reqCtx := &requestContext{Context: ctx, RequestDataStore: store} return reqCtx, func() { - reqCtx.dataStore.cleanUp() + store.cleanUp() processFinished() } } @@ -119,5 +135,5 @@ func NewRequestContext(parentCtx context.Context, profDesc string) (_ context.Co // NewRequestContextForTest creates a new RequestContext for testing purposes // It doesn't add the context to the process manager, nor do cleanup func NewRequestContextForTest(parentCtx context.Context) context.Context { - return &requestContext{Context: parentCtx, dataStore: &requestDataStore{values: make(map[any]any)}} + return &requestContext{Context: parentCtx, RequestDataStore: &requestDataStore{values: make(map[any]any)}} } diff --git a/modules/setting/config_env.go b/modules/setting/config_env.go index dfcb7db3c8..5d94a9641f 100644 --- a/modules/setting/config_env.go +++ b/modules/setting/config_env.go @@ -166,3 +166,25 @@ func EnvironmentToConfig(cfg ConfigProvider, envs []string) (changed bool) { } return changed } + +// InitGiteaEnvVars initializes the environment variables for gitea +func InitGiteaEnvVars() { + // Ideally Gitea should only accept the environment variables which it clearly knows instead of unsetting the ones it doesn't want, + // but the ideal behavior would be a breaking change, and it seems not bringing enough benefits to end users, + // so at the moment we could still keep "unsetting the unnecessary environments" + + // HOME is managed by Gitea, Gitea's git should use "HOME/.gitconfig". + // But git would try "XDG_CONFIG_HOME/git/config" first if "HOME/.gitconfig" does not exist, + // then our git.InitFull would still write to "XDG_CONFIG_HOME/git/config" if XDG_CONFIG_HOME is set. + _ = os.Unsetenv("XDG_CONFIG_HOME") +} + +func InitGiteaEnvVarsForTesting() { + InitGiteaEnvVars() + _ = os.Unsetenv("GIT_AUTHOR_NAME") + _ = os.Unsetenv("GIT_AUTHOR_EMAIL") + _ = os.Unsetenv("GIT_AUTHOR_DATE") + _ = os.Unsetenv("GIT_COMMITTER_NAME") + _ = os.Unsetenv("GIT_COMMITTER_EMAIL") + _ = os.Unsetenv("GIT_COMMITTER_DATE") +} diff --git a/modules/setting/cors.go b/modules/setting/cors.go index 63daaad60b..5260887d9d 100644 --- a/modules/setting/cors.go +++ b/modules/setting/cors.go @@ -5,8 +5,6 @@ package setting import ( "time" - - "code.gitea.io/gitea/modules/log" ) // CORSConfig defines CORS settings @@ -28,7 +26,4 @@ var CORSConfig = struct { func loadCorsFrom(rootCfg ConfigProvider) { mustMapSetting(rootCfg, "cors", &CORSConfig) - if CORSConfig.Enabled { - log.Info("CORS Service Enabled") - } } diff --git a/modules/setting/indexer.go b/modules/setting/indexer.go index 18585602c3..e34baae012 100644 --- a/modules/setting/indexer.go +++ b/modules/setting/indexer.go @@ -31,6 +31,8 @@ var Indexer = struct { IncludePatterns []*GlobMatcher ExcludePatterns []*GlobMatcher ExcludeVendored bool + + TypeBleveMaxFuzzniess int }{ IssueType: "bleve", IssuePath: "indexers/issues.bleve", @@ -88,6 +90,7 @@ func loadIndexerFrom(rootCfg ConfigProvider) { Indexer.ExcludeVendored = sec.Key("REPO_INDEXER_EXCLUDE_VENDORED").MustBool(true) Indexer.MaxIndexerFileSize = sec.Key("MAX_FILE_SIZE").MustInt64(1024 * 1024) Indexer.StartupTimeout = sec.Key("STARTUP_TIMEOUT").MustDuration(30 * time.Second) + Indexer.TypeBleveMaxFuzzniess = sec.Key("TYPE_BLEVE_MAX_FUZZINESS").MustInt(0) } // IndexerGlobFromString parses a comma separated list of patterns and returns a glob.Glob slice suited for repo indexing @@ -97,7 +100,7 @@ func IndexerGlobFromString(globstr string) []*GlobMatcher { expr = strings.TrimSpace(expr) if expr != "" { if g, err := GlobMatcherCompile(expr, '.', '/'); err != nil { - log.Info("Invalid glob expression '%s' (skipped): %v", expr, err) + log.Warn("Invalid glob expression '%s' (skipped): %v", expr, err) } else { extarr = append(extarr, g) } diff --git a/modules/setting/mailer.go b/modules/setting/mailer.go index d4db55dc7b..4c3dff6850 100644 --- a/modules/setting/mailer.go +++ b/modules/setting/mailer.go @@ -255,8 +255,6 @@ func loadMailerFrom(rootCfg ConfigProvider) { MailService.OverrideEnvelopeFrom = true MailService.EnvelopeFrom = parsed.Address } - - log.Info("Mail Service Enabled") } func loadRegisterMailFrom(rootCfg ConfigProvider) { @@ -267,7 +265,6 @@ func loadRegisterMailFrom(rootCfg ConfigProvider) { return } Service.RegisterEmailConfirm = true - log.Info("Register Mail Service Enabled") } func loadNotifyMailFrom(rootCfg ConfigProvider) { @@ -278,7 +275,6 @@ func loadNotifyMailFrom(rootCfg ConfigProvider) { return } Service.EnableNotifyMail = true - log.Info("Notify Mail Service Enabled") } func tryResolveAddr(addr string) []net.IPAddr { diff --git a/modules/setting/security.go b/modules/setting/security.go index 3d12fcf8d9..2f798b75c7 100644 --- a/modules/setting/security.go +++ b/modules/setting/security.go @@ -13,8 +13,9 @@ import ( "code.gitea.io/gitea/modules/log" ) +// Security settings + var ( - // Security settings InstallLock bool SecretKey string InternalToken string // internal access token @@ -27,7 +28,7 @@ var ( ReverseProxyTrustedProxies []string MinPasswordLength int ImportLocalPaths bool - DisableGitHooks bool + DisableGitHooks = true DisableWebhooks bool OnlyAllowPushIfGiteaEnvironmentSet bool PasswordComplexity []string diff --git a/modules/setting/session.go b/modules/setting/session.go index afe63bfdb7..19a05ce2c2 100644 --- a/modules/setting/session.go +++ b/modules/setting/session.go @@ -73,6 +73,4 @@ func loadSessionFrom(rootCfg ConfigProvider) { SessionConfig.ProviderConfig = string(shadowConfig) SessionConfig.OriginalProvider = SessionConfig.Provider SessionConfig.Provider = "VirtualSession" - - log.Info("Session Service Enabled") } diff --git a/modules/setting/time.go b/modules/setting/time.go index 39acba12ef..97988211a9 100644 --- a/modules/setting/time.go +++ b/modules/setting/time.go @@ -20,7 +20,6 @@ func loadTimeFrom(rootCfg ConfigProvider) { if err != nil { log.Fatal("Load time zone failed: %v", err) } - log.Info("Default UI Location is %v", zone) } if DefaultUILocation == nil { DefaultUILocation = time.Local diff --git a/modules/system/appstate_test.go b/modules/system/appstate_test.go index d4b9e167c2..911319d00a 100644 --- a/modules/system/appstate_test.go +++ b/modules/system/appstate_test.go @@ -13,9 +13,7 @@ import ( ) func TestMain(m *testing.M) { - unittest.MainTest(m, &unittest.TestOptions{ - FixtureFiles: []string{""}, // load nothing - }) + unittest.MainTest(m, &unittest.TestOptions{FixtureFiles: []string{ /* load nothing */ }}) } type testItem1 struct { @@ -36,8 +34,6 @@ func (*testItem2) Name() string { } func TestAppStateDB(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - as := &DBStore{} item1 := new(testItem1) diff --git a/modules/templates/helper.go b/modules/templates/helper.go index b673450f5a..48d3a8ff89 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -131,15 +131,9 @@ func NewFuncMap() template.FuncMap { "EnableTimetracking": func() bool { return setting.Service.EnableTimetracking }, - "DisableGitHooks": func() bool { - return setting.DisableGitHooks - }, "DisableWebhooks": func() bool { return setting.DisableWebhooks }, - "DisableImportLocal": func() bool { - return !setting.ImportLocalPaths - }, "UserThemeName": userThemeName, "NotificationSettings": func() map[string]any { return map[string]any{ @@ -264,22 +258,42 @@ func userThemeName(user *user_model.User) string { return setting.UI.DefaultTheme } +func isQueryParamEmpty(v any) bool { + return v == nil || v == false || v == 0 || v == int64(0) || v == "" +} + // QueryBuild builds a query string from a list of key-value pairs. -// It omits the nil and empty strings, but it doesn't omit other zero values, -// because the zero value of number types may have a meaning. +// It omits the nil, false, zero int/int64 and empty string values, +// because they are default empty values for "ctx.FormXxx" calls. +// If 0 or false need to be included, use string values: "0" and "false". +// Build rules: +// * Even parameters: always build as query string: a=b&c=d +// * Odd parameters: +// * * {"/anything", param-pairs...} => "/?param-paris" +// * * {"anything?old-params", new-param-pairs...} => "anything?old-params&new-param-paris" +// * * Otherwise: {"old¶ms", new-param-pairs...} => "old¶ms&new-param-paris" +// * * Other behaviors are undefined yet. func QueryBuild(a ...any) template.URL { - var s string + var reqPath, s string + hasTrailingSep := false if len(a)%2 == 1 { if v, ok := a[0].(string); ok { - if v == "" || (v[0] != '?' && v[0] != '&') { - panic("QueryBuild: invalid argument") - } s = v } else if v, ok := a[0].(template.URL); ok { s = string(v) } else { panic("QueryBuild: invalid argument") } + hasTrailingSep = s != "&" && strings.HasSuffix(s, "&") + if strings.HasPrefix(s, "/") || strings.Contains(s, "?") { + if s1, s2, ok := strings.Cut(s, "?"); ok { + reqPath = s1 + "?" + s = s2 + } else { + reqPath += s + "?" + s = "" + } + } } for i := len(a) % 2; i < len(a); i += 2 { k, ok := a[i].(string) @@ -290,19 +304,16 @@ func QueryBuild(a ...any) template.URL { if va, ok := a[i+1].(string); ok { v = va } else if a[i+1] != nil { - v = fmt.Sprint(a[i+1]) + if !isQueryParamEmpty(a[i+1]) { + v = fmt.Sprint(a[i+1]) + } } // pos1 to pos2 is the "k=v&" part, "&" is optional pos1 := strings.Index(s, "&"+k+"=") if pos1 != -1 { pos1++ - } else { - pos1 = strings.Index(s, "?"+k+"=") - if pos1 != -1 { - pos1++ - } else if strings.HasPrefix(s, k+"=") { - pos1 = 0 - } + } else if strings.HasPrefix(s, k+"=") { + pos1 = 0 } pos2 := len(s) if pos1 == -1 { @@ -315,7 +326,7 @@ func QueryBuild(a ...any) template.URL { } if v != "" { sep := "" - hasPrefixSep := pos1 == 0 || (pos1 <= len(s) && (s[pos1-1] == '?' || s[pos1-1] == '&')) + hasPrefixSep := pos1 == 0 || (pos1 <= len(s) && s[pos1-1] == '&') if !hasPrefixSep { sep = "&" } @@ -324,9 +335,22 @@ func QueryBuild(a ...any) template.URL { s = s[:pos1] + s[pos2:] } } - if s != "" && s != "&" && s[len(s)-1] == '&' { + if s != "" && s[len(s)-1] == '&' && !hasTrailingSep { s = s[:len(s)-1] } + if reqPath != "" { + if s == "" { + s = reqPath + if s != "?" { + s = s[:len(s)-1] + } + } else { + if s[0] == '&' { + s = s[1:] + } + s = reqPath + s + } + } return template.URL(s) } diff --git a/modules/templates/helper_test.go b/modules/templates/helper_test.go index a530d484bc..e35e8a28f8 100644 --- a/modules/templates/helper_test.go +++ b/modules/templates/helper_test.go @@ -118,3 +118,58 @@ func TestTemplateEscape(t *testing.T) { assert.Equal(t, `<>`, actual) }) } + +func TestQueryBuild(t *testing.T) { + t.Run("construct", func(t *testing.T) { + assert.Equal(t, "", string(QueryBuild())) + assert.Equal(t, "", string(QueryBuild("a", nil, "b", false, "c", 0, "d", ""))) + assert.Equal(t, "a=1&b=true", string(QueryBuild("a", 1, "b", "true"))) + + // path with query parameters + assert.Equal(t, "/?k=1", string(QueryBuild("/", "k", 1))) + assert.Equal(t, "/", string(QueryBuild("/?k=a", "k", 0))) + + // no path but question mark with query parameters + assert.Equal(t, "?k=1", string(QueryBuild("?", "k", 1))) + assert.Equal(t, "?", string(QueryBuild("?", "k", 0))) + assert.Equal(t, "path?k=1", string(QueryBuild("path?", "k", 1))) + assert.Equal(t, "path", string(QueryBuild("path?", "k", 0))) + + // only query parameters + assert.Equal(t, "&k=1", string(QueryBuild("&", "k", 1))) + assert.Equal(t, "", string(QueryBuild("&", "k", 0))) + assert.Equal(t, "", string(QueryBuild("&k=a", "k", 0))) + assert.Equal(t, "", string(QueryBuild("k=a&", "k", 0))) + assert.Equal(t, "a=1&b=2", string(QueryBuild("a=1", "b", 2))) + assert.Equal(t, "&a=1&b=2", string(QueryBuild("&a=1", "b", 2))) + assert.Equal(t, "a=1&b=2&", string(QueryBuild("a=1&", "b", 2))) + }) + + t.Run("replace", func(t *testing.T) { + assert.Equal(t, "a=1&c=d&e=f", string(QueryBuild("a=b&c=d&e=f", "a", 1))) + assert.Equal(t, "a=b&c=1&e=f", string(QueryBuild("a=b&c=d&e=f", "c", 1))) + assert.Equal(t, "a=b&c=d&e=1", string(QueryBuild("a=b&c=d&e=f", "e", 1))) + assert.Equal(t, "a=b&c=d&e=f&k=1", string(QueryBuild("a=b&c=d&e=f", "k", 1))) + }) + + t.Run("replace-&", func(t *testing.T) { + assert.Equal(t, "&a=1&c=d&e=f", string(QueryBuild("&a=b&c=d&e=f", "a", 1))) + assert.Equal(t, "&a=b&c=1&e=f", string(QueryBuild("&a=b&c=d&e=f", "c", 1))) + assert.Equal(t, "&a=b&c=d&e=1", string(QueryBuild("&a=b&c=d&e=f", "e", 1))) + assert.Equal(t, "&a=b&c=d&e=f&k=1", string(QueryBuild("&a=b&c=d&e=f", "k", 1))) + }) + + t.Run("delete", func(t *testing.T) { + assert.Equal(t, "c=d&e=f", string(QueryBuild("a=b&c=d&e=f", "a", ""))) + assert.Equal(t, "a=b&e=f", string(QueryBuild("a=b&c=d&e=f", "c", ""))) + assert.Equal(t, "a=b&c=d", string(QueryBuild("a=b&c=d&e=f", "e", ""))) + assert.Equal(t, "a=b&c=d&e=f", string(QueryBuild("a=b&c=d&e=f", "k", ""))) + }) + + t.Run("delete-&", func(t *testing.T) { + assert.Equal(t, "&c=d&e=f", string(QueryBuild("&a=b&c=d&e=f", "a", ""))) + assert.Equal(t, "&a=b&e=f", string(QueryBuild("&a=b&c=d&e=f", "c", ""))) + assert.Equal(t, "&a=b&c=d", string(QueryBuild("&a=b&c=d&e=f", "e", ""))) + assert.Equal(t, "&a=b&c=d&e=f", string(QueryBuild("&a=b&c=d&e=f", "k", ""))) + }) +} diff --git a/modules/util/path.go b/modules/util/path.go index 1272f5af2e..d9f17bd124 100644 --- a/modules/util/path.go +++ b/modules/util/path.go @@ -140,81 +140,51 @@ func IsExist(path string) (bool, error) { return false, err } -func statDir(dirPath, recPath string, includeDir, isDirOnly, followSymlinks bool) ([]string, error) { - dir, err := os.Open(dirPath) +func listDirRecursively(result *[]string, fsDir, recordParentPath string, opts *ListDirOptions) error { + dir, err := os.Open(fsDir) if err != nil { - return nil, err + return err } defer dir.Close() fis, err := dir.Readdir(0) if err != nil { - return nil, err + return err } - statList := make([]string, 0) for _, fi := range fis { - if CommonSkip(fi.Name()) { + if opts.SkipCommonHiddenNames && IsCommonHiddenFileName(fi.Name()) { continue } - - relPath := path.Join(recPath, fi.Name()) - curPath := path.Join(dirPath, fi.Name()) + relPath := path.Join(recordParentPath, fi.Name()) + curPath := filepath.Join(fsDir, fi.Name()) if fi.IsDir() { - if includeDir { - statList = append(statList, relPath+"/") + if opts.IncludeDir { + *result = append(*result, relPath+"/") } - s, err := statDir(curPath, relPath, includeDir, isDirOnly, followSymlinks) - if err != nil { - return nil, err - } - statList = append(statList, s...) - } else if !isDirOnly { - statList = append(statList, relPath) - } else if followSymlinks && fi.Mode()&os.ModeSymlink != 0 { - link, err := os.Readlink(curPath) - if err != nil { - return nil, err - } - - isDir, err := IsDir(link) - if err != nil { - return nil, err - } - if isDir { - if includeDir { - statList = append(statList, relPath+"/") - } - s, err := statDir(curPath, relPath, includeDir, isDirOnly, followSymlinks) - if err != nil { - return nil, err - } - statList = append(statList, s...) + if err = listDirRecursively(result, curPath, relPath, opts); err != nil { + return err } + } else { + *result = append(*result, relPath) } } - return statList, nil + return nil } -// StatDir gathers information of given directory by depth-first. -// It returns slice of file list and includes subdirectories if enabled; -// it returns error and nil slice when error occurs in underlying functions, -// or given path is not a directory or does not exist. -// -// Slice does not include given path itself. -// If subdirectories is enabled, they will have suffix '/'. -func StatDir(rootPath string, includeDir ...bool) ([]string, error) { - if isDir, err := IsDir(rootPath); err != nil { - return nil, err - } else if !isDir { - return nil, errors.New("not a directory or does not exist: " + rootPath) - } +type ListDirOptions struct { + IncludeDir bool // subdirectories are also included with suffix slash + SkipCommonHiddenNames bool +} - isIncludeDir := false - if len(includeDir) != 0 { - isIncludeDir = includeDir[0] +// ListDirRecursively gathers information of given directory by depth-first. +// The paths are always in "dir/slash/file" format (not "\\" even in Windows) +// Slice does not include given path itself. +func ListDirRecursively(rootDir string, opts *ListDirOptions) (res []string, err error) { + if err = listDirRecursively(&res, rootDir, "", opts); err != nil { + return nil, err } - return statDir(rootPath, "", isIncludeDir, false, false) + return res, nil } func isOSWindows() bool { @@ -265,8 +235,8 @@ func HomeDir() (home string, err error) { return home, nil } -// CommonSkip will check a provided name to see if it represents file or directory that should not be watched -func CommonSkip(name string) bool { +// IsCommonHiddenFileName will check a provided name to see if it represents file or directory that should not be watched +func IsCommonHiddenFileName(name string) bool { if name == "" { return true } @@ -275,9 +245,9 @@ func CommonSkip(name string) bool { case '.': return true case 't', 'T': - return name[1:] == "humbs.db" + return name[1:] == "humbs.db" // macOS case 'd', 'D': - return name[1:] == "esktop.ini" + return name[1:] == "esktop.ini" // Windows } return false diff --git a/modules/util/path_test.go b/modules/util/path_test.go index 6a38bf4ace..79c37e55f7 100644 --- a/modules/util/path_test.go +++ b/modules/util/path_test.go @@ -5,10 +5,12 @@ package util import ( "net/url" + "os" "runtime" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestFileURLToPath(t *testing.T) { @@ -210,3 +212,21 @@ func TestCleanPath(t *testing.T) { assert.Equal(t, c.expected, FilePathJoinAbs(c.elems[0], c.elems[1:]...), "case: %v", c.elems) } } + +func TestListDirRecursively(t *testing.T) { + tmpDir := t.TempDir() + _ = os.WriteFile(tmpDir+"/.config", nil, 0o644) + _ = os.Mkdir(tmpDir+"/d1", 0o755) + _ = os.WriteFile(tmpDir+"/d1/f-d1", nil, 0o644) + _ = os.Mkdir(tmpDir+"/d1/s1", 0o755) + _ = os.WriteFile(tmpDir+"/d1/s1/f-d1s1", nil, 0o644) + _ = os.Mkdir(tmpDir+"/d2", 0o755) + + res, err := ListDirRecursively(tmpDir, &ListDirOptions{IncludeDir: true}) + require.NoError(t, err) + assert.ElementsMatch(t, []string{".config", "d1/", "d1/f-d1", "d1/s1/", "d1/s1/f-d1s1", "d2/"}, res) + + res, err = ListDirRecursively(tmpDir, &ListDirOptions{SkipCommonHiddenNames: true}) + require.NoError(t, err) + assert.ElementsMatch(t, []string{"d1/f-d1", "d1/s1/f-d1s1"}, res) +} diff --git a/modules/web/route.go b/modules/web/router.go similarity index 66% rename from modules/web/route.go rename to modules/web/router.go index 729ac46cdc..da06b955b1 100644 --- a/modules/web/route.go +++ b/modules/web/router.go @@ -4,18 +4,14 @@ package web import ( - "fmt" "net/http" "net/url" "reflect" - "regexp" "strings" - "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/htmlutil" "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web/middleware" "gitea.com/go-chi/binding" @@ -45,7 +41,7 @@ func GetForm(dataStore reqctx.RequestDataStore) any { // Router defines a route based on chi's router type Router struct { - chiRouter chi.Router + chiRouter *chi.Mux curGroupPrefix string curMiddlewares []any } @@ -97,16 +93,21 @@ func isNilOrFuncNil(v any) bool { return r.Kind() == reflect.Func && r.IsNil() } -func (r *Router) wrapMiddlewareAndHandler(h []any) ([]func(http.Handler) http.Handler, http.HandlerFunc) { - handlerProviders := make([]func(http.Handler) http.Handler, 0, len(r.curMiddlewares)+len(h)+1) - for _, m := range r.curMiddlewares { +func wrapMiddlewareAndHandler(curMiddlewares, h []any) ([]func(http.Handler) http.Handler, http.HandlerFunc) { + handlerProviders := make([]func(http.Handler) http.Handler, 0, len(curMiddlewares)+len(h)+1) + for _, m := range curMiddlewares { if !isNilOrFuncNil(m) { handlerProviders = append(handlerProviders, toHandlerProvider(m)) } } - for _, m := range h { + if len(h) == 0 { + panic("no endpoint handler provided") + } + for i, m := range h { if !isNilOrFuncNil(m) { handlerProviders = append(handlerProviders, toHandlerProvider(m)) + } else if i == len(h)-1 { + panic("endpoint handler can't be nil") } } middlewares := handlerProviders[:len(handlerProviders)-1] @@ -121,7 +122,7 @@ func (r *Router) wrapMiddlewareAndHandler(h []any) ([]func(http.Handler) http.Ha // Methods adds the same handlers for multiple http "methods" (separated by ","). // If any method is invalid, the lower level router will panic. func (r *Router) Methods(methods, pattern string, h ...any) { - middlewares, handlerFunc := r.wrapMiddlewareAndHandler(h) + middlewares, handlerFunc := wrapMiddlewareAndHandler(r.curMiddlewares, h) fullPattern := r.getPattern(pattern) if strings.Contains(methods, ",") { methods := strings.Split(methods, ",") @@ -141,7 +142,7 @@ func (r *Router) Mount(pattern string, subRouter *Router) { // Any delegate requests for all methods func (r *Router) Any(pattern string, h ...any) { - middlewares, handlerFunc := r.wrapMiddlewareAndHandler(h) + middlewares, handlerFunc := wrapMiddlewareAndHandler(r.curMiddlewares, h) r.chiRouter.With(middlewares...).HandleFunc(r.getPattern(pattern), handlerFunc) } @@ -185,17 +186,6 @@ func (r *Router) NotFound(h http.HandlerFunc) { r.chiRouter.NotFound(h) } -type pathProcessorParam struct { - name string - captureGroup int -} - -type PathProcessor struct { - methods container.Set[string] - re *regexp.Regexp - params []pathProcessorParam -} - func (r *Router) normalizeRequestPath(resp http.ResponseWriter, req *http.Request, next http.Handler) { normalized := false normalizedPath := req.URL.EscapedPath() @@ -253,121 +243,16 @@ func (r *Router) normalizeRequestPath(resp http.ResponseWriter, req *http.Reques next.ServeHTTP(resp, req) } -func (p *PathProcessor) ProcessRequestPath(chiCtx *chi.Context, path string) bool { - if !p.methods.Contains(chiCtx.RouteMethod) { - return false - } - if !strings.HasPrefix(path, "/") { - path = "/" + path - } - pathMatches := p.re.FindStringSubmatchIndex(path) // Golang regexp match pairs [start, end, start, end, ...] - if pathMatches == nil { - return false - } - var paramMatches [][]int - for i := 2; i < len(pathMatches); { - paramMatches = append(paramMatches, []int{pathMatches[i], pathMatches[i+1]}) - pmIdx := len(paramMatches) - 1 - end := pathMatches[i+1] - i += 2 - for ; i < len(pathMatches); i += 2 { - if pathMatches[i] >= end { - break - } - paramMatches[pmIdx] = append(paramMatches[pmIdx], pathMatches[i], pathMatches[i+1]) - } - } - for i, pm := range paramMatches { - groupIdx := p.params[i].captureGroup * 2 - chiCtx.URLParams.Add(p.params[i].name, path[pm[groupIdx]:pm[groupIdx+1]]) - } - return true -} - -func NewPathProcessor(methods, pattern string) *PathProcessor { - p := &PathProcessor{methods: make(container.Set[string])} - for _, method := range strings.Split(methods, ",") { - p.methods.Add(strings.TrimSpace(method)) - } - re := []byte{'^'} - lastEnd := 0 - for lastEnd < len(pattern) { - start := strings.IndexByte(pattern[lastEnd:], '<') - if start == -1 { - re = append(re, pattern[lastEnd:]...) - break - } - end := strings.IndexByte(pattern[lastEnd+start:], '>') - if end == -1 { - panic(fmt.Sprintf("invalid pattern: %s", pattern)) - } - re = append(re, pattern[lastEnd:lastEnd+start]...) - partName, partExp, _ := strings.Cut(pattern[lastEnd+start+1:lastEnd+start+end], ":") - lastEnd += start + end + 1 - - // TODO: it could support to specify a "capture group" for the name, for example: "/" - // it is not used so no need to implement it now - param := pathProcessorParam{} - if partExp == "*" { - re = append(re, "(.*?)/?"...) - if lastEnd < len(pattern) { - if pattern[lastEnd] == '/' { - lastEnd++ - } - } - } else { - partExp = util.IfZero(partExp, "[^/]+") - re = append(re, '(') - re = append(re, partExp...) - re = append(re, ')') - } - param.name = partName - p.params = append(p.params, param) - } - re = append(re, '$') - reStr := string(re) - p.re = regexp.MustCompile(reStr) - return p -} - // Combo delegates requests to Combo func (r *Router) Combo(pattern string, h ...any) *Combo { return &Combo{r, pattern, h} } -// Combo represents a tiny group routes with same pattern -type Combo struct { - r *Router - pattern string - h []any -} - -// Get delegates Get method -func (c *Combo) Get(h ...any) *Combo { - c.r.Get(c.pattern, append(c.h, h...)...) - return c -} - -// Post delegates Post method -func (c *Combo) Post(h ...any) *Combo { - c.r.Post(c.pattern, append(c.h, h...)...) - return c -} - -// Delete delegates Delete method -func (c *Combo) Delete(h ...any) *Combo { - c.r.Delete(c.pattern, append(c.h, h...)...) - return c -} - -// Put delegates Put method -func (c *Combo) Put(h ...any) *Combo { - c.r.Put(c.pattern, append(c.h, h...)...) - return c -} - -// Patch delegates Patch method -func (c *Combo) Patch(h ...any) *Combo { - c.r.Patch(c.pattern, append(c.h, h...)...) - return c +// PathGroup creates a group of paths which could be matched by regexp. +// It is only designed to resolve some special cases which chi router can't handle. +// For most cases, it shouldn't be used because it needs to iterate all rules to find the matched one (inefficient). +func (r *Router) PathGroup(pattern string, fn func(g *RouterPathGroup), h ...any) { + g := &RouterPathGroup{r: r, pathParam: "*"} + fn(g) + r.Any(pattern, append(h, g.ServeHTTP)...) } diff --git a/modules/web/router_combo.go b/modules/web/router_combo.go new file mode 100644 index 0000000000..4478689027 --- /dev/null +++ b/modules/web/router_combo.go @@ -0,0 +1,41 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package web + +// Combo represents a tiny group routes with same pattern +type Combo struct { + r *Router + pattern string + h []any +} + +// Get delegates Get method +func (c *Combo) Get(h ...any) *Combo { + c.r.Get(c.pattern, append(c.h, h...)...) + return c +} + +// Post delegates Post method +func (c *Combo) Post(h ...any) *Combo { + c.r.Post(c.pattern, append(c.h, h...)...) + return c +} + +// Delete delegates Delete method +func (c *Combo) Delete(h ...any) *Combo { + c.r.Delete(c.pattern, append(c.h, h...)...) + return c +} + +// Put delegates Put method +func (c *Combo) Put(h ...any) *Combo { + c.r.Put(c.pattern, append(c.h, h...)...) + return c +} + +// Patch delegates Patch method +func (c *Combo) Patch(h ...any) *Combo { + c.r.Patch(c.pattern, append(c.h, h...)...) + return c +} diff --git a/modules/web/router_path.go b/modules/web/router_path.go new file mode 100644 index 0000000000..39082c0724 --- /dev/null +++ b/modules/web/router_path.go @@ -0,0 +1,135 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package web + +import ( + "fmt" + "net/http" + "regexp" + "strings" + + "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/util" + + "github.com/go-chi/chi/v5" +) + +type RouterPathGroup struct { + r *Router + pathParam string + matchers []*routerPathMatcher +} + +func (g *RouterPathGroup) ServeHTTP(resp http.ResponseWriter, req *http.Request) { + chiCtx := chi.RouteContext(req.Context()) + path := chiCtx.URLParam(g.pathParam) + for _, m := range g.matchers { + if m.matchPath(chiCtx, path) { + handler := m.handlerFunc + for i := len(m.middlewares) - 1; i >= 0; i-- { + handler = m.middlewares[i](handler).ServeHTTP + } + handler(resp, req) + return + } + } + g.r.chiRouter.NotFoundHandler().ServeHTTP(resp, req) +} + +// MatchPath matches the request method, and uses regexp to match the path. +// The pattern uses "<...>" to define path parameters, for example: "/" (different from chi router) +// It is only designed to resolve some special cases which chi router can't handle. +// For most cases, it shouldn't be used because it needs to iterate all rules to find the matched one (inefficient). +func (g *RouterPathGroup) MatchPath(methods, pattern string, h ...any) { + g.matchers = append(g.matchers, newRouterPathMatcher(methods, pattern, h...)) +} + +type routerPathParam struct { + name string + captureGroup int +} + +type routerPathMatcher struct { + methods container.Set[string] + re *regexp.Regexp + params []routerPathParam + middlewares []func(http.Handler) http.Handler + handlerFunc http.HandlerFunc +} + +func (p *routerPathMatcher) matchPath(chiCtx *chi.Context, path string) bool { + if !p.methods.Contains(chiCtx.RouteMethod) { + return false + } + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + pathMatches := p.re.FindStringSubmatchIndex(path) // Golang regexp match pairs [start, end, start, end, ...] + if pathMatches == nil { + return false + } + var paramMatches [][]int + for i := 2; i < len(pathMatches); { + paramMatches = append(paramMatches, []int{pathMatches[i], pathMatches[i+1]}) + pmIdx := len(paramMatches) - 1 + end := pathMatches[i+1] + i += 2 + for ; i < len(pathMatches); i += 2 { + if pathMatches[i] >= end { + break + } + paramMatches[pmIdx] = append(paramMatches[pmIdx], pathMatches[i], pathMatches[i+1]) + } + } + for i, pm := range paramMatches { + groupIdx := p.params[i].captureGroup * 2 + chiCtx.URLParams.Add(p.params[i].name, path[pm[groupIdx]:pm[groupIdx+1]]) + } + return true +} + +func newRouterPathMatcher(methods, pattern string, h ...any) *routerPathMatcher { + middlewares, handlerFunc := wrapMiddlewareAndHandler(nil, h) + p := &routerPathMatcher{methods: make(container.Set[string]), middlewares: middlewares, handlerFunc: handlerFunc} + for _, method := range strings.Split(methods, ",") { + p.methods.Add(strings.TrimSpace(method)) + } + re := []byte{'^'} + lastEnd := 0 + for lastEnd < len(pattern) { + start := strings.IndexByte(pattern[lastEnd:], '<') + if start == -1 { + re = append(re, pattern[lastEnd:]...) + break + } + end := strings.IndexByte(pattern[lastEnd+start:], '>') + if end == -1 { + panic(fmt.Sprintf("invalid pattern: %s", pattern)) + } + re = append(re, pattern[lastEnd:lastEnd+start]...) + partName, partExp, _ := strings.Cut(pattern[lastEnd+start+1:lastEnd+start+end], ":") + lastEnd += start + end + 1 + + // TODO: it could support to specify a "capture group" for the name, for example: "/" + // it is not used so no need to implement it now + param := routerPathParam{} + if partExp == "*" { + re = append(re, "(.*?)/?"...) + if lastEnd < len(pattern) && pattern[lastEnd] == '/' { + lastEnd++ // the "*" pattern is able to handle the last slash, so skip it + } + } else { + partExp = util.IfZero(partExp, "[^/]+") + re = append(re, '(') + re = append(re, partExp...) + re = append(re, ')') + } + param.name = partName + p.params = append(p.params, param) + } + re = append(re, '$') + reStr := string(re) + p.re = regexp.MustCompile(reStr) + return p +} diff --git a/modules/web/route_test.go b/modules/web/router_test.go similarity index 66% rename from modules/web/route_test.go rename to modules/web/router_test.go index ca3134b546..582980a27a 100644 --- a/modules/web/route_test.go +++ b/modules/web/router_test.go @@ -27,17 +27,21 @@ func chiURLParamsToMap(chiCtx *chi.Context) map[string]string { } m[key] = pathParams.Values[i] } - return m + return util.Iif(len(m) == 0, nil, m) } func TestPathProcessor(t *testing.T) { testProcess := func(pattern, uri string, expectedPathParams map[string]string) { chiCtx := chi.NewRouteContext() chiCtx.RouteMethod = "GET" - p := NewPathProcessor("GET", pattern) - assert.True(t, p.ProcessRequestPath(chiCtx, uri), "use pattern %s to process uri %s", pattern, uri) + p := newRouterPathMatcher("GET", pattern, http.NotFound) + assert.True(t, p.matchPath(chiCtx, uri), "use pattern %s to process uri %s", pattern, uri) assert.Equal(t, expectedPathParams, chiURLParamsToMap(chiCtx), "use pattern %s to process uri %s", pattern, uri) } + + // the "<...>" is intentionally designed to distinguish from chi's path parameters, because: + // 1. their behaviors are totally different, we do not want to mislead developers + // 2. we can write regexp in "" easily and parse it easily testProcess("//", "/a/b", map[string]string{"p1": "a", "p2": "b"}) testProcess("/", "", map[string]string{"p1": ""}) // this is a special case, because chi router could use empty path testProcess("/", "/", map[string]string{"p1": ""}) @@ -67,24 +71,31 @@ func TestRouter(t *testing.T) { } } + stopMark := func(optMark ...string) func(resp http.ResponseWriter, req *http.Request) { + mark := util.OptionalArg(optMark, "") + return func(resp http.ResponseWriter, req *http.Request) { + if stop := req.FormValue("stop"); stop != "" && (mark == "" || mark == stop) { + h(stop)(resp, req) + resp.WriteHeader(http.StatusOK) + } + } + } + r := NewRouter() + r.NotFound(h("not-found:/")) r.Get("/{username}/{reponame}/{type:issues|pulls}", h("list-issues-a")) // this one will never be called r.Group("/{username}/{reponame}", func() { r.Get("/{type:issues|pulls}", h("list-issues-b")) r.Group("", func() { r.Get("/{type:issues|pulls}/{index}", h("view-issue")) - }, func(resp http.ResponseWriter, req *http.Request) { - if stop := req.FormValue("stop"); stop != "" { - h(stop)(resp, req) - resp.WriteHeader(http.StatusOK) - } - }) + }, stopMark()) r.Group("/issues/{index}", func() { r.Post("/update", h("update-issue")) }) }) m := NewRouter() + m.NotFound(h("not-found:/api/v1")) r.Mount("/api/v1", m) m.Group("/repos", func() { m.Group("/{username}/{reponame}", func() { @@ -96,11 +107,14 @@ func TestRouter(t *testing.T) { m.Patch("", h()) m.Delete("", h()) }) + m.PathGroup("/*", func(g *RouterPathGroup) { + g.MatchPath("GET", `//`, stopMark("s2"), h("match-path")) + }, stopMark("s1")) }) }) }) - testRoute := func(methodPath string, expected resultStruct) { + testRoute := func(t *testing.T, methodPath string, expected resultStruct) { t.Run(methodPath, func(t *testing.T) { res = resultStruct{} methodPathFields := strings.Fields(methodPath) @@ -111,24 +125,24 @@ func TestRouter(t *testing.T) { }) } - t.Run("Root Router", func(t *testing.T) { - testRoute("GET /the-user/the-repo/other", resultStruct{}) - testRoute("GET /the-user/the-repo/pulls", resultStruct{ + t.Run("RootRouter", func(t *testing.T) { + testRoute(t, "GET /the-user/the-repo/other", resultStruct{method: "GET", handlerMark: "not-found:/"}) + testRoute(t, "GET /the-user/the-repo/pulls", resultStruct{ method: "GET", pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "pulls"}, handlerMark: "list-issues-b", }) - testRoute("GET /the-user/the-repo/issues/123", resultStruct{ + testRoute(t, "GET /the-user/the-repo/issues/123", resultStruct{ method: "GET", pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"}, handlerMark: "view-issue", }) - testRoute("GET /the-user/the-repo/issues/123?stop=hijack", resultStruct{ + testRoute(t, "GET /the-user/the-repo/issues/123?stop=hijack", resultStruct{ method: "GET", pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"}, handlerMark: "hijack", }) - testRoute("POST /the-user/the-repo/issues/123/update", resultStruct{ + testRoute(t, "POST /the-user/the-repo/issues/123/update", resultStruct{ method: "POST", pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "index": "123"}, handlerMark: "update-issue", @@ -136,31 +150,62 @@ func TestRouter(t *testing.T) { }) t.Run("Sub Router", func(t *testing.T) { - testRoute("GET /api/v1/repos/the-user/the-repo/branches", resultStruct{ + testRoute(t, "GET /api/v1/other", resultStruct{method: "GET", handlerMark: "not-found:/api/v1"}) + testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches", resultStruct{ method: "GET", pathParams: map[string]string{"username": "the-user", "reponame": "the-repo"}, }) - testRoute("POST /api/v1/repos/the-user/the-repo/branches", resultStruct{ + testRoute(t, "POST /api/v1/repos/the-user/the-repo/branches", resultStruct{ method: "POST", pathParams: map[string]string{"username": "the-user", "reponame": "the-repo"}, }) - testRoute("GET /api/v1/repos/the-user/the-repo/branches/master", resultStruct{ + testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/master", resultStruct{ method: "GET", pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "name": "master"}, }) - testRoute("PATCH /api/v1/repos/the-user/the-repo/branches/master", resultStruct{ + testRoute(t, "PATCH /api/v1/repos/the-user/the-repo/branches/master", resultStruct{ method: "PATCH", pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "name": "master"}, }) - testRoute("DELETE /api/v1/repos/the-user/the-repo/branches/master", resultStruct{ + testRoute(t, "DELETE /api/v1/repos/the-user/the-repo/branches/master", resultStruct{ method: "DELETE", pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "name": "master"}, }) }) + + t.Run("MatchPath", func(t *testing.T) { + testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn", resultStruct{ + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"}, + handlerMark: "match-path", + }) + testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1%2fd2/fn", resultStruct{ + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1%2fd2/fn", "dir": "d1%2fd2", "file": "fn"}, + handlerMark: "match-path", + }) + testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/000", resultStruct{ + method: "GET", + pathParams: map[string]string{"reponame": "the-repo", "username": "the-user", "*": "d1/d2/000"}, + handlerMark: "not-found:/api/v1", + }) + + testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s1", resultStruct{ + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn"}, + handlerMark: "s1", + }) + + testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s2", resultStruct{ + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"}, + handlerMark: "s2", + }) + }) } func TestRouteNormalizePath(t *testing.T) { diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index 3c7e247d13..beeb1dc3b8 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -647,6 +647,7 @@ joined_on=PÅ™idal/a se %s repositories=Repozitáře activity=VeÅ™ejná aktivita followers=Sledující +show_more=Zobrazit více starred=Oblíbené repozitáře watched=Sledované repozitáře code=Kód @@ -1011,7 +1012,6 @@ new_repo_helper=Repozitář obsahuje vÅ¡echny projektové soubory, vÄetnÄ› hist owner=Vlastník owner_helper=NÄ›které organizace se nemusejí v seznamu zobrazit kvůli maximálnímu dosaženému poÄtu repozitářů. repo_name=Název repozitáře -repo_name_helper=Dobrý název repozitáře vÄ›tÅ¡inou používá krátká, zapamatovatelná a unikátní klíÄová slova. repo_size=Velikost repozitáře template=Å ablona template_select=Vyberte Å¡ablonu. @@ -2832,6 +2832,7 @@ teams.invite.title=Byli jste pozváni do týmu %s v organizaci teams.invite.by=Pozvání od %s teams.invite.description=Pro pÅ™ipojení k týmu kliknÄ›te na tlaÄítko níže. + [admin] maintenance=Údržba dashboard=PÅ™ehled diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index 78bfb26d73..ce2c43cb56 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -93,6 +93,7 @@ remove_all=Alle entfernen remove_label_str=Element "%s " entfernen edit=Bearbeiten view=Anzeigen +test=Test enabled=Aktiviert disabled=Deaktiviert @@ -103,6 +104,7 @@ copy_url=URL kopieren copy_hash=Hash kopieren copy_content=Inhalt kopieren copy_branch=Branchnamen kopieren +copy_path=Pfad kopieren copy_success=Kopiert! copy_error=Kopieren fehlgeschlagen copy_type_unsupported=Dieser Dateityp kann nicht kopiert werden @@ -143,6 +145,7 @@ confirm_delete_selected=Alle ausgewählten Elemente löschen? name=Name value=Wert +readme=Readme filter=Filter filter.clear=Filter leeren @@ -158,12 +161,15 @@ filter.public=Öffentlich filter.private=Privat no_results_found=Es wurden keine Ergebnisse gefunden. +internal_error_skipped=Ein interner Fehler ist aufgetreten, wurde aber übersprungen: %s [search] search=Suche ... type_tooltip=Suchmodus fuzzy=Ähnlich fuzzy_tooltip=Ergebnisse einbeziehen, die dem Suchbegriff ähnlich sind +exact=Exakt +exact_tooltip=Nur Suchbegriffe einbeziehen, die dem exakten Suchbegriff entsprechen repo_kind=Repositories durchsuchen ... user_kind=Benutzer durchsuchen ... org_kind=Organisationen durchsuchen ... @@ -174,9 +180,13 @@ code_search_by_git_grep=Aktuelle Code-Suchergebnisse werden von "git grep" berei package_kind=Pakete durchsuchen ... project_kind=Projekte durchsuchen ... branch_kind=Branches durchsuchen ... +tag_kind=Tags durchsuchen... +tag_tooltip=Suche nach passenden Tags. Benutze '%', um jede Sequenz von Zahlen zu treffen. commit_kind=Commits durchsuchen ... runner_kind=Runner durchsuchen ... no_results=Es wurden keine passenden Ergebnisse gefunden. +issue_kind=Issues durchsuchen ... +pull_kind=Pull-Requests durchsuchen... keyword_search_unavailable=Zurzeit ist die Stichwort-Suche nicht verfügbar. Bitte wende dich an den Website-Administrator. [aria] @@ -201,7 +211,10 @@ buttons.link.tooltip=Link hinzufügen buttons.list.unordered.tooltip=Liste hinzufügen buttons.list.ordered.tooltip=Nummerierte Liste hinzufügen buttons.list.task.tooltip=Aufgabenliste hinzufügen +buttons.table.add.tooltip=Tabelle hinzufügen buttons.table.add.insert=Hinzufügen +buttons.table.rows=Zeilen +buttons.table.cols=Spalten buttons.mention.tooltip=Benutzer oder Team erwähnen buttons.ref.tooltip=Issue oder Pull-Request referenzieren buttons.switch_to_legacy.tooltip=Legacy-Editor verwenden @@ -214,16 +227,20 @@ string.desc=Z–A [error] occurred=Ein Fehler ist aufgetreten +report_message=Wenn du glaubst, dass dies ein Fehler von Gitea ist, suche bitte auf GitHub nach diesem Fehler und erstelle gegebenenfalls einen neuen Bugreport. not_found=Das Ziel konnte nicht gefunden werden. network_error=Netzwerkfehler [startpage] app_desc=Ein einfacher, selbst gehosteter Git-Service install=Einfach zu installieren +install_desc=Starte einfach die Anwendung für deine Plattform oder nutze Docker. Es existieren auch paketierte Versionen. platform=Plattformübergreifend +platform_desc=Gitea läuft überall, wo Go kompiliert: Windows, macOS, Linux, ARM, etc. Wähle das System, das dir am meisten gefällt! lightweight=Leichtgewicht lightweight_desc=Gitea hat minimale Systemanforderungen und kann selbst auf einem günstigen und stromsparenden Raspberry Pi betrieben werden! license=Quelloffen +license_desc=Hol dir den Code unter %[2]s! Leiste deinen Beitrag bei der Verbesserung dieses Projekts. Trau dich! [install] install=Installation @@ -337,6 +354,7 @@ enable_update_checker=Aktualisierungsprüfung aktivieren enable_update_checker_helper=Stellt regelmäßig eine Verbindung zu gitea.io her, um nach neuen Versionen zu prüfen. env_config_keys=Umgebungskonfiguration env_config_keys_prompt=Die folgenden Umgebungsvariablen werden auch auf Ihre Konfigurationsdatei angewendet: +config_write_file_prompt=Diese Konfigurationsoptionen werden in %s geschrieben [home] nav_menu=Navigationsmenü @@ -377,6 +395,8 @@ relevant_repositories=Es werden nur relevante Repositories angezeigt, Liste gestohlener Passwörter, die öffentlich verfügbar sind. Bitte versuche es erneut mit einem anderen Passwort und ziehe in Erwägung, auch anderswo deine Passwörter zu ändern. password_pwned_err=Anfrage an HaveIBeenPwned konnte nicht abgeschlossen werden last_admin=Du kannst den letzten Admin nicht entfernen. Es muss mindestens einen Administrator geben. +signin_passkey=Mit einem Passkey anmelden +back_to_sign_in=Zurück zum Anmelden [mail] view_it_on=Auf %s ansehen @@ -459,6 +486,7 @@ activate_email=Bestätige deine E-Mail-Adresse activate_email.title=%s, bitte verifiziere deine E-Mail-Adresse activate_email.text=Bitte klicke innerhalb von %s auf folgenden Link, um dein Konto zu aktivieren: +register_notify=Willkommen bei %s register_notify.title=%[1]s, willkommen bei %[2]s register_notify.text_1=dies ist deine Bestätigungs-E-Mail für %s! register_notify.text_2=Du kannst dich jetzt mit dem Benutzernamen "%s" anmelden. @@ -560,6 +588,8 @@ lang_select_error=Wähle eine Sprache aus der Liste aus. username_been_taken=Der Benutzername ist bereits vergeben. username_change_not_local_user=Nicht-lokale Benutzer dürfen ihren Nutzernamen nicht ändern. +change_username_disabled=Ändern des Benutzernamens ist deaktiviert. +change_full_name_disabled=Ändern des vollständigen Namens ist deaktiviert. username_has_not_been_changed=Benutzername wurde nicht geändert repo_name_been_taken=Der Repository-Name wird schon verwendet. repository_force_private=Privat erzwingen ist aktiviert: Private Repositories können nicht veröffentlicht werden. @@ -609,6 +639,7 @@ org_still_own_repo=Diese Organisation besitzt noch ein oder mehrere Repositories org_still_own_packages=Diese Organisation besitzt noch ein oder mehrere Pakete, lösche diese zuerst. target_branch_not_exist=Der Ziel-Branch existiert nicht. +target_ref_not_exist=Zielreferenz existiert nicht %s admin_cannot_delete_self=Du kannst dich nicht selbst löschen, wenn du ein Administrator bist. Bitte entferne zuerst deine Administratorrechte. @@ -618,6 +649,7 @@ joined_on=Beigetreten am %s repositories=Repositories activity=Öffentliche Aktivität followers=Follower +show_more=Mehr anzeigen starred=Favoriten watched=Beobachtete Repositories code=Quelltext @@ -684,6 +716,8 @@ public_profile=Öffentliches Profil biography_placeholder=Erzähle uns ein wenig über Dich selbst! (Du kannst Markdown verwenden) location_placeholder=Teile Deinen ungefähren Standort mit anderen profile_desc=Lege fest, wie dein Profil anderen Benutzern angezeigt wird. Deine primäre E-Mail-Adresse wird für Benachrichtigungen, Passwort-Wiederherstellung und webbasierte Git-Operationen verwendet. +password_username_disabled=Du bist nicht berechtigt, den Benutzernamen zu ändern. Bitte kontaktiere Deinen Seitenadministrator für weitere Details. +password_full_name_disabled=Du bist nicht berechtigt, den vollständigen Namen zu ändern. Bitte kontaktiere Deinen Seitenadministrator für weitere Details. full_name=Vollständiger Name website=Webseite location=Standort @@ -701,6 +735,7 @@ cancel=Abbrechen language=Sprache ui=Theme hidden_comment_types=Ausgeblendeter Kommentartypen +hidden_comment_types_description=Die hier markierten Kommentartypen werden nicht innerhalb der Issue-Seiten angezeigt. Beispielsweise entfernt das Markieren von "Label" alle "{user} hat {label} hinzugefügt/entfernt"-Kommentare. hidden_comment_types.ref_tooltip=Kommentare, in denen dieses Issue von einem anderen Issue/Commit referenziert wurde hidden_comment_types.issue_ref_tooltip=Kommentare, bei denen der Benutzer den Branch/Tag des Issues ändert comment_type_group_reference=Verweis auf Mitglieder @@ -732,6 +767,7 @@ uploaded_avatar_not_a_image=Die hochgeladene Datei ist kein Bild. uploaded_avatar_is_too_big=Die hochgeladene Dateigröße (%d KiB) überschreitet die maximale Größe (%d KiB). update_avatar_success=Dein Profilbild wurde geändert. update_user_avatar_success=Der Avatar des Benutzers wurde aktualisiert. +cropper_prompt=Sie können das Bild vor dem Speichern bearbeiten. Das bearbeitete Bild wird als PNG-Datei gespeichert. change_password=Passwort aktualisieren old_password=Aktuelles Passwort @@ -747,6 +783,8 @@ manage_themes=Standard-Theme auswählen manage_openid=OpenID-Adressen verwalten email_desc=Deine primäre E-Mail-Adresse wird für Benachrichtigungen, Passwort-Wiederherstellung und, sofern sie nicht versteckt ist, web-basierte Git-Operationen verwendet. theme_desc=Dies wird dein Standard-Theme auf der Seite sein. +theme_colorblindness_help=Hilfe zum Theme für Farbenblinde +theme_colorblindness_prompt=Gitea erhält aktuell einfache Unterstützung für Farbenblinde durch einige Themes, die nur wenige Farben definiert haben. Die Arbeit ist noch im Gange. Weitere Verbesserungen können durch die Definition von mehr Farben in den CSS-Theme-Dateien vorgenommen werden. primary=Primär activated=Aktiviert requires_activation=Erfordert Aktivierung @@ -871,8 +909,9 @@ repo_and_org_access=Repository- und Organisationszugriff permissions_public_only=Nur öffentlich permissions_access_all=Alle (öffentlich, privat und begrenzt) select_permissions=Berechtigungen auswählen +permission_not_set=Nicht festgelegt permission_no_access=Kein Zugriff -permission_read=Gelesen +permission_read=Lesen permission_write=Lesen und Schreiben access_token_desc=Ausgewählte Token-Berechtigungen beschränken die Authentifizierung auf die entsprechenden API-Routen. Lies die Dokumentation für mehr Informationen. at_least_one_permission=Du musst mindestens eine Berechtigung auswählen, um ein Token zu erstellen @@ -890,6 +929,7 @@ create_oauth2_application_success=Du hast erfolgreich eine neue OAuth2-Anwendung update_oauth2_application_success=Du hast die OAuth2-Anwendung erfolgreich aktualisiert. oauth2_application_name=Name der Anwendung oauth2_confidential_client=Vertraulicher Client. Für Anwendungen aktivieren, die das Geheimnis sicher speichern, z. B. Webanwendungen. Wähle diese Option nicht für native Anwendungen für PCs und Mobilgeräte. +oauth2_skip_secondary_authorization=Autorisierung für öffentliche Clients nach einmaliger Gewährung des Zugriffs überspringen. Dies kann ein Sicherheitsrisiko darstellen. oauth2_redirect_uris=URIs für die Weiterleitung. Bitte verwende eine neue Zeile für jede URI. save_application=Speichern oauth2_client_id=Client-ID @@ -900,6 +940,7 @@ oauth2_client_secret_hint=Das Secret wird nach dem Verlassen oder Aktualisieren oauth2_application_edit=Bearbeiten oauth2_application_create_description=OAuth2 Anwendungen geben deiner Drittanwendung Zugriff auf Benutzeraccounts dieser Gitea-Instanz. oauth2_application_remove_description=Das Entfernen einer OAuth2-Anwendung hat zur Folge, dass diese nicht mehr auf autorisierte Benutzeraccounts auf dieser Instanz zugreifen kann. Möchtest Du fortfahren? +oauth2_application_locked=Wenn es in der Konfiguration aktiviert ist, registriert Gitea einige OAuth2-Anwendungen beim Starten vor. Um unerwartetes Verhalten zu verhindern, können diese weder bearbeitet noch entfernt werden. Weitere Informationen findest Du in der OAuth2-Dokumentation. authorized_oauth2_applications=Autorisierte OAuth2-Anwendungen authorized_oauth2_applications_description=Den folgenden Drittanbieter-Apps hast Du Zugriff auf Deinen persönlichen Gitea-Account gewährt. Bitte widerrufe die Autorisierung für Apps, die Du nicht mehr nutzt. @@ -908,20 +949,26 @@ revoke_oauth2_grant=Autorisierung widerrufen revoke_oauth2_grant_description=Wenn du die Autorisierung widerrufst, kann die Anwendung nicht mehr auf deine Daten zugreifen. Bist du dir sicher? revoke_oauth2_grant_success=Zugriff erfolgreich widerrufen. +twofa_desc=Um dein Konto vor Passwortdiebstahl zu schützen, kannst du ein Smartphone oder ein anderes Gerät verwenden, um zeitbasierte Einmalpasswörter ("TOTP") zu erhalten. twofa_recovery_tip=Wenn du dein Gerät verlierst, kannst du einen einmalig verwendbaren Wiederherstellungsschlüssel nutzen, um den Zugriff auf dein Konto wiederherzustellen. twofa_is_enrolled=Für dein Konto ist die Zwei-Faktor-Authentifizierung eingeschaltet. twofa_not_enrolled=Für dein Konto ist die Zwei-Faktor-Authentifizierung momentan nicht eingeschaltet. twofa_disable=Zwei-Faktor-Authentifizierung deaktivieren +twofa_scratch_token_regenerate=Einweg-Wiederherstellungsschlüssel neu generieren +twofa_scratch_token_regenerated=Dein Einweg-Wiederherstellungsschlüssel ist jetzt %s. Speichere ihn an einem sicheren Ort, er wird nie wieder angezeigt. twofa_enroll=Zwei-Faktor-Authentifizierung aktivieren twofa_disable_note=Du kannst die Zwei-Faktor-Authentifizierung auch wieder deaktivieren. twofa_disable_desc=Wenn du die Zwei-Faktor-Authentifizierung deaktivierst, wird die Sicherheit deines Kontos verringert. Fortfahren? +regenerate_scratch_token_desc=Wenn du deinen Wiederherstellungsschlüssel verlegt oder bereits benutzt hast, kannst du ihn hier zurücksetzen. twofa_disabled=Zwei-Faktor-Authentifizierung wurde deaktiviert. scan_this_image=Scanne diese Grafik mit deiner Authentifizierungs-App: or_enter_secret=Oder gib das Secret ein: %s then_enter_passcode=Und gebe dann die angezeigte PIN der Anwendung ein: passcode_invalid=Die PIN ist falsch. Probiere es erneut. +twofa_enrolled=Die Zwei-Faktor-Authentifizierung wurde für dein Konto aktiviert. Bewahre deinen Einweg-Wiederherstellungsschlüssel (%s) an einem sicheren Ort auf, da er nicht wieder angezeigt werden wird. twofa_failed_get_secret=Fehler beim Abrufen des Secrets. +webauthn_desc=Sicherheitsschlüssel sind Geräte, die kryptografische Schlüssel beeinhalten. Diese können für die Zwei-Faktor-Authentifizierung verwendet werden. Der Sicherheitsschlüssel muss den Standard "WebAuthn" unterstützen. webauthn_register_key=Sicherheitsschlüssel hinzufügen webauthn_nickname=Nickname webauthn_delete_key=Sicherheitsschlüssel entfernen @@ -968,7 +1015,6 @@ new_repo_helper=Ein Repository enthält alle Projektdateien, einschließlich des owner=Besitzer owner_helper=Einige Organisationen könnten in der Dropdown-Liste nicht angezeigt werden, da die Anzahl an Repositories begrenzt ist. repo_name=Repository-Name -repo_name_helper=Ein guter Repository-Name besteht normalerweise aus kurzen, unvergesslichen und einzigartigen Schlagwörtern. repo_size=Repository-Größe template=Template template_select=Vorlage auswählen @@ -987,6 +1033,8 @@ fork_to_different_account=Fork in ein anderes Konto erstellen fork_visibility_helper=Die Sichtbarkeit eines geforkten Repositories kann nicht geändert werden. fork_branch=Branch, der zum Fork geklont werden soll all_branches=Alle Branches +view_all_branches=Alle Branches anzeigen +view_all_tags=Alle Tags anzeigen fork_no_valid_owners=Dieses Repository kann nicht geforkt werden, da keine gültigen Besitzer vorhanden sind. fork.blocked_user=Das Repository kann nicht geforkt werden, da du vom Repository-Eigentümer blockiert wurdest. use_template=Dieses Template verwenden @@ -998,6 +1046,8 @@ generate_repo=Repository erstellen generate_from=Erstelle aus repo_desc=Beschreibung repo_desc_helper=Gib eine kurze Beschreibung an (optional) +repo_no_desc=Keine Beschreibung vorhanden +repo_lang=Sprachen repo_gitignore_helper=Wähle eine .gitignore-Vorlage aus. repo_gitignore_helper_desc=Wähle aus einer Liste an Vorlagen für bekannte Sprachen, welche Dateien ignoriert werden sollen. Typische Artefakte, die durch die Build Tools der gewählten Sprache generiert werden, sind standardmäßig Bestandteil der .gitignore. issue_labels=Issue Label @@ -1005,6 +1055,7 @@ issue_labels_helper=Wähle ein Issue-Label-Set. license=Lizenz license_helper=Wähle eine Lizenz aus. license_helper_desc=Eine Lizenz regelt, was Andere mit deinem Code (nicht) tun können. Unsicher, welches für dein Projekt die Richtige ist? Siehe eine Lizenz wählen. +multiple_licenses=Mehrere Lizenzen object_format=Objektformat object_format_helper=Objektformat des Repositories. Es kann später nicht geändert werden. SHA1 ist am meisten kompatibel. readme=README @@ -1058,13 +1109,16 @@ delete_preexisting_success=Nicht übernommene Dateien in %s gelöscht blame_prior=Blame vor dieser Änderung anzeigen blame.ignore_revs=Revisionen in .git-blame-ignore-revs werden ignoriert. Klicke hier, um das zu umgehen und die normale Blame-Ansicht zu sehen. blame.ignore_revs.failed=Fehler beim Ignorieren der Revisionen in .git-blame-ignore-revs. +user_search_tooltip=Zeigt maximal 30 Benutzer tree_path_not_found_commit=Pfad %[1]s existiert nicht in Commit%[2]s tree_path_not_found_branch=Pfad %[1]s existiert nicht in Branch %[2]s tree_path_not_found_tag=Pfad %[1]s existiert nicht in Tag %[2]s transfer.accept=Übertragung Akzeptieren +transfer.accept_desc=`Übertragung nach "%s"` transfer.reject=Übertragung Ablehnen +transfer.reject_desc=Übertragung nach "%s " abbrechen transfer.no_permission_to_accept=Du hast keine Berechtigung, diesen Transfer anzunehmen. transfer.no_permission_to_reject=Du hast keine Berechtigung, diesen Transfer abzulehnen. @@ -1139,6 +1193,11 @@ migrate.gogs.description=Daten von notabug.org oder anderen Gogs Instanzen migri migrate.onedev.description=Daten von code.onedev.io oder anderen OneDev Instanzen migrieren. migrate.codebase.description=Daten von codebasehq.com migrieren. migrate.gitbucket.description=Daten von GitBucket Instanzen migrieren. +migrate.codecommit.description=Daten von AWS CodeCommit migrieren. +migrate.codecommit.aws_access_key_id=AWS Access Key ID +migrate.codecommit.aws_secret_access_key=AWS Secret Access Key +migrate.codecommit.https_git_credentials_username=HTTPS-Git-Nutzername +migrate.codecommit.https_git_credentials_password=HTTPS-Git-Passwort migrate.migrating_git=Git-Daten werden migriert migrate.migrating_topics=Themen werden migriert migrate.migrating_milestones=Meilensteine werden migriert @@ -1199,6 +1258,7 @@ releases=Releases tag=Tag released_this=hat released tagged_this=hat getaggt +file.title=%s in %s file_raw=Originalformat file_history=Verlauf file_view_source=Quelltext anzeigen @@ -1206,12 +1266,16 @@ file_view_rendered=Ansicht rendern file_view_raw=Originalformat anzeigen file_permalink=Permalink file_too_large=Die Datei ist zu groß zum Anzeigen. +file_is_empty=Die Datei ist leer. +code_preview_line_from_to=Zeilen %[1]d bis %[2]d in %[3]s +code_preview_line_in=Zeile %[1]d in %[2]s invisible_runes_header=`Diese Datei enthält unsichtbare Unicode-Zeichen` invisible_runes_description=`Diese Datei enthält unsichtbare Unicode-Zeichen, die für Menschen nicht unterscheidbar sind, aber von einem Computer unterschiedlich verarbeitet werden können. Wenn du glaubst, dass das absichtlich so ist, kannst du diese Warnung ignorieren. Benutze den „Escape“-Button, um versteckte Zeichen anzuzeigen.` ambiguous_runes_header=`Diese Datei enthält mehrdeutige Unicode-Zeichen` ambiguous_runes_description=`Diese Datei enthält Unicode-Zeichen, die mit anderen Zeichen verwechselt werden können. Wenn du glaubst, dass das absichtlich so ist, kannst du diese Warnung ignorieren. Benutze den „Escape“-Button, um versteckte Zeichen anzuzeigen.` invisible_runes_line=`Diese Zeile enthält unsichtbare Unicode-Zeichen` ambiguous_runes_line=`Diese Zeile enthält mehrdeutige Unicode-Zeichen` +ambiguous_character=`%[1]c [U+%04[1]X] kann mit %[2]c [U+%04[2]X] verwechselt werden` escape_control_characters=Escapen unescape_control_characters=Unescapen @@ -1259,6 +1323,7 @@ editor.or=oder editor.cancel_lower=Abbrechen editor.commit_signed_changes=Committe signierte Änderungen editor.commit_changes=Änderungen committen +editor.add_tmpl='{filename}' hinzufügen editor.add=%s hinzugefügt editor.update=%s aktualisiert editor.delete=%s gelöscht @@ -1342,6 +1407,7 @@ commitstatus.success=Erfolg ext_issues=Zugriff auf Externe Issues ext_issues.desc=Link zu externem Issuetracker. +projects.desc=Verwalte Issues und Pull-Requests in Projekten. projects.description=Beschreibung (optional) projects.description_placeholder=Beschreibung projects.create=Projekt erstellen @@ -1401,7 +1467,9 @@ issues.new.clear_milestone=Meilenstein entfernen issues.new.assignees=Zuständig issues.new.clear_assignees=Zuständige entfernen issues.new.no_assignees=Niemand zuständig +issues.new.no_reviewers=Keine Reviewer issues.new.blocked_user=Das Issue kann nicht erstellt werden, da du vom Repository-Eigentümer blockiert wurdest. +issues.edit.already_changed=Änderungen zum Issue konnten nicht gespeichert werden. Es scheint, dass der Inhalt bereits von einem anderen Benutzer geändert wurde. Bitte aktualisiere die Seite und bearbeite diese erneut, um zu verhindern, dass die Änderungen des anderen Benutzers überschrieben werden issues.edit.blocked_user=Der Inhalt kann nicht bearbeitet werden, da du vom Repository-Eigentümer blockiert wurdest. issues.choose.get_started=Los geht's issues.choose.open_external_link=Öffnen @@ -1428,6 +1496,7 @@ issues.remove_labels=hat die Labels %s %s entfernt issues.add_remove_labels=hat %s hinzugefügt, und %s %s entfernt issues.add_milestone_at=`hat diesen Issue %[2]s zum %[1]s Meilenstein hinzugefügt` issues.add_project_at=`hat dieses zum %s projekt %s hinzugefügt` +issues.move_to_column_of_project=`hat dies zu %s in %s %s verschoben` issues.change_milestone_at=`hat den Meilenstein %[3]s von %[1]s zu %[2]s geändert` issues.change_project_at=`hat das Projekt %[3]s von %[1]s zu %[2]s geändert` issues.remove_milestone_at=`hat dieses Issue %[2]s vom %[1]s Meilenstein entfernt` @@ -1459,6 +1528,8 @@ 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 +issues.filter_user_no_select=Alle Benutzer issues.filter_type=Typ issues.filter_type.all_issues=Alle Issues issues.filter_type.assigned_to_you=Dir zugewiesen @@ -1512,7 +1583,9 @@ issues.no_content=Keine Beschreibung angegeben. issues.close=Issue schließen issues.comment_pull_merged_at=hat Commit %[1]s in %[2]s %[3]s gemerged issues.comment_manually_pull_merged_at=hat Commit %[1]s in %[2]s %[3]s manuell gemerged +issues.close_comment_issue=Kommentieren und schließen issues.reopen_issue=Wieder öffnen +issues.reopen_comment_issue=Kommentieren und wieder öffnen issues.create_comment=Kommentieren issues.comment.blocked_user=Der Kommentar kann nicht erstellt oder bearbeitet werden, da du vom Repository-Eigentümer blockiert wurdest. issues.closed_at=`hat diesen Issue %[2]s geschlossen` @@ -1601,12 +1674,25 @@ issues.delete.title=Dieses Issue löschen? issues.delete.text=Möchtest du dieses Issue wirklich löschen? (Dadurch wird der Inhalt dauerhaft gelöscht. Denke daran, es stattdessen zu schließen, wenn du es archivieren willst) issues.tracker=Zeiterfassung +issues.timetracker_timer_start=Timer starten +issues.timetracker_timer_stop=Timer stoppen +issues.timetracker_timer_discard=Timer verwerfen +issues.timetracker_timer_manually_add=Zeit hinzufügen +issues.time_estimate_set=Geschätzte Zeit festlegen +issues.time_estimate_display=Schätzung: %s +issues.change_time_estimate_at=Zeitschätzung geändert zu %s %s +issues.remove_time_estimate_at=Zeitschätzung %s entfernt +issues.time_estimate_invalid=Format der Zeitschätzung ist ungültig +issues.start_tracking_history=hat die Zeiterfassung %s gestartet issues.tracker_auto_close=Der Timer wird automatisch gestoppt, wenn dieser Issue geschlossen wird issues.tracking_already_started=`Du hast die Zeiterfassung bereits in diesem Issue gestartet!` +issues.stop_tracking_history=hat für %s gearbeitet %s issues.cancel_tracking_history=`hat die Zeiterfassung %s abgebrochen` issues.del_time=Diese Zeiterfassung löschen +issues.add_time_history=hat %s gearbeitete Zeit hinzugefügt %s issues.del_time_history=`hat %s gearbeitete Zeit gelöscht` +issues.add_time_manually=Zeit manuell hinzufügen issues.add_time_hours=Stunden issues.add_time_minutes=Minuten issues.add_time_sum_to_small=Es wurde keine Zeit eingegeben. @@ -1666,6 +1752,7 @@ issues.dependency.add_error_dep_not_same_repo=Beide Issues müssen sich im selbe issues.review.self.approval=Du kannst nicht dein eigenen Pull-Request genehmigen. issues.review.self.rejection=Du kannst keine Änderungen an deinem eigenen Pull-Request anfragen. issues.review.approve=hat die Änderungen %s genehmigt +issues.review.comment=hat %s überprüft issues.review.dismissed=verwarf %ss Review %s issues.review.dismissed_label=Verworfen issues.review.left_comment=hat einen Kommentar hinterlassen @@ -1691,6 +1778,11 @@ issues.review.resolve_conversation=Diskussion als "erledigt" markieren issues.review.un_resolve_conversation=Diskussion als "nicht-erledigt" markieren issues.review.resolved_by=markierte diese Unterhaltung als gelöst issues.review.commented=Kommentieren +issues.review.official=Genehmigt +issues.review.requested=Prüfung ausstehend +issues.review.rejected=Änderungen angefordert +issues.review.stale=Aktualisiert seit der Genehmigung +issues.review.unofficial=Ungezählte Genehmigung issues.assignee.error=Aufgrund eines unerwarteten Fehlers konnten nicht alle Beauftragten hinzugefügt werden. issues.reference_issue.body=Beschreibung issues.content_history.deleted=gelöscht @@ -1707,6 +1799,8 @@ compare.compare_head=vergleichen pulls.desc=Pull-Requests und Code-Reviews aktivieren. pulls.new=Neuer Pull-Request pulls.new.blocked_user=Der Pull Request kann nicht erstellt werden, da du vom Repository-Eigentümer blockiert wurdest. +pulls.new.must_collaborator=Du musst Mitarbeiter sein, um Pull-Requests zu erstellen. +pulls.edit.already_changed=Änderungen zum Pull-Request konnten nicht gespeichert werden. Es scheint, dass der Inhalt bereits von einem anderen Benutzer geändert wurde. Bitte aktualisieren die Seite und bearbeite diesen erneut, um zu verhindern, dass die Änderungen des anderen Benutzers überschrieben werden pulls.view=Pull-Request ansehen pulls.compare_changes=Neuer Pull-Request pulls.allow_edits_from_maintainers=Änderungen von Maintainern erlauben @@ -1762,6 +1856,8 @@ pulls.is_empty=Die Änderungen an diesem Branch sind bereits auf dem Zielbranch. pulls.required_status_check_failed=Einige erforderliche Prüfungen waren nicht erfolgreich. pulls.required_status_check_missing=Einige erforderliche Prüfungen fehlen. pulls.required_status_check_administrator=Als Administrator kannst du diesen Pull-Request weiterhin mergen. +pulls.blocked_by_approvals=Dieser Pull-Request hat noch nicht genügend Genehmigungen. %d von %d Genehmigungen erteilt. +pulls.blocked_by_approvals_whitelisted=Dieser Pull-Request hat noch nicht genug erforderliche Genehmigungen. %d von %d Genehmigungen von Benutzern oder Teams auf der Berechtigungsliste. pulls.blocked_by_rejection=Dieser Pull-Request hat Änderungen, die von einem offiziellen Reviewer angefragt wurden. pulls.blocked_by_official_review_requests=Dieser Pull Request hat offizielle Review-Anfragen. pulls.blocked_by_outdated_branch=Dieser Pull Request ist blockiert, da er veraltet ist. @@ -1803,7 +1899,9 @@ pulls.unrelated_histories=Merge fehlgeschlagen: Der Head des Merges und die Basi pulls.merge_out_of_date=Merge fehlgeschlagen: Während des Mergens wurde die Basis aktualisiert. Hinweis: Versuche es erneut. pulls.head_out_of_date=Mergen fehlgeschlagen: Der Head wurde aktualisiert während der Merge erstellt wurde. Tipp: Versuche es erneut. pulls.has_merged=Fehler: Der Pull-Request wurde gemerged, du kannst den Zielbranch nicht wieder mergen oder ändern. +pulls.push_rejected=Push fehlgeschlagen: Der Push wurde abgelehnt. Überprüfe die Git Hooks für dieses Repository. pulls.push_rejected_summary=Vollständige Ablehnungsmeldung +pulls.push_rejected_no_message=Push fehlgeschlagen: Der Push wurde abgelehnt, aber es gab keine Fehlermeldung. Überprüfe die Git Hooks für dieses Repository pulls.open_unmerged_pull_exists=`Du kannst diesen Pull-Request nicht erneut öffnen, da noch ein anderer (#%d) mit identischen Eigenschaften offen ist.` pulls.status_checking=Einige Prüfungen sind noch ausstehend pulls.status_checks_success=Alle Prüfungen waren erfolgreich @@ -1827,6 +1925,7 @@ pulls.cmd_instruction_checkout_title=Checkout pulls.cmd_instruction_checkout_desc=Wechsle auf einen neuen Branch in deinem lokalen Repository und teste die Änderungen. pulls.cmd_instruction_merge_title=Mergen pulls.cmd_instruction_merge_desc=Die Änderungen mergen und auf Gitea aktualisieren. +pulls.cmd_instruction_merge_warning=Warnung: Dieser Vorgang kann den Pull-Request nicht mergen, da "manueller Merge" nicht aktiviert wurde pulls.clear_merge_message=Merge-Nachricht löschen pulls.clear_merge_message_hint=Das Löschen der Merge-Nachricht wird nur den Inhalt der Commit-Nachricht entfernen und generierte Git-Trailer wie "Co-Authored-By …" erhalten. @@ -1846,9 +1945,15 @@ pulls.delete.title=Diesen Pull-Request löschen? pulls.delete.text=Willst du diesen Pull-Request wirklich löschen? (Dies wird den Inhalt unwiderruflich löschen. Überlege, ob du ihn nicht lieber schließen willst, um ihn zu archivieren) pulls.recently_pushed_new_branches=Du hast auf den Branch %[1]s %[2]s gepusht +pulls.upstream_diverging_prompt_behind_1=Dieser Branch ist %[1]d Commit hinter %[2]s +pulls.upstream_diverging_prompt_behind_n=Dieser Branch ist %[1]d Commits hinter %[2]s +pulls.upstream_diverging_prompt_base_newer=Der Basis-Branch %s hat neue Änderungen +pulls.upstream_diverging_merge=Fork synchronisieren pull.deleted_branch=(gelöscht):%s +pull.agit_documentation=Dokumentation zu AGit durchschauen +comments.edit.already_changed=Änderungen zum Kommentar konnten nicht gespeichert werden. Es scheint, dass der Inhalt bereits von einem anderen Benutzer geändert wurde. Bitte aktualisiere die Seite und bearbeite diesen erneut, um zu verhindern, dass die Änderungen des anderen Benutzers überschrieben werden milestones.new=Neuer Meilenstein milestones.closed=Geschlossen %s @@ -1857,6 +1962,7 @@ milestones.no_due_date=Kein Fälligkeitsdatum milestones.open=Öffnen milestones.close=Schließen milestones.new_subheader=Benutze Meilensteine, um Issues zu organisieren und den Fortschritt darzustellen. +milestones.completeness=%d%% abgeschlossen milestones.create=Meilenstein erstellen milestones.title=Titel milestones.desc=Beschreibung @@ -2041,12 +2147,14 @@ settings.push_mirror_sync_in_progress=Aktuell werden Änderungen auf %s gepusht. settings.site=Webseite settings.update_settings=Einstellungen speichern settings.update_mirror_settings=Mirror-Einstellungen aktualisieren +settings.branches.switch_default_branch=Standardbranch wechseln settings.branches.update_default_branch=Standardbranch aktualisieren settings.branches.add_new_rule=Neue Regel hinzufügen settings.advanced_settings=Erweiterte Einstellungen settings.wiki_desc=Repository-Wiki aktivieren settings.use_internal_wiki=Eingebautes Wiki verwenden settings.default_wiki_branch_name=Standardbezeichnung für Wiki-Branch +settings.default_wiki_everyone_access=Standard-Zugriffsberechtigung für angemeldete Benutzer: settings.failed_to_change_default_wiki_branch=Das Ändern des Standard-Wiki-Branches ist fehlgeschlagen. settings.use_external_wiki=Externes Wiki verwenden settings.external_wiki_url=Externe Wiki-URL @@ -2077,6 +2185,7 @@ settings.pulls.default_delete_branch_after_merge=Standardmäßig bei Pull-Reques settings.pulls.default_allow_edits_from_maintainers=Änderungen von Maintainern standardmäßig erlauben settings.releases_desc=Repository-Releases aktivieren settings.packages_desc=Repository Packages Registry aktivieren +settings.projects_desc=Projekte aktivieren settings.projects_mode_desc=Projekte-Modus (welche Art Projekte angezeigt werden sollen) settings.projects_mode_repo=Nur Repo-Projekte settings.projects_mode_owner=Nur Benutzer- oder Organisations-Projekte @@ -2116,6 +2225,7 @@ settings.transfer_in_progress=Es gibt derzeit eine laufende Übertragung. Bitte settings.transfer_notices_1=– Du wirst keinen Zugriff mehr haben, wenn der neue Besitzer ein individueller Benutzer ist. settings.transfer_notices_2=– Du wirst weiterhin Zugriff haben, wenn der neue Besitzer eine Organisation ist und du einer der Besitzer bist. settings.transfer_notices_3=- Wenn das Repository privat ist und an einen einzelnen Benutzer übertragen wird, wird sichergestellt, dass der Benutzer mindestens Leserechte hat (und die Berechtigungen werden gegebenenfalls ändert). +settings.transfer_notices_4=- Wenn das Repository einer Organisation gehört und du es an eine andere Organisation oder eine andere Person überträgst, verlierst du die Verlinkungen zwischen den Issues des Repositorys und dem Projektboard der Organisation. settings.transfer_owner=Neuer Besitzer settings.transfer_perform=Übertragung durchführen settings.transfer_started=`Für dieses Repository wurde eine Übertragung eingeleitet und wartet nun auf die Bestätigung von "%s"` @@ -2215,6 +2325,7 @@ settings.event_wiki_desc=Wiki-Seite erstellt, umbenannt, bearbeitet oder gelösc settings.event_release=Release settings.event_release_desc=Release in einem Repository veröffentlicht, aktualisiert oder gelöscht. settings.event_push=Push +settings.event_force_push=Force Push settings.event_push_desc=Git push in ein Repository. settings.event_repository=Repository settings.event_repository_desc=Repository erstellt oder gelöscht. @@ -2251,6 +2362,7 @@ settings.event_pull_request_merge=Pull-Request-Merge settings.event_package=Paket settings.event_package_desc=Paket wurde in einem Repository erstellt oder gelöscht. settings.branch_filter=Branch-Filter +settings.branch_filter_desc=Whitelist für Branches für Push-, Erzeugungs- und Löschevents, als Glob-Pattern beschrieben. Es werden Events für alle Branches gemeldet, falls das Pattern * ist, oder falls es leer ist. Siehe die %[2]s Dokumentation für die Syntax (Englisch). Beispiele: master, {master,release*}. settings.authorization_header=Authorization-Header settings.authorization_header_desc=Wird, falls vorhanden, als Authorization-Header mitgesendet. Beispiele: %s. settings.active=Aktiv @@ -2299,22 +2411,50 @@ settings.branches=Branches settings.protected_branch=Branch-Schutz settings.protected_branch.save_rule=Regel speichern settings.protected_branch.delete_rule=Regel löschen +settings.protected_branch_can_push=Push erlauben? +settings.protected_branch_can_push_yes=Du kannst pushen +settings.protected_branch_can_push_no=Du kannst nicht pushen +settings.branch_protection=Branch-Schutz für Branch '%s' settings.protect_this_branch=Branch-Schutz aktivieren settings.protect_this_branch_desc=Verhindert das Löschen und schränkt Git auf Push- und Merge-Änderungen auf dem Branch ein. settings.protect_disable_push=Push deaktivieren settings.protect_disable_push_desc=Kein Push auf diesen Branch erlauben. +settings.protect_disable_force_push=Force-Push deaktivieren +settings.protect_disable_force_push_desc=Force-Push auf diesen Branch nicht erlauben. settings.protect_enable_push=Push aktivieren settings.protect_enable_push_desc=Jeder, der Schreibzugriff hat, darf in diesen Branch Pushen (aber kein Force-Push). +settings.protect_enable_force_push_all=Force-Push aktivieren +settings.protect_enable_force_push_all_desc=Jeder mit Push-Zugriff wird in diesen Branch force-pushen können. +settings.protect_enable_force_push_allowlist=Force-Push beschränkt auf Genehmigungsliste +settings.protect_enable_force_push_allowlist_desc=Nur Benutzer oder Teams auf der Genehmigungsliste mit Push-Zugriff werden in diesen Branch force-pushen können. settings.protect_enable_merge=Merge aktivieren settings.protect_enable_merge_desc=Jeder mit Schreibzugriff darf die Pull-Requests in diesen Branch mergen. +settings.protect_whitelist_committers=Genehmigungsliste für eingeschränkten Push +settings.protect_whitelist_committers_desc=Jeder, der auf der Genehmigungsliste steht, darf in diesen Branch pushen (aber kein Force-Push). +settings.protect_whitelist_deploy_keys=Genehmigungsliste für Deploy-Schlüssel mit Schreibzugriff zum Pushen. +settings.protect_whitelist_users=Nutzer, die pushen dürfen: +settings.protect_whitelist_teams=Teams, die pushen dürfen: +settings.protect_force_push_allowlist_users=Erlaubte Benutzer für Force-Push: +settings.protect_force_push_allowlist_teams=Erlaubte Teams für Force-Push: +settings.protect_force_push_allowlist_deploy_keys=Genehmigungsliste für Deploy-Schlüssel mit Schreibzugriff zum Force-Push. +settings.protect_merge_whitelist_committers=Merge-Genehmigungsliste aktivieren +settings.protect_merge_whitelist_committers_desc=Erlaube Nutzern oder Teams auf der Genehmigungsliste Pull-Requests in diesen Branch zu mergen. +settings.protect_merge_whitelist_users=Nutzer, die mergen dürfen: +settings.protect_merge_whitelist_teams=Teams, die mergen dürfen: settings.protect_check_status_contexts=Statusprüfungen aktivieren settings.protect_status_check_patterns=Statuscheck-Muster: settings.protect_status_check_patterns_desc=Gib Muster ein, um festzulegen, welche Statusüberprüfungen durchgeführt werden müssen, bevor Branches in einen Branch, der dieser Regel entspricht, gemerged werden können. Jede Zeile gibt ein Muster an. Muster dürfen nicht leer sein. +settings.protect_check_status_contexts_desc=Vor dem Mergen müssen Statusprüfungen bestanden werden. Wähle aus, welche Statusprüfungen erfolgreich durchgeführt werden müssen, bevor Branches in einen anderen gemergt werden können, der dieser Regel entspricht. Wenn aktiviert, müssen Commits zuerst auf einen anderen Branch gepusht werden, dann nach bestandener Statusprüfung gemergt oder direkt auf einen Branch gepusht werden, der dieser Regel entspricht. Wenn kein Kontext ausgewählt ist, muss der letzte Commit unabhängig vom Kontext erfolgreich sein. settings.protect_check_status_contexts_list=Statusprüfungen, die in der letzten Woche für dieses Repository gefunden wurden settings.protect_status_check_matched=Übereinstimmung settings.protect_invalid_status_check_pattern=Ungültiges Muster: "%s". settings.protect_no_valid_status_check_patterns=Keine gültigen Statuscheck-Muster. settings.protect_required_approvals=Erforderliche Zustimmungen: +settings.protect_required_approvals_desc=Erlaube das Mergen des Pull-Requests nur mit genügend Genehmigungen. +settings.protect_approvals_whitelist_enabled=Genehmigungen auf Benutzer oder Teams auf der Genehmigungsliste beschränken +settings.protect_approvals_whitelist_enabled_desc=Nur Bewertungen von Benutzern auf der Genehmigungsliste oder Teams zählen zu den erforderlichen Genehmigungen. Gibt es keine Genehmigungsliste, so zählen Reviews von jedem mit Schreibzugriff zu den erforderlichen Genehmigungen. +settings.protect_approvals_whitelist_users=Freigeschaltete Reviewer: +settings.protect_approvals_whitelist_teams=Freigeschaltete Teams: settings.dismiss_stale_approvals=Entferne alte Genehmigungen settings.dismiss_stale_approvals_desc=Wenn neue Commits gepusht werden, die den Inhalt des Pull-Requests ändern, werden alte Genehmigungen entfernt. settings.ignore_stale_approvals=Veraltete Genehmigungen ignorieren @@ -2322,12 +2462,18 @@ settings.ignore_stale_approvals_desc=Genehmigungen, die für ältere Commits ert settings.require_signed_commits=Signierte Commits erforderlich settings.require_signed_commits_desc=Pushes auf diesen Branch ablehnen, wenn Commits nicht signiert oder nicht überprüfbar sind. settings.protect_branch_name_pattern=Muster für geschützte Branchnamen +settings.protect_branch_name_pattern_desc=Geschützte Branch-Namensmuster. Siehe die Dokumentation für die Pattern-Syntax. Beispiele: main, release/** settings.protect_patterns=Muster settings.protect_protected_file_patterns=Geschützte Dateimuster (durch Semikolon ';' getrennt): +settings.protect_protected_file_patterns_desc=Geschützte Dateien dürfen nicht direkt geändert werden, auch wenn der Benutzer Rechte hat, Dateien in diesem Branch hinzuzufügen, zu bearbeiten oder zu löschen. Mehrere Muster können mit Semikolon (';') getrennt werden. Siehe %[2]s Dokumentation zur Pattern-Syntax. Beispiele: .drone.yml, /docs/**/*.txt. settings.protect_unprotected_file_patterns=Ungeschützte Dateimuster (durch Semikolon ';' getrennt): +settings.protect_unprotected_file_patterns_desc=Ungeschützte Dateien, die direkt geändert werden dürfen, wenn der Benutzer Schreibzugriff hat, können die Push-Beschränkung umgehen. Mehrere Muster können mit Semikolon (';') getrennt werden. Siehe %[2]s Dokumentation zur Mustersyntax. Beispiele: .drone.yml, /docs/**/*.txt. +settings.add_protected_branch=Schutz aktivieren +settings.delete_protected_branch=Schutz deaktivieren settings.update_protect_branch_success=Branchschutzregel "%s" wurde geändert. settings.remove_protected_branch_success=Branchschutzregel "%s" wurde deaktiviert. settings.remove_protected_branch_failed=Entfernen der Branchschutzregel "%s" fehlgeschlagen. +settings.protected_branch_deletion=Branch-Schutz deaktivieren settings.protected_branch_deletion_desc=Wenn du den Branch-Schutz deaktivierst, können alle Nutzer mit Schreibrechten auf den Branch pushen. Fortfahren? settings.block_rejected_reviews=Merge bei abgelehnten Reviews blockieren settings.block_rejected_reviews_desc=Mergen ist nicht möglich, wenn Änderungen durch offizielle Reviewer angefragt werden, auch wenn es genügend Zustimmungen gibt. @@ -2335,8 +2481,11 @@ settings.block_on_official_review_requests=Mergen bei offiziellen Review-Anfrage settings.block_on_official_review_requests_desc=Mergen ist nicht möglich wenn offizielle Review-Anfrangen vorliegen, selbst wenn es genügend Zustimmungen gibt. settings.block_outdated_branch=Merge blockieren, wenn der Pull-Request veraltet ist settings.block_outdated_branch_desc=Mergen ist nicht möglich, wenn der Head-Branch hinter dem Basis-Branch ist. +settings.block_admin_merge_override=Administratoren müssen die Schutzregeln für Branches befolgen +settings.block_admin_merge_override_desc=Administratoren müssen die Schutzregeln für Branches befolgen und können sie nicht umgehen. settings.default_branch_desc=Wähle einen Standardbranch für Pull-Requests und Code-Commits: settings.merge_style_desc=Merge-Styles +settings.default_merge_style_desc=Standard-Mergeverhalten für Pull-Requests settings.choose_branch=Branch wählen… settings.no_protected_branch=Es gibt keine geschützten Branches. settings.edit_protected_branch=Bearbeiten @@ -2352,12 +2501,25 @@ settings.tags.protection.allowed.teams=Erlaubte Teams settings.tags.protection.allowed.noone=Niemand settings.tags.protection.create=Tag schützen settings.tags.protection.none=Es gibt keine geschützten Tags. +settings.tags.protection.pattern.description=Du kannst einen einzigen Namen oder ein globales Schema oder einen regulären Ausdruck verwenden, um mehrere Tags zu schützen. Mehr dazu im Guide für geschützte Tags (Englisch). settings.bot_token=Bot-Token settings.chat_id=Chat-ID settings.thread_id=Thread-ID settings.matrix.homeserver_url=Homeserver-URL settings.matrix.room_id=Raum-ID settings.matrix.message_type=Nachrichtentyp +settings.visibility.private.button=Auf privat setzen +settings.visibility.private.text=Das Ändern der Sichtbarkeit auf privat wird das Repository nicht nur für erlaubte Mitglieder sichtbar machen, sondern kann auch die Beziehung zwischen ihm und Forks, Beobachtern und Sternen entfernen. +settings.visibility.private.bullet_title=Das Ändern der Sichtbarkeit auf privat wird: +settings.visibility.private.bullet_one=Das Repository nur für zugelassene Mitglieder sichtbar machen. +settings.visibility.private.bullet_two=Kann die Beziehung zwischen ihm und Forks, Beobachternund Sternen entfernen. +settings.visibility.public.button=Auf öffentlich setzen +settings.visibility.public.text=Das Ändern der Sichtbarkeit auf öffentlich macht das Repository für jeden sichtbar. +settings.visibility.public.bullet_title=Das Ändern der Sichtbarkeit auf öffentlich wird: +settings.visibility.public.bullet_one=Das Repository für jeden sichtbar machen. +settings.visibility.success=Die Sichtbarkeit des Repositorys wurde geändert. +settings.visibility.error=Beim Versuch, die Sichtbarkeit des Repositorys zu ändern, ist ein Fehler aufgetreten. +settings.visibility.fork_error=Die Sichtbarkeit von geforkten Repositories ist nicht veränderbar. settings.archive.button=Repo archivieren settings.archive.header=Dieses Repo archivieren settings.archive.text=Durch das Archivieren wird ein Repo vollständig schreibgeschützt. Es wird vom Dashboard versteckt. Niemand (nicht einmal du!) wird in der Lage sein, neue Commits zu erstellen oder Issues oder Pull-Requests zu öffnen. @@ -2469,6 +2631,7 @@ release.new_release=Neues Release release.draft=Entwurf release.prerelease=Pre-Release release.stable=Stabil +release.latest=Aktuell release.compare=Vergleichen release.edit=bearbeiten release.ahead.commits=%d Commits @@ -2553,6 +2716,7 @@ tag.create_success=Tag "%s" wurde erstellt. topic.manage_topics=Themen verwalten topic.done=Fertig +topic.count_prompt=Du kannst nicht mehr als 25 Themen auswählen topic.format_prompt=Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) und Punkte ('.') enthalten und bis zu 35 Zeichen lang sein. Nur Kleinbuchstaben sind zulässig. find_file.go_to_file=Datei suchen @@ -2650,6 +2814,7 @@ teams.leave.detail=%s verlassen? teams.can_create_org_repo=Repositories erstellen teams.can_create_org_repo_helper=Mitglieder können neue Repositories in der Organisation erstellen. Der Ersteller erhält Administrator-Zugriff auf das neue Repository. teams.none_access=Kein Zugriff +teams.none_access_helper=Mitglieder können keine anderen Aktionen für diese Einheit anzeigen oder durchführen. Dies hat keine Wirkung auf öffentliche Repositories. teams.general_access=Allgemeiner Zugriff teams.general_access_helper=Mitgliederberechtigungen werden durch folgende Berechtigungstabelle festgelegt. teams.read_access=Lesen @@ -2695,7 +2860,9 @@ teams.invite.title=Du wurdest eingeladen, dem Team %s in der Or teams.invite.by=Von %s eingeladen teams.invite.description=Bitte klicke auf die folgende Schaltfläche, um dem Team beizutreten. + [admin] +maintenance=Wartung dashboard=Dashboard self_check=Selbstprüfung identity_access=Identität & Zugriff @@ -2717,7 +2884,9 @@ last_page=Letzte total=Gesamt: %d settings=Administratoreinstellungen +dashboard.new_version_hint=Gitea %s ist jetzt verfügbar, deine derzeitige Version ist %s. Weitere Details findest du im Blog. dashboard.statistic=Übersicht +dashboard.maintenance_operations=Wartungsoperationen dashboard.system_status=System-Status dashboard.operation_name=Name der Operation dashboard.operation_switch=Wechseln @@ -2758,6 +2927,7 @@ dashboard.reinit_missing_repos=Alle Git-Repositories neu einlesen, für die Eint dashboard.sync_external_users=Externe Benutzerdaten synchronisieren dashboard.cleanup_hook_task_table=Hook-Task-Tabelle bereinigen dashboard.cleanup_packages=Veraltete Pakete löschen +dashboard.cleanup_actions=Abgelaufene Ressourcen von Actions bereinigen dashboard.server_uptime=Server-Uptime dashboard.current_goroutine=Aktuelle Goroutinen dashboard.current_memory_usage=Aktuelle Speichernutzung @@ -2787,12 +2957,19 @@ dashboard.total_gc_time=Gesamte GC-Pause dashboard.total_gc_pause=Gesamte GC-Pause dashboard.last_gc_pause=Letzte GC-Pause dashboard.gc_times=Anzahl GC +dashboard.delete_old_actions=Alle alten Aktionen aus der Datenbank löschen +dashboard.delete_old_actions.started=Löschen aller alten Aktionen in der Datenbank gestartet. dashboard.update_checker=Update-Checker dashboard.delete_old_system_notices=Alle alten Systemmeldungen aus der Datenbank löschen dashboard.gc_lfs=Garbage-Collection für LFS Meta-Objekte ausführen +dashboard.stop_zombie_tasks=Zombie-Aufgaben stoppen +dashboard.stop_endless_tasks=Endlose Aktionen stoppen +dashboard.cancel_abandoned_jobs=Aufgegebene Jobs abbrechen +dashboard.start_schedule_tasks=Terminierte Aufgaben starten dashboard.sync_branch.started=Synchronisierung der Branches gestartet dashboard.sync_tag.started=Tag-Synchronisierung gestartet dashboard.rebuild_issue_indexer=Issue-Indexer neu bauen +dashboard.sync_repo_licenses=Repo-Lizenzen synchronisieren users.user_manage_panel=Benutzerkontenverwaltung users.new_account=Benutzerkonto erstellen @@ -2864,6 +3041,10 @@ emails.not_updated=Fehler beim Aktualisieren der angeforderten E-Mail-Adresse: % emails.duplicate_active=Diese E-Mail-Adresse wird bereits von einem Nutzer verwendet. emails.change_email_header=E-Mail-Eigenschaften aktualisieren emails.change_email_text=Bist du dir sicher, dass du diese E-Mail-Adresse aktualisieren möchtest? +emails.delete=E-Mail löschen +emails.delete_desc=Willst du diese E-Mail-Adresse wirklich löschen? +emails.deletion_success=Die E-Mail-Adresse wurde gelöscht. +emails.delete_primary_email_error=Du kannst die primäre E-Mail-Adresse nicht löschen. orgs.org_manage_panel=Organisationsverwaltung orgs.name=Name @@ -2896,10 +3077,12 @@ packages.size=Größe packages.published=Veröffentlicht defaulthooks=Standard-Webhooks +defaulthooks.desc=Webhooks senden automatisch eine HTTP-POST-Anfrage an einen Server, wenn bestimmte Gitea-Events ausgelöst werden. Hier definierte Webhooks sind die Standardwerte, die in alle neuen Repositories kopiert werden. Mehr Infos findest du in der Webhooks-Anleitung (auf Englisch). defaulthooks.add_webhook=Standard-Webhook hinzufügen defaulthooks.update_webhook=Standard-Webhook aktualisieren systemhooks=System-Webhooks +systemhooks.desc=Webhooks senden automatisch HTTP-POST-Anfragen an einen Server, wenn bestimmte Gitea-Events ausgelöst werden. Hier definierte Webhooks werden auf alle Repositories des Systems übertragen, beachte daher mögliche Performance-Einbrüche. Mehr Infos findest du in der Webhooks-Anleitung (auf Englisch). systemhooks.add_webhook=System-Webhook hinzufügen systemhooks.update_webhook=System-Webhook aktualisieren @@ -2994,7 +3177,18 @@ auths.tips=Tipps auths.tips.oauth2.general=OAuth2-Authentifizierung auths.tips.oauth2.general.tip=Beim Registrieren einer OAuth2-Anwendung sollte die Callback-URL folgendermaßen lauten: auths.tip.oauth2_provider=OAuth2-Anbieter +auths.tip.bitbucket=Registriere einen neuen OAuth-Consumer unter %s und füge die Berechtigung 'Account' - 'Read' hinzu auths.tip.nextcloud=Registriere über das "Settings -> Security -> OAuth 2.0 client"-Menü einen neuen "OAuth consumer" auf der Nextcloud-Instanz +auths.tip.dropbox=Erstelle eine neue App auf %s +auths.tip.facebook=Erstelle eine neue Anwendung auf %s und füge das Produkt "Facebook Login“ hinzu +auths.tip.github=Erstelle unter %s eine neue OAuth-Anwendung +auths.tip.gitlab_new=Erstelle eine neue Anwendung unter %s +auths.tip.google_plus=Du erhältst die OAuth2-Client-Zugangsdaten in der Google-API-Konsole unter %s +auths.tip.openid_connect=Benutze die OpenID-Connect-Discovery-URL "https://{server}/.well-known/openid-configuration", um die Endpunkte zu spezifizieren +auths.tip.twitter=Gehe zu %s, erstelle eine Anwendung und stelle sicher, dass die Option „Allow this application to be used to Sign in with Twitter“ aktiviert ist +auths.tip.discord=Erstelle unter %s eine neue Anwendung +auths.tip.gitea=Registriere eine neue OAuth2-Anwendung. Eine Anleitung findest du unter %s +auths.tip.yandex=`Erstelle eine neue Anwendung auf %s. Wähle folgende Berechtigungen aus dem "Yandex.Passport API" Bereich: "Zugriff auf E-Mail-Adresse", "Zugriff auf Benutzeravatar" und "Zugriff auf Benutzername, Vor- und Nachname, Geschlecht"` auths.tip.mastodon=Gebe eine benutzerdefinierte URL für die Mastodon-Instanz ein, mit der du dich authentifizieren möchtest (oder benutze die standardmäßige) auths.edit=Authentifikationsquelle bearbeiten auths.activated=Diese Authentifikationsquelle ist aktiviert @@ -3110,6 +3304,10 @@ config.cache_adapter=Cache-Adapter config.cache_interval=Cache-Intervall config.cache_conn=Cache-Anbindung config.cache_item_ttl=Cache Item-TTL +config.cache_test=Cache testen +config.cache_test_failed=Fehler beim Prüfen des Caches: %v. +config.cache_test_slow=Cache-Test erfolgreich, aber die Antwortzeit ist langsam: %s. +config.cache_test_succeeded=Cache-Test erfolgreich, Antwort in %s erhalten. config.session_config=Session-Konfiguration config.session_provider=Session-Provider @@ -3156,6 +3354,7 @@ monitor.next=Nächste Ausführung monitor.previous=Letzte Ausführung monitor.execute_times=Ausführungen monitor.process=Laufende Prozesse +monitor.stacktrace=Stacktraces monitor.processes_count=%d Prozesse monitor.download_diagnosis_report=Diagnosebericht herunterladen monitor.desc=Beschreibung @@ -3163,6 +3362,8 @@ monitor.start=Startzeit monitor.execute_time=Ausführungszeit monitor.last_execution_result=Ergebnis monitor.process.cancel=Prozess abbrechen +monitor.process.cancel_desc=Abbrechen eines Prozesses kann Datenverlust verursachen +monitor.process.cancel_notices=Abbrechen: %s? monitor.process.children=Subprozesse monitor.queues=Warteschlangen @@ -3201,11 +3402,13 @@ notices.op=Aktion notices.delete_success=Diese Systemmeldung wurde gelöscht. self_check.no_problem_found=Bisher wurde kein Problem festgestellt. +self_check.startup_warnings=Warnungen beim Start: self_check.database_collation_mismatch=Erwarte Datenbank-Kollation: %s self_check.database_collation_case_insensitive=Die Datenbank verwendet die Kollation %s, was eine unsensible Kollation ist. Obwohl Gitea damit arbeiten könnte, gibt es vielleicht einige seltene Fälle, die nicht wie erwartet funktionieren. self_check.database_inconsistent_collation_columns=Die Datenbank verwendet die Kollation %s, aber diese Spalten verwenden unzutreffende Kollationen. Dies könnte zu unerwarteten Problemen führen. self_check.database_fix_mysql=Für MySQL/MariaDB-Benutzer kann man den Befehl "gitea doctor convert" oder manuell auch "ALTER ... COLLATE ..."-SQLs verwenden, um die Sortierprobleme zu beheben. self_check.database_fix_mssql=Für MSSQL-Benutzer kann das Problem im Moment nur durch "ALTER ... COLLATE ..." SQLs manuell behoben werden. +self_check.location_origin_mismatch=Aktuelle URL (%[1]s) stimmt nicht mit der URL überein, die Gitea (%[2]s) sieht. Wenn du einen Reverse-Proxy verwendest, stelle bitte sicher, dass die Header "Host" und "X-Forwarded-Proto" korrekt gesetzt sind. [action] create_repo=hat das Repository %s erstellt @@ -3233,6 +3436,7 @@ mirror_sync_create=neue Referenz %[3]s bei % mirror_sync_delete=hat die Referenz des Mirrors %[2]s in %[3]s synchronisiert und gelöscht approve_pull_request=`hat %[3]s#%[2]s approved` reject_pull_request=`schlug Änderungen für %[3]s#%[2]s vor` +publish_release=`veröffentlichte Release "%[4]s" in %[3]s` review_dismissed=`verwarf das Review von %[4]s in %[3]s#%[2]s` review_dismissed_reason=Grund: create_branch=legte den Branch %[3]s in %[4]s an @@ -3261,6 +3465,8 @@ raw_minutes=Minuten [dropzone] default_message=Zum Hochladen hier klicken oder Datei ablegen. +invalid_input_type=Dateien dieses Dateityps können nicht hochgeladen werden. +file_too_big=Dateigröße ({{filesize}} MB) überschreitet die Maximalgröße ({{maxFilesize}} MB). remove_file=Datei entfernen [notification] @@ -3297,6 +3503,7 @@ error.unit_not_allowed=Du hast keine Berechtigung, um auf diesen Repository-Bere title=Pakete desc=Repository-Pakete verwalten. empty=Noch keine Pakete vorhanden. +no_metadata=Keine Metadaten. empty.documentation=Weitere Informationen zur Paket-Registry findest Du in der Dokumentation. empty.repo=Hast du ein Paket hochgeladen, das hier nicht angezeigt wird? Gehe zu den Paketeinstellungen und verlinke es mit diesem Repo. registry.documentation=Für weitere Informationen zur %s-Registry, schaue in der Dokumentation nach. @@ -3331,6 +3538,8 @@ alpine.repository=Repository-Informationen alpine.repository.branches=Branches alpine.repository.repositories=Repositories alpine.repository.architectures=Architekturen +arch.registry=Server mit gebrauchtem Repository und Architektur zu /etc/pacman.conf hinzufügen: +arch.install=Paket mit pacman synchronisieren: arch.repository=Repository-Informationen arch.repository.repositories=Repositories arch.repository.architectures=Architekturen @@ -3381,6 +3590,7 @@ npm.install=Um das Paket mit npm zu installieren, führe den folgenden Befehl au npm.install2=oder füge es zur package.json-Datei hinzu: npm.dependencies=Abhängigkeiten npm.dependencies.development=Entwicklungsabhängigkeiten +npm.dependencies.bundle=Gebündelte Abhängigkeiten npm.dependencies.peer=Peer Abhängigkeiten npm.dependencies.optional=Optionale Abhängigkeiten npm.details.tag=Tag @@ -3512,6 +3722,7 @@ runners.status.active=Aktiv runners.status.offline=Offline runners.version=Version runners.reset_registration_token=Registrierungs-Token zurücksetzen +runners.reset_registration_token_confirm=Möchtest du den aktuellen Token invalidieren und einen neuen generieren? runners.reset_registration_token_success=Runner-Registrierungstoken erfolgreich zurückgesetzt runs.all_workflows=Alle Workflows @@ -3521,6 +3732,7 @@ runs.pushed_by=gepusht von runs.invalid_workflow_helper=Die Workflow-Konfigurationsdatei ist ungültig. Bitte überprüfe Deine Konfigurationsdatei: %s runs.no_matching_online_runner_helper=Kein passender Runner online mit Label: %s runs.no_job_without_needs=Der Workflow muss mindestens einen Job ohne Abhängigkeiten enthalten. +runs.no_job=Der Workflow muss mindestens einen Job enthalten runs.actor=Initiator runs.status=Status runs.actors_no_select=Alle Initiatoren @@ -3531,12 +3743,18 @@ runs.no_workflows.quick_start=Du weißt nicht, wie du mit Gitea Actions loslegst runs.no_workflows.documentation=Weitere Informationen zu Gitea Actions findest du in der Dokumentation. runs.no_runs=Der Workflow hat noch keine Ausführungen. runs.empty_commit_message=(leere Commit-Nachricht) +runs.expire_log_message=Protokolle wurden geleert, weil sie zu alt waren. workflow.disable=Workflow deaktivieren workflow.disable_success=Workflow '%s' erfolgreich deaktiviert. workflow.enable=Workflow aktivieren workflow.enable_success=Workflow '%s' erfolgreich aktiviert. workflow.disabled=Workflow ist deaktiviert. +workflow.run=Workflow ausführen +workflow.not_found=Workflow '%s' wurde nicht gefunden. +workflow.run_success=Workflow '%s' erfolgreich ausgeführt. +workflow.from_ref=Nutze Workflow von +workflow.has_workflow_dispatch=Dieser Workflow hat einen workflow_dispatch Event-Trigger. need_approval_desc=Um Workflows für den Pull-Request eines Forks auszuführen, ist eine Genehmigung erforderlich. @@ -3556,8 +3774,11 @@ variables.creation.success=Die Variable „%s“ wurde hinzugefügt. variables.update.failed=Fehler beim Bearbeiten der Variable. variables.update.success=Die Variable wurde bearbeitet. +logs.always_auto_scroll=Autoscroll für Logs immer aktivieren +logs.always_expand_running=Laufende Logs immer erweitern [projects] +deleted.display_name=Gelöschtes Projekt type-1.display_name=Individuelles Projekt type-2.display_name=Repository-Projekt type-3.display_name=Organisationsprojekt diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini index db7b3ffae3..31e57bbf97 100644 --- a/options/locale/locale_el-GR.ini +++ b/options/locale/locale_el-GR.ini @@ -580,6 +580,7 @@ joined_on=ΕγγÏάφηκε την %s repositories=ΑποθετήÏια activity=Δημόσια ΔÏαστηÏιότητα followers=Ακόλουθοι +show_more=Εμφάνιση ΠεÏισσότεÏων starred=Αγαπημένα ΑποθετήÏια watched=ΑκολουθοÏμενα ΑποθετήÏια code=Κώδικας @@ -907,7 +908,6 @@ new_repo_helper=Ένα αποθετήÏιο πεÏιέχει όλα τα αÏχ owner=Ιδιοκτήτης owner_helper=ΟÏισμένοι οÏγανισμοί ενδέχεται να μην εμφανίζονται στο αναπτυσσόμενο Î¼ÎµÎ½Î¿Ï Î»ÏŒÎ³Ï‰ του μέγιστου αÏÎ¹Î¸Î¼Î¿Ï Î±Ï€Î¿Î¸ÎµÏ„Î·Ïίων. repo_name=Όνομα αποθετηÏίου -repo_name_helper=Τα καλά ονόματα αποθετηÏίων χÏησιμοποιοÏν σÏντομες, αξέχαστες και μοναδικές λέξεις-κλειδιά. repo_size=Μέγεθος ΑποθετηÏίου template=ΠÏότυπο template_select=Επιλέξτε Ï€Ïότυπο. @@ -2592,6 +2592,7 @@ teams.invite.title=Έχετε Ï€Ïοσκληθεί να συμμετάσχετε teams.invite.by=ΠÏοσκλήθηκε από %s teams.invite.description=ΠαÏακαλώ κάντε κλικ στον παÏακάτω σÏνδεσμο για συμμετοχή στην ομάδα. + [admin] dashboard=Πίνακας Ελέγχου identity_access=Ταυτότητα & ΠÏόσβαση diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 16d894aa26..6029d49ade 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -244,6 +244,7 @@ license_desc = Go get [install] install = Installation +installing_desc = Installing now, please wait... title = Initial Configuration docker_helper = If you run Gitea inside Docker, please read the documentation before changing any settings. require_db_desc = Gitea requires MySQL, PostgreSQL, MSSQL, SQLite3 or TiDB (MySQL protocol). @@ -1015,7 +1016,9 @@ new_repo_helper = A repository contains all project files, including revision hi owner = Owner owner_helper = Some organizations may not show up in the dropdown due to a maximum repository count limit. repo_name = Repository Name -repo_name_helper = Good repository names use short, memorable and unique keywords. +repo_name_profile_public_hint= .profile is a special repository that you can use to add README.md to your public organization profile, visible to anyone. Make sure it’s public and initialize it with a README in the profile directory to get started. +repo_name_profile_private_hint = .profile-private is a special repository that you can use to add a README.md to your organization member profile, visible only to organization members. Make sure it’s private and initialize it with a README in the profile directory to get started. +repo_name_helper = Good repository names use short, memorable and unique keywords. A repository named ".profile" or ".profile-private" could be used to add a README.md for the user/organization profile. repo_size = Repository Size template = Template template_select = Select a template. @@ -2862,6 +2865,10 @@ teams.invite.title = You have been invited to join team %s in o teams.invite.by = Invited by %s teams.invite.description = Please click the button below to join the team. +view_as_role = View as: %s +view_as_public_hint = You are viewing the README as a public user. +view_as_member_hint = You are viewing the README as a member of this organization. + [admin] maintenance = Maintenance dashboard = Dashboard @@ -3531,6 +3538,7 @@ versions = Versions versions.view_all = View all dependency.id = ID dependency.version = Version +search_in_external_registry = Search in %s alpine.registry = Setup this registry by adding the url in your /etc/apk/repositories file: alpine.registry.key = Download the registry public RSA key into the /etc/apk/keys/ folder to verify the index signature: alpine.registry.info = Choose $branch and $repository from the list below. diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index 010bee186d..cdfe1fb2e5 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -577,6 +577,7 @@ joined_on=Se unió el %s repositories=Repositorios activity=Actividad pública followers=Seguidores +show_more=Ver más starred=Repositorios Favoritos watched=Repositorios seguidos code=Código @@ -897,7 +898,6 @@ visibility.private_tooltip=Visible sólo para los miembros de organizaciones a l owner=Propietario owner_helper=Algunas organizaciones pueden no aparecer en el menú desplegable debido a un límite máximo de recuento de repositorios. repo_name=Nombre del repositorio -repo_name_helper=Un buen nombre de repositorio está compuesto por palabras clave cortas, memorables y únicas. repo_size=Tamaño del repositorio template=Plantilla template_select=Seleccionar una plantilla. @@ -2573,6 +2573,7 @@ teams.invite.title=Has sido invitado a unirte al equipo %s en l teams.invite.by=Invitado por %s teams.invite.description=Por favor, haga clic en el botón de abajo para unirse al equipo. + [admin] dashboard=Panel de control identity_access=Identidad y acceso diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini index b92361db34..4d90cf9876 100644 --- a/options/locale/locale_fa-IR.ini +++ b/options/locale/locale_fa-IR.ini @@ -463,6 +463,7 @@ change_avatar=تغییر آواتار… repositories=مخازن activity=ÙØ¹Ø§Ù„یت های عمومی followers=دنبال کنندگان +show_more=نمایش بیشتر starred=مخان ستاره دار watched=مخازنی Ú©Ù‡ دنبال می‌شوند projects=پروژه‌ها @@ -703,7 +704,6 @@ visibility.private=خصوصی owner=مالک owner_helper=بخاطر بیشینه تعداد مخزن، ممکن است برخی از سازمان‌ها در لیست کشویی دیده نشود. repo_name=نام مخزن -repo_name_helper=نام خوب مخزن معمولا از کلمات کلیدی کوتاه Ùˆ به یاد ماندنی Ùˆ منحصر به ÙØ±Ø¯ تشکیل شده است. repo_size=اندازه مخزن template=قالب / الگو template_select=انتخاب یک قالب/ الگو. @@ -1992,6 +1992,7 @@ teams.all_repositories_read_permission_desc=این تیم دسترسی teams.all_repositories_write_permission_desc=این تیم دسترسی نوشتن مخازن همه را Ù…ÛŒ بخشد: اعضا Ù…ÛŒ توانند مخازن را مشاهده Ùˆ درج کنند. teams.all_repositories_admin_permission_desc=این تیم دسترسی مدیر به مخازن همه را Ù…ÛŒ بخشد: اعضا Ù…ÛŒ توانند مخازن را بخواند، همکار Ùˆ مخزن اضاÙÙ‡ کنند. + [admin] dashboard=پیشخوان users=حساب کاربران diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini index b4f3869db4..b5fa5c8afc 100644 --- a/options/locale/locale_fi-FI.ini +++ b/options/locale/locale_fi-FI.ini @@ -635,7 +635,6 @@ visibility.private=Yksityinen owner=Omistaja owner_helper=Jotkin organisaatiot eivät välttämättä näy pudotusvalikossa, koska repojen maksimimäärää on rajoitettu. repo_name=Repon nimi -repo_name_helper=Hyvä repon nimi on lyhyt, mieleenpainuva ja yksilöllinen. repo_size=Repon koko template=Malli template_select=Valitse malli. @@ -1361,6 +1360,7 @@ teams.repositories=Tiimin repot teams.members.none=Ei jäseniä tässä tiimissä. teams.all_repositories=Kaikki repot + [admin] dashboard=Kojelauta users=Käyttäjätilit diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index 0898d0fd29..743b766256 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -649,6 +649,7 @@ joined_on=Inscrit le %s repositories=Dépôts activity=Activité publique followers=abonnés +show_more=Voir plus starred=Dépôts favoris watched=Dépôts surveillés code=Code @@ -1014,7 +1015,6 @@ new_repo_helper=Un dépôt contient tous les fichiers d’un projet, ainsi que l owner=Propriétaire owner_helper=Certaines organisations peuvent ne pas apparaître dans la liste déroulante en raison d'une limite maximale du nombre de dépôts. repo_name=Nom du dépôt -repo_name_helper=Idéalement, le nom d'un dépôt devrait être court, mémorisable et unique. repo_size=Taille du dépôt template=Modèle template_select=Répliquer un modèle @@ -2860,6 +2860,7 @@ teams.invite.title=Vous avez été invité à rejoindre l'équipe %s%[1]s %[2]s +pulls.upstream_diverging_prompt_behind_1=Tá an brainse seo %[1]d tiomantas taobh thiar de %[2]s +pulls.upstream_diverging_prompt_behind_n=Tá an brainse seo %[1]d geallta taobh thiar de %[2]s pulls.upstream_diverging_prompt_base_newer=Tá athruithe nua ar an mbunbhrainse %s pulls.upstream_diverging_merge=Forc sionc @@ -2858,6 +2860,7 @@ teams.invite.title=Tugadh cuireadh duit dul isteach i bhfoireann %spermessi teams.all_repositories_write_permission_desc=Questo team concede permessi di scrittura accesso a tutte le repository: i membri possono leggere e pushare le repository. teams.all_repositories_admin_permission_desc=Questo team concede a Amministratore l'accesso a tutte le repository: i membri possono leggere, pushare e aggiungere collaboratori alle repository. + [admin] dashboard=Pannello di Controllo users=Account utenti diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index da07d77825..89582af89c 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -649,6 +649,7 @@ joined_on=%sã«ç™»éŒ² repositories=リãƒã‚¸ãƒˆãƒª activity=公開アクティビティ followers=フォロワー +show_more=ã•らã«è¡¨ç¤º starred=スター付ãリãƒã‚¸ãƒˆãƒª watched=ウォッãƒä¸­ãƒªãƒã‚¸ãƒˆãƒª code=コード @@ -1014,7 +1015,6 @@ new_repo_helper=リãƒã‚¸ãƒˆãƒªã«ã¯ã€ãƒ—ロジェクトã®ã™ã¹ã¦ã®ãƒ•ã‚¡ owner=オーナー owner_helper=リãƒã‚¸ãƒˆãƒªæ•°ã®ä¸Šé™ã«ã‚ˆã‚Šã€ä¸€éƒ¨ã®çµ„ç¹”ã¯ãƒ‰ãƒ­ãƒƒãƒ—ダウンã«è¡¨ç¤ºã•れãªã„å ´åˆãŒã‚りã¾ã™ã€‚ repo_name=リãƒã‚¸ãƒˆãƒªå -repo_name_helper=リãƒã‚¸ãƒˆãƒªåã¯ã€çŸ­ãã€è¦šãˆã‚„ã™ãã€ä»–ã¨é‡è¤‡ã—ãªã„キーワードを使用ã—ã¾ã—ょã†ã€‚ repo_size=リãƒã‚¸ãƒˆãƒªã‚µã‚¤ã‚º template=テンプレート template_select=ãƒ†ãƒ³ãƒ—ãƒ¬ãƒ¼ãƒˆã‚’é¸æŠžã—ã¦ãã ã•ã„。 @@ -2852,6 +2852,7 @@ teams.invite.title=ã‚ãªãŸã¯çµ„ç¹” %[2]s 内ã®ãƒãƒ¼ãƒ  %[2]s< teams.invite.by=UzaicinÄja %s teams.invite.description=Nospiediet pogu zemÄk, lai pievienotos komandai. + [admin] dashboard=Infopanelis self_check=PaÅ¡pÄrbaude diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini index 5c07b521e9..8a6dabbceb 100644 --- a/options/locale/locale_nl-NL.ini +++ b/options/locale/locale_nl-NL.ini @@ -487,6 +487,7 @@ change_avatar=Wijzig je profielfoto… repositories=repositories activity=Openbare activiteit followers=Volgers +show_more=Meer weergeven starred=Repositories met ster watched=Gevolgde repositories projects=Projecten @@ -752,7 +753,6 @@ visibility.private=Privé owner=Eigenaar owner_helper=Sommige organisaties kunnen niet worden weergegeven in de dropdown vanwege een limiet op het maximale aantal repositories. repo_name=Naam van repository -repo_name_helper=Goede repository-namen zijn kort, makkelijk te onthouden en uniek. repo_size=Repositorygrootte template=Sjabloon template_select=Selecteer een sjabloon. @@ -2054,6 +2054,7 @@ teams.all_repositories=Alle repositories teams.all_repositories_helper=Team heeft toegang tot alle repositories. Door dit te selecteren worden alle bestaande repositories aan het team toegevoegd. teams.all_repositories_read_permission_desc=Dit team heeft Lees toegang tot alle repositories: leden kunnen repositories bekijken en klonen. + [admin] dashboard=Overzicht users=Gebruikersacount diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini index 13c05eebe0..4d049c83d1 100644 --- a/options/locale/locale_pl-PL.ini +++ b/options/locale/locale_pl-PL.ini @@ -711,7 +711,6 @@ visibility.private=Prywatny owner=WÅ‚aÅ›ciciel owner_helper=Niektóre organizacje mogÄ… nie pojawiać siÄ™ w liÅ›cie ze wzglÄ™du na limit maksymalnej liczby repozytoriów. repo_name=Nazwa repozytorium -repo_name_helper=Dobra nazwa repozytorium jest utworzona z krótkich, Å‚atwych do zapamiÄ™tania i unikalnych słów kluczowych. repo_size=Rozmiar repozytorium template=Szablon template_select=Wybierz szablon. @@ -1934,6 +1933,7 @@ teams.all_repositories_read_permission_desc=Ten zespół nadaje uprawnienie Zapisu do wszystkich repozytoriów: jego czÅ‚onkowie mogÄ… odczytywać i przesyÅ‚ać do repozytoriów. teams.all_repositories_admin_permission_desc=Ten zespół nadaje uprawnienia Administratora do wszystkich repozytoriów: jego czÅ‚onkowie mogÄ… odczytywać, przesyÅ‚ać oraz dodawać innych współtwórców do repozytoriów. + [admin] dashboard=Pulpit users=Konta użytkownika diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index c053b33210..f0c034a133 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -582,6 +582,7 @@ joined_on=Inscreveu-se em %s repositories=Repositórios activity=Atividade pública followers=Seguidores +show_more=Mostrar mais starred=Repositórios favoritos watched=Repositórios observados code=Código @@ -907,7 +908,6 @@ new_repo_helper=Um repositório contém todos os arquivos do projeto, inclusive owner=Proprietário owner_helper=Algumas organizações podem não aparecer no menu devido a um limite de contagem dos repositórios. repo_name=Nome do repositório -repo_name_helper=Um bom nome de repositório é composto por palavras curtas, memorizáveis e únicas. repo_size=Tamanho do repositório template=Modelo template_select=Selecione um modelo. @@ -2550,6 +2550,7 @@ teams.invite.title=Você foi convidado para fazer parte da equipe %sdocumentação antes de alterar quaisquer configurações. require_db_desc=Gitea requer MySQL, PostgreSQL, MSSQL, SQLite3 ou TiDB (protocolo MySQL). @@ -649,6 +650,7 @@ joined_on=Inscreveu-se em %s repositories=Repositórios activity=Trabalho público followers=Seguidores +show_more=Mostrar mais starred=Repositórios favoritos watched=Repositórios sob vigilância code=Código @@ -1014,7 +1016,7 @@ new_repo_helper=Um repositório contém todos os ficheiros do trabalho, incluind owner=Proprietário(a) owner_helper=Algumas organizações podem não aparecer na lista suspensa devido a um limite máximo de contagem de repositórios. repo_name=Nome do repositório -repo_name_helper=Um bom nome de repositório utiliza palavras curtas, memoráveis e únicas. +repo_name_profile_public_hint=.profile é um repositório especial que pode usar para adicionar README.md ao seu perfil público da organização, visível para qualquer pessoa. Certifique-se que é público e inicialize-o com um README na pasta do perfil para começar. repo_size=Tamanho do repositório template=Modelo template_select=Escolha um modelo. @@ -1921,8 +1923,8 @@ pulls.close=Encerrar pedido de integração pulls.closed_at=`fechou este pedido de integração %[2]s` pulls.reopened_at=`reabriu este pedido de integração %[2]s` pulls.cmd_instruction_hint=`Ver instruções para a linha de comandos.` -pulls.cmd_instruction_checkout_title=Conferir -pulls.cmd_instruction_checkout_desc=No seu repositório, irá criar um novo ramo para que possa testar as modificações. +pulls.cmd_instruction_checkout_title=Checkout +pulls.cmd_instruction_checkout_desc=A partir do seu repositório, crie um novo ramo e teste nele as modificações. pulls.cmd_instruction_merge_title=Integrar pulls.cmd_instruction_merge_desc=Integrar as modificações e enviar para o Gitea. pulls.cmd_instruction_merge_warning=Aviso: Esta operação não pode executar pedidos de integração porque "auto-identificar integração manual" não estava habilitado @@ -1945,6 +1947,8 @@ pulls.delete.title=Eliminar este pedido de integração? pulls.delete.text=Tem a certeza que quer eliminar este pedido de integração? Isso irá remover todo o conteúdo permanentemente. Como alternativa considere fechá-lo, se pretender mantê-lo em arquivo. pulls.recently_pushed_new_branches=Enviou para o ramo %[1]s %[2]s +pulls.upstream_diverging_prompt_behind_1=Este ramo está %[1]d cometimento atrás de %[2]s +pulls.upstream_diverging_prompt_behind_n=Este ramo está %[1]d cometimentos atrás de %[2]s pulls.upstream_diverging_prompt_base_newer=O ramo base %s tem novas modificações pulls.upstream_diverging_merge=Sincronizar derivação @@ -2858,6 +2862,10 @@ teams.invite.title=Foi-lhe feito um convite para se juntar à equipa %s< teams.invite.by=Convidado(a) por %s teams.invite.description=Clique no botão abaixo para se juntar à equipa. +view_as_role=Ver como: %s +view_as_public_hint=Está a ver o README como um utilizador público. +view_as_member_hint=Está a ver o README como um membro desta organização. + [admin] maintenance=Manutenção dashboard=Painel de controlo @@ -3527,6 +3535,7 @@ versions=Versões versions.view_all=Ver todas dependency.id=ID dependency.version=Versão +search_in_external_registry=Procurar em %s alpine.registry=Configure este registo adicionando o URL no seu ficheiro /etc/apk/repositories: alpine.registry.key=Descarregue a chave RSA pública do registo para dentro da pasta /etc/apk/keys/ para verificar a assinatura do índice: alpine.registry.info=Escolha $branch e $repository da lista abaixo. diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini index 0662c1f5f9..027a2cb19d 100644 --- a/options/locale/locale_ru-RU.ini +++ b/options/locale/locale_ru-RU.ini @@ -578,6 +578,7 @@ joined_on=ПриÑоединил(ÑÑ/аÑÑŒ) %s repositories=Репозитории activity=ÐктивноÑть followers=ПодпиÑчики +show_more=Показать больше starred=Избранные репозитории watched=ОтÑлеживаемые репозитории code=Код @@ -898,7 +899,6 @@ visibility.private_tooltip=Виден только членам организа owner=Владелец owner_helper=Ðекоторые организации могут не отображатьÑÑ Ð² раÑкрывающемÑÑ ÑпиÑке из-за макÑимального Ð¾Ð³Ñ€Ð°Ð½Ð¸Ñ‡ÐµÐ½Ð¸Ñ ÐºÐ¾Ð»Ð¸Ñ‡ÐµÑтва репозиториев. repo_name=Ðазвание Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ð¸Ñ -repo_name_helper=Лучшие Ð½Ð°Ð·Ð²Ð°Ð½Ð¸Ñ Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ð¸ÐµÐ² ÑоÑтоÑÑ‚ из коротких, легко запоминаемых и уникальных ключевых Ñлов. repo_size=Размер Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ð¸Ñ template=Шаблон template_select=Выбрать шаблон. @@ -2541,6 +2541,7 @@ teams.invite.title=Ð’Ð°Ñ Ð¿Ñ€Ð¸Ð³Ð»Ð°Ñили приÑоединитьÑÑ Ðº teams.invite.by=Приглашен(а) %s teams.invite.description=Ðажмите на кнопку ниже, чтобы приÑоединитьÑÑ Ðº команде. + [admin] dashboard=Панель identity_access=Ð˜Ð´ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ñ Ð¸ доÑтуп diff --git a/options/locale/locale_si-LK.ini b/options/locale/locale_si-LK.ini index debcf8e3e9..167ecaf24a 100644 --- a/options/locale/locale_si-LK.ini +++ b/options/locale/locale_si-LK.ini @@ -452,6 +452,7 @@ change_avatar=ඔබගේ අවතà·à¶»à¶º වෙනස් කරන්න… repositories=à¶šà·à·‚්ඨ activity=à¶´à·Šâ€à¶»à·ƒà·’ද්ධ à¶šà·Šâ€à¶»à·’යà·à¶šà·à¶»à¶šà¶¸ followers=අනුගà·à¶¸à·’කයන් +show_more=à¶­à·€ පෙන්වන්න starred=තරු ගබඩà·à·€ watched=නරඹන ලද ගබඩà·à·€à¶½à¶¯à·“ projects=ව්â€à¶ºà·à¶´à·˜à¶­à·’ @@ -692,7 +693,6 @@ visibility.private=පෞද්ගලික owner=හිමිකරු owner_helper=උපරිම නිධි ගණන් සීමà·à·€à¶šà·Š හේතුවෙන් සමහර සංවිධà·à¶± à¶´à·„à¶­ à·€à·à¶§à·“මේ දී පෙන්විය නොහà·à¶š. repo_name=à¶šà·à·‚්ඨයේ නම -repo_name_helper=හොඳ ගබඩà·à·€à¶šà·Š නම් කෙටි, අමතක නොවන සහ අද්විතීය මූල පද à¶·à·à·€à·’ත෠කරයි. repo_size=à¶šà·à·‚්ඨයේ à¶´à·Šâ€à¶»à¶¸à·à¶«à¶º template=à·ƒà·à¶šà·’ල්ල template_select=අච්චුවක් à¶­à·à¶»à¶±à·Šà¶±. @@ -1954,6 +1954,7 @@ teams.all_repositories_read_permission_desc=මෙම à¶šà¶«à·Šà¶©à·à¶ºà¶¸ à¶´ teams.all_repositories_write_permission_desc=මෙම à¶šà¶«à·Šà¶©à·à¶ºà¶¸ ප්රදà·à¶±à¶º කරයි වෙත ප්රවේà·à¶º ලියන්න සියලු ගබඩà·à·€à¶±à·Šà¶§: à·ƒà·à¶¸à·à¶¢à·’කයින්ට කියවීමට සහ ගබඩà·à·€à¶±à·Šà¶§ තල්ලු à¶šà·… à·„à·à¶šà·’ය. teams.all_repositories_admin_permission_desc=මෙම à¶šà¶«à·Šà¶©à·à¶ºà¶¸ ප්රදà·à¶±à¶º කරයි පරිපà·à¶½à¶š වෙත ප්රවේà·à¶º සියලු ගබඩà·à·€à¶±à·Šà¶§: à·ƒà·à¶¸à·à¶¢à·’කයින්ට කියවීමට, තල්ලු කිරීමට සහ ගබඩà·à·€à¶±à·Šà¶§ සහයà·à¶œà·“කයින් à¶‘à¶šà¶­à·” කිරීමට. + [admin] dashboard=උපකරණ පුවරුව users=පරිà·à·“ලක ගිණුම් diff --git a/options/locale/locale_sk-SK.ini b/options/locale/locale_sk-SK.ini index 39c13c358e..43b190098f 100644 --- a/options/locale/locale_sk-SK.ini +++ b/options/locale/locale_sk-SK.ini @@ -817,7 +817,6 @@ visibility.private=Súkromný owner=Vlastník owner_helper=Niektoré organizácie sa nemusia zobraziÅ¥ v rozbaľovacej ponuke z dôvodu maximálneho limitu poÄtu repozitárov. repo_name=Názov repozitára -repo_name_helper=Dobrý názov repozitára sa zvyÄajne skladá z krátkych, jedineÄných a ľahko zapamätateľných kľúÄových slov. repo_size=VeľkosÅ¥ repozitára template=Å ablóna template_select=Vyberte Å¡ablónu. @@ -1235,6 +1234,7 @@ teams.all_repositories_read_permission_desc=Tomuto tímu je pridelený prístup teams.all_repositories_write_permission_desc=Tomuto tímu je pridelený prístup na Zápis do vÅ¡etkých repozitárov: Älenovia môžu prezeraÅ¥ a nahrávaÅ¥ do repozitárov. teams.all_repositories_admin_permission_desc=Tomuto tímu je pridelený Admin prístup ku vÅ¡etkým repozitárom: Älenovia môžu prezeraÅ¥, nahrávaÅ¥ do repozitárov a pridávaÅ¥ do nich spolupracovníkov. + [admin] repositories=Repozitáre hooks=Webhooky diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini index 2fd8277b76..0315ebe9a1 100644 --- a/options/locale/locale_sv-SE.ini +++ b/options/locale/locale_sv-SE.ini @@ -598,7 +598,6 @@ visibility.private=Privat [repo] owner=Ägare repo_name=Utvecklingskatalogens namn -repo_name_helper=Bra namn pÃ¥ utvecklingskataloger bestÃ¥r utav korta, unika nyckelord som är enkla att komma ihÃ¥g. repo_size=Utvecklingskatalogens storlek template=Mall template_select=Välj mall. @@ -1592,6 +1591,7 @@ teams.all_repositories_read_permission_desc=Detta team beviljar LäsSkriv-rättigheter till alla utvecklingskataloger: medlemmar kan läsa frÃ¥n och pusha till utvecklingskataloger. teams.all_repositories_admin_permission_desc=Detta team beviljar Admin-rättigheter till alla utvecklingskataloger: medlemmar kan läsa frÃ¥n, pusha till och lägga till kollaboratörer för utvecklingskatalogerna. + [admin] dashboard=Instrumentpanel users=Användarkonto diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini index d17985dba5..fb30ab3b67 100644 --- a/options/locale/locale_tr-TR.ini +++ b/options/locale/locale_tr-TR.ini @@ -104,6 +104,7 @@ copy_url=URL'yi kopyala copy_hash=Hash'i kopyala copy_content=İçeriÄŸi kopyala copy_branch=Dal adını kopyala +copy_path=Yolu kopyala copy_success=Kopyalandı! copy_error=Kopyalama baÅŸarısız oldu copy_type_unsupported=Bu dosya türü kopyalanamaz @@ -144,6 +145,7 @@ confirm_delete_selected=Tüm seçili öğeleri gerçekten silmek istiyor musunuz name=İsim value=DeÄŸer +readme=Benioku filter=Filtre filter.clear=Filtreyi Temizle @@ -159,6 +161,7 @@ filter.public=Genel filter.private=Özel no_results_found=Sonuç bulunamadı. +internal_error_skipped=Dahili bir hata oluÅŸtu ama atlandı: %s [search] search=Ara... @@ -177,6 +180,8 @@ code_search_by_git_grep=Mevcut kod arama sonuçları "git grep" ile saÄŸlanıyor package_kind=Paketleri ara... project_kind=Projeleri ara... branch_kind=Dalları ara... +tag_kind=Etiketleri ara... +tag_tooltip=EÅŸleÅŸen etiketler için arama. Herhangi bir numara serisi bulmak için '%' kullanın. commit_kind=İşlemeleri ara... runner_kind=Çalıştırıcıları ara... no_results=EÅŸleÅŸen sonuç bulunamadı. @@ -206,7 +211,10 @@ buttons.link.tooltip=BaÄŸlantı ekle buttons.list.unordered.tooltip=Maddeli liste ekle buttons.list.ordered.tooltip=Numaralandırılmış liste ekle buttons.list.task.tooltip=Görev listesi ekle +buttons.table.add.tooltip=Tablo ekle buttons.table.add.insert=Ekle +buttons.table.rows=Satırlar +buttons.table.cols=Sütunlar buttons.mention.tooltip=Bir kiÅŸiye veya takıma deÄŸin buttons.ref.tooltip=Bir konuya veya deÄŸiÅŸiklik isteÄŸine deÄŸin buttons.switch_to_legacy.tooltip=Eski düzenleyiciyi kullan @@ -219,6 +227,7 @@ string.desc=Z - A [error] occurred=Bir hata oluÅŸtu +report_message=Bunun bir Gitea hatası olduÄŸunu düşünüyorsanız, lütfen GitHub sayfasında sorunu arayın veya gerekiyorsa yeni bir sorun oluÅŸturun. not_found=Hedef bulunamadı. network_error=AÄŸ hatası @@ -631,6 +640,7 @@ joined_on=%s tarihinde katıldı repositories=Depolar activity=Genel Aktivite followers=Takipçiler +show_more=Daha Fazla Göster starred=Yıldızlanmış depolar watched=İzlenen Depolar code=Kod @@ -983,7 +993,6 @@ new_repo_helper=Bir depo, sürüm geçmiÅŸi dahil tüm proje dosyalarını içer owner=Sahibi owner_helper=Bazı organizasyonlar, en çok depo sayısı sınırı nedeniyle açılır menüde görünmeyebilir. repo_name=Depo İsmi -repo_name_helper=İyi bir depo ismi kısa, akılda kalıcı ve özgün anahtar kelimelerden oluÅŸur. repo_size=Depo Boyutu template=Åžablon template_select=Bir ÅŸablon seçin. @@ -2746,6 +2755,7 @@ teams.invite.title=%s takımına (Organizasyon: %sÐ—Ð°Ð¿Ð¸Ñ Ð´Ð»Ñ Ð²ÑÑ–Ñ… репозиторіїв: учаÑники можуть переглÑдати та виконувати push в репозиторіÑÑ…. teams.all_repositories_admin_permission_desc=Ð¦Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð° надає дозвіл ÐдмініÑÑ‚Ñ€ÑƒÐ²Ð°Ð½Ð½Ñ Ð´Ð»Ñ Ð²ÑÑ–Ñ… репозиторіїв: учаÑники можуть переглÑдати, виконувати push та додавати Ñпівробітників. + [admin] dashboard=Панель ÑƒÐ¿Ñ€Ð°Ð²Ð»Ñ–Ð½Ð½Ñ users=Облікові запиÑи кориÑтувачів diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index a1fc26528e..5e4723a4cd 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -649,6 +649,7 @@ joined_on=加入于 %s repositories=仓库列表 activity=公开活动 followers=关注者 +show_more=显示更多 starred=已点赞 watched=已关注仓库 code=ä»£ç  @@ -1014,7 +1015,6 @@ new_repo_helper=代ç ä»“库包å«äº†æ‰€æœ‰çš„é¡¹ç›®æ–‡ä»¶ï¼ŒåŒ…æ‹¬ç‰ˆæœ¬åŽ†å² owner=拥有者 owner_helper=由于最大仓库数é‡é™åˆ¶ï¼Œä¸€äº›ç»„织å¯èƒ½ä¸ä¼šæ˜¾ç¤ºåœ¨ä¸‹æ‹‰åˆ—表中。 repo_name=仓库åç§° -repo_name_helper=好的仓库åç§°åº”å½“ä½¿ç”¨ç®€çŸ­ã€æœ‰æ„义和独特的关键字。 repo_size=ä»“åº“å¤§å° template=æ¨¡æ¿ template_select=é€‰æ‹©æ¨¡æ¿ @@ -2860,6 +2860,7 @@ teams.invite.title=您已被邀请加入组织 %s 中的团队 teams.invite.by=邀请人 %s teams.invite.description=请点击下é¢çš„æŒ‰é’®åŠ å…¥å›¢é˜Ÿã€‚ + [admin] maintenance=维护 dashboard=管ç†é¢æ¿ diff --git a/options/locale/locale_zh-HK.ini b/options/locale/locale_zh-HK.ini index e1ca6ebfc8..77f8d8a25d 100644 --- a/options/locale/locale_zh-HK.ini +++ b/options/locale/locale_zh-HK.ini @@ -684,6 +684,7 @@ teams.add_team_member=新增團隊æˆå“¡ teams.delete_team_success=該團隊已被刪除。 teams.repositories=團隊儲存庫 + [admin] dashboard=控制é¢ç‰ˆ organizations=çµ„ç¹”ç®¡ç† diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini index 267f8b4947..948b47bc9c 100644 --- a/options/locale/locale_zh-TW.ini +++ b/options/locale/locale_zh-TW.ini @@ -647,6 +647,7 @@ joined_on=加入於 %s repositories=儲存庫 activity=公開動態 followers=追蹤者 +show_more=顯示更多 starred=已加星號 watched=關注的儲存庫 code=程å¼ç¢¼ @@ -1011,7 +1012,6 @@ new_repo_helper=å„²å­˜åº«åŒ…å«æ‰€æœ‰å°ˆæ¡ˆæª”案,包括修訂歷å²ã€‚已經 owner=æ“æœ‰è€… owner_helper=組織å¯èƒ½å› ç‚ºå„²å­˜åº«æ•¸é‡ä¸Šé™è€Œæœªåˆ—入此é¸å–®ã€‚ repo_name=儲存庫å稱 -repo_name_helper=好的儲存庫å稱通常是簡短的ã€å¥½è¨˜çš„ã€ä¸”ç¨ç‰¹çš„。 repo_size=å„²å­˜åº«å¤§å° template=範本 template_select=鏿“‡ç¯„本 @@ -2851,6 +2851,7 @@ teams.invite.title=您已被邀請加入組織 %s 中的團隊 teams.invite.by=邀請人 %s teams.invite.description=請點擊下方按鈕加入團隊。 + [admin] maintenance=ç¶­è­· dashboard=è³‡è¨Šä¸»é  diff --git a/package-lock.json b/package-lock.json index 60edfa95d0..1e3c5ab155 100644 --- a/package-lock.json +++ b/package-lock.json @@ -87,7 +87,7 @@ "eslint": "8.57.0", "eslint-import-resolver-typescript": "3.7.0", "eslint-plugin-array-func": "4.0.0", - "eslint-plugin-github": "5.1.4", + "eslint-plugin-github": "5.0.2", "eslint-plugin-import-x": "4.6.1", "eslint-plugin-no-jquery": "3.1.0", "eslint-plugin-no-use-extend-native": "0.5.0", @@ -7953,166 +7953,35 @@ } }, "node_modules/eslint-plugin-github": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-github/-/eslint-plugin-github-5.1.4.tgz", - "integrity": "sha512-j5IgIxsDoch06zJzeqPvenfzRXDKI9Z8YwfUg1pm2ay1q44tMSFwvEu6l0uEIrTpA3v8QdPyLr98LqDl1TIhSA==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-github/-/eslint-plugin-github-5.0.2.tgz", + "integrity": "sha512-nMdzWJQ5CimjQDY6SFeJ0KIXuNFf0dgDWEd4eP3UWfuTuP/dXcZJDg7MQRvAFt743T1zUi4+/HdOihfu8xJkLA==", "dev": true, "license": "MIT", "dependencies": { - "@eslint/compat": "^1.2.3", - "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "^9.14.0", "@github/browserslist-config": "^1.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "aria-query": "^5.3.0", "eslint-config-prettier": ">=8.0.0", - "eslint-plugin-escompat": "^3.11.3", + "eslint-plugin-escompat": "^3.3.3", "eslint-plugin-eslint-comments": "^3.2.0", "eslint-plugin-filenames": "^1.3.2", "eslint-plugin-i18n-text": "^1.0.1", "eslint-plugin-import": "^2.25.2", "eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-no-only-tests": "^3.0.0", - "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-prettier": "^5.0.0", "eslint-rule-documentation": ">=1.0.0", - "globals": "^15.12.0", "jsx-ast-utils": "^3.3.2", "prettier": "^3.0.0", - "svg-element-attributes": "^1.3.1", - "typescript-eslint": "^8.14.0" + "svg-element-attributes": "^1.3.1" }, "bin": { "eslint-ignore-errors": "bin/eslint-ignore-errors.js" }, "peerDependencies": { - "eslint": "^8 || ^9" - } - }, - "node_modules/eslint-plugin-github/node_modules/@eslint/compat": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.2.4.tgz", - "integrity": "sha512-S8ZdQj/N69YAtuqFt7653jwcvuUj131+6qGLUyDqfDg1OIoBQ66OCuXC473YQfO2AaxITTutiRQiDwoo7ZLYyg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "peerDependencies": { - "eslint": "^9.10.0" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-github/node_modules/@eslint/eslintrc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", - "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-plugin-github/node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-plugin-github/node_modules/@eslint/js": { - "version": "9.17.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz", - "integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/eslint-plugin-github/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/eslint-plugin-github/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-github/node_modules/globals": { - "version": "15.14.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz", - "integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-plugin-github/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint-plugin-github/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" + "eslint": "^8.0.1" } }, "node_modules/eslint-plugin-i18n-text": { @@ -14063,29 +13932,6 @@ "node": ">=14.17" } }, - "node_modules/typescript-eslint": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.18.1.tgz", - "integrity": "sha512-Mlaw6yxuaDEPQvb/2Qwu3/TfgeBHy9iTJ3mTwe7OvpPmF6KPQjVOfGyEJpPv6Ez2C34OODChhXrzYw/9phI0MQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.18.1", - "@typescript-eslint/parser": "8.18.1", - "@typescript-eslint/utils": "8.18.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" - } - }, "node_modules/typo-js": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.2.5.tgz", diff --git a/package.json b/package.json index 9f5fbe3fdd..6881ddb306 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "eslint": "8.57.0", "eslint-import-resolver-typescript": "3.7.0", "eslint-plugin-array-func": "4.0.0", - "eslint-plugin-github": "5.1.4", + "eslint-plugin-github": "5.0.2", "eslint-plugin-import-x": "4.6.1", "eslint-plugin-no-jquery": "3.1.0", "eslint-plugin-no-use-extend-native": "0.5.0", diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index b50fbd638e..5b035fbb71 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -37,8 +37,6 @@ import ( "code.gitea.io/gitea/routers/api/packages/vagrant" "code.gitea.io/gitea/services/auth" "code.gitea.io/gitea/services/context" - - "github.com/go-chi/chi/v5" ) func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) { @@ -140,39 +138,10 @@ func CommonRoutes() *web.Router { }, reqPackageAccess(perm.AccessModeRead)) r.Group("/arch", func() { r.Methods("HEAD,GET", "/repository.key", arch.GetRepositoryKey) - - reqPutRepository := web.NewPathProcessor("PUT", "/") - reqGetRepoArchFile := web.NewPathProcessor("HEAD,GET", "///") - reqDeleteRepoNameVerArch := web.NewPathProcessor("DELETE", "////") - - r.Any("*", func(ctx *context.Context) { - chiCtx := chi.RouteContext(ctx.Req.Context()) - path := ctx.PathParam("*") - - if reqPutRepository.ProcessRequestPath(chiCtx, path) { - reqPackageAccess(perm.AccessModeWrite)(ctx) - if ctx.Written() { - return - } - arch.UploadPackageFile(ctx) - return - } - - if reqGetRepoArchFile.ProcessRequestPath(chiCtx, path) { - arch.GetPackageOrRepositoryFile(ctx) - return - } - - if reqDeleteRepoNameVerArch.ProcessRequestPath(chiCtx, path) { - reqPackageAccess(perm.AccessModeWrite)(ctx) - if ctx.Written() { - return - } - arch.DeletePackageVersion(ctx) - return - } - - ctx.Status(http.StatusNotFound) + r.PathGroup("*", func(g *web.RouterPathGroup) { + g.MatchPath("PUT", "/", reqPackageAccess(perm.AccessModeWrite), arch.UploadPackageFile) + g.MatchPath("HEAD,GET", "///", arch.GetPackageOrRepositoryFile) + g.MatchPath("DELETE", "////", reqPackageAccess(perm.AccessModeWrite), arch.DeletePackageVersion) }) }, reqPackageAccess(perm.AccessModeRead)) r.Group("/cargo", func() { @@ -461,8 +430,6 @@ func CommonRoutes() *web.Router { r.Post("/api/charts", reqPackageAccess(perm.AccessModeWrite), helm.UploadPackage) }, reqPackageAccess(perm.AccessModeRead)) r.Group("/maven", func() { - // FIXME: this path design is not right. - // It should be `/.../{groupId}/{artifactId}/{version}`, but not `/.../{groupId}-{artifactId}/{version}` r.Put("/*", reqPackageAccess(perm.AccessModeWrite), maven.UploadPackageFile) r.Get("/*", maven.DownloadPackageFile) r.Head("/*", maven.ProvidePackageFileHeader) diff --git a/routers/api/packages/maven/maven.go b/routers/api/packages/maven/maven.go index 9474b17bc7..4d04d4d1e9 100644 --- a/routers/api/packages/maven/maven.go +++ b/routers/api/packages/maven/maven.go @@ -13,7 +13,7 @@ import ( "errors" "io" "net/http" - "path/filepath" + "path" "regexp" "sort" "strconv" @@ -25,6 +25,7 @@ import ( "code.gitea.io/gitea/modules/log" packages_module "code.gitea.io/gitea/modules/packages" maven_module "code.gitea.io/gitea/modules/packages/maven" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/api/packages/helper" "code.gitea.io/gitea/services/context" packages_service "code.gitea.io/gitea/services/packages" @@ -44,7 +45,7 @@ const ( var ( errInvalidParameters = errors.New("request parameters are invalid") - illegalCharacters = regexp.MustCompile(`[\\/:"<>|?\*]`) + illegalCharacters = regexp.MustCompile(`[\\/:"<>|?*]`) ) func apiError(ctx *context.Context, status int, obj any) { @@ -85,8 +86,10 @@ func handlePackageFile(ctx *context.Context, serveContent bool) { func serveMavenMetadata(ctx *context.Context, params parameters) { // /com/foo/project/maven-metadata.xml[.md5/.sha1/.sha256/.sha512] - packageName := params.GroupID + "-" + params.ArtifactID - pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, packageName) + pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageName()) + if errors.Is(err, util.ErrNotExist) { + pvs, err = packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageNameLegacy()) + } if err != nil { apiError(ctx, http.StatusInternalServerError, err) return @@ -116,10 +119,10 @@ func serveMavenMetadata(ctx *context.Context, params parameters) { latest := pds[len(pds)-1] // http.TimeFormat required a UTC time, refer to https://pkg.go.dev/net/http#TimeFormat - lastModifed := latest.Version.CreatedUnix.AsTime().UTC().Format(http.TimeFormat) - ctx.Resp.Header().Set("Last-Modified", lastModifed) + lastModified := latest.Version.CreatedUnix.AsTime().UTC().Format(http.TimeFormat) + ctx.Resp.Header().Set("Last-Modified", lastModified) - ext := strings.ToLower(filepath.Ext(params.Filename)) + ext := strings.ToLower(path.Ext(params.Filename)) if isChecksumExtension(ext) { var hash []byte switch ext { @@ -147,11 +150,12 @@ func serveMavenMetadata(ctx *context.Context, params parameters) { } func servePackageFile(ctx *context.Context, params parameters, serveContent bool) { - packageName := params.GroupID + "-" + params.ArtifactID - - pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, packageName, params.Version) + pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageName(), params.Version) + if errors.Is(err, util.ErrNotExist) { + pv, err = packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageNameLegacy(), params.Version) + } if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -161,14 +165,14 @@ func servePackageFile(ctx *context.Context, params parameters, serveContent bool filename := params.Filename - ext := strings.ToLower(filepath.Ext(filename)) + ext := strings.ToLower(path.Ext(filename)) if isChecksumExtension(ext) { filename = filename[:len(filename)-len(ext)] } pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, filename, packages_model.EmptyFileKey) if err != nil { - if err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -238,15 +242,17 @@ func UploadPackageFile(ctx *context.Context) { return } - log.Trace("Parameters: %+v", params) - // Ignore the package index //maven-metadata.xml if params.IsMeta && params.Version == "" { ctx.Status(http.StatusOK) return } - packageName := params.GroupID + "-" + params.ArtifactID + packageName := params.toInternalPackageName() + if ctx.FormBool("use_legacy_package_name") { + // for testing purpose only + packageName = params.toInternalPackageNameLegacy() + } // for the same package, only one upload at a time releaser, err := globallock.Lock(ctx, mavenPkgNameKey(packageName)) @@ -274,13 +280,26 @@ func UploadPackageFile(ctx *context.Context) { Creator: ctx.Doer, } - ext := filepath.Ext(params.Filename) + // old maven package uses "groupId-artifactId" as package name, so we need to update to the new format "groupId:artifactId" + legacyPackage, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageNameLegacy()) + if err != nil && !errors.Is(err, packages_model.ErrPackageNotExist) { + apiError(ctx, http.StatusInternalServerError, err) + return + } else if legacyPackage != nil { + err = packages_model.UpdatePackageNameByID(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, legacyPackage.ID, packageName) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + } + + ext := path.Ext(params.Filename) // Do not upload checksum files but compare the hashes. if isChecksumExtension(ext) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, pvci.Owner.ID, pvci.PackageType, pvci.Name, pvci.Version) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -289,7 +308,7 @@ func UploadPackageFile(ctx *context.Context) { } pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, params.Filename[:len(params.Filename)-len(ext)], packages_model.EmptyFileKey) if err != nil { - if err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -343,7 +362,7 @@ func UploadPackageFile(ctx *context.Context) { if pvci.Metadata != nil { pv, err := packages_model.GetVersionByNameAndVersion(ctx, pvci.Owner.ID, pvci.PackageType, pvci.Name, pvci.Version) - if err != nil && err != packages_model.ErrPackageNotExist { + if err != nil && !errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusInternalServerError, err) return } @@ -399,9 +418,26 @@ type parameters struct { IsMeta bool } +func (p *parameters) toInternalPackageName() string { + // there cuold be 2 choices: "/" or ":" + // Maven says: "groupId:artifactId:version" in their document: https://maven.apache.org/pom.html#Maven_Coordinates + // but it would be slightly ugly in URL: "/-/packages/maven/group-id%3Aartifact-id" + return p.GroupID + ":" + p.ArtifactID +} + +func (p *parameters) toInternalPackageNameLegacy() string { + return p.GroupID + "-" + p.ArtifactID +} + func extractPathParameters(ctx *context.Context) (parameters, error) { parts := strings.Split(ctx.PathParam("*"), "/") + // formats: + // * /com/group/id/artifactId/maven-metadata.xml[.md5|.sha1|.sha256|.sha512] + // * /com/group/id/artifactId/version-SNAPSHOT/maven-metadata.xml[.md5|.sha1|.sha256|.sha512] + // * /com/group/id/artifactId/version/any-file + // * /com/group/id/artifactId/version-SNAPSHOT/any-file + p := parameters{ Filename: parts[len(parts)-1], } diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index 67dfda39a8..2fcdd02058 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -729,7 +729,7 @@ func CreateBranchProtection(ctx *context.APIContext) { } else { if !isPlainRule { if ctx.Repo.GitRepo == nil { - ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, ctx.Repo.Repository) + ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository) if err != nil { ctx.Error(http.StatusInternalServerError, "OpenRepository", err) return @@ -1057,7 +1057,7 @@ func EditBranchProtection(ctx *context.APIContext) { } else { if !isPlainRule { if ctx.Repo.GitRepo == nil { - ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, ctx.Repo.Repository) + ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository) if err != nil { ctx.Error(http.StatusInternalServerError, "OpenRepository", err) return diff --git a/routers/api/v1/repo/compare.go b/routers/api/v1/repo/compare.go index 87b890cb62..a1813a8a76 100644 --- a/routers/api/v1/repo/compare.go +++ b/routers/api/v1/repo/compare.go @@ -45,7 +45,7 @@ func CompareDiff(ctx *context.APIContext) { if ctx.Repo.GitRepo == nil { var err error - ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, ctx.Repo.Repository) + ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository) if err != nil { ctx.Error(http.StatusInternalServerError, "OpenRepository", err) return diff --git a/routers/api/v1/repo/download.go b/routers/api/v1/repo/download.go index eb967772ed..a8a23c4a8d 100644 --- a/routers/api/v1/repo/download.go +++ b/routers/api/v1/repo/download.go @@ -29,7 +29,7 @@ func DownloadArchive(ctx *context.APIContext) { if ctx.Repo.GitRepo == nil { var err error - ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, ctx.Repo.Repository) + ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository) if err != nil { ctx.Error(http.StatusInternalServerError, "OpenRepository", err) return diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index 6591b9a752..1ad55d225b 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -11,7 +11,6 @@ import ( "fmt" "io" "net/http" - "path" "strings" "time" @@ -242,19 +241,14 @@ func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, entry *git.TreeEn return nil, nil, nil } - info, _, err := git.Entries([]*git.TreeEntry{entry}).GetCommitsInfo(ctx, ctx.Repo.Commit, path.Dir("/" + ctx.Repo.TreePath)[1:]) + latestCommit, err := ctx.Repo.GitRepo.GetTreePathLatestCommit(ctx.Repo.Commit.ID.String(), ctx.Repo.TreePath) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetCommitsInfo", err) + ctx.Error(http.StatusInternalServerError, "GetTreePathLatestCommit", err) return nil, nil, nil } + when := &latestCommit.Committer.When - if len(info) == 1 { - // Not Modified - lastModified = &info[0].Commit.Committer.When - } - blob = entry.Blob() - - return blob, entry, lastModified + return entry.Blob(), entry, when } // GetArchive get archive of a repository @@ -288,7 +282,7 @@ func GetArchive(ctx *context.APIContext) { if ctx.Repo.GitRepo == nil { var err error - ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, ctx.Repo.Repository) + ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository) if err != nil { ctx.Error(http.StatusInternalServerError, "OpenRepository", err) return diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index a192e241b7..ce09e7fc0f 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -726,7 +726,7 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err if ctx.Repo.GitRepo == nil && !repo.IsEmpty { var err error - ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, repo) + ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo) if err != nil { ctx.Error(http.StatusInternalServerError, "Unable to OpenRepository", err) return err diff --git a/routers/common/codesearch.go b/routers/common/codesearch.go new file mode 100644 index 0000000000..a14af126e5 --- /dev/null +++ b/routers/common/codesearch.go @@ -0,0 +1,39 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package common + +import ( + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/services/context" +) + +func PrepareCodeSearch(ctx *context.Context) (ret struct { + Keyword string + Language string + IsFuzzy bool +}, +) { + ret.Language = ctx.FormTrim("l") + ret.Keyword = ctx.FormTrim("q") + + fuzzyDefault := setting.Indexer.RepoIndexerEnabled + fuzzyAllow := true + if setting.Indexer.RepoType == "bleve" && setting.Indexer.TypeBleveMaxFuzzniess == 0 { + fuzzyDefault = false + fuzzyAllow = false + } + isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(fuzzyDefault) + if isFuzzy && !fuzzyAllow { + ctx.Flash.Info("Fuzzy search is disabled by default due to performance reasons") + isFuzzy = false + } + + ctx.Data["IsBleveFuzzyDisabled"] = true + ctx.Data["Keyword"] = ret.Keyword + ctx.Data["Language"] = ret.Language + ctx.Data["IsFuzzy"] = isFuzzy + + ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled + return ret +} diff --git a/routers/private/hook_post_receive.go b/routers/private/hook_post_receive.go index 32c2828739..af40cb3988 100644 --- a/routers/private/hook_post_receive.go +++ b/routers/private/hook_post_receive.go @@ -368,7 +368,7 @@ func handlePullRequestMerging(ctx *gitea_context.PrivateContext, opts *private.H if err := pull_model.DeleteScheduledAutoMerge(ctx, pr.ID); err != nil && !db.IsErrNotExist(err) { return fmt.Errorf("DeleteScheduledAutoMerge[%d]: %v", opts.PullRequestID, err) } - if _, err := pr.SetMerged(ctx); err != nil { + if _, err := pull_service.SetMerged(ctx, pr); err != nil { return fmt.Errorf("SetMerged failed: %s/%s Error: %v", ownerName, repoName, err) } return nil diff --git a/routers/private/internal_repo.go b/routers/private/internal_repo.go index 8a53e1ed23..e111d6689e 100644 --- a/routers/private/internal_repo.go +++ b/routers/private/internal_repo.go @@ -27,7 +27,7 @@ func RepoAssignment(ctx *gitea_context.PrivateContext) { return } - gitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, repo) + gitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo) if err != nil { log.Error("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err) ctx.JSON(http.StatusInternalServerError, private.Response{ diff --git a/routers/web/admin/emails.go b/routers/web/admin/emails.go index e925de8937..23ddfa583a 100644 --- a/routers/web/admin/emails.go +++ b/routers/web/admin/emails.go @@ -94,7 +94,7 @@ func Emails(ctx *context.Context) { ctx.Data["Emails"] = emails pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5) - pager.SetDefaultParams(ctx) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplEmails) diff --git a/routers/web/admin/packages.go b/routers/web/admin/packages.go index da345f2f89..5122342259 100644 --- a/routers/web/admin/packages.go +++ b/routers/web/admin/packages.go @@ -77,9 +77,7 @@ func Packages(ctx *context.Context) { ctx.Data["TotalUnreferencedBlobSize"] = totalUnreferencedBlobSize pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5) - pager.AddParamString("q", query) - pager.AddParamString("type", packageType) - pager.AddParamString("sort", sort) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplPackagesList) diff --git a/routers/web/admin/repos.go b/routers/web/admin/repos.go index 27d39dcf4b..1bc8abb88c 100644 --- a/routers/web/admin/repos.go +++ b/routers/web/admin/repos.go @@ -4,7 +4,6 @@ package admin import ( - "fmt" "net/http" "net/url" "strings" @@ -84,8 +83,7 @@ func UnadoptedRepos(ctx *context.Context) { if !doSearch { pager := context.NewPagination(0, opts.PageSize, opts.Page, 5) - pager.SetDefaultParams(ctx) - pager.AddParamString("search", fmt.Sprint(doSearch)) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplUnadoptedRepos) return @@ -99,8 +97,7 @@ func UnadoptedRepos(ctx *context.Context) { } ctx.Data["Dirs"] = repoNames pager := context.NewPagination(count, opts.PageSize, opts.Page, 5) - pager.SetDefaultParams(ctx) - pager.AddParamString("search", fmt.Sprint(doSearch)) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplUnadoptedRepos) } diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go index fdd18b2f9d..f6a3af1c86 100644 --- a/routers/web/admin/users.go +++ b/routers/web/admin/users.go @@ -47,16 +47,12 @@ func Users(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("admin.users") ctx.Data["PageIsAdminUsers"] = true - extraParamStrings := map[string]string{} statusFilterKeys := []string{"is_active", "is_admin", "is_restricted", "is_2fa_enabled", "is_prohibit_login"} statusFilterMap := map[string]string{} for _, filterKey := range statusFilterKeys { paramKey := "status_filter[" + filterKey + "]" paramVal := ctx.FormString(paramKey) statusFilterMap[filterKey] = paramVal - if paramVal != "" { - extraParamStrings[paramKey] = paramVal - } } sortType := ctx.FormString("sort") @@ -82,7 +78,6 @@ func Users(ctx *context.Context) { IsTwoFactorEnabled: util.OptionalBoolParse(statusFilterMap["is_2fa_enabled"]), IsProhibitLogin: util.OptionalBoolParse(statusFilterMap["is_prohibit_login"]), IncludeReserved: true, // administrator needs to list all accounts include reserved, bot, remote ones - ExtraParamStrings: extraParamStrings, }, tplUsers) } @@ -318,6 +313,8 @@ func editUserCommon(ctx *context.Context) { ctx.Data["PageIsAdminUsers"] = true ctx.Data["DisableRegularOrgCreation"] = setting.Admin.DisableRegularOrgCreation ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations + ctx.Data["DisableGitHooks"] = setting.DisableGitHooks + ctx.Data["DisableImportLocal"] = !setting.ImportLocalPaths ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice() ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx) } diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 42736a423f..3fe1d5970e 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -689,7 +689,7 @@ func Activate(ctx *context.Context) { } // TODO: ctx.Doer/ctx.Data["SignedUser"] could be nil or not the same user as the one being activated - user := user_model.VerifyUserActiveCode(ctx, code) + user := user_model.VerifyUserTimeLimitCode(ctx, &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateAccount}, code) if user == nil { // if code is wrong renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.invalid_code")) return @@ -734,7 +734,7 @@ func ActivatePost(ctx *context.Context) { } // TODO: ctx.Doer/ctx.Data["SignedUser"] could be nil or not the same user as the one being activated - user := user_model.VerifyUserActiveCode(ctx, code) + user := user_model.VerifyUserTimeLimitCode(ctx, &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateAccount}, code) if user == nil { // if code is wrong renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.invalid_code")) return diff --git a/routers/web/auth/password.go b/routers/web/auth/password.go index 3812d582e5..614e086f77 100644 --- a/routers/web/auth/password.go +++ b/routers/web/auth/password.go @@ -113,7 +113,7 @@ func commonResetPassword(ctx *context.Context) (*user_model.User, *auth.TwoFacto } // Fail early, don't frustrate the user - u := user_model.VerifyUserActiveCode(ctx, code) + u := user_model.VerifyUserTimeLimitCode(ctx, &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeResetPassword}, code) if u == nil { ctx.Flash.Error(ctx.Tr("auth.invalid_code_forgot_password", fmt.Sprintf("%s/user/forgot_password", setting.AppSubURL)), true) return nil, nil diff --git a/routers/web/devtest/devtest.go b/routers/web/devtest/devtest.go index 0bc84d2d1e..1ea1398173 100644 --- a/routers/web/devtest/devtest.go +++ b/routers/web/devtest/devtest.go @@ -9,6 +9,10 @@ import ( "strings" "time" + "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/git" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/services/context" ) @@ -41,16 +45,85 @@ func FetchActionTest(ctx *context.Context) { ctx.JSONRedirect("") } -func Tmpl(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 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) + } + 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()}, + }, + }) + + ctx.Data["MockCommits"] = commits + } +} + +func Tmpl(ctx *context.Context) { + prepareMockData(ctx) if ctx.Req.Method == "POST" { _ = ctx.Req.ParseForm() ctx.Flash.Info("form: "+ctx.Req.Method+" "+ctx.Req.RequestURI+"
"+ @@ -60,6 +133,5 @@ func Tmpl(ctx *context.Context) { ) time.Sleep(2 * time.Second) } - ctx.HTML(http.StatusOK, templates.TplName("devtest"+path.Clean("/"+ctx.PathParam("sub")))) } diff --git a/routers/web/explore/code.go b/routers/web/explore/code.go index 4df89253b4..ae5ff3db76 100644 --- a/routers/web/explore/code.go +++ b/routers/web/explore/code.go @@ -11,6 +11,7 @@ import ( code_indexer "code.gitea.io/gitea/modules/indexer/code" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/services/context" ) @@ -32,18 +33,10 @@ func Code(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("explore") ctx.Data["PageIsExplore"] = true ctx.Data["PageIsExploreCode"] = true - - language := ctx.FormTrim("l") - keyword := ctx.FormTrim("q") - - isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true) - - ctx.Data["Keyword"] = keyword - ctx.Data["Language"] = language - ctx.Data["IsFuzzy"] = isFuzzy ctx.Data["PageIsViewCode"] = true - if keyword == "" { + prepareSearch := common.PrepareCodeSearch(ctx) + if prepareSearch.Keyword == "" { ctx.HTML(http.StatusOK, tplExploreCode) return } @@ -80,9 +73,9 @@ func Code(ctx *context.Context) { if (len(repoIDs) > 0) || isAdmin { total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{ RepoIDs: repoIDs, - Keyword: keyword, - IsKeywordFuzzy: isFuzzy, - Language: language, + Keyword: prepareSearch.Keyword, + IsKeywordFuzzy: prepareSearch.IsFuzzy, + Language: prepareSearch.Language, Paginator: &db.ListOptions{ Page: page, PageSize: setting.UI.RepoSearchPagingNum, @@ -137,8 +130,7 @@ func Code(ctx *context.Context) { ctx.Data["SearchResultLanguages"] = searchResultLanguages pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5) - pager.SetDefaultParams(ctx) - pager.AddParamString("l", language) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplExploreCode) diff --git a/routers/web/explore/repo.go b/routers/web/explore/repo.go index c421aea715..cf3128314b 100644 --- a/routers/web/explore/repo.go +++ b/routers/web/explore/repo.go @@ -4,7 +4,6 @@ package explore import ( - "fmt" "net/http" "code.gitea.io/gitea/models/db" @@ -139,25 +138,7 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) { ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled pager := context.NewPagination(int(count), opts.PageSize, page, 5) - pager.SetDefaultParams(ctx) - pager.AddParamString("topic", fmt.Sprint(topicOnly)) - pager.AddParamString("language", language) - pager.AddParamString(relevantReposOnlyParam, fmt.Sprint(opts.OnlyShowRelevant)) - if archived.Has() { - pager.AddParamString("archived", fmt.Sprint(archived.Value())) - } - if fork.Has() { - pager.AddParamString("fork", fmt.Sprint(fork.Value())) - } - if mirror.Has() { - pager.AddParamString("mirror", fmt.Sprint(mirror.Value())) - } - if template.Has() { - pager.AddParamString("template", fmt.Sprint(template.Value())) - } - if private.Has() { - pager.AddParamString("private", fmt.Sprint(private.Value())) - } + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, opts.TplName) diff --git a/routers/web/explore/user.go b/routers/web/explore/user.go index ef103af8cf..b14272c76a 100644 --- a/routers/web/explore/user.go +++ b/routers/web/explore/user.go @@ -120,10 +120,7 @@ func RenderUserSearch(ctx *context.Context, opts *user_model.SearchUserOptions, ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5) - pager.SetDefaultParams(ctx) - for paramKey, paramVal := range opts.ExtraParamStrings { - pager.AddParamString(paramKey, paramVal) - } + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplName) diff --git a/routers/web/org/home.go b/routers/web/org/home.go index bdc43acc30..277adb60ca 100644 --- a/routers/web/org/home.go +++ b/routers/web/org/home.go @@ -4,7 +4,6 @@ package org import ( - "fmt" "net/http" "path" "strings" @@ -13,6 +12,7 @@ import ( "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/renderhelper" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" @@ -22,9 +22,7 @@ import ( "code.gitea.io/gitea/services/context" ) -const ( - tplOrgHome templates.TplName = "org/home" -) +const tplOrgHome templates.TplName = "org/home" // Home show organization home page func Home(ctx *context.Context) { @@ -111,15 +109,19 @@ func home(ctx *context.Context, viewRepositories bool) { ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull ctx.Data["ShowMemberAndTeamTab"] = ctx.Org.IsMember || len(members) > 0 - if !prepareOrgProfileReadme(ctx, viewRepositories) { - ctx.Data["PageIsViewRepositories"] = true + prepareResult, err := shared_user.PrepareOrgHeader(ctx) + if err != nil { + ctx.ServerError("PrepareOrgHeader", err) + return } - var ( - repos []*repo_model.Repository - count int64 - ) - repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ + // if no profile readme, it still means "view repositories" + isViewOverview := !viewRepositories && prepareOrgProfileReadme(ctx, prepareResult) + ctx.Data["PageIsViewRepositories"] = !isViewOverview + ctx.Data["PageIsViewOverview"] = isViewOverview + ctx.Data["ShowOrgProfileReadmeSelector"] = isViewOverview && prepareResult.ProfilePublicReadmeBlob != nil && prepareResult.ProfilePrivateReadmeBlob != nil + + repos, count, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ PageSize: setting.UI.User.RepoPagingNum, Page: page, @@ -146,50 +148,51 @@ func home(ctx *context.Context, viewRepositories bool) { ctx.Data["Total"] = count pager := context.NewPagination(int(count), setting.UI.User.RepoPagingNum, page, 5) - pager.SetDefaultParams(ctx) - pager.AddParamString("language", language) - if archived.Has() { - pager.AddParamString("archived", fmt.Sprint(archived.Value())) - } - if fork.Has() { - pager.AddParamString("fork", fmt.Sprint(fork.Value())) - } - if mirror.Has() { - pager.AddParamString("mirror", fmt.Sprint(mirror.Value())) - } - if template.Has() { - pager.AddParamString("template", fmt.Sprint(template.Value())) - } - if private.Has() { - pager.AddParamString("private", fmt.Sprint(private.Value())) - } + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplOrgHome) } -func prepareOrgProfileReadme(ctx *context.Context, viewRepositories bool) bool { - profileDbRepo, profileGitRepo, profileReadme, profileClose := shared_user.FindUserProfileReadme(ctx, ctx.Doer) - defer profileClose() - ctx.Data["HasProfileReadme"] = profileReadme != nil +func prepareOrgProfileReadme(ctx *context.Context, prepareResult *shared_user.PrepareOrgHeaderResult) bool { + viewAs := ctx.FormString("view_as", util.Iif(ctx.Org.IsMember, "member", "public")) + viewAsMember := viewAs == "member" - if profileGitRepo == nil || profileReadme == nil || viewRepositories { + var profileRepo *repo_model.Repository + var readmeBlob *git.Blob + if viewAsMember { + if prepareResult.ProfilePrivateReadmeBlob != nil { + profileRepo, readmeBlob = prepareResult.ProfilePrivateRepo, prepareResult.ProfilePrivateReadmeBlob + } else { + profileRepo, readmeBlob = prepareResult.ProfilePublicRepo, prepareResult.ProfilePublicReadmeBlob + viewAsMember = false + } + } else { + if prepareResult.ProfilePublicReadmeBlob != nil { + profileRepo, readmeBlob = prepareResult.ProfilePublicRepo, prepareResult.ProfilePublicReadmeBlob + } else { + profileRepo, readmeBlob = prepareResult.ProfilePrivateRepo, prepareResult.ProfilePrivateReadmeBlob + viewAsMember = true + } + } + if readmeBlob == nil { return false } - if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil { - log.Error("failed to GetBlobContent: %v", err) - } else { - rctx := renderhelper.NewRenderContextRepoFile(ctx, profileDbRepo, renderhelper.RepoFileOptions{ - CurrentRefPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)), - }) - if profileContent, err := markdown.RenderString(rctx, bytes); err != nil { - log.Error("failed to RenderString: %v", err) - } else { - ctx.Data["ProfileReadme"] = profileContent - } + readmeBytes, err := readmeBlob.GetBlobContent(setting.UI.MaxDisplayFileSize) + if err != nil { + log.Error("failed to GetBlobContent for profile %q (view as %q) readme: %v", profileRepo.FullName(), viewAs, err) + return false } - ctx.Data["PageIsViewOverview"] = true + rctx := renderhelper.NewRenderContextRepoFile(ctx, profileRepo, renderhelper.RepoFileOptions{ + CurrentRefPath: path.Join("branch", util.PathEscapeSegments(profileRepo.DefaultBranch)), + }) + ctx.Data["ProfileReadmeContent"], err = markdown.RenderString(rctx, readmeBytes) + if err != nil { + log.Error("failed to GetBlobContent for profile %q (view as %q) readme: %v", profileRepo.FullName(), viewAs, err) + return false + } + ctx.Data["IsViewingOrgAsMember"] = viewAsMember return true } diff --git a/routers/web/org/members.go b/routers/web/org/members.go index 5a134caecb..1665a12302 100644 --- a/routers/web/org/members.go +++ b/routers/web/org/members.go @@ -54,9 +54,9 @@ func Members(ctx *context.Context) { return } - err = shared_user.RenderOrgHeader(ctx) + _, err = shared_user.PrepareOrgHeader(ctx) if err != nil { - ctx.ServerError("RenderOrgHeader", err) + ctx.ServerError("PrepareOrgHeader", err) return } diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index efcc8fadc8..32da1b41d1 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -120,7 +120,7 @@ func Projects(ctx *context.Context) { } pager := context.NewPagination(int(total), setting.UI.IssuePagingNum, page, numPages) - pager.AddParamString("state", fmt.Sprint(ctx.Data["State"])) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.Data["CanWriteProjects"] = canWriteProjects(ctx) @@ -211,7 +211,7 @@ func ChangeProjectStatus(ctx *context.Context) { ctx.NotFoundOrServerError("ChangeProjectStatusByRepoIDAndID", project_model.IsErrProjectNotExist, err) return } - ctx.JSONRedirect(fmt.Sprintf("%s/-/projects/%d", ctx.ContextUser.HomeLink(), id)) + ctx.JSONRedirect(project_model.ProjectLinkForOrg(ctx.ContextUser, id)) } // DeleteProject delete a project @@ -261,7 +261,7 @@ func RenderEditProject(ctx *context.Context) { ctx.Data["redirect"] = ctx.FormString("redirect") ctx.Data["HomeLink"] = ctx.ContextUser.HomeLink() ctx.Data["card_type"] = p.CardType - ctx.Data["CancelLink"] = fmt.Sprintf("%s/-/projects/%d", ctx.ContextUser.HomeLink(), p.ID) + ctx.Data["CancelLink"] = project_model.ProjectLinkForOrg(ctx.ContextUser, p.ID) ctx.HTML(http.StatusOK, tplProjectsNew) } @@ -275,7 +275,7 @@ func EditProjectPost(ctx *context.Context) { ctx.Data["PageIsViewProjects"] = true ctx.Data["CanWriteProjects"] = canWriteProjects(ctx) ctx.Data["CardTypes"] = project_model.GetCardConfig() - ctx.Data["CancelLink"] = fmt.Sprintf("%s/-/projects/%d", ctx.ContextUser.HomeLink(), projectID) + ctx.Data["CancelLink"] = project_model.ProjectLinkForOrg(ctx.ContextUser, projectID) shared_user.RenderUserHeader(ctx) diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go index 0137f2cc96..26031029d6 100644 --- a/routers/web/org/teams.go +++ b/routers/web/org/teams.go @@ -58,9 +58,9 @@ func Teams(ctx *context.Context) { } ctx.Data["Teams"] = ctx.Org.Teams - err := shared_user.RenderOrgHeader(ctx) + _, err := shared_user.PrepareOrgHeader(ctx) if err != nil { - ctx.ServerError("RenderOrgHeader", err) + ctx.ServerError("PrepareOrgHeader", err) return } diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go index 099593bff0..f0d8d81fee 100644 --- a/routers/web/repo/actions/actions.go +++ b/routers/web/repo/actions/actions.go @@ -6,7 +6,6 @@ package actions import ( "bytes" stdCtx "context" - "fmt" "net/http" "slices" "strings" @@ -262,10 +261,7 @@ func List(ctx *context.Context) { ctx.Data["StatusInfoList"] = actions_model.GetStatusInfoList(ctx) pager := context.NewPagination(int(total), opts.PageSize, opts.Page, 5) - pager.SetDefaultParams(ctx) - pager.AddParamString("workflow", workflowID) - pager.AddParamString("actor", fmt.Sprint(actorID)) - pager.AddParamString("status", fmt.Sprint(status)) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.Data["HasWorkflowsOrRuns"] = len(workflows) > 0 || len(runs) > 0 diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go index 72fd958e28..2bcd7821b4 100644 --- a/routers/web/repo/branch.go +++ b/routers/web/repo/branch.go @@ -87,7 +87,7 @@ func Branches(ctx *context.Context) { ctx.Data["CommitStatuses"] = commitStatuses ctx.Data["DefaultBranchBranch"] = defaultBranch pager := context.NewPagination(int(branchesCount), pageSize, page, 5) - pager.SetDefaultParams(ctx) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplBranch) } diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index 6f534b9e2e..3655233312 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -101,7 +101,7 @@ func Commits(ctx *context.Context) { ctx.Data["CommitCount"] = commitsCount pager := context.NewPagination(int(commitsCount), pageSize, page, 5) - pager.SetDefaultParams(ctx) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplCommits) } @@ -139,7 +139,6 @@ func Graph(ctx *context.Context) { if err != nil { log.Warn("GetCommitGraphsCount error for generate graph exclude prs: %t branches: %s in %-v, Will Ignore branches and try again. Underlying Error: %v", hidePRRefs, branches, ctx.Repo.Repository, err) realBranches = []string{} - branches = []string{} graphCommitsCount, err = ctx.Repo.GetCommitGraphsCount(ctx, hidePRRefs, realBranches, files) if err != nil { ctx.ServerError("GetCommitGraphsCount", err) @@ -175,14 +174,7 @@ func Graph(ctx *context.Context) { ctx.Data["CommitCount"] = commitsCount paginator := context.NewPagination(int(graphCommitsCount), setting.UI.GraphMaxCommitNum, page, 5) - paginator.AddParamString("mode", mode) - paginator.AddParamString("hide-pr-refs", fmt.Sprint(hidePRRefs)) - for _, branch := range branches { - paginator.AddParamString("branch", branch) - } - for _, file := range files { - paginator.AddParamString("file", file) - } + paginator.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = paginator if ctx.FormBool("div-only") { ctx.HTML(http.StatusOK, tplGraphDiv) @@ -262,7 +254,7 @@ func FileHistory(ctx *context.Context) { ctx.Data["CommitCount"] = commitsCount pager := context.NewPagination(int(commitsCount), setting.Git.CommitsRangeSize, page, 5) - pager.SetDefaultParams(ctx) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplCommits) } diff --git a/routers/web/repo/download.go b/routers/web/repo/download.go index 1ed907b2f9..cb1163c70b 100644 --- a/routers/web/repo/download.go +++ b/routers/web/repo/download.go @@ -5,7 +5,6 @@ package repo import ( - "path" "time" git_model "code.gitea.io/gitea/models/git" @@ -82,7 +81,7 @@ func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob, lastModified *time.Tim return common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, lastModified) } -func getBlobForEntry(ctx *context.Context) (blob *git.Blob, lastModified *time.Time) { +func getBlobForEntry(ctx *context.Context) (*git.Blob, *time.Time) { entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath) if err != nil { if git.IsErrNotExist(err) { @@ -98,19 +97,14 @@ func getBlobForEntry(ctx *context.Context) (blob *git.Blob, lastModified *time.T return nil, nil } - info, _, err := git.Entries([]*git.TreeEntry{entry}).GetCommitsInfo(ctx, ctx.Repo.Commit, path.Dir("/" + ctx.Repo.TreePath)[1:]) + latestCommit, err := ctx.Repo.GitRepo.GetTreePathLatestCommit(ctx.Repo.Commit.ID.String(), ctx.Repo.TreePath) if err != nil { - ctx.ServerError("GetCommitsInfo", err) + ctx.ServerError("GetTreePathLatestCommit", err) return nil, nil } + lastModified := &latestCommit.Committer.When - if len(info) == 1 { - // Not Modified - lastModified = &info[0].Commit.Committer.When - } - blob = entry.Blob() - - return blob, lastModified + return entry.Blob(), lastModified } // SingleDownload download a file by repos path diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index a3a4e73d7b..f150897a2d 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -602,7 +602,7 @@ func updateAttachments(ctx *context.Context, item any, files []string) error { case *issues_model.Issue: err = issues_model.UpdateIssueAttachments(ctx, content.ID, files) case *issues_model.Comment: - err = content.UpdateAttachments(ctx, files) + err = issues_model.UpdateCommentAttachments(ctx, content, files) default: return fmt.Errorf("unknown Type: %T", content) } diff --git a/routers/web/repo/issue_label_test.go b/routers/web/repo/issue_label_test.go index c86a03da51..8a613e2c7e 100644 --- a/routers/web/repo/issue_label_test.go +++ b/routers/web/repo/issue_label_test.go @@ -162,10 +162,11 @@ func TestUpdateIssueLabel_Toggle(t *testing.T) { UpdateIssueLabel(ctx) assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) for _, issueID := range testCase.IssueIDs { - unittest.AssertExistsIf(t, testCase.ExpectedAdd, &issues_model.IssueLabel{ - IssueID: issueID, - LabelID: testCase.LabelID, - }) + if testCase.ExpectedAdd { + unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: testCase.LabelID}) + } else { + unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: testCase.LabelID}) + } } unittest.CheckConsistencyFor(t, &issues_model.Label{}) } diff --git a/routers/web/repo/issue_new.go b/routers/web/repo/issue_new.go index 9a941ce857..32daa3e48f 100644 --- a/routers/web/repo/issue_new.go +++ b/routers/web/repo/issue_new.go @@ -396,8 +396,15 @@ func NewIssuePost(ctx *context.Context) { log.Trace("Issue created: %d/%d", repo.ID, issue.ID) if ctx.FormString("redirect_after_creation") == "project" && projectID > 0 { - ctx.JSONRedirect(ctx.Repo.RepoLink + "/projects/" + strconv.FormatInt(projectID, 10)) - } else { - ctx.JSONRedirect(issue.Link()) + project, err := project_model.GetProjectByID(ctx, projectID) + if err == nil { + if project.Type == project_model.TypeOrganization { + ctx.JSONRedirect(project_model.ProjectLinkForOrg(ctx.Repo.Owner, project.ID)) + } else { + ctx.JSONRedirect(project_model.ProjectLinkForRepo(repo, project.ID)) + } + return + } } + ctx.JSONRedirect(issue.Link()) } diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go index 392d87167a..6a0e6b25a9 100644 --- a/routers/web/repo/milestone.go +++ b/routers/web/repo/milestone.go @@ -4,7 +4,6 @@ package repo import ( - "fmt" "net/http" "net/url" @@ -93,8 +92,7 @@ func Milestones(ctx *context.Context) { ctx.Data["IsShowClosed"] = isShowClosed pager := context.NewPagination(int(total), setting.UI.IssuePagingNum, page, 5) - pager.AddParamString("state", fmt.Sprint(ctx.Data["State"])) - pager.AddParamString("q", keyword) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplMilestone) diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go index c8d3719bc0..5dcfd0454c 100644 --- a/routers/web/repo/packages.go +++ b/routers/web/repo/packages.go @@ -70,8 +70,7 @@ func Packages(ctx *context.Context) { ctx.Data["RepositoryAccessMap"] = map[int64]bool{ctx.Repo.Repository.ID: true} // There is only the current repository pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5) - pager.AddParamString("q", query) - pager.AddParamString("type", packageType) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplPackagesList) diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 4313b6c403..346132102f 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -115,7 +115,7 @@ func Projects(ctx *context.Context) { } pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, numPages) - pager.AddParamString("state", fmt.Sprint(ctx.Data["State"])) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) @@ -181,7 +181,7 @@ func ChangeProjectStatus(ctx *context.Context) { ctx.NotFoundOrServerError("ChangeProjectStatusByRepoIDAndID", project_model.IsErrProjectNotExist, err) return } - ctx.JSONRedirect(fmt.Sprintf("%s/projects/%d", ctx.Repo.RepoLink, id)) + ctx.JSONRedirect(project_model.ProjectLinkForRepo(ctx.Repo.Repository, id)) } // DeleteProject delete a project @@ -235,7 +235,7 @@ func RenderEditProject(ctx *context.Context) { ctx.Data["content"] = p.Description ctx.Data["card_type"] = p.CardType ctx.Data["redirect"] = ctx.FormString("redirect") - ctx.Data["CancelLink"] = fmt.Sprintf("%s/projects/%d", ctx.Repo.Repository.Link(), p.ID) + ctx.Data["CancelLink"] = project_model.ProjectLinkForRepo(ctx.Repo.Repository, p.ID) ctx.HTML(http.StatusOK, tplProjectsNew) } @@ -249,7 +249,7 @@ func EditProjectPost(ctx *context.Context) { ctx.Data["PageIsEditProjects"] = true ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) ctx.Data["CardTypes"] = project_model.GetCardConfig() - ctx.Data["CancelLink"] = fmt.Sprintf("%s/projects/%d", ctx.Repo.Repository.Link(), projectID) + ctx.Data["CancelLink"] = project_model.ProjectLinkForRepo(ctx.Repo.Repository, projectID) if ctx.HasError() { ctx.HTML(http.StatusOK, tplProjectsNew) diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go index b8176cb70b..5b099fe8d4 100644 --- a/routers/web/repo/release.go +++ b/routers/web/repo/release.go @@ -186,7 +186,7 @@ func Releases(ctx *context.Context) { numReleases := ctx.Data["NumReleases"].(int64) pager := context.NewPagination(int(numReleases), listOptions.PageSize, listOptions.Page, 5) - pager.SetDefaultParams(ctx) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplReleasesList) } @@ -240,7 +240,7 @@ func TagsList(ctx *context.Context) { ctx.Data["TagCount"] = count pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5) - pager.SetDefaultParams(ctx) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.Data["PageIsViewCode"] = !ctx.Repo.Repository.UnitEnabled(ctx, unit.TypeReleases) ctx.HTML(http.StatusOK, tplTagsList) diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index 4fa3179829..e8139a019b 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -148,27 +148,33 @@ func getRepoPrivate(ctx *context.Context) bool { } } -// Create render creating repository page -func Create(ctx *context.Context) { +func createCommon(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("new_repo") - - // Give default value for template to render. ctx.Data["Gitignores"] = repo_module.Gitignores ctx.Data["LabelTemplateFiles"] = repo_module.LabelTemplateFiles ctx.Data["Licenses"] = repo_module.Licenses ctx.Data["Readmes"] = repo_module.Readmes - ctx.Data["readme"] = "Default" - ctx.Data["private"] = getRepoPrivate(ctx) ctx.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate - ctx.Data["default_branch"] = setting.Repository.DefaultBranch + ctx.Data["CanCreateRepo"] = ctx.Doer.CanCreateRepo() + ctx.Data["MaxCreationLimit"] = ctx.Doer.MaxCreationLimit() + ctx.Data["SupportedObjectFormats"] = git.DefaultFeatures().SupportedObjectFormats + ctx.Data["DefaultObjectFormat"] = git.Sha1ObjectFormat +} +// Create render creating repository page +func Create(ctx *context.Context) { + createCommon(ctx) ctxUser := checkContextUser(ctx, ctx.FormInt64("org")) if ctx.Written() { return } ctx.Data["ContextUser"] = ctxUser + ctx.Data["readme"] = "Default" + ctx.Data["private"] = getRepoPrivate(ctx) + ctx.Data["default_branch"] = setting.Repository.DefaultBranch ctx.Data["repo_template_name"] = ctx.Tr("repo.template_select") + templateID := ctx.FormInt64("template_id") if templateID > 0 { templateRepo, err := repo_model.GetRepositoryByID(ctx, templateID) @@ -178,11 +184,6 @@ func Create(ctx *context.Context) { } } - ctx.Data["CanCreateRepo"] = ctx.Doer.CanCreateRepo() - ctx.Data["MaxCreationLimit"] = ctx.Doer.MaxCreationLimit() - ctx.Data["SupportedObjectFormats"] = git.DefaultFeatures().SupportedObjectFormats - ctx.Data["DefaultObjectFormat"] = git.Sha1ObjectFormat - ctx.HTML(http.StatusOK, tplCreate) } @@ -220,16 +221,8 @@ func handleCreateError(ctx *context.Context, owner *user_model.User, err error, // CreatePost response for creating repository func CreatePost(ctx *context.Context) { + createCommon(ctx) form := web.GetForm(ctx).(*forms.CreateRepoForm) - ctx.Data["Title"] = ctx.Tr("new_repo") - - ctx.Data["Gitignores"] = repo_module.Gitignores - ctx.Data["LabelTemplateFiles"] = repo_module.LabelTemplateFiles - ctx.Data["Licenses"] = repo_module.Licenses - ctx.Data["Readmes"] = repo_module.Readmes - - ctx.Data["CanCreateRepo"] = ctx.Doer.CanCreateRepo() - ctx.Data["MaxCreationLimit"] = ctx.Doer.MaxCreationLimit() ctxUser := checkContextUser(ctx, form.UID) if ctx.Written() { @@ -237,6 +230,14 @@ func CreatePost(ctx *context.Context) { } ctx.Data["ContextUser"] = ctxUser + if form.RepoTemplate > 0 { + templateRepo, err := repo_model.GetRepositoryByID(ctx, form.RepoTemplate) + if err == nil && access_model.CheckRepoUnitUser(ctx, templateRepo, ctxUser, unit.TypeCode) { + ctx.Data["repo_template"] = form.RepoTemplate + ctx.Data["repo_template_name"] = templateRepo.Name + } + } + if ctx.HasError() { ctx.HTML(http.StatusOK, tplCreate) return diff --git a/routers/web/repo/search.go b/routers/web/repo/search.go index a037a34833..c60301475f 100644 --- a/routers/web/repo/search.go +++ b/routers/web/repo/search.go @@ -12,6 +12,7 @@ import ( code_indexer "code.gitea.io/gitea/modules/indexer/code" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/services/context" ) @@ -29,18 +30,9 @@ func indexSettingToGitGrepPathspecList() (list []string) { // Search render repository search page func Search(ctx *context.Context) { - language := ctx.FormTrim("l") - keyword := ctx.FormTrim("q") - - isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true) - - ctx.Data["Keyword"] = keyword - ctx.Data["Language"] = language - ctx.Data["IsFuzzy"] = isFuzzy ctx.Data["PageIsViewCode"] = true - ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled - - if keyword == "" { + prepareSearch := common.PrepareCodeSearch(ctx) + if prepareSearch.Keyword == "" { ctx.HTML(http.StatusOK, tplSearch) return } @@ -57,9 +49,9 @@ func Search(ctx *context.Context) { var err error total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{ RepoIDs: []int64{ctx.Repo.Repository.ID}, - Keyword: keyword, - IsKeywordFuzzy: isFuzzy, - Language: language, + Keyword: prepareSearch.Keyword, + IsKeywordFuzzy: prepareSearch.IsFuzzy, + Language: prepareSearch.Language, Paginator: &db.ListOptions{ Page: page, PageSize: setting.UI.RepoSearchPagingNum, @@ -75,9 +67,9 @@ func Search(ctx *context.Context) { ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx) } } else { - res, err := git.GrepSearch(ctx, ctx.Repo.GitRepo, keyword, git.GrepOptions{ + res, err := git.GrepSearch(ctx, ctx.Repo.GitRepo, prepareSearch.Keyword, git.GrepOptions{ ContextLineNumber: 1, - IsFuzzy: isFuzzy, + IsFuzzy: prepareSearch.IsFuzzy, RefName: git.RefNameFromBranch(ctx.Repo.BranchName).String(), // BranchName should be default branch or the first existing branch PathspecList: indexSettingToGitGrepPathspecList(), }) @@ -108,8 +100,7 @@ func Search(ctx *context.Context) { ctx.Data["SearchResultLanguages"] = searchResultLanguages pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5) - pager.SetDefaultParams(ctx) - pager.AddParamString("l", language) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplSearch) diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index 3fca7cebea..19366c0104 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -440,8 +440,7 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) ctx.Data["Commits"] = git_model.ConvertFromGitCommit(ctx, commitsHistory, ctx.Repo.Repository) pager := context.NewPagination(int(commitsCount), setting.Git.CommitsRangeSize, page, 5) - pager.SetDefaultParams(ctx) - pager.AddParamString("action", "_revision") + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager return wikiRepo, entry diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go index d388d2b5d9..62b146c7f3 100644 --- a/routers/web/shared/user/header.go +++ b/routers/web/shared/user/header.go @@ -20,6 +20,7 @@ import ( "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" ) @@ -102,37 +103,46 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) { } } -func FindUserProfileReadme(ctx *context.Context, doer *user_model.User) (profileDbRepo *repo_model.Repository, profileGitRepo *git.Repository, profileReadmeBlob *git.Blob, profileClose func()) { - profileDbRepo, err := repo_model.GetRepositoryByName(ctx, ctx.ContextUser.ID, ".profile") - if err == nil { - perm, err := access_model.GetUserRepoPermission(ctx, profileDbRepo, doer) - if err == nil && !profileDbRepo.IsEmpty && perm.CanRead(unit.TypeCode) { - if profileGitRepo, err = gitrepo.OpenRepository(ctx, profileDbRepo); err != nil { - log.Error("FindUserProfileReadme failed to OpenRepository: %v", err) - } else { - if commit, err := profileGitRepo.GetBranchCommit(profileDbRepo.DefaultBranch); err != nil { - log.Error("FindUserProfileReadme failed to GetBranchCommit: %v", err) - } else { - profileReadmeBlob, _ = commit.GetBlobByPath("README.md") - } - } +func FindOwnerProfileReadme(ctx *context.Context, doer *user_model.User, optProfileRepoName ...string) (profileDbRepo *repo_model.Repository, profileReadmeBlob *git.Blob) { + profileRepoName := util.OptionalArg(optProfileRepoName, RepoNameProfile) + profileDbRepo, err := repo_model.GetRepositoryByName(ctx, ctx.ContextUser.ID, profileRepoName) + if err != nil { + if !repo_model.IsErrRepoNotExist(err) { + log.Error("FindOwnerProfileReadme failed to GetRepositoryByName: %v", err) } - } else if !repo_model.IsErrRepoNotExist(err) { - log.Error("FindUserProfileReadme failed to GetRepositoryByName: %v", err) + return nil, nil } - return profileDbRepo, profileGitRepo, profileReadmeBlob, func() { - if profileGitRepo != nil { - _ = profileGitRepo.Close() - } + + perm, err := access_model.GetUserRepoPermission(ctx, profileDbRepo, doer) + if err != nil { + log.Error("FindOwnerProfileReadme failed to GetRepositoryByName: %v", err) + return nil, nil } + if profileDbRepo.IsEmpty || !perm.CanRead(unit.TypeCode) { + return nil, nil + } + + profileGitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, profileDbRepo) + if err != nil { + log.Error("FindOwnerProfileReadme failed to OpenRepository: %v", err) + return nil, nil + } + + commit, err := profileGitRepo.GetBranchCommit(profileDbRepo.DefaultBranch) + if err != nil { + log.Error("FindOwnerProfileReadme failed to GetBranchCommit: %v", err) + return nil, nil + } + + profileReadmeBlob, _ = commit.GetBlobByPath("README.md") // no need to handle this error + return profileDbRepo, profileReadmeBlob } func RenderUserHeader(ctx *context.Context) { prepareContextForCommonProfile(ctx) - _, _, profileReadmeBlob, profileClose := FindUserProfileReadme(ctx, ctx.Doer) - defer profileClose() - ctx.Data["HasProfileReadme"] = profileReadmeBlob != nil + _, profileReadmeBlob := FindOwnerProfileReadme(ctx, ctx.Doer) + ctx.Data["HasUserProfileReadme"] = profileReadmeBlob != nil } func LoadHeaderCount(ctx *context.Context) error { @@ -169,14 +179,28 @@ func LoadHeaderCount(ctx *context.Context) error { return nil } -func RenderOrgHeader(ctx *context.Context) error { - if err := LoadHeaderCount(ctx); err != nil { - return err +const ( + RepoNameProfilePrivate = ".profile-private" + RepoNameProfile = ".profile" +) + +type PrepareOrgHeaderResult struct { + ProfilePublicRepo *repo_model.Repository + ProfilePublicReadmeBlob *git.Blob + ProfilePrivateRepo *repo_model.Repository + ProfilePrivateReadmeBlob *git.Blob + HasOrgProfileReadme bool +} + +func PrepareOrgHeader(ctx *context.Context) (result *PrepareOrgHeaderResult, err error) { + if err = LoadHeaderCount(ctx); err != nil { + return nil, err } - _, _, profileReadmeBlob, profileClose := FindUserProfileReadme(ctx, ctx.Doer) - defer profileClose() - ctx.Data["HasProfileReadme"] = profileReadmeBlob != nil - - return nil + result = &PrepareOrgHeaderResult{} + result.ProfilePublicRepo, result.ProfilePublicReadmeBlob = FindOwnerProfileReadme(ctx, ctx.Doer) + result.ProfilePrivateRepo, result.ProfilePrivateReadmeBlob = FindOwnerProfileReadme(ctx, ctx.Doer, RepoNameProfilePrivate) + result.HasOrgProfileReadme = result.ProfilePublicReadmeBlob != nil || result.ProfilePrivateReadmeBlob != nil + ctx.Data["HasOrgProfileReadme"] = result.HasOrgProfileReadme // many pages need it to show the "overview" tab + return result, nil } diff --git a/routers/web/user/code.go b/routers/web/user/code.go index f805f2b028..665ce1a6a6 100644 --- a/routers/web/user/code.go +++ b/routers/web/user/code.go @@ -11,6 +11,7 @@ import ( code_indexer "code.gitea.io/gitea/modules/indexer/code" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/routers/common" shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/context" ) @@ -34,20 +35,11 @@ func CodeSearch(ctx *context.Context) { } ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled - ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled ctx.Data["Title"] = ctx.Tr("explore.code") - - language := ctx.FormTrim("l") - keyword := ctx.FormTrim("q") - - isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true) - - ctx.Data["Keyword"] = keyword - ctx.Data["Language"] = language - ctx.Data["IsFuzzy"] = isFuzzy ctx.Data["IsCodePage"] = true - if keyword == "" { + prepareSearch := common.PrepareCodeSearch(ctx) + if prepareSearch.Keyword == "" { ctx.HTML(http.StatusOK, tplUserCode) return } @@ -77,9 +69,9 @@ func CodeSearch(ctx *context.Context) { if len(repoIDs) > 0 { total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{ RepoIDs: repoIDs, - Keyword: keyword, - IsKeywordFuzzy: isFuzzy, - Language: language, + Keyword: prepareSearch.Keyword, + IsKeywordFuzzy: prepareSearch.IsFuzzy, + Language: prepareSearch.Language, Paginator: &db.ListOptions{ Page: page, PageSize: setting.UI.RepoSearchPagingNum, @@ -121,8 +113,7 @@ func CodeSearch(ctx *context.Context) { ctx.Data["SearchResultLanguages"] = searchResultLanguages pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5) - pager.SetDefaultParams(ctx) - pager.AddParamString("l", language) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplUserCode) diff --git a/routers/web/user/home.go b/routers/web/user/home.go index b5d3a7294b..c79648a455 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -139,7 +139,7 @@ func Dashboard(ctx *context.Context) { ctx.Data["Feeds"] = feeds pager := context.NewPagination(int(count), setting.UI.FeedPagingNum, page, 5) - pager.AddParamString("date", date) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplDashboard) @@ -330,10 +330,7 @@ func Milestones(ctx *context.Context) { ctx.Data["IsShowClosed"] = isShowClosed pager := context.NewPagination(pagerCount, setting.UI.IssuePagingNum, page, 5) - pager.AddParamString("q", keyword) - pager.AddParamString("repos", reposQuery) - pager.AddParamString("sort", sortType) - pager.AddParamString("state", fmt.Sprint(ctx.Data["State"])) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplMilestones) diff --git a/routers/web/user/notification.go b/routers/web/user/notification.go index 732fac2595..1c91ff6364 100644 --- a/routers/web/user/notification.go +++ b/routers/web/user/notification.go @@ -173,7 +173,7 @@ func getNotifications(ctx *context.Context) { ctx.Data["Status"] = status ctx.Data["Notifications"] = notifications - pager.SetDefaultParams(ctx) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager } @@ -357,8 +357,7 @@ func NotificationSubscriptions(ctx *context.Context) { ctx.Redirect(fmt.Sprintf("/notifications/subscriptions?page=%d", pager.Paginater.Current())) return } - pager.AddParamString("sort", sortType) - pager.AddParamString("state", state) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplNotificationSubscriptions) @@ -446,22 +445,7 @@ func NotificationWatching(ctx *context.Context) { // redirect to last page if request page is more than total pages pager := context.NewPagination(total, setting.UI.User.RepoPagingNum, page, 5) - pager.SetDefaultParams(ctx) - if archived.Has() { - pager.AddParamString("archived", fmt.Sprint(archived.Value())) - } - if fork.Has() { - pager.AddParamString("fork", fmt.Sprint(fork.Value())) - } - if mirror.Has() { - pager.AddParamString("mirror", fmt.Sprint(mirror.Value())) - } - if template.Has() { - pager.AddParamString("template", fmt.Sprint(template.Value())) - } - if private.Has() { - pager.AddParamString("private", fmt.Sprint(private.Value())) - } + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.Data["Status"] = 2 diff --git a/routers/web/user/package.go b/routers/web/user/package.go index 8d78da7c5a..1f75faf1c6 100644 --- a/routers/web/user/package.go +++ b/routers/web/user/package.go @@ -128,8 +128,7 @@ func ListPackages(ctx *context.Context) { } pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5) - pager.AddParamString("q", query) - pager.AddParamString("type", packageType) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplPackagesList) @@ -348,11 +347,6 @@ func ListPackageVersions(ctx *context.Context) { ctx.Data["Query"] = query ctx.Data["Sort"] = sort - pagerParams := map[string]string{ - "q": query, - "sort": sort, - } - var ( total int64 pvs []*packages_model.PackageVersion @@ -361,7 +355,6 @@ func ListPackageVersions(ctx *context.Context) { case packages_model.TypeContainer: tagged := ctx.FormTrim("tagged") - pagerParams["tagged"] = tagged ctx.Data["Tagged"] = tagged pvs, total, err = container_model.SearchImageTags(ctx, &container_model.ImageTagsSearchOptions{ @@ -407,9 +400,7 @@ func ListPackageVersions(ctx *context.Context) { } pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5) - for k, v := range pagerParams { - pager.AddParamString(k, v) - } + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplPackageVersionList) diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index 9c014bffdb..006ffdcf7e 100644 --- a/routers/web/user/profile.go +++ b/routers/web/user/profile.go @@ -74,8 +74,7 @@ func userProfile(ctx *context.Context) { ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data) } - profileDbRepo, _ /*profileGitRepo*/, profileReadmeBlob, profileClose := shared_user.FindUserProfileReadme(ctx, ctx.Doer) - defer profileClose() + profileDbRepo, profileReadmeBlob := shared_user.FindOwnerProfileReadme(ctx, ctx.Doer) showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID) prepareUserProfileTabData(ctx, showPrivate, profileDbRepo, profileReadmeBlob) @@ -96,7 +95,7 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb } } ctx.Data["TabName"] = tab - ctx.Data["HasProfileReadme"] = profileReadme != nil + ctx.Data["HasUserProfileReadme"] = profileReadme != nil page := ctx.FormInt("page") if page <= 0 { @@ -254,7 +253,7 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb if profileContent, err := markdown.RenderString(rctx, bytes); err != nil { log.Error("failed to RenderString: %v", err) } else { - ctx.Data["ProfileReadme"] = profileContent + ctx.Data["ProfileReadmeContent"] = profileContent } } case "organizations": diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go index 4b3c214096..ebf682bf58 100644 --- a/routers/web/user/setting/profile.go +++ b/routers/web/user/setting/profile.go @@ -222,7 +222,7 @@ func Organization(ctx *context.Context) { ctx.Data["Orgs"] = orgs pager := context.NewPagination(int(total), opts.PageSize, opts.Page, 5) - pager.SetDefaultParams(ctx) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplSettingsOrganization) } @@ -329,7 +329,7 @@ func Repos(ctx *context.Context) { } ctx.Data["ContextUser"] = ctxUser pager := context.NewPagination(count, opts.PageSize, opts.Page, 5) - pager.SetDefaultParams(ctx) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplSettingsRepositories) } diff --git a/services/actions/notifier.go b/services/actions/notifier.go index a4ebdf9e88..67e33e7cce 100644 --- a/services/actions/notifier.go +++ b/services/actions/notifier.go @@ -58,7 +58,15 @@ func (n *actionsNotifier) NewIssue(ctx context.Context, issue *issues_model.Issu // IssueChangeContent notifies change content of issue func (n *actionsNotifier) IssueChangeContent(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldContent string) { ctx = withMethod(ctx, "IssueChangeContent") + n.notifyIssueChangeWithTitleOrContent(ctx, doer, issue) +} +func (n *actionsNotifier) IssueChangeTitle(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldTitle string) { + ctx = withMethod(ctx, "IssueChangeTitle") + n.notifyIssueChangeWithTitleOrContent(ctx, doer, issue) +} + +func (n *actionsNotifier) notifyIssueChangeWithTitleOrContent(ctx context.Context, doer *user_model.User, issue *issues_model.Issue) { var err error if err = issue.LoadRepo(ctx); err != nil { log.Error("LoadRepo: %v", err) diff --git a/services/context/api.go b/services/context/api.go index 7b604c5ea1..bda705cb48 100644 --- a/services/context/api.go +++ b/services/context/api.go @@ -274,7 +274,7 @@ func ReferencesGitRepo(allowEmpty ...bool) func(ctx *APIContext) { // For API calls. if ctx.Repo.GitRepo == nil { var err error - ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, ctx.Repo.Repository) + ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository) if err != nil { ctx.Error(http.StatusInternalServerError, fmt.Sprintf("Open Repository %v failed", ctx.Repo.Repository.FullName()), err) return diff --git a/services/context/base_form.go b/services/context/base_form.go index ddf9734f57..5b8cae9e99 100644 --- a/services/context/base_form.go +++ b/services/context/base_form.go @@ -8,11 +8,16 @@ import ( "strings" "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/modules/util" ) // FormString returns the first value matching the provided key in the form as a string -func (b *Base) FormString(key string) string { - return b.Req.FormValue(key) +func (b *Base) FormString(key string, def ...string) string { + s := b.Req.FormValue(key) + if s == "" { + s = util.OptionalArg(def) + } + return s } // FormStrings returns a string slice for the provided key from the form diff --git a/services/context/pagination.go b/services/context/pagination.go index 42117cf96d..d33dd217d0 100644 --- a/services/context/pagination.go +++ b/services/context/pagination.go @@ -27,19 +27,13 @@ func NewPagination(total, pagingNum, current, numPages int) *Pagination { return p } -// AddParamString adds a string parameter directly -func (p *Pagination) AddParamString(key, value string) { - urlParam := fmt.Sprintf("%s=%v", url.QueryEscape(key), url.QueryEscape(value)) - p.urlParams = append(p.urlParams, urlParam) -} - func (p *Pagination) AddParamFromRequest(req *http.Request) { for key, values := range req.URL.Query() { - if key == "page" || len(values) == 0 { + if key == "page" || len(values) == 0 || (len(values) == 1 && values[0] == "") { continue } for _, value := range values { - urlParam := fmt.Sprintf("%s=%v", key, url.QueryEscape(value)) + urlParam := fmt.Sprintf("%s=%v", url.QueryEscape(key), url.QueryEscape(value)) p.urlParams = append(p.urlParams, urlParam) } } @@ -49,17 +43,3 @@ func (p *Pagination) AddParamFromRequest(req *http.Request) { func (p *Pagination) GetParams() template.URL { return template.URL(strings.Join(p.urlParams, "&")) } - -// SetDefaultParams sets common pagination params that are often used -func (p *Pagination) SetDefaultParams(ctx *Context) { - if v, ok := ctx.Data["SortType"].(string); ok { - p.AddParamString("sort", v) - } - if v, ok := ctx.Data["Keyword"].(string); ok { - p.AddParamString("q", v) - } - if v, ok := ctx.Data["IsFuzzy"].(bool); ok { - p.AddParamString("fuzzy", fmt.Sprint(v)) - } - // do not add any more uncommon params here! -} diff --git a/services/context/repo.go b/services/context/repo.go index b537a05036..2a473f4a54 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -622,7 +622,7 @@ func RepoAssignment(ctx *Context) { ctx.Repo.GitRepo = nil } - ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, repo) + ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo) if err != nil { if strings.Contains(err.Error(), "repository does not exist") || strings.Contains(err.Error(), "no such file or directory") { log.Error("Repository %-v has a broken repository on the file system: %s Error: %v", ctx.Repo.Repository, ctx.Repo.Repository.RepoPath(), err) @@ -881,7 +881,7 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func ) if ctx.Repo.GitRepo == nil { - ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, ctx.Repo.Repository) + ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository) if err != nil { ctx.ServerError(fmt.Sprintf("Open Repository %v failed", ctx.Repo.Repository.FullName()), err) return diff --git a/services/mailer/mail.go b/services/mailer/mail.go index a6763e4f03..52e19bde6f 100644 --- a/services/mailer/mail.go +++ b/services/mailer/mail.go @@ -93,7 +93,8 @@ func SendActivateAccountMail(locale translation.Locale, u *user_model.User) { // No mail service configured return } - sendUserMail(locale.Language(), u, mailAuthActivate, u.GenerateEmailActivateCode(u.Email), locale.TrString("mail.activate_account"), "activate account") + opts := &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateAccount} + sendUserMail(locale.Language(), u, mailAuthActivate, user_model.GenerateUserTimeLimitCode(opts, u), locale.TrString("mail.activate_account"), "activate account") } // SendResetPasswordMail sends a password reset mail to the user @@ -103,7 +104,8 @@ func SendResetPasswordMail(u *user_model.User) { return } locale := translation.NewLocale(u.Language) - sendUserMail(u.Language, u, mailAuthResetPassword, u.GenerateEmailActivateCode(u.Email), locale.TrString("mail.reset_password"), "recover account") + opts := &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeResetPassword} + sendUserMail(u.Language, u, mailAuthResetPassword, user_model.GenerateUserTimeLimitCode(opts, u), locale.TrString("mail.reset_password"), "recover account") } // SendActivateEmailMail sends confirmation email to confirm new email address @@ -113,11 +115,12 @@ func SendActivateEmailMail(u *user_model.User, email string) { return } locale := translation.NewLocale(u.Language) + opts := &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateEmail, NewEmail: email} data := map[string]any{ "locale": locale, "DisplayName": u.DisplayName(), "ActiveCodeLives": timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, locale), - "Code": u.GenerateEmailActivateCode(email), + "Code": user_model.GenerateUserTimeLimitCode(opts, u), "Email": email, "Language": locale.Language(), } diff --git a/services/migrations/codecommit.go b/services/migrations/codecommit.go index ccda62fc3d..fead527f5b 100644 --- a/services/migrations/codecommit.go +++ b/services/migrations/codecommit.go @@ -14,11 +14,11 @@ import ( "code.gitea.io/gitea/modules/log" base "code.gitea.io/gitea/modules/migration" "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/codecommit" "github.com/aws/aws-sdk-go-v2/service/codecommit/types" - "github.com/aws/aws-sdk-go/aws" ) var ( @@ -94,7 +94,7 @@ func (c *CodeCommitDownloader) SetContext(ctx context.Context) { // GetRepoInfo returns a repository information func (c *CodeCommitDownloader) GetRepoInfo() (*base.Repository, error) { output, err := c.codeCommitClient.GetRepository(c.ctx, &codecommit.GetRepositoryInput{ - RepositoryName: aws.String(c.repoName), + RepositoryName: util.ToPointer(c.repoName), }) if err != nil { return nil, err @@ -126,7 +126,7 @@ func (c *CodeCommitDownloader) GetComments(commentable base.Commentable) ([]*bas for { resp, err := c.codeCommitClient.GetCommentsForPullRequest(c.ctx, &codecommit.GetCommentsForPullRequestInput{ NextToken: nextToken, - PullRequestId: aws.String(strconv.FormatInt(commentable.GetForeignIndex(), 10)), + PullRequestId: util.ToPointer(strconv.FormatInt(commentable.GetForeignIndex(), 10)), }) if err != nil { return nil, false, err @@ -171,7 +171,7 @@ func (c *CodeCommitDownloader) GetPullRequests(page, perPage int) ([]*base.PullR prs := make([]*base.PullRequest, 0, len(batch)) for _, id := range batch { output, err := c.codeCommitClient.GetPullRequest(c.ctx, &codecommit.GetPullRequestInput{ - PullRequestId: aws.String(id), + PullRequestId: util.ToPointer(id), }) if err != nil { return nil, false, err @@ -243,7 +243,7 @@ func (c *CodeCommitDownloader) getAllPullRequestIDs() ([]string, error) { for { output, err := c.codeCommitClient.ListPullRequests(c.ctx, &codecommit.ListPullRequestsInput{ - RepositoryName: aws.String(c.repoName), + RepositoryName: util.ToPointer(c.repoName), NextToken: nextToken, }) if err != nil { diff --git a/services/org/user_test.go b/services/org/user_test.go index 56d01a3b63..96d1a1c8ca 100644 --- a/services/org/user_test.go +++ b/services/org/user_test.go @@ -47,7 +47,7 @@ func TestRemoveOrgUser(t *testing.T) { testSuccess := func(org *organization.Organization, user *user_model.User) { expectedNumMembers := org.NumMembers - if unittest.BeanExists(t, &organization.OrgUser{OrgID: org.ID, UID: user.ID}) { + if unittest.GetBean(t, &organization.OrgUser{OrgID: org.ID, UID: user.ID}) != nil { expectedNumMembers-- } assert.NoError(t, RemoveOrgUser(db.DefaultContext, org, user)) diff --git a/services/pull/check.go b/services/pull/check.go index bffca394a8..f7aa8eb369 100644 --- a/services/pull/check.go +++ b/services/pull/check.go @@ -300,7 +300,7 @@ func manuallyMerged(ctx context.Context, pr *issues_model.PullRequest) bool { pr.Merger = merger pr.MergerID = merger.ID - if merged, err := pr.SetMerged(ctx); err != nil { + if merged, err := SetMerged(ctx, pr); err != nil { log.Error("%-v setMerged : %v", pr, err) return false } else if !merged { diff --git a/services/pull/merge.go b/services/pull/merge.go index 75011a697c..879fe5a540 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -639,7 +639,7 @@ func MergedManually(ctx context.Context, pr *issues_model.PullRequest, doer *use pr.MergerID = doer.ID var merged bool - if merged, err = pr.SetMerged(ctx); err != nil { + if merged, err = SetMerged(ctx, pr); err != nil { return err } else if !merged { return fmt.Errorf("SetMerged failed") @@ -656,3 +656,62 @@ func MergedManually(ctx context.Context, pr *issues_model.PullRequest, doer *use return handleCloseCrossReferences(ctx, pr, doer) } + +// SetMerged sets a pull request to merged and closes the corresponding issue +func SetMerged(ctx context.Context, pr *issues_model.PullRequest) (bool, error) { + if pr.HasMerged { + return false, fmt.Errorf("PullRequest[%d] already merged", pr.Index) + } + if pr.MergedCommitID == "" || pr.MergedUnix == 0 || pr.Merger == nil { + return false, fmt.Errorf("unable to merge PullRequest[%d], some required fields are empty", pr.Index) + } + + pr.HasMerged = true + sess := db.GetEngine(ctx) + + if _, err := sess.Exec("UPDATE `issue` SET `repo_id` = `repo_id` WHERE `id` = ?", pr.IssueID); err != nil { + return false, err + } + + if _, err := sess.Exec("UPDATE `pull_request` SET `issue_id` = `issue_id` WHERE `id` = ?", pr.ID); err != nil { + return false, err + } + + pr.Issue = nil + if err := pr.LoadIssue(ctx); err != nil { + return false, err + } + + if tmpPr, err := issues_model.GetPullRequestByID(ctx, pr.ID); err != nil { + return false, err + } else if tmpPr.HasMerged { + if pr.Issue.IsClosed { + return false, nil + } + return false, fmt.Errorf("PullRequest[%d] already merged but it's associated issue [%d] is not closed", pr.Index, pr.IssueID) + } else if pr.Issue.IsClosed { + return false, fmt.Errorf("PullRequest[%d] already closed", pr.Index) + } + + if err := pr.Issue.LoadRepo(ctx); err != nil { + return false, err + } + + if err := pr.Issue.Repo.LoadOwner(ctx); err != nil { + return false, err + } + + if _, err := issues_model.ChangeIssueStatus(ctx, pr.Issue, pr.Merger, true, true); err != nil { + return false, fmt.Errorf("ChangeIssueStatus: %w", err) + } + + // reset the conflicted files as there cannot be any if we're merged + pr.ConflictedFiles = []string{} + + // We need to save all of the data used to compute this merge as it may have already been changed by TestPatch. FIXME: need to set some state to prevent TestPatch from running whilst we are merging. + if _, err := sess.Where("id = ?", pr.ID).Cols("has_merged, status, merge_base, merged_commit_id, merger_id, merged_unix, conflicted_files").Update(pr); err != nil { + return false, fmt.Errorf("failed to update pr[%d]: %w", pr.ID, err) + } + + return true, nil +} diff --git a/services/pull/merge_squash.go b/services/pull/merge_squash.go index 8f8a5d82e7..7258671888 100644 --- a/services/pull/merge_squash.go +++ b/services/pull/merge_squash.go @@ -5,6 +5,7 @@ package pull import ( "fmt" + "strings" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" @@ -65,7 +66,10 @@ func doMergeStyleSquash(ctx *mergeContext, message string) error { if setting.Repository.PullRequest.AddCoCommitterTrailers && ctx.committer.String() != sig.String() { // add trailer - message += fmt.Sprintf("\nCo-authored-by: %s\nCo-committed-by: %s\n", sig.String(), sig.String()) + if !strings.Contains(message, fmt.Sprintf("Co-authored-by: %s", sig.String())) { + message += fmt.Sprintf("\nCo-authored-by: %s", sig.String()) + } + message += fmt.Sprintf("\nCo-committed-by: %s\n", sig.String()) } cmdCommit := git.NewCommand(ctx, "commit"). AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email). diff --git a/services/pull/pull.go b/services/pull/pull.go index ac91fd6409..85c36bb16a 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -64,7 +64,8 @@ func NewPullRequest(ctx context.Context, opts *NewPullRequestOptions) error { } // user should be a collaborator or a member of the organization for base repo - if !issue.Poster.IsAdmin { + canCreate := issue.Poster.IsAdmin || pr.Flow == issues_model.PullRequestFlowAGit + if !canCreate { canCreate, err := repo_model.IsOwnerMemberCollaborator(ctx, repo, issue.Poster.ID) if err != nil { return err diff --git a/services/repository/generate.go b/services/repository/generate.go index 24cf9d1b9b..ef9a8dc940 100644 --- a/services/repository/generate.go +++ b/services/repository/generate.go @@ -9,7 +9,6 @@ import ( "context" "fmt" "os" - "path" "path/filepath" "regexp" "strconv" @@ -123,7 +122,7 @@ func (gt *GiteaTemplate) Globs() []glob.Glob { return gt.globs } -func checkGiteaTemplate(tmpDir string) (*GiteaTemplate, error) { +func readGiteaTemplateFile(tmpDir string) (*GiteaTemplate, error) { gtPath := filepath.Join(tmpDir, ".gitea", "template") if _, err := os.Stat(gtPath); os.IsNotExist(err) { return nil, nil @@ -136,12 +135,55 @@ func checkGiteaTemplate(tmpDir string) (*GiteaTemplate, error) { return nil, err } - gt := &GiteaTemplate{ - Path: gtPath, - Content: content, - } + return &GiteaTemplate{Path: gtPath, Content: content}, nil +} - return gt, nil +func processGiteaTemplateFile(tmpDir string, templateRepo, generateRepo *repo_model.Repository, giteaTemplateFile *GiteaTemplate) error { + if err := util.Remove(giteaTemplateFile.Path); err != nil { + return fmt.Errorf("remove .giteatemplate: %w", err) + } + if len(giteaTemplateFile.Globs()) == 0 { + return nil // Avoid walking tree if there are no globs + } + tmpDirSlash := strings.TrimSuffix(filepath.ToSlash(tmpDir), "/") + "/" + return filepath.WalkDir(tmpDirSlash, func(path string, d os.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + + if d.IsDir() { + return nil + } + + base := strings.TrimPrefix(filepath.ToSlash(path), tmpDirSlash) + for _, g := range giteaTemplateFile.Globs() { + if g.Match(base) { + content, err := os.ReadFile(path) + if err != nil { + return err + } + + generatedContent := []byte(generateExpansion(string(content), templateRepo, generateRepo, false)) + if err := os.WriteFile(path, generatedContent, 0o644); err != nil { + return err + } + + substPath := filepath.FromSlash(filepath.Join(tmpDirSlash, generateExpansion(base, templateRepo, generateRepo, true))) + + // Create parent subdirectories if needed or continue silently if it exists + if err = os.MkdirAll(filepath.Dir(substPath), 0o755); err != nil { + return err + } + + // Substitute filename variables + if err = os.Rename(path, substPath); err != nil { + return err + } + break + } + } + return nil + }) // end: WalkDir } func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *repo_model.Repository, tmpDir string) error { @@ -167,81 +209,43 @@ func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *r return fmt.Errorf("git clone: %w", err) } - if err := util.RemoveAll(path.Join(tmpDir, ".git")); err != nil { + // Get active submodules from the template + submodules, err := git.GetTemplateSubmoduleCommits(ctx, tmpDir) + if err != nil { + return fmt.Errorf("GetTemplateSubmoduleCommits: %w", err) + } + + if err = util.RemoveAll(filepath.Join(tmpDir, ".git")); err != nil { return fmt.Errorf("remove git dir: %w", err) } // Variable expansion - gt, err := checkGiteaTemplate(tmpDir) + giteaTemplateFile, err := readGiteaTemplateFile(tmpDir) if err != nil { - return fmt.Errorf("checkGiteaTemplate: %w", err) + return fmt.Errorf("readGiteaTemplateFile: %w", err) } - if gt != nil { - if err := util.Remove(gt.Path); err != nil { - return fmt.Errorf("remove .giteatemplate: %w", err) - } - - // Avoid walking tree if there are no globs - if len(gt.Globs()) > 0 { - tmpDirSlash := strings.TrimSuffix(filepath.ToSlash(tmpDir), "/") + "/" - if err := filepath.WalkDir(tmpDirSlash, func(path string, d os.DirEntry, walkErr error) error { - if walkErr != nil { - return walkErr - } - - if d.IsDir() { - return nil - } - - base := strings.TrimPrefix(filepath.ToSlash(path), tmpDirSlash) - for _, g := range gt.Globs() { - if g.Match(base) { - content, err := os.ReadFile(path) - if err != nil { - return err - } - - if err := os.WriteFile(path, - []byte(generateExpansion(string(content), templateRepo, generateRepo, false)), - 0o644); err != nil { - return err - } - - substPath := filepath.FromSlash(filepath.Join(tmpDirSlash, - generateExpansion(base, templateRepo, generateRepo, true))) - - // Create parent subdirectories if needed or continue silently if it exists - if err := os.MkdirAll(filepath.Dir(substPath), 0o755); err != nil { - return err - } - - // Substitute filename variables - if err := os.Rename(path, substPath); err != nil { - return err - } - - break - } - } - return nil - }); err != nil { - return err - } + if giteaTemplateFile != nil { + err = processGiteaTemplateFile(tmpDir, templateRepo, generateRepo, giteaTemplateFile) + if err != nil { + return err } } - if err := git.InitRepository(ctx, tmpDir, false, templateRepo.ObjectFormatName); err != nil { + if err = git.InitRepository(ctx, tmpDir, false, templateRepo.ObjectFormatName); err != nil { return err } - repoPath := repo.RepoPath() - if stdout, _, err := git.NewCommand(ctx, "remote", "add", "origin").AddDynamicArguments(repoPath). + if stdout, _, err := git.NewCommand(ctx, "remote", "add", "origin").AddDynamicArguments(repo.RepoPath()). RunStdString(&git.RunOpts{Dir: tmpDir, Env: env}); err != nil { log.Error("Unable to add %v as remote origin to temporary repo to %s: stdout %s\nError: %v", repo, tmpDir, stdout, err) return fmt.Errorf("git remote add: %w", err) } + if err = git.AddTemplateSubmoduleIndexes(ctx, tmpDir, submodules); err != nil { + return fmt.Errorf("failed to add submodules: %v", err) + } + // set default branch based on whether it's specified in the newly generated repo or not defaultBranch := repo.DefaultBranch if strings.TrimSpace(defaultBranch) == "" { diff --git a/services/webhook/webhook_test.go b/services/webhook/webhook_test.go index 63cbce1771..6bac02712b 100644 --- a/services/webhook/webhook_test.go +++ b/services/webhook/webhook_test.go @@ -9,11 +9,15 @@ import ( "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" + "code.gitea.io/gitea/services/convert" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestWebhook_GetSlackHook(t *testing.T) { @@ -77,3 +81,11 @@ func TestPrepareWebhooksBranchFilterNoMatch(t *testing.T) { unittest.AssertNotExistsBean(t, hookTask) } } + +func TestWebhookUserMail(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + setting.Service.NoReplyAddress = "no-reply.com" + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + assert.Equal(t, user.GetPlaceholderEmail(), convert.ToUser(db.DefaultContext, user, nil).Email) + assert.Equal(t, user.Email, convert.ToUser(db.DefaultContext, user, user).Email) +} diff --git a/templates/admin/user/edit.tmpl b/templates/admin/user/edit.tmpl index 41b00defb4..d591a645d8 100644 --- a/templates/admin/user/edit.tmpl +++ b/templates/admin/user/edit.tmpl @@ -128,16 +128,16 @@ -
+
- +
-
+
- +
{{if not .DisableRegularOrgCreation}} diff --git a/templates/base/alert_details.tmpl b/templates/base/alert_details.tmpl index 6801c8240f..6d4c1fb2db 100644 --- a/templates/base/alert_details.tmpl +++ b/templates/base/alert_details.tmpl @@ -1,7 +1,11 @@ {{.Message}} +{{if .Details}}
{{.Summary}} - - {{.Details | SanitizeHTML}} - + {{.Details | SanitizeHTML}}
+{{else}} +
+ {{.Summary}} +
+{{end}} diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl index ed7a8d6f24..bf0e7e632b 100644 --- a/templates/base/head_navbar.tmpl +++ b/templates/base/head_navbar.tmpl @@ -192,7 +192,7 @@ {{.ActiveStopwatch.RepoSlug}}#{{.ActiveStopwatch.IssueIndex}}
-
+ {{.CsrfTokenHtml}}
-
+ {{.CsrfTokenHtml}}
+{{template "devtest/devtest-footer"}} diff --git a/templates/org/create.tmpl b/templates/org/create.tmpl index 004cd9be80..2f19fd817f 100644 --- a/templates/org/create.tmpl +++ b/templates/org/create.tmpl @@ -2,22 +2,22 @@
- - {{.CsrfTokenHtml}} -

- {{ctx.Locale.Tr "new_org"}} -

-
- {{template "base/alert" .}} + {{template "base/alert" .}} +

+ {{ctx.Locale.Tr "new_org"}} +

+
+ + {{.CsrfTokenHtml}}
{{ctx.Locale.Tr "org.org_name_helper"}}
-
- -
+
+ +
@@ -35,11 +35,9 @@
-
-
- - -
+
+ +
@@ -49,8 +47,8 @@ {{ctx.Locale.Tr "org.create_org"}}
-
- + +
diff --git a/templates/org/home.tmpl b/templates/org/home.tmpl index 4851b69979..db750692bf 100644 --- a/templates/org/home.tmpl +++ b/templates/org/home.tmpl @@ -5,8 +5,8 @@
- {{if .ProfileReadme}} -
{{.ProfileReadme}}
+ {{if .ProfileReadmeContent}} +
{{.ProfileReadmeContent}}
{{end}} {{template "shared/repo_search" .}} {{template "explore/repo_list" .}} @@ -24,6 +24,29 @@
{{end}} + + {{if and .ShowMemberAndTeamTab .ShowOrgProfileReadmeSelector}} +
+ +
+ {{if .IsViewingOrgAsMember}}{{ctx.Locale.Tr "org.view_as_member_hint"}}{{else}}{{ctx.Locale.Tr "org.view_as_public_hint"}}{{end}} +
+
+ {{end}} + {{if .NumMembers}}

{{ctx.Locale.Tr "org.members"}} diff --git a/templates/org/menu.tmpl b/templates/org/menu.tmpl index 29238f8d6b..4a8aee68a7 100644 --- a/templates/org/menu.tmpl +++ b/templates/org/menu.tmpl @@ -1,12 +1,12 @@
- {{if .HasProfileReadme}} + {{if .HasOrgProfileReadme}} {{svg "octicon-info"}} {{ctx.Locale.Tr "user.overview"}} {{end}} - + {{svg "octicon-repo"}} {{ctx.Locale.Tr "user.repositories"}} {{if .RepoCount}}
{{.RepoCount}}
diff --git a/templates/package/content/nuget.tmpl b/templates/package/content/nuget.tmpl index 5bb98a86dd..4a4ea8ca43 100644 --- a/templates/package/content/nuget.tmpl +++ b/templates/package/content/nuget.tmpl @@ -35,11 +35,12 @@ + {{$tooltipSearchInNuget := ctx.Locale.Tr "packages.search_in_external_registry" "nuget.org"}} {{range $framework, $dependencies := .PackageDescriptor.Metadata.Dependencies}} {{range $dependencies}} - {{.ID}} - {{.Version}} + {{.ID}}
{{svg "octicon-link-external"}} + {{.Version}} {{svg "octicon-link-external"}} {{$framework}} {{end}} diff --git a/templates/post-install.tmpl b/templates/post-install.tmpl index fb234008fb..fa10827295 100644 --- a/templates/post-install.tmpl +++ b/templates/post-install.tmpl @@ -1,23 +1,10 @@ {{template "base/head" .}} -
-
-
-
-
-
-
-
- {{ctx.Locale.Tr -
-
-
- -
-
+
+
diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl index 5484024ff8..cb504e2b75 100644 --- a/templates/repo/branch/list.tmpl +++ b/templates/repo/branch/list.tmpl @@ -29,7 +29,8 @@

{{svg "octicon-git-commit" 16 "tw-mr-1"}}{{ShortSha .DefaultBranchBranch.DBBranch.CommitID}} · {{ctx.RenderUtils.RenderCommitMessage .DefaultBranchBranch.DBBranch.CommitMessage (.Repository.ComposeMetas ctx)}} · {{ctx.Locale.Tr "org.repo_updated"}} {{DateUtils.TimeSince .DefaultBranchBranch.DBBranch.CommitTime}}{{if .DefaultBranchBranch.DBBranch.Pusher}}  {{template "shared/user/avatarlink" dict "user" .DefaultBranchBranch.DBBranch.Pusher}}{{template "shared/user/namelink" .DefaultBranchBranch.DBBranch.Pusher}}{{end}}

- + {{/* FIXME: here and below, the tw-overflow-visible is not quite right but it is still needed the moment: to show the important buttons when the width is narrow */}} + {{if and $.IsWriter (not $.Repository.IsArchived) (not .IsDeleted)}} {{end}} - + {{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}} - {{$class := "ui sha label"}} - {{if .Signature}} - {{$class = (print $class " isSigned")}} - {{if .Verification.Verified}} - {{if eq .Verification.TrustStatus "trusted"}} - {{$class = (print $class " isVerified")}} - {{else if eq .Verification.TrustStatus "untrusted"}} - {{$class = (print $class " isVerifiedUntrusted")}} - {{else}} - {{$class = (print $class " isVerifiedUnmatched")}} - {{end}} - {{else if .Verification.Warning}} - {{$class = (print $class " isWarning")}} - {{end}} - {{end}} - - {{ShortSha .ID.String}} - {{if .Signature}} - {{template "repo/shabox_badge" dict "root" $.root "verification" .Verification}} - {{end}} - + {{template "repo/commit_sign_badge" dict "Commit" . "CommitBaseLink" $commitBaseLink "CommitSignVerification" .Verification}}
{{if IsMultilineCommitMessage .Message}} diff --git a/templates/repo/create.tmpl b/templates/repo/create.tmpl index 2e1de244ea..36283b35e0 100644 --- a/templates/repo/create.tmpl +++ b/templates/repo/create.tmpl @@ -1,21 +1,21 @@ {{template "base/head" .}} -
+
-
- {{.CsrfTokenHtml}} -

- {{ctx.Locale.Tr "new_repo"}} -

-
- {{template "base/alert" .}} - {{template "repo/create_helper" .}} +

+ {{ctx.Locale.Tr "new_repo"}} +

+
+ {{template "base/alert" .}} + {{template "repo/create_helper" .}} - {{if not .CanCreateRepo}} -
-

{{ctx.Locale.TrN .MaxCreationLimit "repo.form.reach_limit_of_creation_1" "repo.form.reach_limit_of_creation_n" .MaxCreationLimit}}

-
- {{end}} + {{if not .CanCreateRepo}} +
+

{{ctx.Locale.TrN .MaxCreationLimit "repo.form.reach_limit_of_creation_1" "repo.form.reach_limit_of_creation_n" .MaxCreationLimit}}

+
+ {{end}} + + {{.CsrfTokenHtml}}

{{ctx.Locale.Tr "settings.twofa_is_enrolled"}}
- {{if .scratch_code}} -
- - -
- - {{else}} -
- - -
- {{end}} + {{if .scratch_code}} +
+ + +
+ + {{else}} +
+ + +
+ {{end}} {{end}}
diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl index dd608e5aa1..fbf86a92bf 100644 --- a/templates/user/auth/signin_inner.tmpl +++ b/templates/user/auth/signin_inner.tmpl @@ -18,9 +18,9 @@
{{if or (not .DisablePassword) .LinkAccountMode}} -
- -
+
+
+ {{ctx.Locale.Tr "auth.forgot_password"}}
diff --git a/templates/user/auth/signup_openid_connect.tmpl b/templates/user/auth/signup_openid_connect.tmpl index e4b7936374..b27093d853 100644 --- a/templates/user/auth/signup_openid_connect.tmpl +++ b/templates/user/auth/signup_openid_connect.tmpl @@ -7,28 +7,28 @@ {{ctx.Locale.Tr "auth.openid_connect_title"}}
-

- {{ctx.Locale.Tr "auth.openid_connect_desc"}} -

-
- {{.CsrfTokenHtml}} -
- - -
-
- - -
-
- - -
-
- - - {{ctx.Locale.Tr "auth.forgot_password"}} -
+ + {{.CsrfTokenHtml}} +
+ {{ctx.Locale.Tr "auth.openid_connect_desc"}} +
+
+ + +
+
+ + +
+
+ + +
+
+ + + {{ctx.Locale.Tr "auth.forgot_password"}} +
diff --git a/templates/user/auth/signup_openid_register.tmpl b/templates/user/auth/signup_openid_register.tmpl index c017a0e65b..df6268d151 100644 --- a/templates/user/auth/signup_openid_register.tmpl +++ b/templates/user/auth/signup_openid_register.tmpl @@ -7,7 +7,7 @@ {{ctx.Locale.Tr "auth.openid_register_title"}}
-

+

{{ctx.Locale.Tr "auth.openid_register_desc"}}

diff --git a/templates/user/overview/header.tmpl b/templates/user/overview/header.tmpl index 275c4e295e..f4664c704d 100644 --- a/templates/user/overview/header.tmpl +++ b/templates/user/overview/header.tmpl @@ -1,6 +1,6 @@
- {{if and .HasProfileReadme .ContextUser.IsIndividual}} + {{if and .HasUserProfileReadme .ContextUser.IsIndividual}} {{svg "octicon-info"}} {{ctx.Locale.Tr "user.overview"}} diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index 78e42777bb..ece136be50 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -40,13 +40,6 @@ func TestMain(m *testing.M) { tests.InitTest(false) testE2eWebRoutes = routers.NormalRoutes() - os.Unsetenv("GIT_AUTHOR_NAME") - os.Unsetenv("GIT_AUTHOR_EMAIL") - os.Unsetenv("GIT_AUTHOR_DATE") - os.Unsetenv("GIT_COMMITTER_NAME") - os.Unsetenv("GIT_COMMITTER_EMAIL") - os.Unsetenv("GIT_COMMITTER_DATE") - err := unittest.InitFixtures( unittest.FixturesOptions{ Dir: filepath.Join(filepath.Dir(setting.AppPath), "models/fixtures/"), diff --git a/tests/gitea-repositories-meta/user2/git_hooks_test.git/hooks/pre-receive.d/pre-receive b/tests/gitea-repositories-meta/user2/git_hooks_test.git/hooks/pre-receive.d/pre-receive index b26a3b9b68..205086810d 100755 --- a/tests/gitea-repositories-meta/user2/git_hooks_test.git/hooks/pre-receive.d/pre-receive +++ b/tests/gitea-repositories-meta/user2/git_hooks_test.git/hooks/pre-receive.d/pre-receive @@ -1,3 +1,2 @@ #!/bin/bash - -echo Hello, World! +echo "TestGitHookScript" diff --git a/tests/integration/actions_job_test.go b/tests/integration/actions_job_test.go new file mode 100644 index 0000000000..e13277678d --- /dev/null +++ b/tests/integration/actions_job_test.go @@ -0,0 +1,410 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "encoding/base64" + "fmt" + "net/http" + "net/url" + "testing" + "time" + + actions_model "code.gitea.io/gitea/models/actions" + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + api "code.gitea.io/gitea/modules/structs" + + runnerv1 "code.gitea.io/actions-proto-go/runner/v1" + "github.com/stretchr/testify/assert" +) + +func TestJobWithNeeds(t *testing.T) { + testCases := []struct { + treePath string + fileContent string + outcomes map[string]*mockTaskOutcome + expectedStatuses map[string]string + }{ + { + treePath: ".gitea/workflows/job-with-needs.yml", + fileContent: `name: job-with-needs +on: + push: + paths: + - '.gitea/workflows/job-with-needs.yml' +jobs: + job1: + runs-on: ubuntu-latest + steps: + - run: echo job1 + job2: + runs-on: ubuntu-latest + needs: [job1] + steps: + - run: echo job2 +`, + outcomes: map[string]*mockTaskOutcome{ + "job1": { + result: runnerv1.Result_RESULT_SUCCESS, + }, + "job2": { + result: runnerv1.Result_RESULT_SUCCESS, + }, + }, + expectedStatuses: map[string]string{ + "job1": actions_model.StatusSuccess.String(), + "job2": actions_model.StatusSuccess.String(), + }, + }, + { + treePath: ".gitea/workflows/job-with-needs-fail.yml", + fileContent: `name: job-with-needs-fail +on: + push: + paths: + - '.gitea/workflows/job-with-needs-fail.yml' +jobs: + job1: + runs-on: ubuntu-latest + steps: + - run: echo job1 + job2: + runs-on: ubuntu-latest + needs: [job1] + steps: + - run: echo job2 +`, + outcomes: map[string]*mockTaskOutcome{ + "job1": { + result: runnerv1.Result_RESULT_FAILURE, + }, + }, + expectedStatuses: map[string]string{ + "job1": actions_model.StatusFailure.String(), + "job2": actions_model.StatusSkipped.String(), + }, + }, + { + treePath: ".gitea/workflows/job-with-needs-fail-if.yml", + fileContent: `name: job-with-needs-fail-if +on: + push: + paths: + - '.gitea/workflows/job-with-needs-fail-if.yml' +jobs: + job1: + runs-on: ubuntu-latest + steps: + - run: echo job1 + job2: + runs-on: ubuntu-latest + if: ${{ always() }} + needs: [job1] + steps: + - run: echo job2 +`, + outcomes: map[string]*mockTaskOutcome{ + "job1": { + result: runnerv1.Result_RESULT_FAILURE, + }, + "job2": { + result: runnerv1.Result_RESULT_SUCCESS, + }, + }, + expectedStatuses: map[string]string{ + "job1": actions_model.StatusFailure.String(), + "job2": actions_model.StatusSuccess.String(), + }, + }, + } + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, user2.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + apiRepo := createActionsTestRepo(t, token, "actions-jobs-with-needs", false) + runner := newMockRunner() + runner.registerAsRepoRunner(t, user2.Name, apiRepo.Name, "mock-runner", []string{"ubuntu-latest"}) + + for _, tc := range testCases { + t.Run(fmt.Sprintf("test %s", tc.treePath), func(t *testing.T) { + // create the workflow file + opts := getWorkflowCreateFileOptions(user2, apiRepo.DefaultBranch, fmt.Sprintf("create %s", tc.treePath), tc.fileContent) + fileResp := createWorkflowFile(t, token, user2.Name, apiRepo.Name, tc.treePath, opts) + + // fetch and execute task + for i := 0; i < len(tc.outcomes); i++ { + task := runner.fetchTask(t) + jobName := getTaskJobNameByTaskID(t, token, user2.Name, apiRepo.Name, task.Id) + outcome := tc.outcomes[jobName] + assert.NotNil(t, outcome) + runner.execTask(t, task, outcome) + } + + // check result + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/tasks", user2.Name, apiRepo.Name)). + AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var actionTaskRespAfter api.ActionTaskResponse + DecodeJSON(t, resp, &actionTaskRespAfter) + for _, apiTask := range actionTaskRespAfter.Entries { + if apiTask.HeadSHA != fileResp.Commit.SHA { + continue + } + status := apiTask.Status + assert.Equal(t, status, tc.expectedStatuses[apiTask.Name]) + } + }) + } + + httpContext := NewAPITestContext(t, user2.Name, apiRepo.Name, auth_model.AccessTokenScopeWriteRepository) + doAPIDeleteRepository(httpContext)(t) + }) +} + +func TestJobNeedsMatrix(t *testing.T) { + testCases := []struct { + treePath string + fileContent string + outcomes map[string]*mockTaskOutcome + expectedTaskNeeds map[string]*runnerv1.TaskNeed // jobID => TaskNeed + }{ + { + treePath: ".gitea/workflows/jobs-outputs-with-matrix.yml", + fileContent: `name: jobs-outputs-with-matrix +on: + push: + paths: + - '.gitea/workflows/jobs-outputs-with-matrix.yml' +jobs: + job1: + runs-on: ubuntu-latest + outputs: + output_1: ${{ steps.gen_output.outputs.output_1 }} + output_2: ${{ steps.gen_output.outputs.output_2 }} + output_3: ${{ steps.gen_output.outputs.output_3 }} + strategy: + matrix: + version: [1, 2, 3] + steps: + - name: Generate output + id: gen_output + run: | + version="${{ matrix.version }}" + echo "output_${version}=${version}" >> "$GITHUB_OUTPUT" + job2: + runs-on: ubuntu-latest + needs: [job1] + steps: + - run: echo '${{ toJSON(needs.job1.outputs) }}' +`, + outcomes: map[string]*mockTaskOutcome{ + "job1 (1)": { + result: runnerv1.Result_RESULT_SUCCESS, + outputs: map[string]string{ + "output_1": "1", + "output_2": "", + "output_3": "", + }, + }, + "job1 (2)": { + result: runnerv1.Result_RESULT_SUCCESS, + outputs: map[string]string{ + "output_1": "", + "output_2": "2", + "output_3": "", + }, + }, + "job1 (3)": { + result: runnerv1.Result_RESULT_SUCCESS, + outputs: map[string]string{ + "output_1": "", + "output_2": "", + "output_3": "3", + }, + }, + }, + expectedTaskNeeds: map[string]*runnerv1.TaskNeed{ + "job1": { + Result: runnerv1.Result_RESULT_SUCCESS, + Outputs: map[string]string{ + "output_1": "1", + "output_2": "2", + "output_3": "3", + }, + }, + }, + }, + { + treePath: ".gitea/workflows/jobs-outputs-with-matrix-failure.yml", + fileContent: `name: jobs-outputs-with-matrix-failure +on: + push: + paths: + - '.gitea/workflows/jobs-outputs-with-matrix-failure.yml' +jobs: + job1: + runs-on: ubuntu-latest + outputs: + output_1: ${{ steps.gen_output.outputs.output_1 }} + output_2: ${{ steps.gen_output.outputs.output_2 }} + output_3: ${{ steps.gen_output.outputs.output_3 }} + strategy: + matrix: + version: [1, 2, 3] + steps: + - name: Generate output + id: gen_output + run: | + version="${{ matrix.version }}" + echo "output_${version}=${version}" >> "$GITHUB_OUTPUT" + job2: + runs-on: ubuntu-latest + if: ${{ always() }} + needs: [job1] + steps: + - run: echo '${{ toJSON(needs.job1.outputs) }}' +`, + outcomes: map[string]*mockTaskOutcome{ + "job1 (1)": { + result: runnerv1.Result_RESULT_SUCCESS, + outputs: map[string]string{ + "output_1": "1", + "output_2": "", + "output_3": "", + }, + }, + "job1 (2)": { + result: runnerv1.Result_RESULT_FAILURE, + outputs: map[string]string{ + "output_1": "", + "output_2": "", + "output_3": "", + }, + }, + "job1 (3)": { + result: runnerv1.Result_RESULT_SUCCESS, + outputs: map[string]string{ + "output_1": "", + "output_2": "", + "output_3": "3", + }, + }, + }, + expectedTaskNeeds: map[string]*runnerv1.TaskNeed{ + "job1": { + Result: runnerv1.Result_RESULT_FAILURE, + Outputs: map[string]string{ + "output_1": "1", + "output_2": "", + "output_3": "3", + }, + }, + }, + }, + } + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, user2.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + apiRepo := createActionsTestRepo(t, token, "actions-jobs-outputs-with-matrix", false) + runner := newMockRunner() + runner.registerAsRepoRunner(t, user2.Name, apiRepo.Name, "mock-runner", []string{"ubuntu-latest"}) + + for _, tc := range testCases { + t.Run(fmt.Sprintf("test %s", tc.treePath), func(t *testing.T) { + opts := getWorkflowCreateFileOptions(user2, apiRepo.DefaultBranch, fmt.Sprintf("create %s", tc.treePath), tc.fileContent) + createWorkflowFile(t, token, user2.Name, apiRepo.Name, tc.treePath, opts) + + for i := 0; i < len(tc.outcomes); i++ { + task := runner.fetchTask(t) + jobName := getTaskJobNameByTaskID(t, token, user2.Name, apiRepo.Name, task.Id) + outcome := tc.outcomes[jobName] + assert.NotNil(t, outcome) + runner.execTask(t, task, outcome) + } + + task := runner.fetchTask(t) + actualTaskNeeds := task.Needs + assert.Len(t, actualTaskNeeds, len(tc.expectedTaskNeeds)) + for jobID, tn := range tc.expectedTaskNeeds { + actualNeed := actualTaskNeeds[jobID] + assert.Equal(t, tn.Result, actualNeed.Result) + assert.Len(t, actualNeed.Outputs, len(tn.Outputs)) + for outputKey, outputValue := range tn.Outputs { + assert.Equal(t, outputValue, actualNeed.Outputs[outputKey]) + } + } + }) + } + + httpContext := NewAPITestContext(t, user2.Name, apiRepo.Name, auth_model.AccessTokenScopeWriteRepository) + doAPIDeleteRepository(httpContext)(t) + }) +} + +func createActionsTestRepo(t *testing.T, authToken, repoName string, isPrivate bool) *api.Repository { + req := NewRequestWithJSON(t, "POST", "/api/v1/user/repos", &api.CreateRepoOption{ + Name: repoName, + Private: isPrivate, + Readme: "Default", + AutoInit: true, + DefaultBranch: "main", + }).AddTokenAuth(authToken) + resp := MakeRequest(t, req, http.StatusCreated) + var apiRepo api.Repository + DecodeJSON(t, resp, &apiRepo) + return &apiRepo +} + +func getWorkflowCreateFileOptions(u *user_model.User, branch, msg, content string) *api.CreateFileOptions { + return &api.CreateFileOptions{ + FileOptions: api.FileOptions{ + BranchName: branch, + Message: msg, + Author: api.Identity{ + Name: u.Name, + Email: u.Email, + }, + Committer: api.Identity{ + Name: u.Name, + Email: u.Email, + }, + Dates: api.CommitDateOptions{ + Author: time.Now(), + Committer: time.Now(), + }, + }, + ContentBase64: base64.StdEncoding.EncodeToString([]byte(content)), + } +} + +func createWorkflowFile(t *testing.T, authToken, ownerName, repoName, treePath string, opts *api.CreateFileOptions) *api.FileResponse { + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", ownerName, repoName, treePath), opts). + AddTokenAuth(authToken) + resp := MakeRequest(t, req, http.StatusCreated) + var fileResponse api.FileResponse + DecodeJSON(t, resp, &fileResponse) + return &fileResponse +} + +// getTaskJobNameByTaskID get the job name of the task by task ID +// there is currently not an API for querying a task by ID so we have to list all the tasks +func getTaskJobNameByTaskID(t *testing.T, authToken, ownerName, repoName string, taskID int64) string { + // FIXME: we may need to query several pages + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/tasks", ownerName, repoName)). + AddTokenAuth(authToken) + resp := MakeRequest(t, req, http.StatusOK) + var taskRespBefore api.ActionTaskResponse + DecodeJSON(t, resp, &taskRespBefore) + for _, apiTask := range taskRespBefore.Entries { + if apiTask.ID == taskID { + return apiTask.Name + } + } + return "" +} diff --git a/tests/integration/actions_log_test.go b/tests/integration/actions_log_test.go new file mode 100644 index 0000000000..fd055fc4c4 --- /dev/null +++ b/tests/integration/actions_log_test.go @@ -0,0 +1,159 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "fmt" + "net/http" + "net/url" + "strings" + "testing" + "time" + + auth_model "code.gitea.io/gitea/models/auth" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/storage" + "code.gitea.io/gitea/modules/test" + + runnerv1 "code.gitea.io/actions-proto-go/runner/v1" + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestDownloadTaskLogs(t *testing.T) { + now := time.Now() + testCases := []struct { + treePath string + fileContent string + outcome *mockTaskOutcome + zstdEnabled bool + }{ + { + treePath: ".gitea/workflows/download-task-logs-zstd.yml", + fileContent: `name: download-task-logs-zstd +on: + push: + paths: + - '.gitea/workflows/download-task-logs-zstd.yml' +jobs: + job1: + runs-on: ubuntu-latest + steps: + - run: echo job1 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", + }, + { + Time: timestamppb.New(now.Add(2 * time.Second)), + Content: "job1 zstd enabled", + }, + { + Time: timestamppb.New(now.Add(3 * time.Second)), + Content: "\U0001F3C1 Job succeeded", + }, + }, + }, + zstdEnabled: true, + }, + { + treePath: ".gitea/workflows/download-task-logs-no-zstd.yml", + fileContent: `name: download-task-logs-no-zstd +on: + push: + paths: + - '.gitea/workflows/download-task-logs-no-zstd.yml' +jobs: + job1: + runs-on: ubuntu-latest + steps: + - run: echo job1 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", + }, + { + Time: timestamppb.New(now.Add(6 * time.Second)), + Content: "\U0001F3C1 Job succeeded", + }, + }, + }, + zstdEnabled: false, + }, + } + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, user2.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + apiRepo := createActionsTestRepo(t, token, "actions-download-task-logs", false) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID}) + runner := newMockRunner() + runner.registerAsRepoRunner(t, user2.Name, repo.Name, "mock-runner", []string{"ubuntu-latest"}) + + for _, tc := range testCases { + t.Run(fmt.Sprintf("test %s", tc.treePath), func(t *testing.T) { + var resetFunc func() + if tc.zstdEnabled { + resetFunc = test.MockVariableValue(&setting.Actions.LogCompression, "zstd") + assert.True(t, setting.Actions.LogCompression.IsZstd()) + } else { + resetFunc = test.MockVariableValue(&setting.Actions.LogCompression, "none") + assert.False(t, setting.Actions.LogCompression.IsZstd()) + } + + // create the workflow file + 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) + + // 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], + ) + } + + resetFunc() + }) + } + + httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository) + doAPIDeleteRepository(httpContext)(t) + }) +} diff --git a/tests/integration/actions_runner_test.go b/tests/integration/actions_runner_test.go new file mode 100644 index 0000000000..355ea1705e --- /dev/null +++ b/tests/integration/actions_runner_test.go @@ -0,0 +1,157 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" + + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/modules/setting" + + pingv1 "code.gitea.io/actions-proto-go/ping/v1" + "code.gitea.io/actions-proto-go/ping/v1/pingv1connect" + runnerv1 "code.gitea.io/actions-proto-go/runner/v1" + "code.gitea.io/actions-proto-go/runner/v1/runnerv1connect" + "connectrpc.com/connect" + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/types/known/timestamppb" +) + +type mockRunner struct { + client *mockRunnerClient +} + +type mockRunnerClient struct { + pingServiceClient pingv1connect.PingServiceClient + runnerServiceClient runnerv1connect.RunnerServiceClient +} + +func newMockRunner() *mockRunner { + client := newMockRunnerClient("", "") + return &mockRunner{client: client} +} + +func newMockRunnerClient(uuid, token string) *mockRunnerClient { + baseURL := fmt.Sprintf("%sapi/actions", setting.AppURL) + + opt := connect.WithInterceptors(connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { + if uuid != "" { + req.Header().Set("x-runner-uuid", uuid) + } + if token != "" { + req.Header().Set("x-runner-token", token) + } + return next(ctx, req) + } + })) + + client := &mockRunnerClient{ + pingServiceClient: pingv1connect.NewPingServiceClient(http.DefaultClient, baseURL, opt), + runnerServiceClient: runnerv1connect.NewRunnerServiceClient(http.DefaultClient, baseURL, opt), + } + + return client +} + +func (r *mockRunner) doPing(t *testing.T) { + resp, err := r.client.pingServiceClient.Ping(context.Background(), connect.NewRequest(&pingv1.PingRequest{ + Data: "mock-runner", + })) + assert.NoError(t, err) + assert.Equal(t, "Hello, mock-runner!", resp.Msg.Data) +} + +func (r *mockRunner) doRegister(t *testing.T, name, token string, labels []string) { + r.doPing(t) + resp, err := r.client.runnerServiceClient.Register(context.Background(), connect.NewRequest(&runnerv1.RegisterRequest{ + Name: name, + Token: token, + Version: "mock-runner-version", + Labels: labels, + })) + assert.NoError(t, err) + r.client = newMockRunnerClient(resp.Msg.Runner.Uuid, resp.Msg.Runner.Token) +} + +func (r *mockRunner) registerAsRepoRunner(t *testing.T, ownerName, repoName, runnerName string, labels []string) { + session := loginUser(t, ownerName) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners/registration-token", ownerName, repoName)).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var registrationToken struct { + Token string `json:"token"` + } + DecodeJSON(t, resp, ®istrationToken) + r.doRegister(t, runnerName, registrationToken.Token, labels) +} + +func (r *mockRunner) fetchTask(t *testing.T, timeout ...time.Duration) *runnerv1.Task { + fetchTimeout := 10 * time.Second + if len(timeout) > 0 { + fetchTimeout = timeout[0] + } + ddl := time.Now().Add(fetchTimeout) + var task *runnerv1.Task + for time.Now().Before(ddl) { + resp, err := r.client.runnerServiceClient.FetchTask(context.Background(), connect.NewRequest(&runnerv1.FetchTaskRequest{ + TasksVersion: 0, + })) + assert.NoError(t, err) + if resp.Msg.Task != nil { + task = resp.Msg.Task + break + } + time.Sleep(time.Second) + } + assert.NotNil(t, task, "failed to fetch a task") + return task +} + +type mockTaskOutcome struct { + result runnerv1.Result + outputs map[string]string + logRows []*runnerv1.LogRow + execTime time.Duration +} + +func (r *mockRunner) execTask(t *testing.T, task *runnerv1.Task, outcome *mockTaskOutcome) { + for idx, lr := range outcome.logRows { + resp, err := r.client.runnerServiceClient.UpdateLog(context.Background(), connect.NewRequest(&runnerv1.UpdateLogRequest{ + TaskId: task.Id, + Index: int64(idx), + Rows: []*runnerv1.LogRow{lr}, + NoMore: idx == len(outcome.logRows)-1, + })) + assert.NoError(t, err) + assert.EqualValues(t, idx+1, resp.Msg.AckIndex) + } + sentOutputKeys := make([]string, 0, len(outcome.outputs)) + for outputKey, outputValue := range outcome.outputs { + resp, err := r.client.runnerServiceClient.UpdateTask(context.Background(), connect.NewRequest(&runnerv1.UpdateTaskRequest{ + State: &runnerv1.TaskState{ + Id: task.Id, + Result: runnerv1.Result_RESULT_UNSPECIFIED, + }, + Outputs: map[string]string{outputKey: outputValue}, + })) + assert.NoError(t, err) + sentOutputKeys = append(sentOutputKeys, outputKey) + assert.ElementsMatch(t, sentOutputKeys, resp.Msg.SentOutputs) + } + time.Sleep(outcome.execTime) + resp, err := r.client.runnerServiceClient.UpdateTask(context.Background(), connect.NewRequest(&runnerv1.UpdateTaskRequest{ + State: &runnerv1.TaskState{ + Id: task.Id, + Result: outcome.result, + StoppedAt: timestamppb.Now(), + }, + })) + assert.NoError(t, err) + assert.Equal(t, outcome.result, resp.Msg.State.Result) +} diff --git a/tests/integration/api_packages_maven_test.go b/tests/integration/api_packages_maven_test.go index e54238858c..486a5af93e 100644 --- a/tests/integration/api_packages_maven_test.go +++ b/tests/integration/api_packages_maven_test.go @@ -6,6 +6,7 @@ package integration import ( "fmt" "net/http" + "net/url" "strconv" "strings" "sync" @@ -20,6 +21,7 @@ import ( "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestPackageMaven(t *testing.T) { @@ -29,16 +31,14 @@ func TestPackageMaven(t *testing.T) { groupID := "com.gitea" artifactID := "test-project" - packageName := groupID + "-" + artifactID packageVersion := "1.0.1" packageDescription := "Test Description" - root := fmt.Sprintf("/api/packages/%s/maven/%s/%s", user.Name, strings.ReplaceAll(groupID, ".", "/"), artifactID) - filename := fmt.Sprintf("%s-%s.jar", packageName, packageVersion) + root := "/api/packages/user2/maven/com/gitea/test-project" + filename := "any-name.jar" putFile := func(t *testing.T, path, content string, expectedStatus int) { - req := NewRequestWithBody(t, "PUT", root+path, strings.NewReader(content)). - AddBasicAuth(user.Name) + req := NewRequestWithBody(t, "PUT", root+path, strings.NewReader(content)).AddBasicAuth(user.Name) MakeRequest(t, req, expectedStatus) } @@ -56,27 +56,67 @@ func TestPackageMaven(t *testing.T) { putFile(t, "/maven-metadata.xml", "test", http.StatusOK) pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeMaven) - assert.NoError(t, err) + require.NoError(t, err) assert.Len(t, pvs, 1) pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0]) - assert.NoError(t, err) + require.NoError(t, err) assert.Nil(t, pd.SemVer) assert.Nil(t, pd.Metadata) - assert.Equal(t, packageName, pd.Package.Name) + assert.Equal(t, groupID+":"+artifactID, pd.Package.Name) assert.Equal(t, packageVersion, pd.Version.Version) pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID) - assert.NoError(t, err) + require.NoError(t, err) assert.Len(t, pfs, 1) assert.Equal(t, filename, pfs[0].Name) assert.False(t, pfs[0].IsLead) pb, err := packages.GetBlobByID(db.DefaultContext, pfs[0].BlobID) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, int64(4), pb.Size) }) + t.Run("UploadLegacy", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + legacyRootLink := "/api/packages/user2/maven/com/gitea/legacy-project" + req := NewRequestWithBody(t, "PUT", legacyRootLink+"/1.0.2/any-file-name?use_legacy_package_name=1", strings.NewReader("test-content")).AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusCreated) + p, err := packages.GetPackageByName(db.DefaultContext, user.ID, packages.TypeMaven, "com.gitea-legacy-project") + require.NoError(t, err) + assert.Equal(t, "com.gitea-legacy-project", p.Name) + + req = NewRequest(t, "HEAD", legacyRootLink+"/1.0.2/any-file-name").AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusOK) + + req = NewRequest(t, "GET", "/user2/-/packages/maven/com.gitea-legacy-project/1.0.2") + MakeRequest(t, req, http.StatusOK) + req = NewRequest(t, "GET", "/user2/-/packages/maven/com.gitea:legacy-project/1.0.2") + MakeRequest(t, req, http.StatusNotFound) + req = NewRequest(t, "GET", "/user2/-/packages/maven/com.gitea%3Alegacy-project/1.0.2") + MakeRequest(t, req, http.StatusNotFound) + + req = NewRequestWithBody(t, "PUT", legacyRootLink+"/1.0.3/any-file-name", strings.NewReader("test-content")).AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusCreated) + _, err = packages.GetPackageByName(db.DefaultContext, user.ID, packages.TypeMaven, "com.gitea-legacy-project") + require.ErrorIs(t, err, packages.ErrPackageNotExist) + p, err = packages.GetPackageByName(db.DefaultContext, user.ID, packages.TypeMaven, "com.gitea:legacy-project") + require.NoError(t, err) + assert.Equal(t, "com.gitea:legacy-project", p.Name) + req = NewRequest(t, "HEAD", legacyRootLink+"/1.0.2/any-file-name").AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusOK) + + req = NewRequest(t, "GET", "/user2/-/packages/maven/com.gitea-legacy-project/1.0.2") + MakeRequest(t, req, http.StatusNotFound) + req = NewRequest(t, "GET", "/user2/-/packages/maven/com.gitea:legacy-project/1.0.2") + MakeRequest(t, req, http.StatusOK) + req = NewRequest(t, "GET", "/user2/-/packages/maven/com.gitea%3Alegacy-project/1.0.2") + MakeRequest(t, req, http.StatusOK) + + require.NoError(t, packages.DeletePackageByID(db.DefaultContext, p.ID)) + }) + t.Run("UploadExists", func(t *testing.T) { defer tests.PrintCurrentTest(t)() @@ -86,14 +126,12 @@ func TestPackageMaven(t *testing.T) { t.Run("Download", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - req := NewRequest(t, "HEAD", fmt.Sprintf("%s/%s/%s", root, packageVersion, filename)). - AddBasicAuth(user.Name) + req := NewRequest(t, "HEAD", fmt.Sprintf("%s/%s/%s", root, packageVersion, filename)).AddBasicAuth(user.Name) resp := MakeRequest(t, req, http.StatusOK) checkHeaders(t, resp.Header(), "application/java-archive", 4) - req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s", root, packageVersion, filename)). - AddBasicAuth(user.Name) + req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s", root, packageVersion, filename)).AddBasicAuth(user.Name) resp = MakeRequest(t, req, http.StatusOK) checkHeaders(t, resp.Header(), "application/java-archive", 4) @@ -101,7 +139,7 @@ func TestPackageMaven(t *testing.T) { assert.Equal(t, []byte("test"), resp.Body.Bytes()) pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeMaven) - assert.NoError(t, err) + require.NoError(t, err) assert.Len(t, pvs, 1) assert.Equal(t, int64(0), pvs[0].DownloadCount) }) @@ -133,26 +171,26 @@ func TestPackageMaven(t *testing.T) { defer tests.PrintCurrentTest(t)() pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeMaven) - assert.NoError(t, err) + require.NoError(t, err) assert.Len(t, pvs, 1) pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0]) - assert.NoError(t, err) + require.NoError(t, err) assert.Nil(t, pd.Metadata) putFile(t, fmt.Sprintf("/%s/%s.pom", packageVersion, filename), pomContent, http.StatusCreated) pvs, err = packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeMaven) - assert.NoError(t, err) + require.NoError(t, err) assert.Len(t, pvs, 1) pd, err = packages.GetPackageDescriptor(db.DefaultContext, pvs[0]) - assert.NoError(t, err) + require.NoError(t, err) assert.IsType(t, &maven.Metadata{}, pd.Metadata) assert.Equal(t, packageDescription, pd.Metadata.(*maven.Metadata).Description) pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID) - assert.NoError(t, err) + require.NoError(t, err) assert.Len(t, pfs, 2) for _, pf := range pfs { if strings.HasSuffix(pf.Name, ".pom") { @@ -167,14 +205,12 @@ func TestPackageMaven(t *testing.T) { t.Run("DownloadPOM", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - req := NewRequest(t, "HEAD", fmt.Sprintf("%s/%s/%s.pom", root, packageVersion, filename)). - AddBasicAuth(user.Name) + req := NewRequest(t, "HEAD", fmt.Sprintf("%s/%s/%s.pom", root, packageVersion, filename)).AddBasicAuth(user.Name) resp := MakeRequest(t, req, http.StatusOK) checkHeaders(t, resp.Header(), "text/xml", int64(len(pomContent))) - req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s.pom", root, packageVersion, filename)). - AddBasicAuth(user.Name) + req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s.pom", root, packageVersion, filename)).AddBasicAuth(user.Name) resp = MakeRequest(t, req, http.StatusOK) checkHeaders(t, resp.Header(), "text/xml", int64(len(pomContent))) @@ -182,7 +218,7 @@ func TestPackageMaven(t *testing.T) { assert.Equal(t, []byte(pomContent), resp.Body.Bytes()) pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeMaven) - assert.NoError(t, err) + require.NoError(t, err) assert.Len(t, pvs, 1) assert.Equal(t, int64(1), pvs[0].DownloadCount) }) @@ -190,8 +226,7 @@ func TestPackageMaven(t *testing.T) { t.Run("DownloadChecksums", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - req := NewRequest(t, "GET", fmt.Sprintf("%s/1.2.3/%s", root, filename)). - AddBasicAuth(user.Name) + req := NewRequest(t, "GET", fmt.Sprintf("%s/1.2.3/%s", root, filename)).AddBasicAuth(user.Name) MakeRequest(t, req, http.StatusNotFound) for key, checksum := range map[string]string{ @@ -200,8 +235,7 @@ func TestPackageMaven(t *testing.T) { "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "sha512": "ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff", } { - req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s.%s", root, packageVersion, filename, key)). - AddBasicAuth(user.Name) + req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s.%s", root, packageVersion, filename, key)).AddBasicAuth(user.Name) resp := MakeRequest(t, req, http.StatusOK) assert.Equal(t, checksum, resp.Body.String()) @@ -211,8 +245,7 @@ func TestPackageMaven(t *testing.T) { t.Run("DownloadMetadata", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - req := NewRequest(t, "GET", root+"/maven-metadata.xml"). - AddBasicAuth(user.Name) + req := NewRequest(t, "GET", root+"/maven-metadata.xml").AddBasicAuth(user.Name) resp := MakeRequest(t, req, http.StatusOK) expectedMetadata := `` + "\ncom.giteatest-project1.0.11.0.11.0.1" @@ -227,8 +260,7 @@ func TestPackageMaven(t *testing.T) { "sha256": "3f48322f81c4b2c3bb8649ae1e5c9801476162b520e1c2734ac06b2c06143208", "sha512": "cb075aa2e2ef1a83cdc14dd1e08c505b72d633399b39e73a21f00f0deecb39a3e2c79f157c1163f8a3854828750706e0dec3a0f5e4778e91f8ec2cf351a855f2", } { - req := NewRequest(t, "GET", fmt.Sprintf("%s/maven-metadata.xml.%s", root, key)). - AddBasicAuth(user.Name) + req := NewRequest(t, "GET", fmt.Sprintf("%s/maven-metadata.xml.%s", root, key)).AddBasicAuth(user.Name) resp := MakeRequest(t, req, http.StatusOK) assert.Equal(t, checksum, resp.Body.String()) @@ -245,9 +277,10 @@ func TestPackageMaven(t *testing.T) { }) t.Run("InvalidFile", func(t *testing.T) { - ver := packageVersion + "-invalid" - putFile(t, fmt.Sprintf("/%s/%s", ver, filename), "any invalid content", http.StatusCreated) - req := NewRequestf(t, "GET", "/%s/-/packages/maven/%s-%s/%s", user.Name, groupID, artifactID, ver) + invalidVersion := packageVersion + "-invalid" + putFile(t, fmt.Sprintf("/%s/%s", invalidVersion, filename), "any invalid content", http.StatusCreated) + + req := NewRequestf(t, "GET", "/%s/-/packages/maven/%s/%s", user.Name, url.QueryEscape(groupID+":"+artifactID), invalidVersion) resp := MakeRequest(t, req, http.StatusOK) assert.Contains(t, resp.Body.String(), "No metadata.") assert.True(t, test.IsNormalPageCompleted(resp.Body.String())) @@ -266,8 +299,7 @@ func TestPackageMavenConcurrent(t *testing.T) { root := fmt.Sprintf("/api/packages/%s/maven/%s/%s", user.Name, strings.ReplaceAll(groupID, ".", "/"), artifactID) putFile := func(t *testing.T, path, content string, expectedStatus int) { - req := NewRequestWithBody(t, "PUT", root+path, strings.NewReader(content)). - AddBasicAuth(user.Name) + req := NewRequestWithBody(t, "PUT", root+path, strings.NewReader(content)).AddBasicAuth(user.Name) MakeRequest(t, req, expectedStatus) } diff --git a/tests/integration/api_repo_git_hook_test.go b/tests/integration/api_repo_git_hook_test.go index 9917b41790..c28c4336e2 100644 --- a/tests/integration/api_repo_git_hook_test.go +++ b/tests/integration/api_repo_git_hook_test.go @@ -12,185 +12,190 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" ) -const testHookContent = `#!/bin/bash +func TestAPIGitHooks(t *testing.T) { + defer tests.PrepareTestEnv(t)() + defer test.MockVariableValue(&setting.DisableGitHooks, false)() -echo Hello, World! + const testHookContent = `#!/bin/bash +echo "TestGitHookScript" ` -func TestAPIListGitHooks(t *testing.T) { - defer tests.PrepareTestEnv(t)() + t.Run("ListGitHooks", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 37}) - owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 37}) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) - // user1 is an admin user - session := loginUser(t, "user1") - token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git", owner.Name, repo.Name). - AddTokenAuth(token) - resp := MakeRequest(t, req, http.StatusOK) - var apiGitHooks []*api.GitHook - DecodeJSON(t, resp, &apiGitHooks) - assert.Len(t, apiGitHooks, 3) - for _, apiGitHook := range apiGitHooks { - if apiGitHook.Name == "pre-receive" { - assert.True(t, apiGitHook.IsActive) - assert.Equal(t, testHookContent, apiGitHook.Content) - } else { + // user1 is an admin user + session := loginUser(t, "user1") + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git", owner.Name, repo.Name). + AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var apiGitHooks []*api.GitHook + DecodeJSON(t, resp, &apiGitHooks) + assert.Len(t, apiGitHooks, 3) + for _, apiGitHook := range apiGitHooks { + if apiGitHook.Name == "pre-receive" { + assert.True(t, apiGitHook.IsActive) + assert.Equal(t, testHookContent, apiGitHook.Content) + } else { + assert.False(t, apiGitHook.IsActive) + assert.Empty(t, apiGitHook.Content) + } + } + }) + + t.Run("NoGitHooks", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + + // user1 is an admin user + session := loginUser(t, "user1") + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git", owner.Name, repo.Name). + AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var apiGitHooks []*api.GitHook + DecodeJSON(t, resp, &apiGitHooks) + assert.Len(t, apiGitHooks, 3) + for _, apiGitHook := range apiGitHooks { assert.False(t, apiGitHook.IsActive) assert.Empty(t, apiGitHook.Content) } - } -} - -func TestAPIListGitHooksNoHooks(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) - - // user1 is an admin user - session := loginUser(t, "user1") - token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git", owner.Name, repo.Name). - AddTokenAuth(token) - resp := MakeRequest(t, req, http.StatusOK) - var apiGitHooks []*api.GitHook - DecodeJSON(t, resp, &apiGitHooks) - assert.Len(t, apiGitHooks, 3) - for _, apiGitHook := range apiGitHooks { - assert.False(t, apiGitHook.IsActive) - assert.Empty(t, apiGitHook.Content) - } -} - -func TestAPIListGitHooksNoAccess(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) - - session := loginUser(t, owner.Name) - token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git", owner.Name, repo.Name). - AddTokenAuth(token) - MakeRequest(t, req, http.StatusForbidden) -} - -func TestAPIGetGitHook(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 37}) - owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) - - // user1 is an admin user - session := loginUser(t, "user1") - token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git/pre-receive", owner.Name, repo.Name). - AddTokenAuth(token) - resp := MakeRequest(t, req, http.StatusOK) - var apiGitHook *api.GitHook - DecodeJSON(t, resp, &apiGitHook) - assert.True(t, apiGitHook.IsActive) - assert.Equal(t, testHookContent, apiGitHook.Content) -} - -func TestAPIGetGitHookNoAccess(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) - - session := loginUser(t, owner.Name) - token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git/pre-receive", owner.Name, repo.Name). - AddTokenAuth(token) - MakeRequest(t, req, http.StatusForbidden) -} - -func TestAPIEditGitHook(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) - - // user1 is an admin user - session := loginUser(t, "user1") - token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) - - urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/hooks/git/pre-receive", - owner.Name, repo.Name) - req := NewRequestWithJSON(t, "PATCH", urlStr, &api.EditGitHookOption{ - Content: testHookContent, - }).AddTokenAuth(token) - resp := MakeRequest(t, req, http.StatusOK) - var apiGitHook *api.GitHook - DecodeJSON(t, resp, &apiGitHook) - assert.True(t, apiGitHook.IsActive) - assert.Equal(t, testHookContent, apiGitHook.Content) - - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git/pre-receive", owner.Name, repo.Name). - AddTokenAuth(token) - resp = MakeRequest(t, req, http.StatusOK) - var apiGitHook2 *api.GitHook - DecodeJSON(t, resp, &apiGitHook2) - assert.True(t, apiGitHook2.IsActive) - assert.Equal(t, testHookContent, apiGitHook2.Content) -} - -func TestAPIEditGitHookNoAccess(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) - - session := loginUser(t, owner.Name) - token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) - urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/hooks/git/pre-receive", owner.Name, repo.Name) - req := NewRequestWithJSON(t, "PATCH", urlStr, &api.EditGitHookOption{ - Content: testHookContent, - }).AddTokenAuth(token) - MakeRequest(t, req, http.StatusForbidden) -} - -func TestAPIDeleteGitHook(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 37}) - owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) - - // user1 is an admin user - session := loginUser(t, "user1") - token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) - - req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/hooks/git/pre-receive", owner.Name, repo.Name). - AddTokenAuth(token) - MakeRequest(t, req, http.StatusNoContent) - - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git/pre-receive", owner.Name, repo.Name). - AddTokenAuth(token) - resp := MakeRequest(t, req, http.StatusOK) - var apiGitHook2 *api.GitHook - DecodeJSON(t, resp, &apiGitHook2) - assert.False(t, apiGitHook2.IsActive) - assert.Empty(t, apiGitHook2.Content) -} - -func TestAPIDeleteGitHookNoAccess(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) - - session := loginUser(t, owner.Name) - token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) - req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/hooks/git/pre-receive", owner.Name, repo.Name). - AddTokenAuth(token) - MakeRequest(t, req, http.StatusForbidden) + }) + + t.Run("ListGitHooksNoAccess", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + + session := loginUser(t, owner.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git", owner.Name, repo.Name). + AddTokenAuth(token) + MakeRequest(t, req, http.StatusForbidden) + }) + + t.Run("GetGitHook", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 37}) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + + // user1 is an admin user + session := loginUser(t, "user1") + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git/pre-receive", owner.Name, repo.Name). + AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var apiGitHook *api.GitHook + DecodeJSON(t, resp, &apiGitHook) + assert.True(t, apiGitHook.IsActive) + assert.Equal(t, testHookContent, apiGitHook.Content) + }) + t.Run("GetGitHookNoAccess", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + + session := loginUser(t, owner.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git/pre-receive", owner.Name, repo.Name). + AddTokenAuth(token) + MakeRequest(t, req, http.StatusForbidden) + }) + + t.Run("EditGitHook", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + + // user1 is an admin user + session := loginUser(t, "user1") + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/hooks/git/pre-receive", + owner.Name, repo.Name) + req := NewRequestWithJSON(t, "PATCH", urlStr, &api.EditGitHookOption{ + Content: testHookContent, + }).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var apiGitHook *api.GitHook + DecodeJSON(t, resp, &apiGitHook) + assert.True(t, apiGitHook.IsActive) + assert.Equal(t, testHookContent, apiGitHook.Content) + + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git/pre-receive", owner.Name, repo.Name). + AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + var apiGitHook2 *api.GitHook + DecodeJSON(t, resp, &apiGitHook2) + assert.True(t, apiGitHook2.IsActive) + assert.Equal(t, testHookContent, apiGitHook2.Content) + }) + + t.Run("EditGitHookNoAccess", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + + session := loginUser(t, owner.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/hooks/git/pre-receive", owner.Name, repo.Name) + req := NewRequestWithJSON(t, "PATCH", urlStr, &api.EditGitHookOption{ + Content: testHookContent, + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusForbidden) + }) + + t.Run("DeleteGitHook", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 37}) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + + // user1 is an admin user + session := loginUser(t, "user1") + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + + req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/hooks/git/pre-receive", owner.Name, repo.Name). + AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git/pre-receive", owner.Name, repo.Name). + AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var apiGitHook2 *api.GitHook + DecodeJSON(t, resp, &apiGitHook2) + assert.False(t, apiGitHook2.IsActive) + assert.Empty(t, apiGitHook2.Content) + }) + + t.Run("DeleteGitHookNoAccess", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + + session := loginUser(t, owner.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/hooks/git/pre-receive", owner.Name, repo.Name). + AddTokenAuth(token) + MakeRequest(t, req, http.StatusForbidden) + }) } diff --git a/tests/integration/auth_ldap_test.go b/tests/integration/auth_ldap_test.go index 5d37244331..5c50fd0288 100644 --- a/tests/integration/auth_ldap_test.go +++ b/tests/integration/auth_ldap_test.go @@ -332,7 +332,7 @@ func TestLDAPUserSyncWithGroupFilter(t *testing.T) { // Assert members of LDAP group "cn=git" are added for _, gitLDAPUser := range te.gitLDAPUsers { - unittest.BeanExists(t, &user_model.User{ + unittest.AssertExistsAndLoadBean(t, &user_model.User{ Name: gitLDAPUser.UserName, }) } diff --git a/tests/integration/compare_test.go b/tests/integration/compare_test.go index d960416b3a..cbf927813e 100644 --- a/tests/integration/compare_test.go +++ b/tests/integration/compare_test.go @@ -27,7 +27,7 @@ func TestCompareTag(t *testing.T) { req := NewRequest(t, "GET", "/user2/repo1/compare/v1.1...master") resp := session.MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) - selection := htmlDoc.doc.Find(".choose.branch .filter.dropdown") + selection := htmlDoc.doc.Find(".ui.dropdown.select-branch") // A dropdown for both base and head. assert.Lenf(t, selection.Nodes, 2, "The template has changed") @@ -44,7 +44,7 @@ func TestCompareDefault(t *testing.T) { req := NewRequest(t, "GET", "/user2/repo1/compare/v1.1") resp := session.MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) - selection := htmlDoc.doc.Find(".choose.branch .filter.dropdown") + selection := htmlDoc.doc.Find(".ui.dropdown.select-branch") assert.Lenf(t, selection.Nodes, 2, "The template has changed") } diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index 9b3b2f2b92..46b93b0a10 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -88,18 +88,6 @@ func TestMain(m *testing.M) { tests.InitTest(true) testWebRoutes = routers.NormalRoutes() - os.Unsetenv("GIT_AUTHOR_NAME") - os.Unsetenv("GIT_AUTHOR_EMAIL") - os.Unsetenv("GIT_AUTHOR_DATE") - os.Unsetenv("GIT_COMMITTER_NAME") - os.Unsetenv("GIT_COMMITTER_EMAIL") - os.Unsetenv("GIT_COMMITTER_DATE") - - // Avoid loading the default system config. On MacOS, this config - // sets the osxkeychain credential helper, which will cause tests - // to freeze with a dialog. - os.Setenv("GIT_CONFIG_NOSYSTEM", "true") - err := unittest.InitFixtures( unittest.FixturesOptions{ Dir: filepath.Join(filepath.Dir(setting.AppPath), "models/fixtures/"), diff --git a/tests/integration/org_profile_test.go b/tests/integration/org_profile_test.go new file mode 100644 index 0000000000..73cafd85c2 --- /dev/null +++ b/tests/integration/org_profile_test.go @@ -0,0 +1,132 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "encoding/base64" + "fmt" + "net/http" + "net/url" + "testing" + "time" + + auth_model "code.gitea.io/gitea/models/auth" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/routers/web/shared/user" + + "github.com/stretchr/testify/assert" +) + +func getCreateProfileReadmeFileOptions(content string) api.CreateFileOptions { + contentEncoded := base64.StdEncoding.EncodeToString([]byte(content)) + return api.CreateFileOptions{ + FileOptions: api.FileOptions{ + BranchName: "main", + NewBranchName: "main", + Message: "create the profile README.md", + Dates: api.CommitDateOptions{ + Author: time.Unix(946684810, 0), + Committer: time.Unix(978307190, 0), + }, + }, + ContentBase64: contentEncoded, + } +} + +func createTestProfile(t *testing.T, orgName, profileRepoName, readmeContent string) { + isPrivate := profileRepoName == user.RepoNameProfilePrivate + + ctx := NewAPITestContext(t, "user1", profileRepoName, auth_model.AccessTokenScopeAll) + session := loginUser(t, "user1") + tokenAdmin := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll) + + // create repo + doAPICreateOrganizationRepository(ctx, orgName, &api.CreateRepoOption{Name: profileRepoName, Private: isPrivate})(t) + + // create readme + createFileOptions := getCreateProfileReadmeFileOptions(readmeContent) + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", orgName, profileRepoName, "README.md"), &createFileOptions). + AddTokenAuth(tokenAdmin) + MakeRequest(t, req, http.StatusCreated) +} + +func TestOrgProfile(t *testing.T) { + onGiteaRun(t, testOrgProfile) +} + +func testOrgProfile(t *testing.T, u *url.URL) { + const contentPublicReadme = "Public Readme Content" + const contentPrivateReadme = "Private Readme Content" + // HTML: "#org-home-view-as-dropdown" (indicate whether the view as dropdown menu is present) + + // PART 1: Test Both Private and Public + createTestProfile(t, "org3", user.RepoNameProfile, contentPublicReadme) + createTestProfile(t, "org3", user.RepoNameProfilePrivate, contentPrivateReadme) + + // Anonymous User + req := NewRequest(t, "GET", "org3") + resp := MakeRequest(t, req, http.StatusOK) + bodyString := util.UnsafeBytesToString(resp.Body.Bytes()) + assert.Contains(t, bodyString, contentPublicReadme) + assert.NotContains(t, bodyString, `id="org-home-view-as-dropdown"`) + + // Logged in but not member + session := loginUser(t, "user24") + req = NewRequest(t, "GET", "org3") + resp = session.MakeRequest(t, req, http.StatusOK) + bodyString = util.UnsafeBytesToString(resp.Body.Bytes()) + assert.Contains(t, bodyString, contentPublicReadme) + assert.NotContains(t, bodyString, `id="org-home-view-as-dropdown"`) + + // Site Admin + session = loginUser(t, "user1") + req = NewRequest(t, "GET", "/org3") + resp = session.MakeRequest(t, req, http.StatusOK) + bodyString = util.UnsafeBytesToString(resp.Body.Bytes()) + assert.Contains(t, bodyString, contentPrivateReadme) // as an org member, default to show the private profile + assert.Contains(t, bodyString, `id="org-home-view-as-dropdown"`) + + req = NewRequest(t, "GET", "/org3?view_as=member") + resp = session.MakeRequest(t, req, http.StatusOK) + bodyString = util.UnsafeBytesToString(resp.Body.Bytes()) + assert.Contains(t, bodyString, contentPrivateReadme) + assert.Contains(t, bodyString, `id="org-home-view-as-dropdown"`) + + req = NewRequest(t, "GET", "/org3?view_as=public") + resp = session.MakeRequest(t, req, http.StatusOK) + bodyString = util.UnsafeBytesToString(resp.Body.Bytes()) + assert.Contains(t, bodyString, contentPublicReadme) + assert.Contains(t, bodyString, `id="org-home-view-as-dropdown"`) + + // PART 2: Each org has either one of private pr public profile + createTestProfile(t, "org41", user.RepoNameProfile, contentPublicReadme) + createTestProfile(t, "org42", user.RepoNameProfilePrivate, contentPrivateReadme) + + // Anonymous User + req = NewRequest(t, "GET", "/org41") + resp = MakeRequest(t, req, http.StatusOK) + bodyString = util.UnsafeBytesToString(resp.Body.Bytes()) + assert.Contains(t, bodyString, contentPublicReadme) + assert.NotContains(t, bodyString, `id="org-home-view-as-dropdown"`) + + req = NewRequest(t, "GET", "/org42") + resp = MakeRequest(t, req, http.StatusOK) + bodyString = util.UnsafeBytesToString(resp.Body.Bytes()) + assert.NotContains(t, bodyString, contentPrivateReadme) + assert.NotContains(t, bodyString, `id="org-home-view-as-dropdown"`) + + // Site Admin + req = NewRequest(t, "GET", "/org41") + resp = session.MakeRequest(t, req, http.StatusOK) + bodyString = util.UnsafeBytesToString(resp.Body.Bytes()) + assert.Contains(t, bodyString, contentPublicReadme) + assert.NotContains(t, bodyString, `id="org-home-view-as-dropdown"`) + + req = NewRequest(t, "GET", "/org42") + resp = session.MakeRequest(t, req, http.StatusOK) + bodyString = util.UnsafeBytesToString(resp.Body.Bytes()) + assert.Contains(t, bodyString, contentPrivateReadme) + assert.NotContains(t, bodyString, `id="org-home-view-as-dropdown"`) +} diff --git a/tests/integration/org_team_invite_test.go b/tests/integration/org_team_invite_test.go index 274fde4085..4c1053702e 100644 --- a/tests/integration/org_team_invite_test.go +++ b/tests/integration/org_team_invite_test.go @@ -274,7 +274,8 @@ func TestOrgTeamEmailInviteRedirectsNewUserWithActivation(t *testing.T) { user, err := user_model.GetUserByName(db.DefaultContext, "doesnotexist") assert.NoError(t, err) - activateURL := fmt.Sprintf("/user/activate?code=%s", user.GenerateEmailActivateCode("doesnotexist@example.com")) + activationCode := user_model.GenerateUserTimeLimitCode(&user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateAccount}, user) + activateURL := fmt.Sprintf("/user/activate?code=%s", activationCode) req = NewRequestWithValues(t, "POST", activateURL, map[string]string{ "password": "examplePassword!1", }) diff --git a/tests/integration/pull_create_test.go b/tests/integration/pull_create_test.go index 9812d2073d..162ea532c8 100644 --- a/tests/integration/pull_create_test.go +++ b/tests/integration/pull_create_test.go @@ -12,6 +12,8 @@ import ( "strings" "testing" + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/tests" @@ -82,6 +84,30 @@ func testPullCreateDirectly(t *testing.T, session *TestSession, baseRepoOwner, b return resp } +func testPullCreateFailure(t *testing.T, session *TestSession, baseRepoOwner, baseRepoName, baseBranch, headRepoOwner, headRepoName, headBranch, title string) *httptest.ResponseRecorder { + headCompare := headBranch + if headRepoOwner != "" { + if headRepoName != "" { + headCompare = fmt.Sprintf("%s/%s:%s", headRepoOwner, headRepoName, headBranch) + } else { + headCompare = fmt.Sprintf("%s:%s", headRepoOwner, headBranch) + } + } + req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/compare/%s...%s", baseRepoOwner, baseRepoName, baseBranch, headCompare)) + resp := session.MakeRequest(t, req, http.StatusOK) + + // Submit the form for creating the pull + htmlDoc := NewHTMLParser(t, resp.Body) + link, exists := htmlDoc.doc.Find("form.ui.form").Attr("action") + assert.True(t, exists, "The template has changed") + req = NewRequestWithValues(t, "POST", link, map[string]string{ + "_csrf": htmlDoc.GetCSRF(), + "title": title, + }) + resp = session.MakeRequest(t, req, http.StatusBadRequest) + return resp +} + func TestPullCreate(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user1") @@ -226,3 +252,64 @@ func TestPullCreatePrFromBaseToFork(t *testing.T) { assert.Regexp(t, "^/user1/repo1/pulls/[0-9]*$", url) }) } + +func TestCreateAgitPullWithReadPermission(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + dstPath := t.TempDir() + + u.Path = "user2/repo1.git" + u.User = url.UserPassword("user4", userPassword) + + t.Run("Clone", doGitClone(dstPath, u)) + + t.Run("add commit", doGitAddSomeCommits(dstPath, "master")) + + t.Run("do agit pull create", func(t *testing.T) { + err := git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master", "-o").AddDynamicArguments("topic=" + "test-topic").Run(&git.RunOpts{Dir: dstPath}) + assert.NoError(t, err) + }) + }) +} + +/* +Setup: user2 has repository, user1 forks it +--- + +1. User2 blocks User1 +2. User1 adds changes to fork +3. User1 attempts to create a pull request +4. User1 sees alert that the action is not allowed because of the block +*/ +func TestCreatePullWhenBlocked(t *testing.T) { + RepoOwner := "user2" + ForkOwner := "user16" + onGiteaRun(t, func(t *testing.T, u *url.URL) { + // Setup + // User1 forks repo1 from User2 + sessionFork := loginUser(t, ForkOwner) + testRepoFork(t, sessionFork, RepoOwner, "repo1", ForkOwner, "forkrepo1", "") + + // 1. User2 blocks user1 + // sessionBase := loginUser(t, "user2") + token := getUserToken(t, RepoOwner, auth_model.AccessTokenScopeWriteUser) + + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/blocks/%s", ForkOwner)). + AddTokenAuth(token) + MakeRequest(t, req, http.StatusNotFound) + req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/blocks/%s", ForkOwner)). + AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + + // 2. User1 adds changes to fork + testEditFile(t, sessionFork, ForkOwner, "forkrepo1", "master", "README.md", "Hello, World (Edited)\n") + + // 3. User1 attempts to create a pull request + testPullCreateFailure(t, sessionFork, RepoOwner, "repo1", "master", ForkOwner, "forkrepo1", "master", "This is a pull title") + + // Teardown + // Unblock user + req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/user/blocks/%s", ForkOwner)). + AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + }) +} diff --git a/tests/integration/pull_review_test.go b/tests/integration/pull_review_test.go index 5ecf3ef469..68de421413 100644 --- a/tests/integration/pull_review_test.go +++ b/tests/integration/pull_review_test.go @@ -92,7 +92,7 @@ func TestPullView_CodeOwner(t *testing.T) { testPullCreate(t, session, "user2", "test_codeowner", false, repo.DefaultBranch, "codeowner-basebranch", "Test Pull Request") pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadRepoID: repo.ID, HeadBranch: "codeowner-basebranch"}) - unittest.AssertExistsIf(t, true, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 5}) + unittest.AssertExistsAndLoadBean(t, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 5}) assert.NoError(t, pr.LoadIssue(db.DefaultContext)) err := issue_service.ChangeTitle(db.DefaultContext, pr.Issue, user2, "[WIP] Test Pull Request") @@ -139,7 +139,7 @@ func TestPullView_CodeOwner(t *testing.T) { testPullCreate(t, session, "user2", "test_codeowner", false, repo.DefaultBranch, "codeowner-basebranch2", "Test Pull Request2") pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadBranch: "codeowner-basebranch2"}) - unittest.AssertExistsIf(t, true, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8}) + unittest.AssertExistsAndLoadBean(t, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8}) }) t.Run("Forked Repo Pull Request", func(t *testing.T) { @@ -169,13 +169,13 @@ func TestPullView_CodeOwner(t *testing.T) { testPullCreateDirectly(t, session, "user5", "test_codeowner", forkedRepo.DefaultBranch, "", "", "codeowner-basebranch-forked", "Test Pull Request on Forked Repository") pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: forkedRepo.ID, HeadBranch: "codeowner-basebranch-forked"}) - unittest.AssertExistsIf(t, false, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8}) + unittest.AssertNotExistsBean(t, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8}) // create a pull request to base repository, code reviewers should be mentioned testPullCreateDirectly(t, session, repo.OwnerName, repo.Name, repo.DefaultBranch, forkedRepo.OwnerName, forkedRepo.Name, "codeowner-basebranch-forked", "Test Pull Request3") pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadRepoID: forkedRepo.ID, HeadBranch: "codeowner-basebranch-forked"}) - unittest.AssertExistsIf(t, true, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8}) + unittest.AssertExistsAndLoadBean(t, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8}) }) }) } diff --git a/tests/integration/repo_commits_test.go b/tests/integration/repo_commits_test.go index bb65d9e04a..fc066e06d3 100644 --- a/tests/integration/repo_commits_test.go +++ b/tests/integration/repo_commits_test.go @@ -30,7 +30,7 @@ func TestRepoCommits(t *testing.T) { resp := session.MakeRequest(t, req, http.StatusOK) doc := NewHTMLParser(t, resp.Body) - commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Attr("href") + commitURL, exists := doc.doc.Find("#commits-table .commit-id-short").Attr("href") assert.True(t, exists) assert.NotEmpty(t, commitURL) } @@ -46,7 +46,7 @@ func doTestRepoCommitWithStatus(t *testing.T, state string, classes ...string) { doc := NewHTMLParser(t, resp.Body) // Get first commit URL - commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Attr("href") + commitURL, exists := doc.doc.Find("#commits-table .commit-id-short").Attr("href") assert.True(t, exists) assert.NotEmpty(t, commitURL) @@ -64,7 +64,7 @@ func doTestRepoCommitWithStatus(t *testing.T, state string, classes ...string) { doc = NewHTMLParser(t, resp.Body) // Check if commit status is displayed in message column (.tippy-target to ignore the tippy trigger) - sel := doc.doc.Find("#commits-table tbody tr td.message .tippy-target .commit-status") + sel := doc.doc.Find("#commits-table .message .tippy-target .commit-status") assert.Equal(t, 1, sel.Length()) for _, class := range classes { assert.True(t, sel.HasClass(class)) @@ -140,7 +140,7 @@ func TestRepoCommitsStatusParallel(t *testing.T) { doc := NewHTMLParser(t, resp.Body) // Get first commit URL - commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Attr("href") + commitURL, exists := doc.doc.Find("#commits-table .commit-id-short").Attr("href") assert.True(t, exists) assert.NotEmpty(t, commitURL) @@ -175,7 +175,7 @@ func TestRepoCommitsStatusMultiple(t *testing.T) { doc := NewHTMLParser(t, resp.Body) // Get first commit URL - commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Attr("href") + commitURL, exists := doc.doc.Find("#commits-table .commit-id-short").Attr("href") assert.True(t, exists) assert.NotEmpty(t, commitURL) @@ -200,6 +200,6 @@ func TestRepoCommitsStatusMultiple(t *testing.T) { doc = NewHTMLParser(t, resp.Body) // Check that the data-tippy="commit-statuses" (for trigger) and commit-status (svg) are present - sel := doc.doc.Find("#commits-table tbody tr td.message [data-tippy=\"commit-statuses\"] .commit-status") + sel := doc.doc.Find("#commits-table .message [data-tippy=\"commit-statuses\"] .commit-status") assert.Equal(t, 1, sel.Length()) } diff --git a/tests/integration/signin_test.go b/tests/integration/signin_test.go index abad9eb5e5..d7c0b1bcd3 100644 --- a/tests/integration/signin_test.go +++ b/tests/integration/signin_test.go @@ -9,6 +9,7 @@ import ( "strings" "testing" + "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" @@ -17,6 +18,7 @@ import ( "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func testLoginFailed(t *testing.T, username, password, message string) { @@ -42,7 +44,7 @@ func TestSignin(t *testing.T) { user.Name = "testuser" user.LowerName = strings.ToLower(user.Name) user.ID = 0 - unittest.AssertSuccessfulInsert(t, user) + require.NoError(t, db.Insert(db.DefaultContext, user)) samples := []struct { username string diff --git a/tests/integration/signup_test.go b/tests/integration/signup_test.go index e9a05201ee..e86851352e 100644 --- a/tests/integration/signup_test.go +++ b/tests/integration/signup_test.go @@ -9,6 +9,7 @@ import ( "strings" "testing" + "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" @@ -99,34 +100,39 @@ func TestSignupEmailActive(t *testing.T) { // try to sign up and send the activation email req := NewRequestWithValues(t, "POST", "/user/sign_up", map[string]string{ - "user_name": "test-user-1", - "email": "email-1@example.com", + "user_name": "Test-User-1", + "email": "EmAiL-1@example.com", "password": "password1", "retype": "password1", }) resp := MakeRequest(t, req, http.StatusOK) - assert.Contains(t, resp.Body.String(), `A new confirmation email has been sent to email-1@example.com.`) + assert.Contains(t, resp.Body.String(), `A new confirmation email has been sent to EmAiL-1@example.com.`) // access "user/activate" means trying to re-send the activation email session := loginUserWithPassword(t, "test-user-1", "password1") resp = session.MakeRequest(t, NewRequest(t, "GET", "/user/activate"), http.StatusOK) assert.Contains(t, resp.Body.String(), "You have already requested an activation email recently") - // access anywhere else will see a "Activate Your Account" prompt, and there is a chance to change email + // access anywhere else will see an "Activate Your Account" prompt, and there is a chance to change email resp = session.MakeRequest(t, NewRequest(t, "GET", "/user/issues"), http.StatusOK) assert.Contains(t, resp.Body.String(), ` .ui { - display: block; - margin-top: 5px; - margin-bottom: 10px; -} - -.inline-grouped-list > .ui:first-child { - margin-top: 1px; -} - .lines-blame-btn { padding: 0 0 0 5px; display: flex; diff --git a/web_src/css/explore.css b/web_src/css/explore.css index 5cdee823c0..968a103ce9 100644 --- a/web_src/css/explore.css +++ b/web_src/css/explore.css @@ -1,13 +1,4 @@ -.explore .secondary-nav { - border-width: 1px !important; -} - -.explore .secondary-nav .svg { - width: 16px; - text-align: center; - margin-right: 5px; -} - +/* FIXME: need to refactor the repo branches list page and move these styles to proper place */ .ui.repository.branches .info { font-size: 12px; color: var(--color-text-light); @@ -20,12 +11,3 @@ overflow: hidden; text-overflow: ellipsis; } - -.ui.repository.branches .overflow-visible { - overflow: visible; -} - -/* fix alignment of PR popup in branches table */ -.ui.repository.branches table .ui.popup { - text-align: left; -} diff --git a/web_src/css/features/gitgraph.css b/web_src/css/features/gitgraph.css index f8f7e35cdc..1ed541a695 100644 --- a/web_src/css/features/gitgraph.css +++ b/web_src/css/features/gitgraph.css @@ -57,6 +57,12 @@ white-space: nowrap; display: flex; align-items: center; + gap: 0.25em; +} + +#git-graph-container li .ui.label.commit-id-short { + padding-top: 2px; + padding-bottom: 2px; } #git-graph-container li .node-relation { @@ -112,17 +118,6 @@ text-overflow: ellipsis; } -#git-graph-container #rev-list .sha.label { - padding-top: 5px; - padding-bottom: 3px; -} - -#git-graph-container #rev-list .sha.label .ui.detail.icon.button { - padding-top: 3px; - margin-top: -5px; - padding-bottom: 1px; -} - #git-graph-container #graph-raw-list { margin: 0; } diff --git a/web_src/css/form.css b/web_src/css/form.css index 5dd5e05bec..266b8c73f2 100644 --- a/web_src/css/form.css +++ b/web_src/css/form.css @@ -38,11 +38,6 @@ textarea, color: var(--color-input-text); } -/* fix fomantic small dropdown having inconsistent padding with input */ -.ui.small.selection.dropdown { - padding: .67857143em 1.6em .67857143em 1em; -} - input:hover, textarea:hover, .ui.input input:hover, @@ -109,9 +104,8 @@ textarea:focus, color: var(--color-input-text); } -/* match */ -.ui.form select { - padding: 0.67857143em 1em; +.ui.form .field > .selection.dropdown { + min-width: 14em; /* matches the default min width */ } .form .help { @@ -120,47 +114,6 @@ textarea:focus, display: inline-block; } -#create-page-form form { - margin: auto; -} - -#create-page-form form .ui.message { - text-align: center; -} - -@media (min-width: 768px) { - #create-page-form form { - width: 800px !important; - } - #create-page-form form .header { - padding-left: 280px !important; - } - #create-page-form form .inline.field > label { - text-align: right; - width: 250px !important; - word-wrap: break-word; - } - #create-page-form form .help { - margin-left: 265px !important; - } - #create-page-form form .optional .title { - margin-left: 250px !important; - } - #create-page-form form .inline.field > input, - #create-page-form form .inline.field > textarea { - width: 50%; - } -} - -@media (max-width: 767.98px) { - #create-page-form form .optional .title { - margin-left: 15px; - } - #create-page-form form .inline.field > label { - display: block; - } -} - .m-captcha-style { width: 100%; height: 5em; @@ -187,7 +140,7 @@ textarea:focus, } @media (max-height: 575px) { - #rc-imageselect, + #rc-imageselect, /* google recaptcha */ .g-recaptcha-style, .h-captcha-style { transform: scale(0.77); @@ -195,295 +148,40 @@ textarea:focus, } } -.user.forgot.password form, -.user.reset.password form, -.user.signup form { - margin: auto; - width: 700px !important; -} - -.user.activate form .ui.message, -.user.forgot.password form .ui.message, -.user.reset.password form .ui.message, -.user.link-account form .ui.message, -.user.signin form .ui.message, -.user.signup form .ui.message { - text-align: center; -} - -@media (min-width: 768px) { - .user.activate form, - .user.forgot.password form, - .user.reset.password form, - .user.link-account form, - .user.signin form, - .user.signup form { - width: 800px !important; - } - .user.activate form .header, - .user.forgot.password form .header, - .user.reset.password form .header, - .user.link-account form .header, - .user.signin form .header, - .user.signup form .header { - padding-left: 280px !important; - } - .user.activate form .inline.field > label { - text-align: right; - width: 250px !important; - word-wrap: break-word; - } - .user.activate form .help, - .user.forgot.password form .help, - .user.reset.password form .help, - .user.link-account form .help, - .user.signin form .help, - .user.signup form .help { - margin-left: 265px !important; - } - .user.activate form .optional .title, - .user.forgot.password form .optional .title, - .user.reset.password form .optional .title, - .user.link-account form .optional .title, - .user.signin form .optional .title, - .user.signup form .optional .title { - margin-left: 250px !important; - } -} - -@media (max-width: 767.98px) { - .user.activate form .optional .title, - .user.forgot.password form .optional .title, - .user.reset.password form .optional .title, - .user.link-account form .optional .title, - .user.signin form .optional .title, - .user.signup form .optional .title { - margin-left: 15px; - } - .user.activate form .inline.field > label, - .user.forgot.password form .inline.field > label, - .user.reset.password form .inline.field > label, - .user.link-account form .inline.field > label, - .user.signin form .inline.field > label, - .user.signup form .inline.field > label { - display: block; - } -} - -.user.activate form .header, -.user.forgot.password form .header, -.user.reset.password form .header, -.user.link-account form .header, -.user.signin form .header, -.user.signup form .header { - padding-left: 0 !important; - text-align: center; -} - -.user.activate form .inline.field > label, -.user.forgot.password form .inline.field > label, -.user.reset.password form .inline.field > label, -.user.link-account form .inline.field > label, -.user.signin form .inline.field > label, -.user.signup form .inline.field > label { - width: 200px; -} - -@media (max-width: 767.98px) { - .user.activate form .inline.field > label, - .user.forgot.password form .inline.field > label, - .user.reset.password form .inline.field > label, - .user.link-account form .inline.field > label, - .user.signin form .inline.field > label, - .user.signup form .inline.field > label { - width: 100% !important; - } -} - -.user.activate form input[type="number"], -.user.forgot.password form input[type="number"], -.user.reset.password form input[type="number"], -.user.link-account form input[type="number"], -.user.signin form input[type="number"], -.user.signup form input[type="number"] { - -moz-appearance: textfield; -} - -.user.activate form input::-webkit-outer-spin-button, -.user.forgot.password form input::-webkit-outer-spin-button, -.user.reset.password form input::-webkit-outer-spin-button, -.user.link-account form input::-webkit-outer-spin-button, -.user.signin form input::-webkit-outer-spin-button, -.user.signup form input::-webkit-outer-spin-button, -.user.activate form input::-webkit-inner-spin-button, -.user.forgot.password form input::-webkit-inner-spin-button, -.user.reset.password form input::-webkit-inner-spin-button, -.user.link-account form input::-webkit-inner-spin-button, -.user.signin form input::-webkit-inner-spin-button, -.user.signup form input::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; -} - -.repository.new.repo form, -.repository.new.migrate form, -.repository.new.fork form { - margin: auto; -} - -.repository.new.repo form .ui.message, -.repository.new.migrate form .ui.message, -.repository.new.fork form .ui.message { - text-align: center; -} - -@media (min-width: 768px) { - .repository.new.repo form, - .repository.new.migrate form, - .repository.new.fork form { - width: 800px !important; - } - .repository.new.repo form .header, - .repository.new.migrate form .header, - .repository.new.fork form .header { - padding-left: 280px !important; - } - .repository.new.repo form .inline.field > label, - .repository.new.migrate form .inline.field > label, - .repository.new.fork form .inline.field > label { - text-align: right; - width: 250px !important; - word-wrap: break-word; - } - .repository.new.repo form .help, - .repository.new.migrate form .help, - .repository.new.fork form .help { - margin-left: 265px !important; - } - .repository.new.repo form .optional .title, - .repository.new.migrate form .optional .title, - .repository.new.fork form .optional .title { - margin-left: 250px !important; - } - .repository.new.repo form .inline.field > input, - .repository.new.migrate form .inline.field > input, - .repository.new.fork form .inline.field > input, - .repository.new.repo form .inline.field > textarea, - .repository.new.migrate form .inline.field > textarea, - .repository.new.fork form .inline.field > textarea { - width: 50%; - } -} - -@media (max-width: 767.98px) { - .repository.new.repo form .optional .title, - .repository.new.migrate form .optional .title, - .repository.new.fork form .optional .title { - margin-left: 15px; - } - .repository.new.repo form .inline.field > label, - .repository.new.migrate form .inline.field > label, - .repository.new.fork form .inline.field > label { - display: block; - } -} - -.repository.new.repo form .dropdown .text, -.repository.new.migrate form .dropdown .text, -.repository.new.fork form .dropdown .text { - margin-right: 0 !important; -} - -.repository.new.repo form .header, -.repository.new.migrate form .header, -.repository.new.fork form .header { - padding-left: 0 !important; - text-align: center; -} - -.repository.new.repo form .selection.dropdown, -.repository.new.migrate form .selection.dropdown, -.repository.new.fork form .selection.dropdown, -.repository.new.fork form .field a { - vertical-align: middle; - width: 50% !important; -} - -@media (max-width: 767.98px) { - .repository.new.repo form label, - .repository.new.migrate form label, - .repository.new.fork form label, - .repository.new.repo form .inline.field > input, - .repository.new.migrate form .inline.field > input, - .repository.new.fork form .inline.field > input, - .repository.new.fork form .field a, - .repository.new.repo form .selection.dropdown, - .repository.new.migrate form .selection.dropdown, - .repository.new.fork form .selection.dropdown { - width: 100% !important; - } - .repository.new.repo form .field button, - .repository.new.migrate form .field button, - .repository.new.fork form .field button, - .repository.new.repo form .field a, - .repository.new.migrate form .field a { - margin-bottom: 1em; - width: 100%; - } -} - -@media (min-width: 768px) { - .repository.new.repo .ui.form #auto-init { - margin-left: 265px !important; - } -} - -.repository.new.repo .ui.form .selection.dropdown:not(.owner) { - width: 50% !important; -} - -@media (max-width: 767.98px) { - .repository.new.repo .ui.form .selection.dropdown:not(.owner) { - width: 100% !important; - } -} - -/* form fields with additional content besides their label, used on login form - * use like
*/ -.form-field-content-aside-label { - display: grid; - grid-template-columns: 1fr 1fr; -} -.form-field-content-aside-label > *:nth-child(2) { +.ui.form.left-right-form .inline.field > label { text-align: right; -} -.form-field-content-aside-label input { - grid-column: span 2; + width: 250px; + margin-right: 10px; } -.ui.form .field > .selection.dropdown { - min-width: 14em; /* matches the default min width */ +.ui.form.left-right-form .inline.field > .help { + margin-left: calc(250px + 15px); } -.new.webhook form .help { - margin-left: 25px; +.ui.form.left-right-form .inline.field input:not([type="checkbox"], [type="radio"]), +.ui.form.left-right-form .inline.field .ui.dropdown, +.ui.form.left-right-form .inline.field textarea { + width: 50%; } -.new.webhook .events.fields .column { - padding-left: 40px; -} - -.githook textarea { - font-family: var(--fonts-monospace); +.ui.form.left-right-form .inline.field .inline-right { + display: inline-flex; + flex-direction: column; + gap: 0.5em; } @media (max-width: 767.98px) { - .new.org .ui.form .field button, - .new.org .ui.form .field a { - margin-bottom: 1em; + .ui.form.left-right-form .inline.field > label { + width: 100%; + margin: 0; + text-align: left; + } + .ui.form.left-right-form .inline.field > .help { + margin: 0; + } + .ui.form.left-right-form .inline.field input:not([type="checkbox"], [type="radio"]), + .ui.form.left-right-form .inline.field .ui.dropdown, + .ui.form.left-right-form .inline.field textarea { width: 100%; } - .new.org .ui.form .field input { - width: 100% !important; - } } diff --git a/web_src/css/index.css b/web_src/css/index.css index 02513aebc1..ce1a23b245 100644 --- a/web_src/css/index.css +++ b/web_src/css/index.css @@ -82,5 +82,6 @@ @import "./review.css"; @import "./actions.css"; -@tailwind utilities; @import "./helpers.css"; + +@tailwind utilities; diff --git a/web_src/css/modules/navbar.css b/web_src/css/modules/navbar.css index b5bc95b058..b60d25977d 100644 --- a/web_src/css/modules/navbar.css +++ b/web_src/css/modules/navbar.css @@ -103,11 +103,11 @@ #navbar .ui.dropdown .navbar-profile-admin { display: block; position: absolute; - font-size: 10px; + font-size: 9px; font-weight: var(--font-weight-bold); color: var(--color-nav-bg); background: var(--color-primary); - padding: 2px 4px; + padding: 2px 3px; border-radius: 10px; top: -1px; left: 18px; diff --git a/web_src/css/org.css b/web_src/css/org.css index 1082625041..48b41de297 100644 --- a/web_src/css/org.css +++ b/web_src/css/org.css @@ -1,94 +1,7 @@ -#create-page-form form { - margin: auto; -} - -#create-page-form form .ui.message { - text-align: center; -} - -@media (min-width: 768px) { - #create-page-form form { - width: 800px !important; - } - #create-page-form form .header { - padding-left: 280px !important; - } - #create-page-form form .inline.field > label { - text-align: right; - width: 250px !important; - word-wrap: break-word; - } - #create-page-form form .help { - margin-left: 265px !important; - } - #create-page-form form .optional .title { - margin-left: 250px !important; - } - #create-page-form form .inline.field > input, - #create-page-form form .inline.field > textarea { - width: 50%; - } -} - -@media (max-width: 767.98px) { - #create-page-form form .optional .title { - margin-left: 15px; - } - #create-page-form form .inline.field > label { - display: block; - } -} - .organization .head .ui.header .ui.right { margin-top: 5px; } -.organization.new.org form { - margin: auto; -} - -.organization.new.org form .ui.message { - text-align: center; -} - -@media (min-width: 768px) { - .organization.new.org form { - width: 800px !important; - } - .organization.new.org form .header { - padding-left: 280px !important; - } - .organization.new.org form .inline.field > label { - text-align: right; - width: 250px !important; - word-wrap: break-word; - } - .organization.new.org form .help { - margin-left: 265px !important; - } - .organization.new.org form .optional .title { - margin-left: 250px !important; - } - .organization.new.org form .inline.field > input, - .organization.new.org form .inline.field > textarea { - width: 50%; - } -} - -@media (max-width: 767.98px) { - .organization.new.org form .optional .title { - margin-left: 15px; - } - .organization.new.org form .inline.field > label { - display: block; - } -} - -.organization.new.org form .header { - padding-left: 0 !important; - text-align: center; -} - .page-content.organization .org-avatar { margin-right: 15px; } diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 6fdc9ec2a8..22bbe3cc23 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -120,15 +120,13 @@ td .commit-summary { align-items: center; overflow: hidden; text-overflow: ellipsis; + gap: 0.25em; } @media (max-width: 767.98px) { - .latest-commit .sha { + .latest-commit .commit-id-short { display: none; } - .latest-commit .commit-summary { - margin-left: 8px; - } } .repo-path { @@ -605,15 +603,6 @@ td .commit-summary { margin-right: 0.25em; } -.singular-commit { - display: flex; - align-items: center; -} - -.singular-commit .badge { - height: 30px !important; -} - .repository.view.issue .comment-list .timeline-item.event > .commit-status-link { float: right; margin-right: 8px; @@ -836,10 +825,6 @@ td .commit-summary { height: 10px; } -.repository.compare.pull .show-form-container { - text-align: left; -} - .repository .choose.branch { display: flex; align-items: center; @@ -877,11 +862,6 @@ td .commit-summary { margin-top: -8px; } -.repository.compare.pull .pullrequest-form { - margin-top: 16px; - margin-bottom: 16px; -} - .repository.compare.pull .markup { font-size: 14px; } @@ -936,14 +916,6 @@ td .commit-summary { width: 200px; } -.repository #commits-table thead .shatd { - text-align: center; -} - -.repository #commits-table td.sha .sha.label { - margin: 0; -} - .repository #commits-table.ui.basic.striped.table tbody tr:nth-child(2n) { background-color: var(--color-light) !important; } @@ -1440,12 +1412,6 @@ td .commit-summary { padding-top: 15px; } -.commit-header-row { - min-height: 50px !important; - padding-top: 0 !important; - padding-bottom: 0 !important; -} - .commit-header-buttons { display: flex; gap: 4px; @@ -1622,7 +1588,7 @@ td .commit-summary { align-items: center; } -.labels-list .label { +.labels-list .label, .scope-parent > .label { padding: 0 6px; min-height: 20px; line-height: 1.3; /* there is a `font-size: 1.25em` for inside emoji, so here the line-height needs to be larger slightly */ @@ -1701,6 +1667,10 @@ tbody.commit-list { white-space: nowrap; } +.latest-commit .message-wrapper { + max-width: calc(100% - 2.5rem); +} + /* in the commit list, messages can wrap so we can use inline */ .commit-list .message-wrapper { display: inline; @@ -2128,18 +2098,6 @@ tbody.commit-list { .repository.view.issue .comment-list .timeline .comment-header-right .role-label { display: none; } - .commit-header-row .ui.horizontal.list { - width: 100%; - overflow-x: auto; - margin-top: 2px; - } - .commit-header-row .ui.horizontal.list .item { - align-items: center; - display: flex; - } - .commit-header-row .author { - padding: 3px 0; - } .commit-header h3 { flex-basis: auto !important; margin-bottom: 0.5rem !important; diff --git a/web_src/css/repo/commit-sign.css b/web_src/css/repo/commit-sign.css index e757030419..834fdd95d1 100644 --- a/web_src/css/repo/commit-sign.css +++ b/web_src/css/repo/commit-sign.css @@ -1,272 +1,60 @@ - -.repository .ui.attached.isSigned.isWarning { - border-left: 1px solid var(--color-error-border); - border-right: 1px solid var(--color-error-border); -} - -.repository .ui.attached.isSigned.isWarning.top, -.repository .ui.attached.isSigned.isWarning.message { - border-top: 1px solid var(--color-error-border); -} - -.repository .ui.attached.isSigned.isWarning.message { - box-shadow: none; - background-color: var(--color-error-bg); - color: var(--color-error-text); -} - -.repository .ui.attached.isSigned.isWarning.message .ui.text { - color: var(--color-error-text); -} - -.repository .ui.attached.isSigned.isWarning:last-child, -.repository .ui.attached.isSigned.isWarning.bottom { - border-bottom: 1px solid var(--color-error-border); -} - -.repository .ui.attached.isSigned.isVerified { - border-left: 1px solid var(--color-success-border); - border-right: 1px solid var(--color-success-border); -} - -.repository .ui.attached.isSigned.isVerified.top, -.repository .ui.attached.isSigned.isVerified.message { - border-top: 1px solid var(--color-success-border); -} - -.repository .ui.attached.isSigned.isVerified.message { - box-shadow: none; - background-color: var(--color-success-bg); - color: var(--color-success-text); -} - -.repository .ui.attached.isSigned.isVerified.message .pull-right { - color: var(--color-text); -} - -.repository .ui.attached.isSigned.isVerified.message .ui.text { - color: var(--color-success-text); -} - -.repository .ui.attached.isSigned.isVerified:last-child, -.repository .ui.attached.isSigned.isVerified.bottom { - border-bottom: 1px solid var(--color-success-border); -} - -.repository .ui.attached.isSigned.isVerifiedUntrusted, -.repository .ui.attached.isSigned.isVerifiedUnmatched { - border-left: 1px solid var(--color-warning-border); - border-right: 1px solid var(--color-warning-border); -} - -.repository .ui.attached.isSigned.isVerifiedUntrusted.top, -.repository .ui.attached.isSigned.isVerifiedUnmatched.top, -.repository .ui.attached.isSigned.isVerifiedUntrusted.message, -.repository .ui.attached.isSigned.isVerifiedUnmatched.message { - border-top: 1px solid var(--color-warning-border); -} - -.repository .ui.attached.isSigned.isVerifiedUntrusted.message, -.repository .ui.attached.isSigned.isVerifiedUnmatched.message { - box-shadow: none; - background-color: var(--color-warning-bg); - color: var(--color-warning-text); -} - -.repository .ui.attached.isSigned.isVerifiedUntrusted.message .ui.text, -.repository .ui.attached.isSigned.isVerifiedUnmatched.message .ui.text { - color: var(--color-warning-text); -} - -.repository .ui.attached.isSigned.isVerifiedUntrusted:last-child, -.repository .ui.attached.isSigned.isVerifiedUnmatched:last-child, -.repository .ui.attached.isSigned.isVerifiedUntrusted.bottom, -.repository .ui.attached.isSigned.isVerifiedUnmatched.bottom { - border-bottom: 1px solid var(--color-warning-border); -} - -.repository #commits-table td.sha .sha.label, -.repository #repo-files-table .sha.label, -.repository #repo-file-commit-box .sha.label, -.repository #rev-list .sha.label, -.repository .timeline-item.commits-list .singular-commit .sha.label { +.ui.label.commit-id-short, +.ui.label.commit-sign-badge { border: 1px solid var(--color-light-border); + font-size: 13px; + font-weight: var(--font-weight-normal); + padding: 3px 5px; + flex-shrink: 0; } -.repository #commits-table td.sha .sha.label .detail.icon, -.repository #repo-files-table .sha.label .detail.icon, -.repository #repo-file-commit-box .sha.label .detail.icon, -.repository #rev-list .sha.label .detail.icon, -.repository .timeline-item.commits-list .singular-commit .sha.label .detail.icon { - background: var(--color-light); - margin: -6px -10px -4px 0; - padding: 5px 4px 5px 6px; - border-left: 1px solid var(--color-light-border); - border-top: 0; - border-right: 0; - border-bottom: 0; - border-top-left-radius: 0; - border-bottom-left-radius: 0; +.ui.label.commit-id-short { + font-family: var(--fonts-monospace); } -.repository #commits-table td.sha .sha.label .detail.icon .svg, -.repository #repo-files-table .sha.label .detail.icon .svg, -.repository #repo-file-commit-box .sha.label .detail.icon .svg, -.repository #rev-list .sha.label .detail.icon .svg, -.repository .timeline-item.commits-list .singular-commit .sha.label .detail.icon .svg { - margin: 0 0.25em 0 0; -} - -.repository #commits-table td.sha .sha.label .detail.icon > div, -.repository #repo-files-table .sha.label .detail.icon > div, -.repository #repo-file-commit-box .sha.label .detail.icon > div, -.repository #rev-list .sha.label .detail.icon > div, -.repository .timeline-item.commits-list .singular-commit .sha.label .detail.icon > div { - display: flex; - align-items: center; -} - -.repository #commits-table td.sha .sha.label.isSigned.isWarning, -.repository #repo-files-table .sha.label.isSigned.isWarning, -.repository #repo-file-commit-box .sha.label.isSigned.isWarning, -.repository #rev-list .sha.label.isSigned.isWarning, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isWarning { - border: 1px solid var(--color-red-badge); - background: var(--color-red-badge-bg); -} - -.repository #commits-table td.sha .sha.label.isSigned.isWarning .detail.icon, -.repository #repo-files-table .sha.label.isSigned.isWarning .detail.icon, -.repository #repo-file-commit-box .sha.label.isSigned.isWarning .detail.icon, -.repository #rev-list .sha.label.isSigned.isWarning .detail.icon, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isWarning .detail.icon { - border-left: 1px solid var(--color-red-badge); - color: var(--color-red-badge); -} - -.repository #commits-table td.sha .sha.label.isSigned.isWarning:hover, -.repository #repo-files-table .sha.label.isSigned.isWarning:hover, -.repository #repo-file-commit-box .sha.label.isSigned.isWarning:hover, -.repository #rev-list .sha.label.isSigned.isWarning:hover, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isWarning:hover { - background: var(--color-red-badge-hover-bg) !important; -} - -.repository #commits-table td.sha .sha.label.isSigned.isVerified, -.repository #repo-files-table .sha.label.isSigned.isVerified, -.repository #repo-file-commit-box .sha.label.isSigned.isVerified, -.repository #rev-list .sha.label.isSigned.isVerified, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerified { - border: 1px solid var(--color-green-badge); - background: var(--color-green-badge-bg); -} - -.repository #commits-table td.sha .sha.label.isSigned.isVerified .detail.icon, -.repository #repo-files-table .sha.label.isSigned.isVerified .detail.icon, -.repository #repo-file-commit-box .sha.label.isSigned.isVerified .detail.icon, -.repository #rev-list .sha.label.isSigned.isVerified .detail.icon, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerified .detail.icon { - border-left: 1px solid var(--color-green-badge); - color: var(--color-green-badge); -} - -.repository #commits-table td.sha .sha.label.isSigned.isVerified:hover, -.repository #repo-files-table .sha.label.isSigned.isVerified:hover, -.repository #repo-file-commit-box .sha.label.isSigned.isVerified:hover, -.repository #rev-list .sha.label.isSigned.isVerified:hover, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerified:hover { - background: var(--color-green-badge-hover-bg) !important; -} - -.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUntrusted, -.repository #repo-files-table .sha.label.isSigned.isVerifiedUntrusted, -.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUntrusted, -.repository #rev-list .sha.label.isSigned.isVerifiedUntrusted, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUntrusted { - border: 1px solid var(--color-yellow-badge); - background: var(--color-yellow-badge-bg); -} - -.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUntrusted .detail.icon, -.repository #repo-files-table .sha.label.isSigned.isVerifiedUntrusted .detail.icon, -.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUntrusted .detail.icon, -.repository #rev-list .sha.label.isSigned.isVerifiedUntrusted .detail.icon, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUntrusted .detail.icon { - border-left: 1px solid var(--color-yellow-badge); - color: var(--color-yellow-badge); -} - -.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUntrusted:hover, -.repository #repo-files-table .sha.label.isSigned.isVerifiedUntrusted:hover, -.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUntrusted:hover, -.repository #rev-list .sha.label.isSigned.isVerifiedUntrusted:hover, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUntrusted:hover { - background: var(--color-yellow-badge-hover-bg) !important; -} - -.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUnmatched, -.repository #repo-files-table .sha.label.isSigned.isVerifiedUnmatched, -.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUnmatched, -.repository #rev-list .sha.label.isSigned.isVerifiedUnmatched, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUnmatched { - border: 1px solid var(--color-orange-badge); - background: var(--color-orange-badge-bg); -} - -.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUnmatched .detail.icon, -.repository #repo-files-table .sha.label.isSigned.isVerifiedUnmatched .detail.icon, -.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUnmatched .detail.icon, -.repository #rev-list .sha.label.isSigned.isVerifiedUnmatched .detail.icon, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUnmatched .detail.icon { - border-left: 1px solid var(--color-orange-badge); - color: var(--color-orange-badge); -} - -.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUnmatched:hover, -.repository #repo-files-table .sha.label.isSigned.isVerifiedUnmatched:hover, -.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUnmatched:hover, -.repository #rev-list .sha.label.isSigned.isVerifiedUnmatched:hover, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUnmatched:hover { - background: var(--color-orange-badge-hover-bg) !important; -} - -.singular-commit .shabox .sha.label { +.ui.label.commit-id-short > .commit-sign-badge { margin: 0; - border: 1px solid var(--color-light-border); + padding: 0; + border: 0 !important; + border-radius: 0; + background: transparent; } -.singular-commit .shabox .sha.label.isSigned.isWarning { - border: 1px solid var(--color-red-badge); - background: var(--color-red-badge-bg); +.ui.label.commit-id-short > .commit-sign-badge:hover { + background: transparent !important; } -.singular-commit .shabox .sha.label.isSigned.isWarning:hover { - background: var(--color-red-badge-hover-bg) !important; +.commit-is-signed.sign-trusted { + border: 1px solid var(--color-green-badge) !important; + background: var(--color-green-badge-bg) !important; } -.singular-commit .shabox .sha.label.isSigned.isVerified { - border: 1px solid var(--color-green-badge); - background: var(--color-green-badge-bg); -} - -.singular-commit .shabox .sha.label.isSigned.isVerified:hover { +.commit-is-signed.sign-trusted:hover { background: var(--color-green-badge-hover-bg) !important; } -.singular-commit .shabox .sha.label.isSigned.isVerifiedUntrusted { - border: 1px solid var(--color-yellow-badge); - background: var(--color-yellow-badge-bg); +.commit-is-signed.sign-untrusted { + border: 1px solid var(--color-yellow-badge) !important; + background: var(--color-yellow-badge-bg) !important; } -.singular-commit .shabox .sha.label.isSigned.isVerifiedUntrusted:hover { +.commit-is-signed.sign-untrusted:hover { background: var(--color-yellow-badge-hover-bg) !important; } -.singular-commit .shabox .sha.label.isSigned.isVerifiedUnmatched { - border: 1px solid var(--color-orange-badge); - background: var(--color-orange-badge-bg); +.commit-is-signed.sign-unmatched { + border: 1px solid var(--color-orange-badge) !important; + background: var(--color-orange-badge-bg) !important; } -.singular-commit .shabox .sha.label.isSigned.isVerifiedUnmatched:hover { +.commit-is-signed.sign-unmatched:hover { background: var(--color-orange-badge-hover-bg) !important; } + +.commit-is-signed.sign-warning { + border: 1px solid var(--color-red-badge) !important; + background: var(--color-red-badge-bg) !important; +} + +.commit-is-signed.sign-warning:hover { + background: var(--color-red-badge-hover-bg) !important; +} diff --git a/web_src/js/components/PullRequestMergeForm.vue b/web_src/js/components/PullRequestMergeForm.vue index e8bcee70db..bafeec6c97 100644 --- a/web_src/js/components/PullRequestMergeForm.vue +++ b/web_src/js/components/PullRequestMergeForm.vue @@ -147,7 +147,7 @@ function clearMergeMessage() { -