diff --git a/.eslintrc.cjs b/.eslintrc.cjs index af744db3e16..f52da3fa5da 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 86e6e4fefdc..619cb1cabbe 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 ad02ecc7557..f0caae4d221 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 32ea823e1e7..5524cfb08cd 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 64c3b8b51c7..a20494184bd 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", @@ -909,6 +874,11 @@ "path": "github.com/mitchellh/mapstructure/LICENSE", "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2013 Mitchell Hashimoto\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n" }, + { + "name": "github.com/mmcloughlin/avo", + "path": "github.com/mmcloughlin/avo/LICENSE", + "licenseText": "BSD 3-Clause License\n\nCopyright (c) 2018, Michael McLoughlin\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\n* Neither the name of the copyright holder nor the names of its\n contributors may be used to endorse or promote products derived from\n this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + }, { "name": "github.com/modern-go/concurrent", "path": "github.com/modern-go/concurrent/LICENSE", @@ -964,11 +934,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", @@ -977,7 +942,7 @@ { "name": "github.com/pjbgf/sha1cd", "path": "github.com/pjbgf/sha1cd/LICENSE", - "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" + "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright 2023 pjbgf\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" }, { "name": "github.com/pkg/errors", @@ -1059,21 +1024,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", @@ -1139,11 +1094,6 @@ "path": "github.com/x448/float16/LICENSE", "licenseText": "MIT License\n\nCopyright (c) 2019 Montgomery Edwardsâ´â´â¸ and Faye Amacker\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n" }, - { - "name": "github.com/xanzy/go-gitlab", - "path": "github.com/xanzy/go-gitlab/LICENSE", - "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"{}\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright {yyyy} {name of copyright owner}\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" - }, { "name": "github.com/xanzy/ssh-agent", "path": "github.com/xanzy/ssh-agent/LICENSE", @@ -1179,21 +1129,16 @@ "path": "github.com/zeebo/blake3/LICENSE", "licenseText": "This work is released into the public domain with CC0 1.0.\n\n-------------------------------------------------------------------------------\n\nCreative Commons Legal Code\n\nCC0 1.0 Universal\n\n CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE\n LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN\n ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS\n INFORMATION ON AN \"AS-IS\" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES\n REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS\n PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM\n THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED\n HEREUNDER.\n\nStatement of Purpose\n\nThe laws of most jurisdictions throughout the world automatically confer\nexclusive Copyright and Related Rights (defined below) upon the creator\nand subsequent owner(s) (each and all, an \"owner\") of an original work of\nauthorship and/or a database (each, a \"Work\").\n\nCertain owners wish to permanently relinquish those rights to a Work for\nthe purpose of contributing to a commons of creative, cultural and\nscientific works (\"Commons\") that the public can reliably and without fear\nof later claims of infringement build upon, modify, incorporate in other\nworks, reuse and redistribute as freely as possible in any form whatsoever\nand for any purposes, including without limitation commercial purposes.\nThese owners may contribute to the Commons to promote the ideal of a free\nculture and the further production of creative, cultural and scientific\nworks, or to gain reputation or greater distribution for their Work in\npart through the use and efforts of others.\n\nFor these and/or other purposes and motivations, and without any\nexpectation of additional consideration or compensation, the person\nassociating CC0 with a Work (the \"Affirmer\"), to the extent that he or she\nis an owner of Copyright and Related Rights in the Work, voluntarily\nelects to apply CC0 to the Work and publicly distribute the Work under its\nterms, with knowledge of his or her Copyright and Related Rights in the\nWork and the meaning and intended legal effect of CC0 on those rights.\n\n1. Copyright and Related Rights. A Work made available under CC0 may be\nprotected by copyright and related or neighboring rights (\"Copyright and\nRelated Rights\"). Copyright and Related Rights include, but are not\nlimited to, the following:\n\n i. the right to reproduce, adapt, distribute, perform, display,\n communicate, and translate a Work;\n ii. moral rights retained by the original author(s) and/or performer(s);\niii. publicity and privacy rights pertaining to a person's image or\n likeness depicted in a Work;\n iv. rights protecting against unfair competition in regards to a Work,\n subject to the limitations in paragraph 4(a), below;\n v. rights protecting the extraction, dissemination, use and reuse of data\n in a Work;\n vi. database rights (such as those arising under Directive 96/9/EC of the\n European Parliament and of the Council of 11 March 1996 on the legal\n protection of databases, and under any national implementation\n thereof, including any amended or successor version of such\n directive); and\nvii. other similar, equivalent or corresponding rights throughout the\n world based on applicable law or treaty, and any national\n implementations thereof.\n\n2. Waiver. To the greatest extent permitted by, but not in contravention\nof, applicable law, Affirmer hereby overtly, fully, permanently,\nirrevocably and unconditionally waives, abandons, and surrenders all of\nAffirmer's Copyright and Related Rights and associated claims and causes\nof action, whether now known or unknown (including existing as well as\nfuture claims and causes of action), in the Work (i) in all territories\nworldwide, (ii) for the maximum duration provided by applicable law or\ntreaty (including future time extensions), (iii) in any current or future\nmedium and for any number of copies, and (iv) for any purpose whatsoever,\nincluding without limitation commercial, advertising or promotional\npurposes (the \"Waiver\"). Affirmer makes the Waiver for the benefit of each\nmember of the public at large and to the detriment of Affirmer's heirs and\nsuccessors, fully intending that such Waiver shall not be subject to\nrevocation, rescission, cancellation, termination, or any other legal or\nequitable action to disrupt the quiet enjoyment of the Work by the public\nas contemplated by Affirmer's express Statement of Purpose.\n\n3. Public License Fallback. Should any part of the Waiver for any reason\nbe judged legally invalid or ineffective under applicable law, then the\nWaiver shall be preserved to the maximum extent permitted taking into\naccount Affirmer's express Statement of Purpose. In addition, to the\nextent the Waiver is so judged Affirmer hereby grants to each affected\nperson a royalty-free, non transferable, non sublicensable, non exclusive,\nirrevocable and unconditional license to exercise Affirmer's Copyright and\nRelated Rights in the Work (i) in all territories worldwide, (ii) for the\nmaximum duration provided by applicable law or treaty (including future\ntime extensions), (iii) in any current or future medium and for any number\nof copies, and (iv) for any purpose whatsoever, including without\nlimitation commercial, advertising or promotional purposes (the\n\"License\"). The License shall be deemed effective as of the date CC0 was\napplied by Affirmer to the Work. Should any part of the License for any\nreason be judged legally invalid or ineffective under applicable law, such\npartial invalidity or ineffectiveness shall not invalidate the remainder\nof the License, and in such case Affirmer hereby affirms that he or she\nwill not (i) exercise any of his or her remaining Copyright and Related\nRights in the Work or (ii) assert any associated claims and causes of\naction with respect to the Work, in either case contrary to Affirmer's\nexpress Statement of Purpose.\n\n4. Limitations and Disclaimers.\n\n a. No trademark or patent rights held by Affirmer are waived, abandoned,\n surrendered, licensed or otherwise affected by this document.\n b. Affirmer offers the Work as-is and makes no representations or\n warranties of any kind concerning the Work, express, implied,\n statutory or otherwise, including without limitation warranties of\n title, merchantability, fitness for a particular purpose, non\n infringement, or the absence of latent or other defects, accuracy, or\n the present or absence of errors, whether or not discoverable, all to\n the greatest extent permissible under applicable law.\n c. Affirmer disclaims responsibility for clearing rights of other persons\n that may apply to the Work or any use thereof, including without\n limitation any person's Copyright and Related Rights in the Work.\n Further, Affirmer disclaims responsibility for obtaining any necessary\n consents, permissions or other rights required for any use of the\n Work.\n d. Affirmer understands and acknowledges that Creative Commons is not a\n party to this document and has no duty or obligation with respect to\n this CC0 or use of the Work.\n" }, + { + "name": "gitlab.com/gitlab-org/api/client-go", + "path": "gitlab.com/gitlab-org/api/client-go/LICENSE", + "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"{}\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright {yyyy} {name of copyright owner}\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" + }, { "name": "go.etcd.io/bbolt", "path": "go.etcd.io/bbolt/LICENSE", "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", @@ -1254,6 +1199,11 @@ "path": "golang.org/x/time/rate/LICENSE", "licenseText": "Copyright 2009 The Go Authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google LLC nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" }, + { + "name": "golang.org/x/tools", + "path": "golang.org/x/tools/LICENSE", + "licenseText": "Copyright 2009 The Go Authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google LLC nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + }, { "name": "google.golang.org/genproto/googleapis/rpc/status", "path": "google.golang.org/genproto/googleapis/rpc/status/LICENSE", diff --git a/cmd/admin_user_create.go b/cmd/admin_user_create.go index 106d14b25a7..bf8cbc7c4c0 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 fd648946efa..7251bd09a3f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -165,6 +165,7 @@ func NewMainApp(appVer AppVersion) *cli.App { app.Commands = append(app.Commands, subCmdWithConfig...) app.Commands = append(app.Commands, subCmdStandalone...) + setting.InitGiteaEnvVars() return app } diff --git a/cmd/main_test.go b/cmd/main_test.go index c182b440199..3ec584d3236 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -6,7 +6,6 @@ package cmd import ( "fmt" "io" - "os" "path/filepath" "strings" "testing" @@ -113,37 +112,17 @@ func TestCliCmd(t *testing.T) { _, _ = fmt.Fprint(ctx.App.Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf)) return nil }) - var envBackup []string - for _, s := range os.Environ() { - if strings.HasPrefix(s, "GITEA_") && strings.Contains(s, "=") { - envBackup = append(envBackup, s) - } - } - clearGiteaEnv := func() { - for _, s := range os.Environ() { - if strings.HasPrefix(s, "GITEA_") { - _ = os.Unsetenv(s) - } - } - } - defer func() { - clearGiteaEnv() - for _, s := range envBackup { - k, v, _ := strings.Cut(s, "=") - _ = os.Setenv(k, v) - } - }() - for _, c := range cases { - clearGiteaEnv() - for k, v := range c.env { - _ = os.Setenv(k, v) - } - args := strings.Split(c.cmd, " ") // for test only, "split" is good enough - r, err := runTestApp(app, args...) - assert.NoError(t, err, c.cmd) - assert.NotEmpty(t, c.exp, c.cmd) - assert.Contains(t, r.Stdout, c.exp, c.cmd) + t.Run(c.cmd, func(t *testing.T) { + for k, v := range c.env { + t.Setenv(k, v) + } + args := strings.Split(c.cmd, " ") // for test only, "split" is good enough + r, err := runTestApp(app, args...) + assert.NoError(t, err, c.cmd) + assert.NotEmpty(t, c.exp, c.cmd) + assert.Contains(t, r.Stdout, c.exp, c.cmd) + }) } } diff --git a/cmd/migrate.go b/cmd/migrate.go index 4e4dd45af3a..459805a76d7 100644 --- a/cmd/migrate.go +++ b/cmd/migrate.go @@ -18,7 +18,7 @@ import ( var CmdMigrate = &cli.Command{ Name: "migrate", Usage: "Migrate the database", - Description: "This is a command for migrating the database, so that you can run gitea admin create-user before starting the server.", + Description: `This is a command for migrating the database, so that you can run "gitea admin create user" before starting the server.`, Action: runMigrate, } diff --git a/cmd/web.go b/cmd/web.go index ef8a7426c14..f8217758e55 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 90e4a02764b..2fe14c1f54c 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 6377ebf9d2a..8e64c834d73 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -78,8 +78,9 @@ RUN_USER = ; git ;; Set the domain for the server ;DOMAIN = localhost ;; -;; Overwrite the automatically generated public URL. Necessary for proxies and docker. -;ROOT_URL = %(PROTOCOL)s://%(DOMAIN)s:%(HTTP_PORT)s/ +;; The AppURL used by Gitea to generate absolute links, defaults to "{PROTOCOL}://{DOMAIN}:{HTTP_PORT}/". +;; Most users should set it to the real website URL of their Gitea instance. +;ROOT_URL = ;; ;; For development purpose only. It makes Gitea handle sub-path ("/sub-path/owner/repo/...") directly when debugging without a reverse proxy. ;; DO NOT USE IT IN PRODUCTION!!! @@ -103,8 +104,8 @@ RUN_USER = ; git ;REDIRECT_OTHER_PORT = false ;PORT_TO_REDIRECT = 80 ;; -;; expect PROXY protocol header on connections to https redirector. -;REDIRECTOR_USE_PROXY_PROTOCOL = %(USE_PROXY_PROTOCOL)s +;; expect PROXY protocol header on connections to https redirector, defaults to USE_PROXY_PROTOCOL +;REDIRECTOR_USE_PROXY_PROTOCOL = ;; Minimum and maximum supported TLS versions ;SSL_MIN_VERSION=TLSv1.2 ;SSL_MAX_VERSION= @@ -128,13 +129,14 @@ RUN_USER = ; git ;; most cases you do not need to change the default value. Alter it only if ;; your SSH server node is not the same as HTTP node. For different protocol, the default ;; values are different. If `PROTOCOL` is `http+unix`, the default value is `http://unix/`. -;; If `PROTOCOL` is `fcgi` or `fcgi+unix`, the default value is `%(PROTOCOL)s://%(HTTP_ADDR)s:%(HTTP_PORT)s/`. -;; If listen on `0.0.0.0`, the default value is `%(PROTOCOL)s://localhost:%(HTTP_PORT)s/`, Otherwise the default -;; value is `%(PROTOCOL)s://%(HTTP_ADDR)s:%(HTTP_PORT)s/`. -;LOCAL_ROOT_URL = %(PROTOCOL)s://%(HTTP_ADDR)s:%(HTTP_PORT)s/ +;; If `PROTOCOL` is `fcgi` or `fcgi+unix`, the default value is `{PROTOCOL}://{HTTP_ADDR}:{HTTP_PORT}/`. +;; If listen on `0.0.0.0`, the default value is `{PROTOCOL}://localhost:{HTTP_PORT}/`. +;; Otherwise the default value is `{PROTOCOL}://{HTTP_ADDR}:{HTTP_PORT}/`. +;; Most users don't need (and shouldn't) set this value. +;LOCAL_ROOT_URL = ;; -;; When making local connections pass the PROXY protocol header. -;LOCAL_USE_PROXY_PROTOCOL = %(USE_PROXY_PROTOCOL)s +;; When making local connections pass the PROXY protocol header, defaults to USE_PROXY_PROTOCOL +;LOCAL_USE_PROXY_PROTOCOL = ;; ;; Disable SSH feature when not available ;DISABLE_SSH = false @@ -146,13 +148,17 @@ RUN_USER = ; git ;SSH_SERVER_USE_PROXY_PROTOCOL = false ;; ;; Username to use for the builtin SSH server. If blank, then it is the value of RUN_USER. -;BUILTIN_SSH_SERVER_USER = %(RUN_USER)s +;BUILTIN_SSH_SERVER_USER = ;; -;; Domain name to be exposed in clone URL -;SSH_DOMAIN = %(DOMAIN)s +;; Domain name to be exposed in clone URL, defaults to DOMAIN or the domain part of ROOT_URL +;SSH_DOMAIN = ;; -;; SSH username displayed in clone URLs. -;SSH_USER = %(BUILTIN_SSH_SERVER_USER)s +;; SSH username displayed in clone URLs. It defaults to BUILTIN_SSH_SERVER_USER or RUN_USER. +;; If it is set to "(DOER_USERNAME)", it will use current signed-in user's username. +;; This option is only for some advanced users who have configured their SSH reverse-proxy +;; and need to use different usernames for git SSH clone. +;; Most users should just leave it blank. +;SSH_USER = ;; ;; The network interface the builtin SSH server should listen on ;SSH_LISTEN_HOST = @@ -160,8 +166,8 @@ RUN_USER = ; git ;; Port number to be exposed in clone URL ;SSH_PORT = 22 ;; -;; The port number the builtin SSH server should listen on -;SSH_LISTEN_PORT = %(SSH_PORT)s +;; The port number the builtin SSH server should listen on, defaults to SSH_PORT +;SSH_LISTEN_PORT = ;; ;; Root path of SSH directory, default is '~/.ssh', but you have to use '/home/git/.ssh'. ;SSH_ROOT_PATH = @@ -188,7 +194,7 @@ RUN_USER = ; git ;; ;; For the built-in SSH server, choose the keypair to offer as the host key ;; The private key should be at SSH_SERVER_HOST_KEY and the public SSH_SERVER_HOST_KEY.pub -;; relative paths are made absolute relative to the %(APP_DATA_PATH)s +;; relative paths are made absolute relative to the APP_DATA_PATH ;SSH_SERVER_HOST_KEYS=ssh/gitea.rsa, ssh/gogs.rsa ;; ;; Directory to create temporary files in when testing public keys using ssh-keygen, @@ -582,7 +588,7 @@ ENABLED = true [log] ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Root path for the log files - defaults to %(GITEA_WORK_DIR)/log +;; Root path for the log files - defaults to "{AppWorkPath}/log" ;ROOT_PATH = ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -682,8 +688,8 @@ LEVEL = Info ;; The path of git executable. If empty, Gitea searches through the PATH environment. ;PATH = ;; -;; The HOME directory for Git -;HOME_PATH = %(APP_DATA_PATH)s/home +;; The HOME directory for Git, defaults to "{APP_DATA_PATH}/home" +;HOME_PATH = ;; ;; Disables highlight of added and removed changes ;DISABLE_DIFF_HIGHLIGHT = false @@ -946,8 +952,8 @@ LEVEL = Info ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;[repository] ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Root path for storing all repository data. By default, it is set to %(APP_DATA_PATH)s/gitea-repositories. -;; A relative path is interpreted as _`AppWorkPath`_/%(ROOT)s +;; Root path for storing all repository data. By default, it is set to "{APP_DATA_PATH}/gitea-repositories". +;; A relative path is interpreted as "{AppWorkPath}/{ROOT}" (use AppWorkPath as base path). ;ROOT = ;; ;; The script type this server supports. Usually this is `bash`, but some users report that only `sh` is available. @@ -1339,6 +1345,9 @@ LEVEL = Info ;; Number of repos that are displayed on one page ;REPO_PAGING_NUM = 15 +;; Number of orgs that are displayed on profile page +;ORG_PAGING_NUM = 15 + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;[ui.meta] @@ -1482,6 +1491,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 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -1499,7 +1512,8 @@ LEVEL = Info ;TYPE = persistable-channel ;; ;; data-dir for storing persistable queues and level queues, individual queues will default to `queues/common` meaning the queue is shared. -;DATADIR = queues/ ; Relative paths will be made absolute against `%(APP_DATA_PATH)s`. +;; Relative paths will be made absolute against "APP_DATA_PATH" +;DATADIR = queues/ ;; ;; Default queue length before a channel queue will block ;LENGTH = 100000 diff --git a/go.mod b/go.mod index 9b99face841..0ee4257f130 100644 --- a/go.mod +++ b/go.mod @@ -24,11 +24,10 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.4.1 github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 - github.com/ProtonMail/go-crypto v1.0.0 + github.com/ProtonMail/go-crypto v1.1.4 github.com/PuerkitoBio/goquery v1.10.0 github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.7.3 - github.com/alecthomas/chroma/v2 v2.14.0 - github.com/aws/aws-sdk-go v1.55.5 + github.com/alecthomas/chroma/v2 v2.15.0 github.com/aws/aws-sdk-go-v2/credentials v1.17.42 github.com/aws/aws-sdk-go-v2/service/codecommit v1.27.3 github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb @@ -55,13 +54,12 @@ require ( github.com/go-chi/cors v1.2.1 github.com/go-co-op/gocron v1.37.0 github.com/go-enry/go-enry/v2 v2.9.1 - github.com/go-git/go-billy/v5 v5.6.0 - github.com/go-git/go-git/v5 v5.12.0 + github.com/go-git/go-billy/v5 v5.6.1 + github.com/go-git/go-git/v5 v5.13.1 github.com/go-ldap/ldap/v3 v3.4.8 github.com/go-redsync/redsync/v4 v4.13.0 github.com/go-sql-driver/mysql v1.8.1 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 @@ -73,7 +71,6 @@ require ( github.com/google/uuid v1.6.0 github.com/gorilla/feeds v1.2.0 github.com/gorilla/sessions v1.4.0 - github.com/h2non/gock v1.2.0 github.com/hashicorp/go-version v1.7.0 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/huandu/xstrings v1.5.0 @@ -109,28 +106,28 @@ require ( github.com/sassoftware/go-rpmutils v0.4.0 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.10.0 github.com/syndtr/goleveldb v1.0.0 github.com/tstranex/u2f v1.0.0 github.com/ulikunitz/xz v0.5.12 github.com/urfave/cli/v2 v2.27.5 github.com/wneessen/go-mail v0.5.2 - github.com/xanzy/go-gitlab v0.112.0 github.com/xeipuuv/gojsonschema v1.2.0 github.com/yohcop/openid-go v1.0.1 github.com/yuin/goldmark v1.7.8 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc github.com/yuin/goldmark-meta v1.1.0 - golang.org/x/crypto v0.31.0 + gitlab.com/gitlab-org/api/client-go v0.119.0 + golang.org/x/crypto v0.32.0 golang.org/x/image v0.21.0 - golang.org/x/net v0.33.0 - golang.org/x/oauth2 v0.23.0 + golang.org/x/net v0.34.0 + golang.org/x/oauth2 v0.24.0 golang.org/x/sync v0.10.0 - golang.org/x/sys v0.28.0 + golang.org/x/sys v0.29.0 golang.org/x/text v0.21.0 - golang.org/x/tools v0.26.0 + golang.org/x/tools v0.29.0 google.golang.org/grpc v1.67.1 - google.golang.org/protobuf v1.35.1 + google.golang.org/protobuf v1.36.0 gopkg.in/ini.v1 v1.67.0 gopkg.in/yaml.v3 v3.0.1 mvdan.cc/xurls/v2 v2.5.0 @@ -145,8 +142,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 @@ -191,7 +186,7 @@ require ( github.com/couchbase/gomemcached v0.3.2 // indirect github.com/couchbase/goutils v0.1.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect - github.com/cyphar/filepath-securejoin v0.3.4 // indirect + github.com/cyphar/filepath-securejoin v0.3.6 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davidmz/go-pageant v1.0.2 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect @@ -204,8 +199,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 @@ -226,7 +219,7 @@ require ( github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/btree v1.1.3 // indirect @@ -236,7 +229,6 @@ require ( github.com/gorilla/handlers v1.5.2 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/securecookie v1.1.2 // indirect - github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -261,6 +253,7 @@ require ( github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/mmcloughlin/avo v0.6.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 // indirect @@ -270,10 +263,9 @@ 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 + github.com/pjbgf/sha1cd v0.3.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.60.1 // indirect @@ -285,7 +277,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,14 +301,12 @@ 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 golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect - golang.org/x/mod v0.21.0 // indirect - golang.org/x/time v0.7.0 // indirect + golang.org/x/mod v0.22.0 // indirect + golang.org/x/time v0.8.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 93b2f9fa999..c145fa6beb9 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= @@ -75,8 +71,8 @@ github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSC github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= -github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/ProtonMail/go-crypto v1.1.4 h1:G5U5asvD5N/6/36oIw3k2bOfBn5XVcZrb7PBjzzKKoE= +github.com/ProtonMail/go-crypto v1.1.4/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4= github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4= github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo= @@ -85,11 +81,11 @@ github.com/RoaringBitmap/roaring v1.9.4 h1:yhEIoH4YezLYT04s1nHehNO64EKFTop/wBhxv github.com/RoaringBitmap/roaring v1.9.4/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90= github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.7.3 h1:BP0HiyNT3AQEYi+if3wkRcIdQFHtsw6xX3Kx0glckgA= github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.7.3/go.mod h1:hMNtySovKkn2gdDuLqnqveP+mfhUSaBdoBcr2I7Zt0E= -github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= -github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= -github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= -github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= +github.com/alecthomas/chroma/v2 v2.15.0 h1:LxXTQHFoYrstG2nnV9y2X5O94sOBzf0CIUpSTbpxvMc= +github.com/alecthomas/chroma/v2 v2.15.0/go.mod h1:gUhVLrPDXPtp/f+L1jo9xepo9gL4eLwRuGAunSZMkio= github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= @@ -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= @@ -194,7 +188,6 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/buildkite/terminal-to-html/v3 v3.16.3 h1:IGuJjboHjuMLWOGsKZKNxbbn41emOLiHzXPmQZk31fk= github.com/buildkite/terminal-to-html/v3 v3.16.3/go.mod h1:r/J7cC9c3EzBzP3/wDz0RJLPwv5PUAMp+KF2w+ntMc0= -github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/caddyserver/certmagic v0.21.4 h1:e7VobB8rffHv8ZZpSiZtEwnLDHUwLVYLWzWSa1FfKI0= github.com/caddyserver/certmagic v0.21.4/go.mod h1:swUXjQ1T9ZtMv95qj7/InJvWLXURU85r+CfG0T+ZbDE= github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA= @@ -211,7 +204,6 @@ github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moA github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= -github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys= github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= @@ -229,16 +221,14 @@ github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwc github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/cyphar/filepath-securejoin v0.3.4 h1:VBWugsJh2ZxJmLFSM06/0qzQyiQX2Qs0ViKrUAcqdZ8= -github.com/cyphar/filepath-securejoin v0.3.4/go.mod h1:8s/MCNJREmFK0H02MF6Ihv1nakJe4L/w3WZLHNkvlYM= +github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM= +github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 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= @@ -262,8 +252,8 @@ github.com/dvyukov/go-fuzz v0.0.0-20210429054444-fca39067bc72/go.mod h1:11Gm+ccJ github.com/editorconfig/editorconfig-core-go/v2 v2.6.2 h1:dKG8sc7n321deIVRcQtwlMNoBEra7j0qQ8RwxO8RN0w= github.com/editorconfig/editorconfig-core-go/v2 v2.6.2/go.mod h1:7dvD3GCm7eBw53xZ/lsiq72LqobdMg3ITbMBxnmJmqY= github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= -github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= -github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/elazarl/goproxy v1.2.3 h1:xwIyKHbaP5yfT6O9KIeYJR5549MXRQkoQMRXGztz8YQ= +github.com/elazarl/goproxy v1.2.3/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64= github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA= github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY= github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4= @@ -317,20 +307,16 @@ 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= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8= -github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM= +github.com/go-git/go-billy/v5 v5.6.1 h1:u+dcrgaguSSkbjzHwelEjc0Yj300NUevrrPphk/SoRA= +github.com/go-git/go-billy/v5 v5.6.1/go.mod h1:0AsLr1z2+Uksi4NlElmMblP5rPcDZNRCD8ujZCRR2BE= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= -github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= +github.com/go-git/go-git/v5 v5.13.1 h1:DAQ9APonnlvSWpvolXWIuV6Q6zXy2wHbN4cVlNR5Q+M= +github.com/go-git/go-git/v5 v5.13.1/go.mod h1:qryJB4cSBoq3FRoBRf5A77joojuBcmPJ0qu3XXXVixc= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-ldap/ldap/v3 v3.4.8 h1:loKJyspcRezt2Q3ZRMq2p/0v8iOurlmeXDPw6fikSvQ= @@ -372,8 +358,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 +369,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= @@ -400,8 +383,8 @@ github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I= github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= @@ -410,7 +393,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= @@ -468,10 +450,6 @@ github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/z github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= -github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE= -github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk= -github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= -github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -497,22 +475,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 +495,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 +508,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= @@ -624,12 +579,13 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mmcloughlin/avo v0.6.0 h1:QH6FU8SKoTLaVs80GA8TJuLNkUYl4VokHKlPhVDg4YY= +github.com/mmcloughlin/avo v0.6.0/go.mod h1:8CoAGaCSYXtCPR+8y18Y9aB/kxb8JSS6FRI7mSkvD+8= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 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= @@ -639,8 +595,6 @@ github.com/msteinert/pam v1.2.0 h1:mYfjlvN2KYs2Pb9G6nb/1f/nPfAttT/Jee5Sq9r3bGE= github.com/msteinert/pam v1.2.0/go.mod h1:d2n0DCUK8rGecChV3JzvmsDjOY4R7AYbsNxAT+ftQl0= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= -github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/niklasfasching/go-org v1.7.0 h1:vyMdcMWWTe/XmANk19F4k8XGBYg0GQ/gJGMimOjGMek= github.com/niklasfasching/go-org v1.7.0/go.mod h1:WuVm4d45oePiE0eX25GqTDQIt/qPW1T9DGkRscqLW5o= github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= @@ -670,9 +624,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= @@ -680,8 +631,8 @@ github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= -github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pjbgf/sha1cd v0.3.1 h1:Dh2GYdpJnO84lIw0LJwTFXjcNbasP/bklicSznyAaPI= +github.com/pjbgf/sha1cd v0.3.1/go.mod h1:Y8t7jSB/dEI/lQE04A1HVKteqjj9bX5O4+Cex0TCu8s= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -735,8 +686,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,21 +732,19 @@ 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= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203 h1:QVqDTf3h2WHt08YuiTGPZLls0Wq99X9bWd0Q5ZSBesM= github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKNihBbwlX8dLpwxCl3+HnXKV/R0e+sRLd9C8= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 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= @@ -819,13 +766,8 @@ github.com/wneessen/go-mail v0.5.2 h1:MZKwgHJoRboLJ+EHMLuHpZc95wo+u1xViL/4XSswDT github.com/wneessen/go-mail v0.5.2/go.mod h1:kRroJvEq2hOSEPFRiKjN7Csrz0G1w+RpiGR3b6yo+Ck= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/xanzy/go-gitlab v0.112.0 h1:6Z0cqEooCvBMfBIHw+CgO4AKGRV8na/9781xOb0+DKw= -github.com/xanzy/go-gitlab v0.112.0/go.mod h1:wKNKh3GkYDMOsGmnfuX+ITCmDuSDWFO0G+C4AygL9RY= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= -github.com/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 +784,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= @@ -860,16 +800,13 @@ github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= +gitlab.com/gitlab-org/api/client-go v0.119.0 h1:YBZyx9XUTtEDBBYtY36cZWz6JmT7om/8HPSk37IS95g= +gitlab.com/gitlab-org/api/client-go v0.119.0/go.mod h1:ygHmS3AU3TpvK+AC6DYO1QuAxLlv6yxYK+/Votr/WFQ= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= -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= @@ -886,16 +823,14 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s= @@ -909,8 +844,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= -golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -922,26 +857,23 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= -golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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= @@ -974,8 +906,6 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -985,14 +915,12 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= @@ -1000,15 +928,13 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= -golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= @@ -1016,22 +942,20 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= -golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= +golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 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= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= -golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= +golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1046,10 +970,8 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.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= +google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ= +google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/models/actions/run.go b/models/actions/run.go index f40bc1eb3db..147a8d8b3e2 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -154,7 +154,7 @@ func (run *ActionRun) GetPushEventPayload() (*api.PushPayload, error) { } func (run *ActionRun) GetPullRequestEventPayload() (*api.PullRequestPayload, error) { - if run.Event == webhook_module.HookEventPullRequest || run.Event == webhook_module.HookEventPullRequestSync { + if run.Event.IsPullRequest() { var payload api.PullRequestPayload if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil { return nil, err @@ -275,7 +275,7 @@ func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWork return err } run.Index = index - run.Title, _ = util.SplitStringAtByteN(run.Title, 255) + run.Title = util.EllipsisDisplayString(run.Title, 255) if err := db.Insert(ctx, run); err != nil { return err @@ -308,7 +308,7 @@ func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWork } else { hasWaiting = true } - job.Name, _ = util.SplitStringAtByteN(job.Name, 255) + job.Name = util.EllipsisDisplayString(job.Name, 255) runJobs = append(runJobs, &ActionRunJob{ RunID: run.ID, RepoID: run.RepoID, @@ -402,7 +402,7 @@ func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error { if len(cols) > 0 { sess.Cols(cols...) } - run.Title, _ = util.SplitStringAtByteN(run.Title, 255) + run.Title = util.EllipsisDisplayString(run.Title, 255) affected, err := sess.Update(run) if err != nil { return err diff --git a/models/actions/runner.go b/models/actions/runner.go index b35a76680c2..0d5464a5bef 100644 --- a/models/actions/runner.go +++ b/models/actions/runner.go @@ -252,7 +252,7 @@ func GetRunnerByID(ctx context.Context, id int64) (*ActionRunner, error) { // UpdateRunner updates runner's information. func UpdateRunner(ctx context.Context, r *ActionRunner, cols ...string) error { e := db.GetEngine(ctx) - r.Name, _ = util.SplitStringAtByteN(r.Name, 255) + r.Name = util.EllipsisDisplayString(r.Name, 255) var err error if len(cols) == 0 { _, err = e.ID(r.ID).AllCols().Update(r) @@ -279,7 +279,7 @@ func CreateRunner(ctx context.Context, t *ActionRunner) error { // Remove OwnerID to avoid confusion; it's not worth returning an error here. t.OwnerID = 0 } - t.Name, _ = util.SplitStringAtByteN(t.Name, 255) + t.Name = util.EllipsisDisplayString(t.Name, 255) return db.Insert(ctx, t) } diff --git a/models/actions/runner_token.go b/models/actions/runner_token.go index 1eab5efcce7..bbd2af73b65 100644 --- a/models/actions/runner_token.go +++ b/models/actions/runner_token.go @@ -10,7 +10,6 @@ import ( "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" ) @@ -52,7 +51,7 @@ func GetRunnerToken(ctx context.Context, token string) (*ActionRunnerToken, erro if err != nil { return nil, err } else if !has { - return nil, fmt.Errorf(`runner token "%s...": %w`, base.TruncateString(token, 3), util.ErrNotExist) + return nil, fmt.Errorf(`runner token "%s...": %w`, util.TruncateRunes(token, 3), util.ErrNotExist) } return &runnerToken, nil } diff --git a/models/actions/schedule.go b/models/actions/schedule.go index 961ffd0851c..e2cc32eedca 100644 --- a/models/actions/schedule.go +++ b/models/actions/schedule.go @@ -68,7 +68,7 @@ func CreateScheduleTask(ctx context.Context, rows []*ActionSchedule) error { // Loop through each schedule row for _, row := range rows { - row.Title, _ = util.SplitStringAtByteN(row.Title, 255) + row.Title = util.EllipsisDisplayString(row.Title, 255) // Create new schedule row if err = db.Insert(ctx, row); err != nil { return err diff --git a/models/actions/task.go b/models/actions/task.go index af74faf937e..9f13ff94c9e 100644 --- a/models/actions/task.go +++ b/models/actions/task.go @@ -298,7 +298,7 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask if len(workflowJob.Steps) > 0 { steps := make([]*ActionTaskStep, len(workflowJob.Steps)) for i, v := range workflowJob.Steps { - name, _ := util.SplitStringAtByteN(v.String(), 255) + name := util.EllipsisDisplayString(v.String(), 255) steps[i] = &ActionTaskStep{ Name: name, TaskID: task.ID, diff --git a/models/activities/action.go b/models/activities/action.go index ff7fdb2f106..8304210188b 100644 --- a/models/activities/action.go +++ b/models/activities/action.go @@ -20,12 +20,12 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" "xorm.io/builder" "xorm.io/xorm/schemas" @@ -226,7 +226,7 @@ func (a *Action) GetActUserName(ctx context.Context) string { // ShortActUserName gets the action's user name trimmed to max 20 // chars. func (a *Action) ShortActUserName(ctx context.Context) string { - return base.EllipsisString(a.GetActUserName(ctx), 20) + return util.EllipsisDisplayString(a.GetActUserName(ctx), 20) } // GetActDisplayName gets the action's display name based on DEFAULT_SHOW_FULL_NAME, or falls back to the username if it is blank. @@ -260,7 +260,7 @@ func (a *Action) GetRepoUserName(ctx context.Context) string { // ShortRepoUserName returns the name of the action repository owner // trimmed to max 20 chars. func (a *Action) ShortRepoUserName(ctx context.Context) string { - return base.EllipsisString(a.GetRepoUserName(ctx), 20) + return util.EllipsisDisplayString(a.GetRepoUserName(ctx), 20) } // GetRepoName returns the name of the action repository. @@ -275,7 +275,7 @@ func (a *Action) GetRepoName(ctx context.Context) string { // ShortRepoName returns the name of the action repository // trimmed to max 33 chars. func (a *Action) ShortRepoName(ctx context.Context) string { - return base.EllipsisString(a.GetRepoName(ctx), 33) + return util.EllipsisDisplayString(a.GetRepoName(ctx), 33) } // GetRepoPath returns the virtual path to the action repository. diff --git a/models/activities/repo_activity.go b/models/activities/repo_activity.go index 3ffad035b7a..3ccdbd47d3d 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 a039fd36132..380045d3c5d 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 f1cf398adf5..654427e9743 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 55c9dffb6ab..5f98edbb28d 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 9b6f5b9a887..8837e6ec2d8 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/label.yml b/models/fixtures/label.yml index 2242b90dcdc..acfac749686 100644 --- a/models/fixtures/label.yml +++ b/models/fixtures/label.yml @@ -96,3 +96,14 @@ num_issues: 0 num_closed_issues: 0 archived_unix: 0 + +- + id: 10 + repo_id: 3 + org_id: 0 + name: repo3label1 + color: '#112233' + exclusive: false + num_issues: 0 + num_closed_issues: 0 + archived_unix: 0 diff --git a/models/fixtures/protected_tag.yml b/models/fixtures/protected_tag.yml index dbec52c0c23..1944e7bd84c 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 e4537aa872e..a7ec8f57fc4 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 - } - defer committer.Close() - - attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, uuids) - if err != nil { - return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", uuids, 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 } - 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 d81f33f953d..ae0bc3ce17d 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/dependency_test.go b/models/issues/dependency_test.go index 6eed483cc9b..67418039ded 100644 --- a/models/issues/dependency_test.go +++ b/models/issues/dependency_test.go @@ -49,9 +49,13 @@ func TestCreateIssueDependency(t *testing.T) { assert.False(t, left) // Close #2 and check again - _, err = issues_model.ChangeIssueStatus(db.DefaultContext, issue2, user1, true) + _, err = issues_model.CloseIssue(db.DefaultContext, issue2, user1) assert.NoError(t, err) + issue2Closed, err := issues_model.GetIssueByID(db.DefaultContext, 2) + assert.NoError(t, err) + assert.True(t, issue2Closed.IsClosed) + left, err = issues_model.IssueNoDependenciesLeft(db.DefaultContext, issue1) assert.NoError(t, err) assert.True(t, left) @@ -59,4 +63,11 @@ func TestCreateIssueDependency(t *testing.T) { // Test removing the dependency err = issues_model.RemoveIssueDependency(db.DefaultContext, user1, issue1, issue2, issues_model.DependencyTypeBlockedBy) assert.NoError(t, err) + + _, err = issues_model.ReopenIssue(db.DefaultContext, issue2, user1) + assert.NoError(t, err) + + issue2Reopened, err := issues_model.GetIssueByID(db.DefaultContext, 2) + assert.NoError(t, err) + assert.False(t, issue2Reopened.IsClosed) } diff --git a/models/issues/issue.go b/models/issues/issue.go index fe347c27156..1777fbb6a68 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -46,23 +46,6 @@ func (err ErrIssueNotExist) Unwrap() error { return util.ErrNotExist } -// ErrIssueIsClosed represents a "IssueIsClosed" kind of error. -type ErrIssueIsClosed struct { - ID int64 - RepoID int64 - Index int64 -} - -// IsErrIssueIsClosed checks if an error is a ErrIssueNotExist. -func IsErrIssueIsClosed(err error) bool { - _, ok := err.(ErrIssueIsClosed) - return ok -} - -func (err ErrIssueIsClosed) Error() string { - return fmt.Sprintf("issue is closed [id: %d, repo_id: %d, index: %d]", err.ID, err.RepoID, err.Index) -} - // ErrNewIssueInsert is used when the INSERT statement in newIssue fails type ErrNewIssueInsert struct { OriginalError error @@ -78,22 +61,6 @@ func (err ErrNewIssueInsert) Error() string { return err.OriginalError.Error() } -// ErrIssueWasClosed is used when close a closed issue -type ErrIssueWasClosed struct { - ID int64 - Index int64 -} - -// IsErrIssueWasClosed checks if an error is a ErrIssueWasClosed. -func IsErrIssueWasClosed(err error) bool { - _, ok := err.(ErrIssueWasClosed) - return ok -} - -func (err ErrIssueWasClosed) Error() string { - return fmt.Sprintf("Issue [%d] %d was already closed", err.ID, err.Index) -} - var ErrIssueAlreadyChanged = util.NewInvalidArgumentErrorf("the issue is already changed") // Issue represents an issue or pull request of repository. diff --git a/models/issues/issue_update.go b/models/issues/issue_update.go index 5b929c9115b..7b3fe04aa5b 100644 --- a/models/issues/issue_update.go +++ b/models/issues/issue_update.go @@ -28,38 +28,40 @@ import ( // UpdateIssueCols updates cols of issue func UpdateIssueCols(ctx context.Context, issue *Issue, cols ...string) error { - if _, err := db.GetEngine(ctx).ID(issue.ID).Cols(cols...).Update(issue); err != nil { - return err - } - return nil + _, err := db.GetEngine(ctx).ID(issue.ID).Cols(cols...).Update(issue) + return err } -func changeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isClosed, isMergePull bool) (*Comment, error) { - // Reload the issue - currentIssue, err := GetIssueByID(ctx, issue.ID) - if err != nil { - return nil, err - } +// ErrIssueIsClosed is used when close a closed issue +type ErrIssueIsClosed struct { + ID int64 + RepoID int64 + Index int64 + IsPull bool +} - // Nothing should be performed if current status is same as target status - if currentIssue.IsClosed == isClosed { - if !issue.IsPull { - return nil, ErrIssueWasClosed{ - ID: issue.ID, - } - } - return nil, ErrPullWasClosed{ - ID: issue.ID, - } - } +// IsErrIssueIsClosed checks if an error is a ErrIssueIsClosed. +func IsErrIssueIsClosed(err error) bool { + _, ok := err.(ErrIssueIsClosed) + return ok +} - issue.IsClosed = isClosed - return doChangeIssueStatus(ctx, issue, doer, isMergePull) +func (err ErrIssueIsClosed) Error() string { + return fmt.Sprintf("%s [id: %d, repo_id: %d, index: %d] is already closed", util.Iif(err.IsPull, "Pull Request", "Issue"), err.ID, err.RepoID, err.Index) } -func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isMergePull bool) (*Comment, error) { +func SetIssueAsClosed(ctx context.Context, issue *Issue, doer *user_model.User, isMergePull bool) (*Comment, error) { + if issue.IsClosed { + return nil, ErrIssueIsClosed{ + ID: issue.ID, + RepoID: issue.RepoID, + Index: issue.Index, + IsPull: issue.IsPull, + } + } + // Check for open dependencies - if issue.IsClosed && issue.Repo.IsDependenciesEnabled(ctx) { + if issue.Repo.IsDependenciesEnabled(ctx) { // only check if dependencies are enabled and we're about to close an issue, otherwise reopening an issue would fail when there are unsatisfied dependencies noDeps, err := IssueNoDependenciesLeft(ctx, issue) if err != nil { @@ -71,16 +73,63 @@ func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.Use } } - if issue.IsClosed { - issue.ClosedUnix = timeutil.TimeStampNow() - } else { - issue.ClosedUnix = 0 + issue.IsClosed = true + issue.ClosedUnix = timeutil.TimeStampNow() + + if cnt, err := db.GetEngine(ctx).ID(issue.ID).Cols("is_closed", "closed_unix"). + Where("is_closed = ?", false). + Update(issue); err != nil { + return nil, err + } else if cnt != 1 { + return nil, ErrIssueAlreadyChanged } - if err := UpdateIssueCols(ctx, issue, "is_closed", "closed_unix"); err != nil { + return updateIssueNumbers(ctx, issue, doer, util.Iif(isMergePull, CommentTypeMergePull, CommentTypeClose)) +} + +// ErrIssueIsOpen is used when reopen an opened issue +type ErrIssueIsOpen struct { + ID int64 + RepoID int64 + IsPull bool + Index int64 +} + +// IsErrIssueIsOpen checks if an error is a ErrIssueIsOpen. +func IsErrIssueIsOpen(err error) bool { + _, ok := err.(ErrIssueIsOpen) + return ok +} + +func (err ErrIssueIsOpen) Error() string { + return fmt.Sprintf("%s [id: %d, repo_id: %d, index: %d] is already open", util.Iif(err.IsPull, "Pull Request", "Issue"), err.ID, err.RepoID, err.Index) +} + +func setIssueAsReopen(ctx context.Context, issue *Issue, doer *user_model.User) (*Comment, error) { + if !issue.IsClosed { + return nil, ErrIssueIsOpen{ + ID: issue.ID, + RepoID: issue.RepoID, + Index: issue.Index, + IsPull: issue.IsPull, + } + } + + issue.IsClosed = false + issue.ClosedUnix = 0 + + if cnt, err := db.GetEngine(ctx).ID(issue.ID).Cols("is_closed", "closed_unix"). + Where("is_closed = ?", true). + Update(issue); err != nil { return nil, err + } else if cnt != 1 { + return nil, ErrIssueAlreadyChanged } + return updateIssueNumbers(ctx, issue, doer, CommentTypeReopen) +} + +func updateIssueNumbers(ctx context.Context, issue *Issue, doer *user_model.User, cmtType CommentType) (*Comment, error) { // Update issue count of labels if err := issue.LoadLabels(ctx); err != nil { return nil, err @@ -103,14 +152,6 @@ func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.Use return nil, err } - // New action comment - cmtType := CommentTypeClose - if !issue.IsClosed { - cmtType = CommentTypeReopen - } else if isMergePull { - cmtType = CommentTypeMergePull - } - return CreateComment(ctx, &CreateCommentOptions{ Type: cmtType, Doer: doer, @@ -119,8 +160,8 @@ func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.Use }) } -// ChangeIssueStatus changes issue status to open or closed. -func ChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isClosed bool) (*Comment, error) { +// CloseIssue changes issue status to closed. +func CloseIssue(ctx context.Context, issue *Issue, doer *user_model.User) (*Comment, error) { if err := issue.LoadRepo(ctx); err != nil { return nil, err } @@ -128,7 +169,45 @@ func ChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, return nil, err } - return changeIssueStatus(ctx, issue, doer, isClosed, false) + ctx, committer, err := db.TxContext(ctx) + if err != nil { + return nil, err + } + defer committer.Close() + + comment, err := SetIssueAsClosed(ctx, issue, doer, false) + if err != nil { + return nil, err + } + if err := committer.Commit(); err != nil { + return nil, err + } + return comment, nil +} + +// ReopenIssue changes issue status to open. +func ReopenIssue(ctx context.Context, issue *Issue, doer *user_model.User) (*Comment, error) { + if err := issue.LoadRepo(ctx); err != nil { + return nil, err + } + if err := issue.LoadPoster(ctx); err != nil { + return nil, err + } + + ctx, committer, err := db.TxContext(ctx) + if err != nil { + return nil, err + } + defer committer.Close() + + comment, err := setIssueAsReopen(ctx, issue, doer) + if err != nil { + return nil, err + } + if err := committer.Commit(); err != nil { + return nil, err + } + return comment, nil } // ChangeIssueTitle changes the title of this issue, as the given user. @@ -139,7 +218,7 @@ func ChangeIssueTitle(ctx context.Context, issue *Issue, doer *user_model.User, } defer committer.Close() - issue.Title, _ = util.SplitStringAtByteN(issue.Title, 255) + issue.Title = util.EllipsisDisplayString(issue.Title, 255) if err = UpdateIssueCols(ctx, issue, "name"); err != nil { return fmt.Errorf("updateIssueCols: %w", err) } @@ -367,19 +446,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 } @@ -402,7 +472,7 @@ func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *Issue, la } issue.Index = idx - issue.Title, _ = util.SplitStringAtByteN(issue.Title, 255) + issue.Title = util.EllipsisDisplayString(issue.Title, 255) if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{ Repo: repo, diff --git a/models/issues/issue_user_test.go b/models/issues/issue_user_test.go index ce47adb53a0..7c21aa15eef 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/issue_xref_test.go b/models/issues/issue_xref_test.go index f1b1bb2a6b1..7f257330b76 100644 --- a/models/issues/issue_xref_test.go +++ b/models/issues/issue_xref_test.go @@ -98,7 +98,7 @@ func TestXRef_ResolveCrossReferences(t *testing.T) { i1 := testCreateIssue(t, 1, 2, "title1", "content1", false) i2 := testCreateIssue(t, 1, 2, "title2", "content2", false) i3 := testCreateIssue(t, 1, 2, "title3", "content3", false) - _, err := issues_model.ChangeIssueStatus(db.DefaultContext, i3, d, true) + _, err := issues_model.CloseIssue(db.DefaultContext, i3, d) assert.NoError(t, err) pr := testCreatePR(t, 1, 2, "titlepr", fmt.Sprintf("closes #%d", i1.Index)) diff --git a/models/issues/label.go b/models/issues/label.go index d80578193e5..b9d24bbe991 100644 --- a/models/issues/label.go +++ b/models/issues/label.go @@ -349,6 +349,17 @@ func GetLabelIDsInRepoByNames(ctx context.Context, repoID int64, labelNames []st Find(&labelIDs) } +// GetLabelIDsInOrgByNames returns a list of labelIDs by names in a given org. +func GetLabelIDsInOrgByNames(ctx context.Context, orgID int64, labelNames []string) ([]int64, error) { + labelIDs := make([]int64, 0, len(labelNames)) + return labelIDs, db.GetEngine(ctx).Table("label"). + Where("org_id = ?", orgID). + In("name", labelNames). + Asc("name"). + Cols("id"). + Find(&labelIDs) +} + // BuildLabelNamesIssueIDsCondition returns a builder where get issue ids match label names func BuildLabelNamesIssueIDsCondition(labelNames []string) *builder.Builder { return builder.Select("issue_label.issue_id"). diff --git a/models/issues/label_test.go b/models/issues/label_test.go index 1d4b6f4684c..185fa11bbcf 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 853e2a69e62..e3af00224de 100644 --- a/models/issues/pull.go +++ b/models/issues/pull.go @@ -80,22 +80,6 @@ func (err ErrPullRequestAlreadyExists) Unwrap() error { return util.ErrAlreadyExist } -// ErrPullWasClosed is used close a closed pull request -type ErrPullWasClosed struct { - ID int64 - Index int64 -} - -// IsErrPullWasClosed checks if an error is a ErrErrPullWasClosed. -func IsErrPullWasClosed(err error) bool { - _, ok := err.(ErrPullWasClosed) - return ok -} - -func (err ErrPullWasClosed) Error() string { - return fmt.Sprintf("Pull request [%d] %d was already closed", err.ID, err.Index) -} - // PullRequestType defines pull request type type PullRequestType int @@ -301,7 +285,7 @@ func (pr *PullRequest) LoadRequestedReviewers(ctx context.Context) error { return nil } - reviews, err := GetReviewsByIssueID(ctx, pr.Issue.ID) + reviews, _, err := GetReviewsByIssueID(ctx, pr.Issue.ID) if err != nil { return err } @@ -320,7 +304,7 @@ func (pr *PullRequest) LoadRequestedReviewers(ctx context.Context) error { // LoadRequestedReviewersTeams loads the requested reviewers teams. func (pr *PullRequest) LoadRequestedReviewersTeams(ctx context.Context) error { - reviews, err := GetReviewsByIssueID(ctx, pr.Issue.ID) + reviews, _, err := GetReviewsByIssueID(ctx, pr.Issue.ID) if err != nil { return err } @@ -499,65 +483,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) @@ -572,7 +497,7 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *Iss } issue.Index = idx - issue.Title, _ = util.SplitStringAtByteN(issue.Title, 255) + issue.Title = util.EllipsisDisplayString(issue.Title, 255) if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{ Repo: repo, diff --git a/models/issues/review.go b/models/issues/review.go index 8b345e5fd80..3e787273be8 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/issues/review_list.go b/models/issues/review_list.go index bc7d7ec0f01..928f24fb2dc 100644 --- a/models/issues/review_list.go +++ b/models/issues/review_list.go @@ -5,6 +5,8 @@ package issues import ( "context" + "slices" + "sort" "code.gitea.io/gitea/models/db" organization_model "code.gitea.io/gitea/models/organization" @@ -153,43 +155,60 @@ func CountReviews(ctx context.Context, opts FindReviewOptions) (int64, error) { return db.GetEngine(ctx).Where(opts.toCond()).Count(&Review{}) } -// GetReviewersFromOriginalAuthorsByIssueID gets the latest review of each original authors for a pull request -func GetReviewersFromOriginalAuthorsByIssueID(ctx context.Context, issueID int64) (ReviewList, error) { +// GetReviewsByIssueID gets the latest review of each reviewer for a pull request +// The first returned parameter is the latest review of each individual reviewer or team +// The second returned parameter is the latest review of each original author which is migrated from other systems +// The reviews are sorted by updated time +func GetReviewsByIssueID(ctx context.Context, issueID int64) (latestReviews, migratedOriginalReviews ReviewList, err error) { reviews := make([]*Review, 0, 10) - // Get latest review of each reviewer, sorted in order they were made - if err := db.GetEngine(ctx).SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_team_id = 0 AND type in (?, ?, ?) AND original_author_id <> 0 GROUP BY issue_id, original_author_id) ORDER BY review.updated_unix ASC", - issueID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest). - Find(&reviews); err != nil { - return nil, err + // Get all reviews for the issue id + if err := db.GetEngine(ctx).Where("issue_id=?", issueID).OrderBy("updated_unix ASC").Find(&reviews); err != nil { + return nil, nil, err } - return reviews, nil -} - -// GetReviewsByIssueID gets the latest review of each reviewer for a pull request -func GetReviewsByIssueID(ctx context.Context, issueID int64) (ReviewList, error) { - reviews := make([]*Review, 0, 10) - - sess := db.GetEngine(ctx) + // filter them in memory to get the latest review of each reviewer + // Since the reviews should not be too many for one issue, less than 100 commonly, it's acceptable to do this in memory + // And since there are too less indexes in review table, it will be very slow to filter in the database + reviewersMap := make(map[int64][]*Review) // key is reviewer id + originalReviewersMap := make(map[int64][]*Review) // key is original author id + reviewTeamsMap := make(map[int64][]*Review) // key is reviewer team id + countedReivewTypes := []ReviewType{ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest} + for _, review := range reviews { + if review.ReviewerTeamID == 0 && slices.Contains(countedReivewTypes, review.Type) && !review.Dismissed { + if review.OriginalAuthorID != 0 { + originalReviewersMap[review.OriginalAuthorID] = append(originalReviewersMap[review.OriginalAuthorID], review) + } else { + reviewersMap[review.ReviewerID] = append(reviewersMap[review.ReviewerID], review) + } + } else if review.ReviewerTeamID != 0 && review.OriginalAuthorID == 0 { + reviewTeamsMap[review.ReviewerTeamID] = append(reviewTeamsMap[review.ReviewerTeamID], review) + } + } - // Get latest review of each reviewer, sorted in order they were made - if err := sess.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_team_id = 0 AND type in (?, ?, ?) AND dismissed = ? AND original_author_id = 0 GROUP BY issue_id, reviewer_id) ORDER BY review.updated_unix ASC", - issueID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest, false). - Find(&reviews); err != nil { - return nil, err + individualReviews := make([]*Review, 0, 10) + for _, reviews := range reviewersMap { + individualReviews = append(individualReviews, reviews[len(reviews)-1]) } + sort.Slice(individualReviews, func(i, j int) bool { + return individualReviews[i].UpdatedUnix < individualReviews[j].UpdatedUnix + }) - teamReviewRequests := make([]*Review, 0, 5) - if err := sess.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_team_id <> 0 AND original_author_id = 0 GROUP BY issue_id, reviewer_team_id) ORDER BY review.updated_unix ASC", - issueID). - Find(&teamReviewRequests); err != nil { - return nil, err + originalReviews := make([]*Review, 0, 10) + for _, reviews := range originalReviewersMap { + originalReviews = append(originalReviews, reviews[len(reviews)-1]) } + sort.Slice(originalReviews, func(i, j int) bool { + return originalReviews[i].UpdatedUnix < originalReviews[j].UpdatedUnix + }) - if len(teamReviewRequests) > 0 { - reviews = append(reviews, teamReviewRequests...) + teamReviewRequests := make([]*Review, 0, 5) + for _, reviews := range reviewTeamsMap { + teamReviewRequests = append(teamReviewRequests, reviews[len(reviews)-1]) } + sort.Slice(teamReviewRequests, func(i, j int) bool { + return teamReviewRequests[i].UpdatedUnix < teamReviewRequests[j].UpdatedUnix + }) - return reviews, nil + return append(individualReviews, teamReviewRequests...), originalReviews, nil } diff --git a/models/issues/review_test.go b/models/issues/review_test.go index 50330e3ff2c..2588b8ba41b 100644 --- a/models/issues/review_test.go +++ b/models/issues/review_test.go @@ -162,8 +162,9 @@ func TestGetReviewersByIssueID(t *testing.T) { }, ) - allReviews, err := issues_model.GetReviewsByIssueID(db.DefaultContext, issue.ID) + allReviews, migratedReviews, err := issues_model.GetReviewsByIssueID(db.DefaultContext, issue.ID) assert.NoError(t, err) + assert.Empty(t, migratedReviews) for _, review := range allReviews { assert.NoError(t, review.LoadReviewer(db.DefaultContext)) } diff --git a/models/migrations/base/tests.go b/models/migrations/base/tests.go index 2eb85cd8a74..fe6de9c517a 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/migrations/v1_21/v276.go b/models/migrations/v1_21/v276.go index 15177bf0406..9d22c9052e1 100644 --- a/models/migrations/v1_21/v276.go +++ b/models/migrations/v1_21/v276.go @@ -172,7 +172,7 @@ func getRemoteAddress(ownerName, repoName, remoteName string) (string, error) { return "", fmt.Errorf("get remote %s's address of %s/%s failed: %v", remoteName, ownerName, repoName, err) } - u, err := giturl.Parse(remoteURL) + u, err := giturl.ParseGitURL(remoteURL) if err != nil { return "", err } diff --git a/models/organization/org_user_test.go b/models/organization/org_user_test.go index 55abb63203e..c5110b2a348 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 c12f345f0e2..31e1277a6e3 100644 --- a/models/packages/package.go +++ b/models/packages/package.go @@ -248,6 +248,18 @@ func GetPackageByID(ctx context.Context, packageID int64) (*Package, error) { return p, nil } +// UpdatePackageNameByID updates the package's name, it is only for internal usage, for example: rename some legacy packages +func UpdatePackageNameByID(ctx context.Context, ownerID int64, packageType Type, packageID int64, name string) error { + var cond builder.Cond = builder.Eq{ + "package.id": packageID, + "package.owner_id": ownerID, + "package.type": packageType, + "package.is_internal": false, + } + _, err := db.GetEngine(ctx).Where(cond).Update(&Package{Name: name, LowerName: strings.ToLower(name)}) + return err +} + // GetPackageByName gets a package by name func GetPackageByName(ctx context.Context, ownerID int64, packageType Type, name string) (*Package, error) { var cond builder.Cond = builder.Eq{ diff --git a/models/project/project.go b/models/project/project.go index 28a95ac292c..7b5e00512c3 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -135,6 +135,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 { @@ -143,7 +151,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) @@ -151,7 +159,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 "" } @@ -265,7 +273,7 @@ func NewProject(ctx context.Context, p *Project) error { return util.NewInvalidArgumentErrorf("project type is not valid") } - p.Title, _ = util.SplitStringAtByteN(p.Title, 255) + p.Title = util.EllipsisDisplayString(p.Title, 255) return db.WithTx(ctx, func(ctx context.Context) error { if err := db.Insert(ctx, p); err != nil { @@ -333,7 +341,7 @@ func UpdateProject(ctx context.Context, p *Project) error { p.CardType = CardTypeTextOnly } - p.Title, _ = util.SplitStringAtByteN(p.Title, 255) + p.Title = util.EllipsisDisplayString(p.Title, 255) _, err := db.GetEngine(ctx).ID(p.ID).Cols( "title", "description", diff --git a/models/repo.go b/models/repo.go index 3e9c52fdd9c..9bc67079a94 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/release.go b/models/repo/release.go index ba7a3b3159a..1c2e4a48e39 100644 --- a/models/repo/release.go +++ b/models/repo/release.go @@ -156,7 +156,7 @@ func IsReleaseExist(ctx context.Context, repoID int64, tagName string) (bool, er // UpdateRelease updates all columns of a release func UpdateRelease(ctx context.Context, rel *Release) error { - rel.Title, _ = util.SplitStringAtByteN(rel.Title, 255) + rel.Title = util.EllipsisDisplayString(rel.Title, 255) _, err := db.GetEngine(ctx).ID(rel.ID).AllCols().Update(rel) return err } diff --git a/models/repo/repo.go b/models/repo/repo.go index 2d9b9de88d4..fb8a6642f50 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -11,6 +11,7 @@ import ( "net" "net/url" "path/filepath" + "regexp" "strconv" "strings" @@ -19,6 +20,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/git" + giturl "code.gitea.io/gitea/modules/git/url" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" @@ -60,13 +62,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} } @@ -276,6 +280,8 @@ func (repo *Repository) IsBroken() bool { } // MarkAsBrokenEmpty marks the repo as broken and empty +// FIXME: the status "broken" and "is_empty" were abused, +// The code always set them together, no way to distinguish whether a repo is really "empty" or "broken" func (repo *Repository) MarkAsBrokenEmpty() { repo.Status = RepositoryBroken repo.IsEmpty = true @@ -632,14 +638,26 @@ type CloneLink struct { } // ComposeHTTPSCloneURL returns HTTPS clone URL based on given owner and repository name. -func ComposeHTTPSCloneURL(owner, repo string) string { - return fmt.Sprintf("%s%s/%s.git", setting.AppURL, url.PathEscape(owner), url.PathEscape(repo)) +func ComposeHTTPSCloneURL(ctx context.Context, owner, repo string) string { + return fmt.Sprintf("%s%s/%s.git", httplib.GuessCurrentAppURL(ctx), url.PathEscape(owner), url.PathEscape(repo)) } -func ComposeSSHCloneURL(ownerName, repoName string) string { +func ComposeSSHCloneURL(doer *user_model.User, ownerName, repoName string) string { sshUser := setting.SSH.User sshDomain := setting.SSH.Domain + if sshUser == "(DOER_USERNAME)" { + // Some users use SSH reverse-proxy and need to use the current signed-in username as the SSH user + // to make the SSH reverse-proxy could prepare the user's public keys ahead. + // For most cases we have the correct "doer", then use it as the SSH user. + // If we can't get the doer, then use the built-in SSH user. + if doer != nil { + sshUser = doer.Name + } else { + sshUser = setting.SSH.BuiltinServerUser + } + } + // non-standard port, it must use full URI if setting.SSH.Port != 22 { sshHost := net.JoinHostPort(sshDomain, strconv.Itoa(setting.SSH.Port)) @@ -657,21 +675,20 @@ func ComposeSSHCloneURL(ownerName, repoName string) string { return fmt.Sprintf("%s@%s:%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName)) } -func (repo *Repository) cloneLink(isWiki bool) *CloneLink { - repoName := repo.Name - if isWiki { - repoName += ".wiki" - } - +func (repo *Repository) cloneLink(ctx context.Context, doer *user_model.User, repoPathName string) *CloneLink { cl := new(CloneLink) - cl.SSH = ComposeSSHCloneURL(repo.OwnerName, repoName) - cl.HTTPS = ComposeHTTPSCloneURL(repo.OwnerName, repoName) + cl.SSH = ComposeSSHCloneURL(doer, repo.OwnerName, repoPathName) + cl.HTTPS = ComposeHTTPSCloneURL(ctx, repo.OwnerName, repoPathName) return cl } // CloneLink returns clone URLs of repository. -func (repo *Repository) CloneLink() (cl *CloneLink) { - return repo.cloneLink(false) +func (repo *Repository) CloneLink(ctx context.Context, doer *user_model.User) (cl *CloneLink) { + return repo.cloneLink(ctx, doer, repo.Name) +} + +func (repo *Repository) CloneLinkGeneral(ctx context.Context) (cl *CloneLink) { + return repo.cloneLink(ctx, nil /* no doer, use a general git user */, repo.Name) } // GetOriginalURLHostname returns the hostname of a URL or the URL @@ -767,47 +784,25 @@ func GetRepositoryByName(ctx context.Context, ownerID int64, name string) (*Repo return &repo, err } -// getRepositoryURLPathSegments returns segments (owner, reponame) extracted from a url -func getRepositoryURLPathSegments(repoURL string) []string { - if strings.HasPrefix(repoURL, setting.AppURL) { - return strings.Split(strings.TrimPrefix(repoURL, setting.AppURL), "/") - } - - sshURLVariants := [4]string{ - setting.SSH.Domain + ":", - setting.SSH.User + "@" + setting.SSH.Domain + ":", - "git+ssh://" + setting.SSH.Domain + "/", - "git+ssh://" + setting.SSH.User + "@" + setting.SSH.Domain + "/", - } - - for _, sshURL := range sshURLVariants { - if strings.HasPrefix(repoURL, sshURL) { - return strings.Split(strings.TrimPrefix(repoURL, sshURL), "/") - } - } - - return nil -} - // GetRepositoryByURL returns the repository by given url func GetRepositoryByURL(ctx context.Context, repoURL string) (*Repository, error) { - // possible urls for git: - // https://my.domain/sub-path//.git - // https://my.domain/sub-path// - // git+ssh://user@my.domain//.git - // git+ssh://user@my.domain// - // user@my.domain:/.git - // user@my.domain:/ - - pathSegments := getRepositoryURLPathSegments(repoURL) - - if len(pathSegments) != 2 { + ret, err := giturl.ParseRepositoryURL(ctx, repoURL) + if err != nil || ret.OwnerName == "" { return nil, fmt.Errorf("unknown or malformed repository URL") } + return GetRepositoryByOwnerAndName(ctx, ret.OwnerName, ret.RepoName) +} - ownerName := pathSegments[0] - repoName := strings.TrimSuffix(pathSegments[1], ".git") - return GetRepositoryByOwnerAndName(ctx, ownerName, repoName) +// GetRepositoryByURLRelax also accepts an SSH clone URL without user part +func GetRepositoryByURLRelax(ctx context.Context, repoURL string) (*Repository, error) { + if !strings.Contains(repoURL, "://") && !strings.Contains(repoURL, "@") { + // convert "example.com:owner/repo" to "@example.com:owner/repo" + p1, p2, p3 := strings.Index(repoURL, "."), strings.Index(repoURL, ":"), strings.Index(repoURL, "/") + if 0 < p1 && p1 < p2 && p2 < p3 { + repoURL = "@" + repoURL + } + } + return GetRepositoryByURL(ctx, repoURL) } // GetRepositoryByID returns the repository by given id if exists. diff --git a/models/repo/repo_test.go b/models/repo/repo_test.go index 6d88d170da3..a9e2cdfb757 100644 --- a/models/repo/repo_test.go +++ b/models/repo/repo_test.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/test" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var ( @@ -132,60 +133,43 @@ func TestGetRepositoryByURL(t *testing.T) { t.Run("InvalidPath", func(t *testing.T) { repo, err := GetRepositoryByURL(db.DefaultContext, "something") - assert.Nil(t, repo) assert.Error(t, err) }) - t.Run("ValidHttpURL", func(t *testing.T) { - test := func(t *testing.T, url string) { - repo, err := GetRepositoryByURL(db.DefaultContext, url) - - assert.NotNil(t, repo) - assert.NoError(t, err) - - assert.Equal(t, int64(2), repo.ID) - assert.Equal(t, int64(2), repo.OwnerID) - } + testRepo2 := func(t *testing.T, url string) { + repo, err := GetRepositoryByURL(db.DefaultContext, url) + require.NoError(t, err) + assert.EqualValues(t, 2, repo.ID) + assert.EqualValues(t, 2, repo.OwnerID) + } - test(t, "https://try.gitea.io/user2/repo2") - test(t, "https://try.gitea.io/user2/repo2.git") + t.Run("ValidHttpURL", func(t *testing.T) { + testRepo2(t, "https://try.gitea.io/user2/repo2") + testRepo2(t, "https://try.gitea.io/user2/repo2.git") }) t.Run("ValidGitSshURL", func(t *testing.T) { - test := func(t *testing.T, url string) { - repo, err := GetRepositoryByURL(db.DefaultContext, url) - - assert.NotNil(t, repo) - assert.NoError(t, err) + testRepo2(t, "git+ssh://sshuser@try.gitea.io/user2/repo2") + testRepo2(t, "git+ssh://sshuser@try.gitea.io/user2/repo2.git") - assert.Equal(t, int64(2), repo.ID) - assert.Equal(t, int64(2), repo.OwnerID) - } - - test(t, "git+ssh://sshuser@try.gitea.io/user2/repo2") - test(t, "git+ssh://sshuser@try.gitea.io/user2/repo2.git") - - test(t, "git+ssh://try.gitea.io/user2/repo2") - test(t, "git+ssh://try.gitea.io/user2/repo2.git") + testRepo2(t, "git+ssh://try.gitea.io/user2/repo2") + testRepo2(t, "git+ssh://try.gitea.io/user2/repo2.git") }) t.Run("ValidImplicitSshURL", func(t *testing.T) { - test := func(t *testing.T, url string) { - repo, err := GetRepositoryByURL(db.DefaultContext, url) - - assert.NotNil(t, repo) - assert.NoError(t, err) + testRepo2(t, "sshuser@try.gitea.io:user2/repo2") + testRepo2(t, "sshuser@try.gitea.io:user2/repo2.git") + testRelax := func(t *testing.T, url string) { + repo, err := GetRepositoryByURLRelax(db.DefaultContext, url) + require.NoError(t, err) assert.Equal(t, int64(2), repo.ID) assert.Equal(t, int64(2), repo.OwnerID) } - - test(t, "sshuser@try.gitea.io:user2/repo2") - test(t, "sshuser@try.gitea.io:user2/repo2.git") - - test(t, "try.gitea.io:user2/repo2") - test(t, "try.gitea.io:user2/repo2.git") + // TODO: it doesn't seem to be common git ssh URL, should we really support this? + testRelax(t, "try.gitea.io:user2/repo2") + testRelax(t, "try.gitea.io:user2/repo2.git") }) } @@ -199,21 +183,40 @@ func TestComposeSSHCloneURL(t *testing.T) { setting.SSH.Domain = "domain" setting.SSH.Port = 22 setting.Repository.UseCompatSSHURI = false - assert.Equal(t, "git@domain:user/repo.git", ComposeSSHCloneURL("user", "repo")) + assert.Equal(t, "git@domain:user/repo.git", ComposeSSHCloneURL(&user_model.User{Name: "doer"}, "user", "repo")) setting.Repository.UseCompatSSHURI = true - assert.Equal(t, "ssh://git@domain/user/repo.git", ComposeSSHCloneURL("user", "repo")) + assert.Equal(t, "ssh://git@domain/user/repo.git", ComposeSSHCloneURL(&user_model.User{Name: "doer"}, "user", "repo")) // test SSH_DOMAIN while use non-standard SSH port setting.SSH.Port = 123 setting.Repository.UseCompatSSHURI = false - assert.Equal(t, "ssh://git@domain:123/user/repo.git", ComposeSSHCloneURL("user", "repo")) + assert.Equal(t, "ssh://git@domain:123/user/repo.git", ComposeSSHCloneURL(nil, "user", "repo")) setting.Repository.UseCompatSSHURI = true - assert.Equal(t, "ssh://git@domain:123/user/repo.git", ComposeSSHCloneURL("user", "repo")) + assert.Equal(t, "ssh://git@domain:123/user/repo.git", ComposeSSHCloneURL(nil, "user", "repo")) // test IPv6 SSH_DOMAIN setting.Repository.UseCompatSSHURI = false setting.SSH.Domain = "::1" setting.SSH.Port = 22 - assert.Equal(t, "git@[::1]:user/repo.git", ComposeSSHCloneURL("user", "repo")) + assert.Equal(t, "git@[::1]:user/repo.git", ComposeSSHCloneURL(nil, "user", "repo")) + setting.SSH.Port = 123 + assert.Equal(t, "ssh://git@[::1]:123/user/repo.git", ComposeSSHCloneURL(nil, "user", "repo")) + + setting.SSH.User = "(DOER_USERNAME)" + setting.SSH.Domain = "domain" + setting.SSH.Port = 22 + assert.Equal(t, "doer@domain:user/repo.git", ComposeSSHCloneURL(&user_model.User{Name: "doer"}, "user", "repo")) setting.SSH.Port = 123 - assert.Equal(t, "ssh://git@[::1]:123/user/repo.git", ComposeSSHCloneURL("user", "repo")) + assert.Equal(t, "ssh://doer@domain:123/user/repo.git", ComposeSSHCloneURL(&user_model.User{Name: "doer"}, "user", "repo")) +} + +func TestIsUsableRepoName(t *testing.T) { + assert.NoError(t, IsUsableRepoName("a")) + assert.NoError(t, IsUsableRepoName("-1_.")) + assert.NoError(t, IsUsableRepoName(".profile")) + + assert.Error(t, IsUsableRepoName("-")) + assert.Error(t, IsUsableRepoName("🌞")) + assert.Error(t, IsUsableRepoName("the..repo")) + assert.Error(t, IsUsableRepoName("foo.wiki")) + assert.Error(t, IsUsableRepoName("foo.git")) } diff --git a/models/repo/update.go b/models/repo/update.go index e7ca2240282..fce357a1acf 100644 --- a/models/repo/update.go +++ b/models/repo/update.go @@ -46,6 +46,12 @@ func UpdateRepositoryCols(ctx context.Context, repo *Repository, cols ...string) return err } +// UpdateRepositoryColsNoAutoTime updates repository's columns and but applies time change automatically +func UpdateRepositoryColsNoAutoTime(ctx context.Context, repo *Repository, cols ...string) error { + _, err := db.GetEngine(ctx).ID(repo.ID).Cols(cols...).NoAutoTime().Update(repo) + return err +} + // ErrReachLimitOfRepo represents a "ReachLimitOfRepo" kind of error. type ErrReachLimitOfRepo struct { Limit int diff --git a/models/repo/wiki.go b/models/repo/wiki.go index b378666a206..4239a815b28 100644 --- a/models/repo/wiki.go +++ b/models/repo/wiki.go @@ -5,6 +5,7 @@ package repo import ( + "context" "fmt" "path/filepath" "strings" @@ -72,8 +73,8 @@ func (err ErrWikiInvalidFileName) Unwrap() error { } // WikiCloneLink returns clone URLs of repository wiki. -func (repo *Repository) WikiCloneLink() *CloneLink { - return repo.cloneLink(true) +func (repo *Repository) WikiCloneLink(ctx context.Context, doer *user_model.User) *CloneLink { + return repo.cloneLink(ctx, doer, repo.Name+".wiki") } // WikiPath returns wiki data path by given user and repository name. diff --git a/models/repo/wiki_test.go b/models/repo/wiki_test.go index 629986f741a..0157b7735d0 100644 --- a/models/repo/wiki_test.go +++ b/models/repo/wiki_test.go @@ -4,6 +4,7 @@ package repo_test import ( + "context" "path/filepath" "testing" @@ -18,7 +19,7 @@ func TestRepository_WikiCloneLink(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - cloneLink := repo.WikiCloneLink() + cloneLink := repo.WikiCloneLink(context.Background(), nil) assert.Equal(t, "ssh://sshuser@try.gitea.io:3000/user2/repo1.wiki.git", cloneLink.SSH) assert.Equal(t, "https://try.gitea.io/user2/repo1.wiki.git", cloneLink.HTTPS) } diff --git a/models/repo_test.go b/models/repo_test.go index 2a8a4a743ed..bcf62237f08 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 4dde5410d6d..fb2d2d0085c 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 - -// GetXORMEngine gets the XORM engine -func GetXORMEngine(engine ...*xorm.Engine) (x *xorm.Engine) { - if len(engine) == 1 { - return engine[0] - } - return db.GetEngine(db.DefaultContext).(*xorm.Engine) +type FixturesLoader interface { + Load() error } -// 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()) - } +var fixturesLoader FixturesLoader - 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 +// GetXORMEngine gets the XORM engine +func GetXORMEngine() (x *xorm.Engine) { + return db.GetEngine(db.DefaultContext).(*xorm.Engine) } -// 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 00000000000..14686caf63d --- /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 00000000000..a4c55f4e558 --- /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 4d7ee2151dc..690089bbc50 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 141fc66b998..bc96a059733 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 5794d5109ed..7a9ca9698d0 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,44 +71,20 @@ 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) } setting.IsInTesting = true setting.AppURL = "https://try.gitea.io/" + setting.Domain = "try.gitea.io" setting.RunUser = "runuser" setting.SSH.User = "sshuser" setting.SSH.BuiltinServerUser = "builtinuser" @@ -172,16 +140,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) } } @@ -206,7 +174,7 @@ func CreateTestEngine(opts FixturesOptions) error { x, err := xorm.NewEngine("sqlite3", "file::memory:?cache=shared&_txlock=immediate") if err != nil { if strings.Contains(err.Error(), "unknown driver") { - return fmt.Errorf(`sqlite3 requires: import _ "github.com/mattn/go-sqlite3" or -tags sqlite,sqlite_unlock_notify%s%w`, "\n", err) + return fmt.Errorf(`sqlite3 requires: -tags sqlite,sqlite_unlock_notify%s%w`, "\n", err) } return err } diff --git a/models/unittest/unit_tests.go b/models/unittest/unit_tests.go index 4ac858e04ea..1c5595aef85 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 5c04909ed7c..74ba5f617a5 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 6af33892373..85915f4020f 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 72caafc3baa..19879fbcc79 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 { @@ -190,9 +191,9 @@ func (u *User) BeforeUpdate() { } u.LowerName = strings.ToLower(u.Name) - u.Location = base.TruncateString(u.Location, 255) - u.Website = base.TruncateString(u.Website, 255) - u.Description = base.TruncateString(u.Description, 255) + u.Location = util.TruncateRunes(u.Location, 255) + u.Website = util.TruncateRunes(u.Website, 255) + u.Description = util.TruncateRunes(u.Description, 255) } // AfterLoad is invoked from XORM after filling all the fields of this object. @@ -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). @@ -501,9 +491,9 @@ func (u *User) GitName() string { // ShortName ellipses username to length func (u *User) ShortName(length int) string { if setting.UI.DefaultShowFullName && len(u.FullName) > 0 { - return base.EllipsisString(u.FullName, length) + return util.EllipsisDisplayString(u.FullName, length) } - return base.EllipsisString(u.Name, length) + return util.EllipsisDisplayString(u.Name, length) } // IsMailable checks if a user is eligible @@ -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 9678d23ad67..4f3811ba2b2 100644 --- a/modules/assetfs/layered.go +++ b/modules/assetfs/layered.go @@ -103,7 +103,7 @@ func (l *LayeredFS) ReadLayeredFile(elems ...string) ([]byte, string, error) { } func shouldInclude(info fs.FileInfo, fileMode ...bool) bool { - if util.CommonSkip(info.Name()) { + if util.IsCommonHiddenFileName(info.Name()) { return false } if len(fileMode) == 0 { diff --git a/modules/auth/password/pwn/pwn_test.go b/modules/auth/password/pwn/pwn_test.go index b3e7734c3fc..ae03fabc579 100644 --- a/modules/auth/password/pwn/pwn_test.go +++ b/modules/auth/password/pwn/pwn_test.go @@ -4,46 +4,57 @@ package pwn import ( + "errors" + "io" "net/http" + "strings" "testing" - "time" - "github.com/h2non/gock" "github.com/stretchr/testify/assert" ) -var client = New(WithHTTP(&http.Client{ - Timeout: time.Second * 2, -})) +type mockTransport struct{} + +func (mockTransport) RoundTrip(req *http.Request) (*http.Response, error) { + if req.URL.Host != "api.pwnedpasswords.com" { + return nil, errors.New("unsupported host") + } + respMap := map[string]string{ + "/range/5c1d8": "EAF2F254732680E8AC339B84F3266ECCBB5:1\r\nFC446EB88938834178CB9322C1EE273C2A7:2", + "/range/ba189": "FD4CB34F0378BCB15D23F6FFD28F0775C9E:3\r\nFDF342FCD8C3611DAE4D76E8A992A3E4169:4", + "/range/a1733": "C4CE0F1F0062B27B9E2F41AF0C08218017C:1\r\nFC446EB88938834178CB9322C1EE273C2A7:2\r\nFE81480327C992FE62065A827429DD1318B:0", + "/range/5617b": "FD4CB34F0378BCB15D23F6FFD28F0775C9E:3\r\nFDF342FCD8C3611DAE4D76E8A992A3E4169:4\r\nFE81480327C992FE62065A827429DD1318B:0", + "/range/79082": "FDF342FCD8C3611DAE4D76E8A992A3E4169:4\r\nFE81480327C992FE62065A827429DD1318B:0\r\nAFEF386F56EB0B4BE314E07696E5E6E6536:0", + } + if resp, ok := respMap[req.URL.Path]; ok { + return &http.Response{Request: req, Body: io.NopCloser(strings.NewReader(resp))}, nil + } + return nil, errors.New("unsupported path") +} func TestPassword(t *testing.T) { - defer gock.Off() + client := New(WithHTTP(&http.Client{Transport: mockTransport{}})) count, err := client.CheckPassword("", false) assert.ErrorIs(t, err, ErrEmptyPassword, "blank input should return ErrEmptyPassword") assert.Equal(t, -1, count) - gock.New("https://api.pwnedpasswords.com").Get("/range/5c1d8").Times(1).Reply(200).BodyString("EAF2F254732680E8AC339B84F3266ECCBB5:1\r\nFC446EB88938834178CB9322C1EE273C2A7:2") count, err = client.CheckPassword("pwned", false) assert.NoError(t, err) assert.Equal(t, 1, count) - gock.New("https://api.pwnedpasswords.com").Get("/range/ba189").Times(1).Reply(200).BodyString("FD4CB34F0378BCB15D23F6FFD28F0775C9E:3\r\nFDF342FCD8C3611DAE4D76E8A992A3E4169:4") count, err = client.CheckPassword("notpwned", false) assert.NoError(t, err) assert.Equal(t, 0, count) - gock.New("https://api.pwnedpasswords.com").Get("/range/a1733").Times(1).Reply(200).BodyString("C4CE0F1F0062B27B9E2F41AF0C08218017C:1\r\nFC446EB88938834178CB9322C1EE273C2A7:2\r\nFE81480327C992FE62065A827429DD1318B:0") count, err = client.CheckPassword("paddedpwned", true) assert.NoError(t, err) assert.Equal(t, 1, count) - gock.New("https://api.pwnedpasswords.com").Get("/range/5617b").Times(1).Reply(200).BodyString("FD4CB34F0378BCB15D23F6FFD28F0775C9E:3\r\nFDF342FCD8C3611DAE4D76E8A992A3E4169:4\r\nFE81480327C992FE62065A827429DD1318B:0") count, err = client.CheckPassword("paddednotpwned", true) assert.NoError(t, err) assert.Equal(t, 0, count) - gock.New("https://api.pwnedpasswords.com").Get("/range/79082").Times(1).Reply(200).BodyString("FDF342FCD8C3611DAE4D76E8A992A3E4169:4\r\nFE81480327C992FE62065A827429DD1318B:0\r\nAFEF386F56EB0B4BE314E07696E5E6E6536:0") count, err = client.CheckPassword("paddednotpwnedzero", true) assert.NoError(t, err) assert.Equal(t, 0, count) diff --git a/modules/base/tool.go b/modules/base/tool.go index 2303e64a689..1d16186bc5d 100644 --- a/modules/base/tool.go +++ b/modules/base/tool.go @@ -16,11 +16,11 @@ import ( "strconv" "strings" "time" - "unicode/utf8" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" "github.com/dustin/go-humanize" ) @@ -35,7 +35,7 @@ func EncodeSha256(str string) string { // ShortSha is basically just truncating. // It is DEPRECATED and will be removed in the future. func ShortSha(sha1 string) string { - return TruncateString(sha1, 10) + return util.TruncateRunes(sha1, 10) } // BasicAuthDecode decode basic auth string @@ -116,27 +116,6 @@ func FileSize(s int64) string { return humanize.IBytes(uint64(s)) } -// EllipsisString returns a truncated short string, -// it appends '...' in the end of the length of string is too large. -func EllipsisString(str string, length int) string { - if length <= 3 { - return "..." - } - if utf8.RuneCountInString(str) <= length { - return str - } - return string([]rune(str)[:length-3]) + "..." -} - -// TruncateString returns a truncated string with given limit, -// it returns input string if length is not reached limit. -func TruncateString(str string, limit int) string { - if utf8.RuneCountInString(str) < limit { - return str - } - return string([]rune(str)[:limit]) -} - // StringsToInt64s converts a slice of string to a slice of int64. func StringsToInt64s(strs []string) ([]int64, error) { if strs == nil { diff --git a/modules/base/tool_test.go b/modules/base/tool_test.go index de6c3118566..c821a55c19c 100644 --- a/modules/base/tool_test.go +++ b/modules/base/tool_test.go @@ -113,36 +113,6 @@ func TestFileSize(t *testing.T) { assert.Equal(t, "2.0 EiB", FileSize(size)) } -func TestEllipsisString(t *testing.T) { - assert.Equal(t, "...", EllipsisString("foobar", 0)) - assert.Equal(t, "...", EllipsisString("foobar", 1)) - assert.Equal(t, "...", EllipsisString("foobar", 2)) - assert.Equal(t, "...", EllipsisString("foobar", 3)) - assert.Equal(t, "f...", EllipsisString("foobar", 4)) - assert.Equal(t, "fo...", EllipsisString("foobar", 5)) - assert.Equal(t, "foobar", EllipsisString("foobar", 6)) - assert.Equal(t, "foobar", EllipsisString("foobar", 10)) - assert.Equal(t, "测...", EllipsisString("测试文本一二三四", 4)) - assert.Equal(t, "测试...", EllipsisString("测试文本一二三四", 5)) - assert.Equal(t, "测试文...", EllipsisString("测试文本一二三四", 6)) - assert.Equal(t, "测试文本一二三四", EllipsisString("测试文本一二三四", 10)) -} - -func TestTruncateString(t *testing.T) { - assert.Equal(t, "", TruncateString("foobar", 0)) - assert.Equal(t, "f", TruncateString("foobar", 1)) - assert.Equal(t, "fo", TruncateString("foobar", 2)) - assert.Equal(t, "foo", TruncateString("foobar", 3)) - assert.Equal(t, "foob", TruncateString("foobar", 4)) - assert.Equal(t, "fooba", TruncateString("foobar", 5)) - assert.Equal(t, "foobar", TruncateString("foobar", 6)) - assert.Equal(t, "foobar", TruncateString("foobar", 7)) - assert.Equal(t, "测试文本", TruncateString("测试文本一二三四", 4)) - assert.Equal(t, "测试文本一", TruncateString("测试文本一二三四", 5)) - assert.Equal(t, "测试文本一二", TruncateString("测试文本一二三四", 6)) - assert.Equal(t, "测试文本一二三", TruncateString("测试文本一二三四", 7)) -} - func TestStringsToInt64s(t *testing.T) { testSuccess := func(input []string, expected []int64) { result, err := StringsToInt64s(input) diff --git a/modules/git/batch_reader.go b/modules/git/batch_reader.go index 532dbad9894..33e54fe75cb 100644 --- a/modules/git/batch_reader.go +++ b/modules/git/batch_reader.go @@ -242,7 +242,7 @@ func BinToHex(objectFormat ObjectFormat, sha, out []byte) []byte { return out } -// ParseTreeLine reads an entry from a tree in a cat-file --batch stream +// ParseCatFileTreeLine reads an entry from a tree in a cat-file --batch stream // This carefully avoids allocations - except where fnameBuf is too small. // It is recommended therefore to pass in an fnameBuf large enough to avoid almost all allocations // @@ -250,7 +250,7 @@ func BinToHex(objectFormat ObjectFormat, sha, out []byte) []byte { // SP NUL // // We don't attempt to convert the raw HASH to save a lot of time -func ParseTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBuf, shaBuf []byte) (mode, fname, sha []byte, n int, err error) { +func ParseCatFileTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBuf, shaBuf []byte) (mode, fname, sha []byte, n int, err error) { var readBytes []byte // Read the Mode & fname @@ -260,7 +260,7 @@ func ParseTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBu } idx := bytes.IndexByte(readBytes, ' ') if idx < 0 { - log.Debug("missing space in readBytes ParseTreeLine: %s", readBytes) + log.Debug("missing space in readBytes ParseCatFileTreeLine: %s", readBytes) return mode, fname, sha, n, &ErrNotExist{} } diff --git a/modules/git/command.go b/modules/git/command.go index b231c3beea0..2584e3cc57c 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/commit_info.go b/modules/git/commit_info.go index 545081275b7..c046acbb508 100644 --- a/modules/git/commit_info.go +++ b/modules/git/commit_info.go @@ -7,5 +7,5 @@ package git type CommitInfo struct { Entry *TreeEntry Commit *Commit - SubModuleFile *CommitSubModuleFile + SubmoduleFile *CommitSubmoduleFile } diff --git a/modules/git/commit_info_gogit.go b/modules/git/commit_info_gogit.go index 11b44f7c356..314c2df7284 100644 --- a/modules/git/commit_info_gogit.go +++ b/modules/git/commit_info_gogit.go @@ -85,8 +85,8 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath } else if subModule != nil { subModuleURL = subModule.URL } - subModuleFile := NewCommitSubModuleFile(subModuleURL, entry.ID.String()) - commitsInfo[i].SubModuleFile = subModuleFile + subModuleFile := NewCommitSubmoduleFile(subModuleURL, entry.ID.String()) + commitsInfo[i].SubmoduleFile = subModuleFile } } diff --git a/modules/git/commit_info_nogogit.go b/modules/git/commit_info_nogogit.go index 20d586f0ff5..ef2df0b133a 100644 --- a/modules/git/commit_info_nogogit.go +++ b/modules/git/commit_info_nogogit.go @@ -79,8 +79,8 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath } else if subModule != nil { subModuleURL = subModule.URL } - subModuleFile := NewCommitSubModuleFile(subModuleURL, entry.ID.String()) - commitsInfo[i].SubModuleFile = subModuleFile + subModuleFile := NewCommitSubmoduleFile(subModuleURL, entry.ID.String()) + commitsInfo[i].SubmoduleFile = subModuleFile } } diff --git a/modules/git/commit_submodule.go b/modules/git/commit_submodule.go index 6603061da29..031fd4e5d02 100644 --- a/modules/git/commit_submodule.go +++ b/modules/git/commit_submodule.go @@ -3,6 +3,10 @@ package git +type SubmoduleWebLink struct { + RepoWebLink, CommitWebLink string +} + // GetSubModules get all the submodules of current revision git tree func (c *Commit) GetSubModules() (*ObjectCache[*SubModule], error) { if c.submoduleCache != nil { diff --git a/modules/git/commit_submodule_file.go b/modules/git/commit_submodule_file.go index bdec35f6829..2ac744fbf61 100644 --- a/modules/git/commit_submodule_file.go +++ b/modules/git/commit_submodule_file.go @@ -5,107 +5,50 @@ package git import ( - "fmt" - "net" - "net/url" - "path" - "regexp" - "strings" -) + "context" -var scpSyntax = regexp.MustCompile(`^([a-zA-Z0-9_]+@)?([a-zA-Z0-9._-]+):(.*)$`) + giturl "code.gitea.io/gitea/modules/git/url" +) -// CommitSubModuleFile represents a file with submodule type. -type CommitSubModuleFile struct { - refURL string - refID string +// CommitSubmoduleFile represents a file with submodule type. +type CommitSubmoduleFile struct { + refURL string + parsedURL *giturl.RepositoryURL + parsed bool + refID string + repoLink string } -// NewCommitSubModuleFile create a new submodule file -func NewCommitSubModuleFile(refURL, refID string) *CommitSubModuleFile { - return &CommitSubModuleFile{ - refURL: refURL, - refID: refID, - } +// NewCommitSubmoduleFile create a new submodule file +func NewCommitSubmoduleFile(refURL, refID string) *CommitSubmoduleFile { + return &CommitSubmoduleFile{refURL: refURL, refID: refID} } -func getRefURL(refURL, urlPrefix, repoFullName, sshDomain string) string { - if refURL == "" { - return "" - } - - refURI := strings.TrimSuffix(refURL, ".git") - - prefixURL, _ := url.Parse(urlPrefix) - urlPrefixHostname, _, err := net.SplitHostPort(prefixURL.Host) - if err != nil { - urlPrefixHostname = prefixURL.Host - } - - urlPrefix = strings.TrimSuffix(urlPrefix, "/") - - // FIXME: Need to consider branch - which will require changes in modules/git/commit.go:GetSubModules - // Relative url prefix check (according to git submodule documentation) - if strings.HasPrefix(refURI, "./") || strings.HasPrefix(refURI, "../") { - return urlPrefix + path.Clean(path.Join("/", repoFullName, refURI)) - } - - if !strings.Contains(refURI, "://") { - // scp style syntax which contains *no* port number after the : (and is not parsed by net/url) - // ex: git@try.gitea.io:go-gitea/gitea - match := scpSyntax.FindAllStringSubmatch(refURI, -1) - if len(match) > 0 { - m := match[0] - refHostname := m[2] - pth := m[3] - - if !strings.HasPrefix(pth, "/") { - pth = "/" + pth - } - - if urlPrefixHostname == refHostname || refHostname == sshDomain { - return urlPrefix + path.Clean(path.Join("/", pth)) - } - return "http://" + refHostname + pth - } - } - - ref, err := url.Parse(refURI) - if err != nil { - return "" - } +func (sf *CommitSubmoduleFile) RefID() string { + return sf.refID // this function is only used in templates +} - refHostname, _, err := net.SplitHostPort(ref.Host) - if err != nil { - refHostname = ref.Host +// SubmoduleWebLink tries to make some web links for a submodule, it also works on "nil" receiver +func (sf *CommitSubmoduleFile) SubmoduleWebLink(ctx context.Context, optCommitID ...string) *SubmoduleWebLink { + if sf == nil { + return nil } - - supportedSchemes := []string{"http", "https", "git", "ssh", "git+ssh"} - - for _, scheme := range supportedSchemes { - if ref.Scheme == scheme { - if ref.Scheme == "http" || ref.Scheme == "https" { - if len(ref.User.Username()) > 0 { - return ref.Scheme + "://" + fmt.Sprintf("%v", ref.User) + "@" + ref.Host + ref.Path - } - return ref.Scheme + "://" + ref.Host + ref.Path - } else if urlPrefixHostname == refHostname || refHostname == sshDomain { - return urlPrefix + path.Clean(path.Join("/", ref.Path)) - } - return "http://" + refHostname + ref.Path + if !sf.parsed { + sf.parsed = true + parsedURL, err := giturl.ParseRepositoryURL(ctx, sf.refURL) + if err != nil { + return nil } - } - - return "" -} - -// RefURL guesses and returns reference URL. -// FIXME: template passes AppURL as urlPrefix, it needs to figure out the correct approach (no hard-coded AppURL anymore) -func (sf *CommitSubModuleFile) RefURL(urlPrefix, repoFullName, sshDomain string) string { - return getRefURL(sf.refURL, urlPrefix, repoFullName, sshDomain) -} - -// RefID returns reference ID. -func (sf *CommitSubModuleFile) RefID() string { - return sf.refID + sf.parsedURL = parsedURL + sf.repoLink = giturl.MakeRepositoryWebLink(sf.parsedURL) + } + var commitLink string + if len(optCommitID) == 2 { + commitLink = sf.repoLink + "/compare/" + optCommitID[0] + "..." + optCommitID[1] + } else if len(optCommitID) == 1 { + commitLink = sf.repoLink + "/commit/" + optCommitID[0] + } else { + commitLink = sf.repoLink + "/commit/" + sf.refID + } + return &SubmoduleWebLink{RepoWebLink: sf.repoLink, CommitWebLink: commitLink} } diff --git a/modules/git/commit_submodule_file_test.go b/modules/git/commit_submodule_file_test.go index 473b996b820..4b5b7676126 100644 --- a/modules/git/commit_submodule_file_test.go +++ b/modules/git/commit_submodule_file_test.go @@ -1,42 +1,30 @@ -// Copyright 2018 The Gitea Authors. All rights reserved. +// Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package git import ( + "context" "testing" "github.com/stretchr/testify/assert" ) -func TestCommitSubModuleFileGetRefURL(t *testing.T) { - kases := []struct { - refURL string - prefixURL string - parentPath string - SSHDomain string - expect string - }{ - {"git://github.com/user1/repo1", "/", "user1/repo2", "", "http://github.com/user1/repo1"}, - {"https://localhost/user1/repo1.git", "/", "user1/repo2", "", "https://localhost/user1/repo1"}, - {"http://localhost/user1/repo1.git", "/", "owner/reponame", "", "http://localhost/user1/repo1"}, - {"git@github.com:user1/repo1.git", "/", "owner/reponame", "", "http://github.com/user1/repo1"}, - {"ssh://git@git.zefie.net:2222/zefie/lge_g6_kernel_scripts.git", "/", "zefie/lge_g6_kernel", "", "http://git.zefie.net/zefie/lge_g6_kernel_scripts"}, - {"git@git.zefie.net:2222/zefie/lge_g6_kernel_scripts.git", "/", "zefie/lge_g6_kernel", "", "http://git.zefie.net/2222/zefie/lge_g6_kernel_scripts"}, - {"git@try.gitea.io:go-gitea/gitea", "https://try.gitea.io/", "go-gitea/sdk", "", "https://try.gitea.io/go-gitea/gitea"}, - {"ssh://git@try.gitea.io:9999/go-gitea/gitea", "https://try.gitea.io/", "go-gitea/sdk", "", "https://try.gitea.io/go-gitea/gitea"}, - {"git://git@try.gitea.io:9999/go-gitea/gitea", "https://try.gitea.io/", "go-gitea/sdk", "", "https://try.gitea.io/go-gitea/gitea"}, - {"ssh://git@127.0.0.1:9999/go-gitea/gitea", "https://127.0.0.1:3000/", "go-gitea/sdk", "", "https://127.0.0.1:3000/go-gitea/gitea"}, - {"https://gitea.com:3000/user1/repo1.git", "https://127.0.0.1:3000/", "user/repo2", "", "https://gitea.com:3000/user1/repo1"}, - {"https://example.gitea.com/gitea/user1/repo1.git", "https://example.gitea.com/gitea/", "", "user/repo2", "https://example.gitea.com/gitea/user1/repo1"}, - {"https://username:password@github.com/username/repository.git", "/", "username/repository2", "", "https://username:password@github.com/username/repository"}, - {"somethingbad", "https://127.0.0.1:3000/go-gitea/gitea", "/", "", ""}, - {"git@localhost:user/repo", "https://localhost/", "user2/repo1", "", "https://localhost/user/repo"}, - {"../path/to/repo.git/", "https://localhost/", "user/repo2", "", "https://localhost/user/path/to/repo.git"}, - {"ssh://git@ssh.gitea.io:2222/go-gitea/gitea", "https://try.gitea.io/", "go-gitea/sdk", "ssh.gitea.io", "https://try.gitea.io/go-gitea/gitea"}, - } - - for _, kase := range kases { - assert.EqualValues(t, kase.expect, getRefURL(kase.refURL, kase.prefixURL, kase.parentPath, kase.SSHDomain)) - } +func TestCommitSubmoduleLink(t *testing.T) { + sf := NewCommitSubmoduleFile("git@github.com:user/repo.git", "aaaa") + + wl := sf.SubmoduleWebLink(context.Background()) + assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink) + assert.Equal(t, "https://github.com/user/repo/commit/aaaa", wl.CommitWebLink) + + wl = sf.SubmoduleWebLink(context.Background(), "1111") + assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink) + assert.Equal(t, "https://github.com/user/repo/commit/1111", wl.CommitWebLink) + + wl = sf.SubmoduleWebLink(context.Background(), "1111", "2222") + assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink) + assert.Equal(t, "https://github.com/user/repo/compare/1111...2222", wl.CommitWebLink) + + wl = (*CommitSubmoduleFile)(nil).SubmoduleWebLink(context.Background()) + assert.Nil(t, wl) } diff --git a/modules/git/parse.go b/modules/git/parse.go new file mode 100644 index 00000000000..eb26632cc0e --- /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 546b38be379..676bb3c76c0 100644 --- a/modules/git/parse_nogogit.go +++ b/modules/git/parse_nogogit.go @@ -10,8 +10,6 @@ import ( "bytes" "fmt" "io" - "strconv" - "strings" "code.gitea.io/gitea/modules/log" ) @@ -21,71 +19,30 @@ func ParseTreeEntries(data []byte) ([]*TreeEntry, error) { return parseTreeEntries(data, nil) } -var sepSpace = []byte{' '} - +// parseTreeEntries FIXME this function's design is not right, it should make the caller read all data into memory func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) { - var err error entries := make([]*TreeEntry, 0, bytes.Count(data, []byte{'\n'})+1) for pos := 0; pos < len(data); { - // expect line to be of the form: - // \t - // \t posEnd := bytes.IndexByte(data[pos:], '\n') if posEnd == -1 { posEnd = len(data) } else { posEnd += pos } - line := data[pos:posEnd] - posTab := bytes.IndexByte(line, '\t') - if posTab == -1 { - return nil, fmt.Errorf("invalid ls-tree output (no tab): %q", line) - } - - entry := new(TreeEntry) - entry.ptree = ptree - - entryAttrs := line[:posTab] - entryName := line[posTab+1:] - - entryMode, entryAttrs, _ := bytes.Cut(entryAttrs, sepSpace) - _ /* entryType */, entryAttrs, _ = bytes.Cut(entryAttrs, sepSpace) // the type is not used, the mode is enough to determine the type - entryObjectID, entryAttrs, _ := bytes.Cut(entryAttrs, sepSpace) - if len(entryAttrs) > 0 { - entrySize := entryAttrs // the last field is the space-padded-size - entry.size, _ = strconv.ParseInt(strings.TrimSpace(string(entrySize)), 10, 64) - entry.sized = true - } - switch string(entryMode) { - case "100644": - entry.entryMode = EntryModeBlob - case "100755": - entry.entryMode = EntryModeExec - case "120000": - entry.entryMode = EntryModeSymlink - case "160000": - entry.entryMode = EntryModeCommit - case "040000", "040755": // git uses 040000 for tree object, but some users may get 040755 for unknown reasons - entry.entryMode = EntryModeTree - default: - return nil, fmt.Errorf("unknown type: %v", string(entryMode)) - } - - entry.ID, err = NewIDFromString(string(entryObjectID)) + line := data[pos:posEnd] + lsTreeLine, err := parseLsTreeLine(line) if err != nil { - return nil, fmt.Errorf("invalid ls-tree output (invalid object id): %q, err: %w", line, err) + return nil, err } - - if len(entryName) > 0 && entryName[0] == '"' { - entry.name, err = strconv.Unquote(string(entryName)) - if err != nil { - return nil, fmt.Errorf("invalid ls-tree output (invalid name): %q, err: %w", line, err) - } - } else { - entry.name = string(entryName) + entry := &TreeEntry{ + ptree: ptree, + ID: lsTreeLine.ID, + entryMode: lsTreeLine.EntryMode, + name: lsTreeLine.Name, + size: lsTreeLine.Size.Value(), + sized: lsTreeLine.Size.Has(), } - pos = posEnd + 1 entries = append(entries, entry) } @@ -100,7 +57,7 @@ func catBatchParseTreeEntries(objectFormat ObjectFormat, ptree *Tree, rd *bufio. loop: for sz > 0 { - mode, fname, sha, count, err := ParseTreeLine(objectFormat, rd, modeBuf, fnameBuf, shaBuf) + mode, fname, sha, count, err := ParseCatFileTreeLine(objectFormat, rd, modeBuf, fnameBuf, shaBuf) if err != nil { if err == io.EOF { break loop diff --git a/modules/git/pipeline/lfs_nogogit.go b/modules/git/pipeline/lfs_nogogit.go index b22805c1327..92e35c5a102 100644 --- a/modules/git/pipeline/lfs_nogogit.go +++ b/modules/git/pipeline/lfs_nogogit.go @@ -114,7 +114,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err case "tree": var n int64 for n < size { - mode, fname, binObjectID, count, err := git.ParseTreeLine(objectID.Type(), batchReader, modeBuf, fnameBuf, workingShaBuf) + mode, fname, binObjectID, count, err := git.ParseCatFileTreeLine(objectID.Type(), batchReader, modeBuf, fnameBuf, workingShaBuf) if err != nil { return nil, err } diff --git a/modules/git/remote.go b/modules/git/remote.go index de8d74eded7..ff8c040eb15 100644 --- a/modules/git/remote.go +++ b/modules/git/remote.go @@ -39,7 +39,7 @@ func GetRemoteURL(ctx context.Context, repoPath, remoteName string) (*giturl.Git if err != nil { return nil, err } - return giturl.Parse(addr) + return giturl.ParseGitURL(addr) } // ErrInvalidCloneAddr represents a "InvalidCloneAddr" kind of error. @@ -79,6 +79,15 @@ func (err *ErrInvalidCloneAddr) Unwrap() error { return util.ErrInvalidArgument } +// IsRemoteNotExistError checks the prefix of the error message to see whether a remote does not exist. +func IsRemoteNotExistError(err error) bool { + // see: https://github.com/go-gitea/gitea/issues/32889#issuecomment-2571848216 + // Should not add space in the end, sometimes git will add a `:` + prefix1 := "exit status 128 - fatal: No such remote" // git < 2.30 + prefix2 := "exit status 2 - error: No such remote" // git >= 2.30 + return strings.HasPrefix(err.Error(), prefix1) || strings.HasPrefix(err.Error(), prefix2) +} + // ParseRemoteAddr checks if given remote address is valid, // and returns composed URL with needed username and password. func ParseRemoteAddr(remoteAddr, authUsername, authPassword string) (string, error) { diff --git a/modules/git/repo_archive.go b/modules/git/repo_archive.go index 1bf1aa41b94..2b45a50f191 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/repo_compare.go b/modules/git/repo_compare.go index 16fcdcf4c8f..877a7ff3b86 100644 --- a/modules/git/repo_compare.go +++ b/modules/git/repo_compare.go @@ -233,72 +233,34 @@ func parseDiffStat(stdout string) (numFiles, totalAdditions, totalDeletions int, return numFiles, totalAdditions, totalDeletions, err } -// GetDiffOrPatch generates either diff or formatted patch data between given revisions -func (repo *Repository) GetDiffOrPatch(base, head string, w io.Writer, patch, binary bool) error { - if patch { - return repo.GetPatch(base, head, w) - } - if binary { - return repo.GetDiffBinary(base, head, w) - } - return repo.GetDiff(base, head, w) -} - // GetDiff generates and returns patch data between given revisions, optimized for human readability -func (repo *Repository) GetDiff(base, head string, w io.Writer) error { +func (repo *Repository) GetDiff(compareArg string, w io.Writer) error { stderr := new(bytes.Buffer) - err := NewCommand(repo.Ctx, "diff", "-p").AddDynamicArguments(base + "..." + head). + return NewCommand(repo.Ctx, "diff", "-p").AddDynamicArguments(compareArg). Run(&RunOpts{ Dir: repo.Path, Stdout: w, Stderr: stderr, }) - if err != nil && bytes.Contains(stderr.Bytes(), []byte("no merge base")) { - return NewCommand(repo.Ctx, "diff", "-p").AddDynamicArguments(base, head). - Run(&RunOpts{ - Dir: repo.Path, - Stdout: w, - }) - } - return err } // GetDiffBinary generates and returns patch data between given revisions, including binary diffs. -func (repo *Repository) GetDiffBinary(base, head string, w io.Writer) error { - stderr := new(bytes.Buffer) - err := NewCommand(repo.Ctx, "diff", "-p", "--binary", "--histogram").AddDynamicArguments(base + "..." + head). - Run(&RunOpts{ - Dir: repo.Path, - Stdout: w, - Stderr: stderr, - }) - if err != nil && bytes.Contains(stderr.Bytes(), []byte("no merge base")) { - return NewCommand(repo.Ctx, "diff", "-p", "--binary", "--histogram").AddDynamicArguments(base, head). - Run(&RunOpts{ - Dir: repo.Path, - Stdout: w, - }) - } - return err +func (repo *Repository) GetDiffBinary(compareArg string, w io.Writer) error { + return NewCommand(repo.Ctx, "diff", "-p", "--binary", "--histogram").AddDynamicArguments(compareArg).Run(&RunOpts{ + Dir: repo.Path, + Stdout: w, + }) } // GetPatch generates and returns format-patch data between given revisions, able to be used with `git apply` -func (repo *Repository) GetPatch(base, head string, w io.Writer) error { +func (repo *Repository) GetPatch(compareArg string, w io.Writer) error { stderr := new(bytes.Buffer) - err := NewCommand(repo.Ctx, "format-patch", "--binary", "--stdout").AddDynamicArguments(base + "..." + head). + return NewCommand(repo.Ctx, "format-patch", "--binary", "--stdout").AddDynamicArguments(compareArg). Run(&RunOpts{ Dir: repo.Path, Stdout: w, Stderr: stderr, }) - if err != nil && bytes.Contains(stderr.Bytes(), []byte("no merge base")) { - return NewCommand(repo.Ctx, "format-patch", "--binary", "--stdout").AddDynamicArguments(base, head). - Run(&RunOpts{ - Dir: repo.Path, - Stdout: w, - }) - } - return err } // GetFilesChangedBetween returns a list of all files that have been changed between the given commits @@ -329,21 +291,6 @@ func (repo *Repository) GetFilesChangedBetween(base, head string) ([]string, err return split, err } -// GetDiffFromMergeBase generates and return patch data from merge base to head -func (repo *Repository) GetDiffFromMergeBase(base, head string, w io.Writer) error { - stderr := new(bytes.Buffer) - err := NewCommand(repo.Ctx, "diff", "-p", "--binary").AddDynamicArguments(base + "..." + head). - Run(&RunOpts{ - Dir: repo.Path, - Stdout: w, - Stderr: stderr, - }) - if err != nil && bytes.Contains(stderr.Bytes(), []byte("no merge base")) { - return repo.GetDiffBinary(base, head, w) - } - return err -} - // ReadPatchCommit will check if a diff patch exists and return stats func (repo *Repository) ReadPatchCommit(prID int64) (commitSHA string, err error) { // Migrated repositories download patches to "pulls" location diff --git a/modules/git/repo_compare_test.go b/modules/git/repo_compare_test.go index 454ed6b9f85..25ee4c51985 100644 --- a/modules/git/repo_compare_test.go +++ b/modules/git/repo_compare_test.go @@ -28,7 +28,7 @@ func TestGetFormatPatch(t *testing.T) { defer repo.Close() rd := &bytes.Buffer{} - err = repo.GetPatch("8d92fc95^", "8d92fc95", rd) + err = repo.GetPatch("8d92fc95^...8d92fc95", rd) if err != nil { assert.NoError(t, err) return diff --git a/modules/git/submodule.go b/modules/git/submodule.go new file mode 100644 index 00000000000..017b644052b --- /dev/null +++ b/modules/git/submodule.go @@ -0,0 +1,66 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "bufio" + "context" + "fmt" + "os" + + "code.gitea.io/gitea/modules/log" +) + +type TemplateSubmoduleCommit struct { + Path string + Commit string +} + +// GetTemplateSubmoduleCommits returns a list of submodules paths and their commits from a repository +// This function is only for generating new repos based on existing template, the template couldn't be too large. +func GetTemplateSubmoduleCommits(ctx context.Context, repoPath string) (submoduleCommits []TemplateSubmoduleCommit, _ error) { + stdoutReader, stdoutWriter, err := os.Pipe() + if err != nil { + return nil, err + } + opts := &RunOpts{ + Dir: repoPath, + Stdout: stdoutWriter, + PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error { + _ = stdoutWriter.Close() + defer stdoutReader.Close() + + scanner := bufio.NewScanner(stdoutReader) + for scanner.Scan() { + entry, err := parseLsTreeLine(scanner.Bytes()) + if err != nil { + cancel() + return err + } + if entry.EntryMode == EntryModeCommit { + submoduleCommits = append(submoduleCommits, TemplateSubmoduleCommit{Path: entry.Name, Commit: entry.ID.String()}) + } + } + return scanner.Err() + }, + } + err = NewCommand(ctx, "ls-tree", "-r", "--", "HEAD").Run(opts) + if err != nil { + return nil, fmt.Errorf("GetTemplateSubmoduleCommits: error running git ls-tree: %v", err) + } + return submoduleCommits, nil +} + +// AddTemplateSubmoduleIndexes Adds the given submodules to the git index. +// It is only for generating new repos based on existing template, requires the .gitmodules file to be already present in the work dir. +func AddTemplateSubmoduleIndexes(ctx context.Context, repoPath string, submodules []TemplateSubmoduleCommit) error { + for _, submodule := range submodules { + cmd := NewCommand(ctx, "update-index", "--add", "--cacheinfo", "160000").AddDynamicArguments(submodule.Commit, submodule.Path) + if stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath}); err != nil { + log.Error("Unable to add %s as submodule to repo %s: stdout %s\nError: %v", submodule.Path, repoPath, stdout, err) + return err + } + } + return nil +} diff --git a/modules/git/submodule_test.go b/modules/git/submodule_test.go new file mode 100644 index 00000000000..d53946a27d4 --- /dev/null +++ b/modules/git/submodule_test.go @@ -0,0 +1,48 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetTemplateSubmoduleCommits(t *testing.T) { + testRepoPath := filepath.Join(testReposDir, "repo4_submodules") + submodules, err := GetTemplateSubmoduleCommits(DefaultContext, testRepoPath) + require.NoError(t, err) + + assert.Len(t, submodules, 2) + + assert.EqualValues(t, "<°)))><", submodules[0].Path) + assert.EqualValues(t, "d2932de67963f23d43e1c7ecf20173e92ee6c43c", submodules[0].Commit) + + assert.EqualValues(t, "libtest", submodules[1].Path) + assert.EqualValues(t, "1234567890123456789012345678901234567890", submodules[1].Commit) +} + +func TestAddTemplateSubmoduleIndexes(t *testing.T) { + ctx := context.Background() + tmpDir := t.TempDir() + var err error + _, _, err = NewCommand(ctx, "init").RunStdString(&RunOpts{Dir: tmpDir}) + require.NoError(t, err) + _ = os.Mkdir(filepath.Join(tmpDir, "new-dir"), 0o755) + err = AddTemplateSubmoduleIndexes(ctx, tmpDir, []TemplateSubmoduleCommit{{Path: "new-dir", Commit: "1234567890123456789012345678901234567890"}}) + require.NoError(t, err) + _, _, err = NewCommand(ctx, "add", "--all").RunStdString(&RunOpts{Dir: tmpDir}) + require.NoError(t, err) + _, _, err = NewCommand(ctx, "-c", "user.name=a", "-c", "user.email=b", "commit", "-m=test").RunStdString(&RunOpts{Dir: tmpDir}) + require.NoError(t, err) + submodules, err := GetTemplateSubmoduleCommits(DefaultContext, tmpDir) + require.NoError(t, err) + assert.Len(t, submodules, 1) + assert.EqualValues(t, "new-dir", submodules[0].Path) + assert.EqualValues(t, "1234567890123456789012345678901234567890", submodules[0].Commit) +} diff --git a/modules/git/tests/repos/repo4_submodules/HEAD b/modules/git/tests/repos/repo4_submodules/HEAD new file mode 100644 index 00000000000..cb089cd89a7 --- /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 00000000000..07d359d07cf --- /dev/null +++ b/modules/git/tests/repos/repo4_submodules/config @@ -0,0 +1,4 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = true diff --git a/modules/git/tests/repos/repo4_submodules/objects/97/c3d30df0e6492348292600920a6482feaebb74 b/modules/git/tests/repos/repo4_submodules/objects/97/c3d30df0e6492348292600920a6482feaebb74 new file mode 100644 index 00000000000..7596090b49f Binary files /dev/null and b/modules/git/tests/repos/repo4_submodules/objects/97/c3d30df0e6492348292600920a6482feaebb74 differ diff --git a/modules/git/tests/repos/repo4_submodules/objects/c7/e064ed49b44523cba8a5dfbc37d2ce1bb41d34 b/modules/git/tests/repos/repo4_submodules/objects/c7/e064ed49b44523cba8a5dfbc37d2ce1bb41d34 new file mode 100644 index 00000000000..e3a13c156dc Binary files /dev/null and b/modules/git/tests/repos/repo4_submodules/objects/c7/e064ed49b44523cba8a5dfbc37d2ce1bb41d34 differ diff --git a/modules/git/tests/repos/repo4_submodules/objects/e1/e59caba97193d48862d6809912043871f37437 b/modules/git/tests/repos/repo4_submodules/objects/e1/e59caba97193d48862d6809912043871f37437 new file mode 100644 index 00000000000..a8d6e5c17c8 --- /dev/null +++ b/modules/git/tests/repos/repo4_submodules/objects/e1/e59caba97193d48862d6809912043871f37437 @@ -0,0 +1,2 @@ +x[ +Â0EýÎ*æ_é$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 00000000000..102bc34da8c --- /dev/null +++ b/modules/git/tests/repos/repo4_submodules/refs/heads/master @@ -0,0 +1 @@ +e1e59caba97193d48862d6809912043871f37437 diff --git a/modules/git/tree.go b/modules/git/tree.go index 1da4a9fa5de..5a644f6c87a 100644 --- a/modules/git/tree.go +++ b/modules/git/tree.go @@ -17,7 +17,7 @@ func NewTree(repo *Repository, id ObjectID) *Tree { } } -// SubTree get a sub tree by the sub dir path +// SubTree get a subtree by the sub dir path func (t *Tree) SubTree(rpath string) (*Tree, error) { if len(rpath) == 0 { return t, nil @@ -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 92d3d107a74..b7bcf40edd2 100644 --- a/modules/git/tree_blob_nogogit.go +++ b/modules/git/tree_blob_nogogit.go @@ -17,7 +17,6 @@ func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) { ptree: t, ID: t.ID, name: "", - fullName: "", entryMode: EntryModeTree, }, nil } diff --git a/modules/git/tree_entry_nogogit.go b/modules/git/tree_entry_nogogit.go index 1c3bcd197a0..81fb638d56f 100644 --- a/modules/git/tree_entry_nogogit.go +++ b/modules/git/tree_entry_nogogit.go @@ -9,23 +9,17 @@ import "code.gitea.io/gitea/modules/log" // TreeEntry the leaf in the git tree type TreeEntry struct { - ID ObjectID - + ID ObjectID ptree *Tree entryMode EntryMode name string - - size int64 - sized bool - fullName string + size int64 + sized bool } // Name returns the name of the entry func (te *TreeEntry) Name() string { - if te.fullName != "" { - return te.fullName - } return te.name } diff --git a/modules/git/tree_test.go b/modules/git/tree_test.go index 6d2b5c84d50..5fee64b0387 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/git/url/url.go b/modules/git/url/url.go index 637685183e5..1c5e8377a6c 100644 --- a/modules/git/url/url.go +++ b/modules/git/url/url.go @@ -4,9 +4,15 @@ package url import ( + "context" "fmt" + "net" stdurl "net/url" "strings" + + "code.gitea.io/gitea/modules/httplib" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" ) // ErrWrongURLFormat represents an error with wrong url format @@ -21,7 +27,7 @@ func (err ErrWrongURLFormat) Error() string { // GitURL represents a git URL type GitURL struct { *stdurl.URL - extraMark int // 0 no extra 1 scp 2 file path with no prefix + extraMark int // 0: standard URL with scheme, 1: scp short syntax (no scheme), 2: file path with no prefix } // String returns the URL's string @@ -38,8 +44,11 @@ func (u *GitURL) String() string { } } -// Parse parse all kinds of git URL -func Parse(remote string) (*GitURL, error) { +// ParseGitURL parse all kinds of git URL: +// * Full URL: http://git@host/path, http://git@host:port/path +// * SCP short syntax: git@host:/path +// * File path: /dir/repo/path +func ParseGitURL(remote string) (*GitURL, error) { if strings.Contains(remote, "://") { u, err := stdurl.Parse(remote) if err != nil { @@ -87,3 +96,86 @@ func Parse(remote string) (*GitURL, error) { extraMark: 2, }, nil } + +type RepositoryURL struct { + GitURL *GitURL + + // if the URL belongs to current Gitea instance, then the below fields have values + OwnerName string + RepoName string + RemainingPath string +} + +// ParseRepositoryURL tries to parse a Git URL and extract the owner/repository name if it belongs to current Gitea instance. +func ParseRepositoryURL(ctx context.Context, repoURL string) (*RepositoryURL, error) { + // possible urls for git: + // https://my.domain/sub-path//[.git] + // git+ssh://user@my.domain//[.git] + // ssh://user@my.domain//[.git] + // user@my.domain:/[.git] + parsed, err := ParseGitURL(repoURL) + if err != nil { + return nil, err + } + + ret := &RepositoryURL{} + ret.GitURL = parsed + + fillPathParts := func(s string) { + s = strings.TrimPrefix(s, "/") + fields := strings.SplitN(s, "/", 3) + if len(fields) >= 2 { + ret.OwnerName = fields[0] + ret.RepoName = strings.TrimSuffix(fields[1], ".git") + if len(fields) == 3 { + ret.RemainingPath = "/" + fields[2] + } + } + } + + if parsed.URL.Scheme == "http" || parsed.URL.Scheme == "https" { + if !httplib.IsCurrentGiteaSiteURL(ctx, repoURL) { + return ret, nil + } + fillPathParts(strings.TrimPrefix(parsed.URL.Path, setting.AppSubURL)) + } else if parsed.URL.Scheme == "ssh" || parsed.URL.Scheme == "git+ssh" { + domainSSH := setting.SSH.Domain + domainCur := httplib.GuessCurrentHostDomain(ctx) + urlDomain, _, _ := net.SplitHostPort(parsed.URL.Host) + urlDomain = util.IfZero(urlDomain, parsed.URL.Host) + if urlDomain == "" { + return ret, nil + } + // check whether URL domain is the App domain + domainMatches := domainSSH == urlDomain + // check whether URL domain is current domain from context + domainMatches = domainMatches || (domainCur != "" && domainCur == urlDomain) + if domainMatches { + fillPathParts(parsed.URL.Path) + } + } + return ret, nil +} + +// MakeRepositoryWebLink generates a web link (http/https) for a git repository (by guessing sometimes) +func MakeRepositoryWebLink(repoURL *RepositoryURL) string { + if repoURL.OwnerName != "" { + return setting.AppSubURL + "/" + repoURL.OwnerName + "/" + repoURL.RepoName + } + + // now, let's guess, for example: + // * git@github.com:owner/submodule.git + // * https://github.com/example/submodule1.git + if repoURL.GitURL.Scheme == "http" || repoURL.GitURL.Scheme == "https" { + return strings.TrimSuffix(repoURL.GitURL.String(), ".git") + } else if repoURL.GitURL.Scheme == "ssh" || repoURL.GitURL.Scheme == "git+ssh" { + hostname, _, _ := net.SplitHostPort(repoURL.GitURL.Host) + hostname = util.IfZero(hostname, repoURL.GitURL.Host) + urlPath := strings.TrimSuffix(repoURL.GitURL.Path, ".git") + urlPath = strings.TrimPrefix(urlPath, "/") + urlFull := fmt.Sprintf("https://%s/%s", hostname, urlPath) + urlFull = strings.TrimSuffix(urlFull, "/") + return urlFull + } + return "" +} diff --git a/modules/git/url/url_test.go b/modules/git/url/url_test.go index da820ed8897..9c020adb4d9 100644 --- a/modules/git/url/url_test.go +++ b/modules/git/url/url_test.go @@ -4,9 +4,15 @@ package url import ( + "context" + "net/http" "net/url" "testing" + "code.gitea.io/gitea/modules/httplib" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + "github.com/stretchr/testify/assert" ) @@ -157,10 +163,105 @@ func TestParseGitURLs(t *testing.T) { for _, kase := range kases { t.Run(kase.kase, func(t *testing.T) { - u, err := Parse(kase.kase) + u, err := ParseGitURL(kase.kase) assert.NoError(t, err) assert.EqualValues(t, kase.expected.extraMark, u.extraMark) assert.EqualValues(t, *kase.expected, *u) }) } } + +func TestParseRepositoryURL(t *testing.T) { + defer test.MockVariableValue(&setting.AppURL, "https://localhost:3000")() + defer test.MockVariableValue(&setting.SSH.Domain, "try.gitea.io")() + + ctxURL, _ := url.Parse("https://gitea") + ctxReq := &http.Request{URL: ctxURL, Header: http.Header{}} + ctxReq.Host = ctxURL.Host + ctxReq.Header.Add("X-Forwarded-Proto", ctxURL.Scheme) + ctx := context.WithValue(context.Background(), httplib.RequestContextKey, ctxReq) + cases := []struct { + input string + ownerName, repoName, remaining string + }{ + {input: "/user/repo"}, + + {input: "https://localhost:3000/user/repo", ownerName: "user", repoName: "repo"}, + {input: "https://external:3000/user/repo"}, + + {input: "https://localhost:3000/user/repo.git/other", ownerName: "user", repoName: "repo", remaining: "/other"}, + + {input: "https://gitea/user/repo", ownerName: "user", repoName: "repo"}, + {input: "https://gitea:3333/user/repo"}, + + {input: "ssh://try.gitea.io:2222/user/repo", ownerName: "user", repoName: "repo"}, + {input: "ssh://external:2222/user/repo"}, + + {input: "git+ssh://user@try.gitea.io/user/repo.git", ownerName: "user", repoName: "repo"}, + {input: "git+ssh://user@external/user/repo.git"}, + + {input: "root@try.gitea.io:user/repo.git", ownerName: "user", repoName: "repo"}, + {input: "root@gitea:user/repo.git", ownerName: "user", repoName: "repo"}, + {input: "root@external:user/repo.git"}, + } + + for _, c := range cases { + t.Run(c.input, func(t *testing.T) { + ret, _ := ParseRepositoryURL(ctx, c.input) + assert.Equal(t, c.ownerName, ret.OwnerName) + assert.Equal(t, c.repoName, ret.RepoName) + assert.Equal(t, c.remaining, ret.RemainingPath) + }) + } + + t.Run("WithSubpath", func(t *testing.T) { + defer test.MockVariableValue(&setting.AppURL, "https://localhost:3000/subpath")() + defer test.MockVariableValue(&setting.AppSubURL, "/subpath")() + cases = []struct { + input string + ownerName, repoName, remaining string + }{ + {input: "https://localhost:3000/user/repo"}, + {input: "https://localhost:3000/subpath/user/repo.git/other", ownerName: "user", repoName: "repo", remaining: "/other"}, + + {input: "ssh://try.gitea.io:2222/user/repo", ownerName: "user", repoName: "repo"}, + {input: "ssh://external:2222/user/repo"}, + + {input: "git+ssh://user@try.gitea.io/user/repo.git", ownerName: "user", repoName: "repo"}, + {input: "git+ssh://user@external/user/repo.git"}, + + {input: "root@try.gitea.io:user/repo.git", ownerName: "user", repoName: "repo"}, + {input: "root@external:user/repo.git"}, + } + + for _, c := range cases { + t.Run(c.input, func(t *testing.T) { + ret, _ := ParseRepositoryURL(ctx, c.input) + assert.Equal(t, c.ownerName, ret.OwnerName) + assert.Equal(t, c.repoName, ret.RepoName) + assert.Equal(t, c.remaining, ret.RemainingPath) + }) + } + }) +} + +func TestMakeRepositoryBaseLink(t *testing.T) { + defer test.MockVariableValue(&setting.AppURL, "https://localhost:3000/subpath")() + defer test.MockVariableValue(&setting.AppSubURL, "/subpath")() + + u, err := ParseRepositoryURL(context.Background(), "https://localhost:3000/subpath/user/repo.git") + assert.NoError(t, err) + assert.Equal(t, "/subpath/user/repo", MakeRepositoryWebLink(u)) + + u, err = ParseRepositoryURL(context.Background(), "https://github.com/owner/repo.git") + assert.NoError(t, err) + assert.Equal(t, "https://github.com/owner/repo", MakeRepositoryWebLink(u)) + + u, err = ParseRepositoryURL(context.Background(), "git@github.com:owner/repo.git") + assert.NoError(t, err) + assert.Equal(t, "https://github.com/owner/repo", MakeRepositoryWebLink(u)) + + u, err = ParseRepositoryURL(context.Background(), "git+ssh://other:123/owner/repo.git") + assert.NoError(t, err) + assert.Equal(t, "https://other/owner/repo", MakeRepositoryWebLink(u)) +} diff --git a/modules/gitrepo/gitrepo.go b/modules/gitrepo/gitrepo.go index 14d809aedbe..540b7244892 100644 --- a/modules/gitrepo/gitrepo.go +++ b/modules/gitrepo/gitrepo.go @@ -10,6 +10,7 @@ import ( "strings" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" ) @@ -38,63 +39,33 @@ func OpenWikiRepository(ctx context.Context, repo Repository) (*git.Repository, // contextKey is a value for use with context.WithValue. type contextKey struct { - name string -} - -// RepositoryContextKey is a context key. It is used with context.Value() to get the current Repository for the context -var RepositoryContextKey = &contextKey{"repository"} - -// RepositoryFromContext attempts to get the repository from the context -func repositoryFromContext(ctx context.Context, repo Repository) *git.Repository { - value := ctx.Value(RepositoryContextKey) - if value == nil { - return nil - } - - if gitRepo, ok := value.(*git.Repository); ok && gitRepo != nil { - if gitRepo.Path == repoPath(repo) { - return gitRepo - } - } - - return nil + repoPath string } // 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) { - gitRepo := repositoryFromContext(ctx, repo) - if gitRepo != nil { - return gitRepo, util.NopCloser{}, nil + 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 } -// repositoryFromContextPath attempts to get the repository from the context -func repositoryFromContextPath(ctx context.Context, path string) *git.Repository { - value := ctx.Value(RepositoryContextKey) - if value == nil { - return nil +// 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 } - - if repo, ok := value.(*git.Repository); ok && repo != nil { - if repo.Path == path { - return repo - } + gitRepo, err := git.OpenRepository(ctx, ck.repoPath) + if err != nil { + return nil, err } - - return nil -} - -// RepositoryFromContextOrOpenPath attempts to get the repository from the context or just opens it -// Deprecated: Use RepositoryFromContextOrOpen instead -func RepositoryFromContextOrOpenPath(ctx context.Context, path string) (*git.Repository, io.Closer, error) { - gitRepo := repositoryFromContextPath(ctx, path) - if gitRepo != nil { - return gitRepo, util.NopCloser{}, nil - } - - gitRepo, err := git.OpenRepository(ctx, path) - return gitRepo, gitRepo, err + ctx.AddCloser(gitRepo) + ctx.SetContextValue(ck, gitRepo) + return gitRepo, nil } diff --git a/modules/gitrepo/walk_gogit.go b/modules/gitrepo/walk_gogit.go index 6370faf08e7..709897ba0cf 100644 --- a/modules/gitrepo/walk_gogit.go +++ b/modules/gitrepo/walk_gogit.go @@ -14,15 +14,11 @@ import ( // WalkReferences walks all the references from the repository // refname is empty, ObjectTag or ObjectBranch. All other values should be treated as equivalent to empty. func WalkReferences(ctx context.Context, repo Repository, walkfn func(sha1, refname string) error) (int, error) { - gitRepo := repositoryFromContext(ctx, repo) - if gitRepo == nil { - var err error - gitRepo, err = OpenRepository(ctx, repo) - if err != nil { - return 0, err - } - defer gitRepo.Close() + gitRepo, closer, err := RepositoryFromContextOrOpen(ctx, repo) + if err != nil { + return 0, err } + defer closer.Close() i := 0 iter, err := gitRepo.GoGitRepo().References() diff --git a/modules/htmlutil/html.go b/modules/htmlutil/html.go index 9b5f5a92d89..0ab0e71689d 100644 --- a/modules/htmlutil/html.go +++ b/modules/htmlutil/html.go @@ -30,7 +30,7 @@ func ParseSizeAndClass(defaultSize int, defaultClass string, others ...any) (int return size, class } -func HTMLFormat(s string, rawArgs ...any) template.HTML { +func HTMLFormat(s template.HTML, rawArgs ...any) template.HTML { args := slices.Clone(rawArgs) for i, v := range args { switch v := v.(type) { @@ -44,5 +44,5 @@ func HTMLFormat(s string, rawArgs ...any) template.HTML { args[i] = template.HTMLEscapeString(fmt.Sprint(v)) } } - return template.HTML(fmt.Sprintf(s, args...)) + return template.HTML(fmt.Sprintf(string(s), args...)) } diff --git a/modules/httplib/url.go b/modules/httplib/url.go index e3bad1e5fba..f543c091906 100644 --- a/modules/httplib/url.go +++ b/modules/httplib/url.go @@ -5,6 +5,7 @@ package httplib import ( "context" + "net" "net/http" "net/url" "strings" @@ -81,6 +82,12 @@ func GuessCurrentHostURL(ctx context.Context) string { return reqScheme + "://" + req.Host } +func GuessCurrentHostDomain(ctx context.Context) string { + _, host, _ := strings.Cut(GuessCurrentHostURL(ctx), "://") + domain, _, _ := net.SplitHostPort(host) + return util.IfZero(domain, host) +} + // MakeAbsoluteURL tries to make a link to an absolute URL: // * If link is empty, it returns the current app URL. // * If link is absolute, it returns the link. @@ -105,7 +112,7 @@ func IsCurrentGiteaSiteURL(ctx context.Context, s string) bool { if cleanedPath == "" || cleanedPath == "." { u.Path = "/" } else { - u.Path += "/" + cleanedPath + "/" + u.Path = "/" + cleanedPath + "/" } } if urlIsRelative(s, u) { diff --git a/modules/indexer/code/indexer.go b/modules/indexer/code/indexer.go index c1ab26569c6..728b37fab6e 100644 --- a/modules/indexer/code/indexer.go +++ b/modules/indexer/code/indexer.go @@ -123,13 +123,12 @@ func Init() { for _, indexerData := range items { log.Trace("IndexerData Process Repo: %d", indexerData.RepoID) if err := index(ctx, indexer, indexerData.RepoID); err != nil { - unhandled = append(unhandled, indexerData) if !setting.IsInTesting { log.Error("Codes indexer handler: index error for repo %v: %v", indexerData.RepoID, err) } } } - return unhandled + return nil // do not re-queue the failed items, otherwise some broken repo will block the queue } indexerQueue = queue.CreateUniqueQueue(ctx, "code_indexer", handler) diff --git a/modules/indexer/code/indexer_test.go b/modules/indexer/code/indexer_test.go index d04088531ac..f358bbe7851 100644 --- a/modules/indexer/code/indexer_test.go +++ b/modules/indexer/code/indexer_test.go @@ -15,6 +15,8 @@ import ( "code.gitea.io/gitea/modules/indexer/code/bleve" "code.gitea.io/gitea/modules/indexer/code/elasticsearch" "code.gitea.io/gitea/modules/indexer/code/internal" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" _ "code.gitea.io/gitea/models" _ "code.gitea.io/gitea/models/actions" @@ -279,7 +281,7 @@ func testIndexer(name string, t *testing.T, indexer internal.Indexer) { func TestBleveIndexAndSearch(t *testing.T) { unittest.PrepareTestEnv(t) - + defer test.MockVariableValue(&setting.Indexer.TypeBleveMaxFuzzniess, 2)() dir := t.TempDir() idx := bleve.NewIndexer(dir) diff --git a/modules/indexer/internal/bleve/util.go b/modules/indexer/internal/bleve/util.go index a0c3dc4ad45..b6daa9e14b4 100644 --- a/modules/indexer/internal/bleve/util.go +++ b/modules/indexer/internal/bleve/util.go @@ -9,6 +9,7 @@ import ( "unicode" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "github.com/blevesearch/bleve/v2" @@ -54,9 +55,9 @@ func openIndexer(path string, latestVersion int) (bleve.Index, int, error) { return index, 0, nil } -// This method test the GuessFuzzinessByKeyword method. The fuzziness is based on the levenshtein distance and determines how many chars -// may be different on two string and they still be considered equivalent. -// Given a phrasse, its shortest word determines its fuzziness. If a phrase uses CJK (eg: `갃갃갃` `å•Šå•Šå•Š`), the fuzziness is zero. +// GuessFuzzinessByKeyword guesses fuzziness based on the levenshtein distance and determines how many chars +// may be different on two string, and they still be considered equivalent. +// Given a phrase, its shortest word determines its fuzziness. If a phrase uses CJK (eg: `갃갃갃` `å•Šå•Šå•Š`), the fuzziness is zero. func GuessFuzzinessByKeyword(s string) int { tokenizer := unicode_tokenizer.NewUnicodeTokenizer() tokens := tokenizer.Tokenize([]byte(s)) @@ -85,5 +86,5 @@ func guessFuzzinessByKeyword(s string) int { return 0 } } - return min(maxFuzziness, len(s)/4) + return min(min(setting.Indexer.TypeBleveMaxFuzzniess, maxFuzziness), len(s)/4) } diff --git a/modules/indexer/internal/bleve/util_test.go b/modules/indexer/internal/bleve/util_test.go index 8f7844464e7..1a7e4db0f42 100644 --- a/modules/indexer/internal/bleve/util_test.go +++ b/modules/indexer/internal/bleve/util_test.go @@ -7,10 +7,15 @@ import ( "fmt" "testing" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + "github.com/stretchr/testify/assert" ) func TestBleveGuessFuzzinessByKeyword(t *testing.T) { + defer test.MockVariableValue(&setting.Indexer.TypeBleveMaxFuzzniess, 2)() + scenarios := []struct { Input string Fuzziness int // See util.go for the definition of fuzziness in this particular context @@ -46,7 +51,7 @@ func TestBleveGuessFuzzinessByKeyword(t *testing.T) { } for _, scenario := range scenarios { - t.Run(fmt.Sprintf("ensure fuzziness of '%s' is '%d'", scenario.Input, scenario.Fuzziness), func(t *testing.T) { + t.Run(fmt.Sprintf("Fuziniess:%s=%d", scenario.Input, scenario.Fuzziness), func(t *testing.T) { assert.Equal(t, scenario.Fuzziness, GuessFuzzinessByKeyword(scenario.Input)) }) } diff --git a/modules/issue/template/unmarshal.go b/modules/issue/template/unmarshal.go index 0fc13d7ddfc..1d8e9dd02d9 100644 --- a/modules/issue/template/unmarshal.go +++ b/modules/issue/template/unmarshal.go @@ -109,7 +109,7 @@ func unmarshal(filename string, content []byte) (*api.IssueTemplate, error) { it.Content = string(content) it.Name = path.Base(it.FileName) // paths in Git are always '/' separated - do not use filepath! - it.About, _ = util.SplitStringAtByteN(it.Content, 80) + it.About = util.EllipsisDisplayString(it.Content, 80) } else { it.Content = templateBody if it.About == "" { diff --git a/modules/markup/html_commit.go b/modules/markup/html_commit.go index 358e7b06ba5..aa1b7d034a5 100644 --- a/modules/markup/html_commit.go +++ b/modules/markup/html_commit.go @@ -8,6 +8,7 @@ import ( "strings" "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/references" "code.gitea.io/gitea/modules/util" "golang.org/x/net/html" @@ -194,3 +195,21 @@ func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) { node = node.NextSibling.NextSibling } } + +func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) { + next := node.NextSibling + + for node != nil && node != next { + found, ref := references.FindRenderizableCommitCrossReference(node.Data) + if !found { + return + } + + reftext := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha) + linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(ref.Owner, ref.Name, "commit", ref.CommitSha), LinkTypeApp) + link := createLink(ctx, linkHref, reftext, "commit") + + replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link) + node = node.NextSibling.NextSibling + } +} diff --git a/modules/markup/html_issue.go b/modules/markup/html_issue.go index e64ec76c3d2..7a6f33011a7 100644 --- a/modules/markup/html_issue.go +++ b/modules/markup/html_issue.go @@ -4,9 +4,9 @@ package markup import ( + "strconv" "strings" - "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/references" @@ -16,8 +16,16 @@ import ( "code.gitea.io/gitea/modules/util" "golang.org/x/net/html" + "golang.org/x/net/html/atom" ) +type RenderIssueIconTitleOptions struct { + OwnerName string + RepoName string + LinkHref string + IssueIndex int64 +} + func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) { if ctx.RenderOptions.Metas == nil { return @@ -66,6 +74,27 @@ func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) { } } +func createIssueLinkContentWithSummary(ctx *RenderContext, linkHref string, ref *references.RenderizableReference) *html.Node { + if DefaultRenderHelperFuncs.RenderRepoIssueIconTitle == nil { + return nil + } + issueIndex, _ := strconv.ParseInt(ref.Issue, 10, 64) + h, err := DefaultRenderHelperFuncs.RenderRepoIssueIconTitle(ctx, RenderIssueIconTitleOptions{ + OwnerName: ref.Owner, + RepoName: ref.Name, + LinkHref: linkHref, + IssueIndex: issueIndex, + }) + if err != nil { + log.Error("RenderRepoIssueIconTitle failed: %v", err) + return nil + } + if h == "" { + return nil + } + return &html.Node{Type: html.RawNode, Data: string(ctx.RenderInternal.ProtectSafeAttrs(h))} +} + func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) { if ctx.RenderOptions.Metas == nil { return @@ -76,32 +105,28 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) { // old logic: crossLinkOnly := ctx.RenderOptions.Metas["mode"] == "document" && !ctx.IsWiki crossLinkOnly := ctx.RenderOptions.Metas["markupAllowShortIssuePattern"] != "true" - var ( - found bool - ref *references.RenderizableReference - ) + var ref *references.RenderizableReference next := node.NextSibling - for node != nil && node != next { _, hasExtTrackFormat := ctx.RenderOptions.Metas["format"] // Repos with external issue trackers might still need to reference local PRs // We need to concern with the first one that shows up in the text, whichever it is isNumericStyle := ctx.RenderOptions.Metas["style"] == "" || ctx.RenderOptions.Metas["style"] == IssueNameStyleNumeric - foundNumeric, refNumeric := references.FindRenderizableReferenceNumeric(node.Data, hasExtTrackFormat && !isNumericStyle, crossLinkOnly) + refNumeric := references.FindRenderizableReferenceNumeric(node.Data, hasExtTrackFormat && !isNumericStyle, crossLinkOnly) switch ctx.RenderOptions.Metas["style"] { case "", IssueNameStyleNumeric: - found, ref = foundNumeric, refNumeric + ref = refNumeric case IssueNameStyleAlphanumeric: - found, ref = references.FindRenderizableReferenceAlphanumeric(node.Data) + ref = references.FindRenderizableReferenceAlphanumeric(node.Data) case IssueNameStyleRegexp: pattern, err := regexplru.GetCompiled(ctx.RenderOptions.Metas["regexp"]) if err != nil { return } - found, ref = references.FindRenderizableReferenceRegexp(node.Data, pattern) + ref = references.FindRenderizableReferenceRegexp(node.Data, pattern) } // Repos with external issue trackers might still need to reference local PRs @@ -109,17 +134,17 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) { if hasExtTrackFormat && !isNumericStyle && refNumeric != nil { // If numeric (PR) was found, and it was BEFORE the non-numeric pattern, use that // Allow a free-pass when non-numeric pattern wasn't found. - if found && (ref == nil || refNumeric.RefLocation.Start < ref.RefLocation.Start) { - found = foundNumeric + if ref == nil || refNumeric.RefLocation.Start < ref.RefLocation.Start { ref = refNumeric } } - if !found { + + if ref == nil { return } var link *html.Node - reftext := node.Data[ref.RefLocation.Start:ref.RefLocation.End] + refText := node.Data[ref.RefLocation.Start:ref.RefLocation.End] if hasExtTrackFormat && !ref.IsPull { ctx.RenderOptions.Metas["index"] = ref.Issue @@ -129,18 +154,23 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) { log.Error("unable to expand template vars for ref %s, err: %v", ref.Issue, err) } - link = createLink(ctx, res, reftext, "ref-issue ref-external-issue") + link = createLink(ctx, res, refText, "ref-issue ref-external-issue") } else { // Path determines the type of link that will be rendered. It's unknown at this point whether // the linked item is actually a PR or an issue. Luckily it's of no real consequence because // Gitea will redirect on click as appropriate. + issueOwner := util.Iif(ref.Owner == "", ctx.RenderOptions.Metas["user"], ref.Owner) + issueRepo := util.Iif(ref.Owner == "", ctx.RenderOptions.Metas["repo"], ref.Name) issuePath := util.Iif(ref.IsPull, "pulls", "issues") - if ref.Owner == "" { - linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(ctx.RenderOptions.Metas["user"], ctx.RenderOptions.Metas["repo"], issuePath, ref.Issue), LinkTypeApp) - link = createLink(ctx, linkHref, reftext, "ref-issue") - } else { - linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(ref.Owner, ref.Name, issuePath, ref.Issue), LinkTypeApp) - link = createLink(ctx, linkHref, reftext, "ref-issue") + linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(issueOwner, issueRepo, issuePath, ref.Issue), LinkTypeApp) + + // at the moment, only render the issue index in a full line (or simple line) as icon+title + // otherwise it would be too noisy for "take #1 as an example" in a sentence + if node.Parent.DataAtom == atom.Li && ref.RefLocation.Start < 20 && ref.RefLocation.End == len(node.Data) { + link = createIssueLinkContentWithSummary(ctx, linkHref, ref) + } + if link == nil { + link = createLink(ctx, linkHref, refText, "ref-issue") } } @@ -168,21 +198,3 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) { node = node.NextSibling.NextSibling.NextSibling.NextSibling } } - -func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) { - next := node.NextSibling - - for node != nil && node != next { - found, ref := references.FindRenderizableCommitCrossReference(node.Data) - if !found { - return - } - - reftext := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha) - linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(ref.Owner, ref.Name, "commit", ref.CommitSha), LinkTypeApp) - link := createLink(ctx, linkHref, reftext, "commit") - - replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link) - node = node.NextSibling.NextSibling - } -} diff --git a/modules/markup/html_issue_test.go b/modules/markup/html_issue_test.go new file mode 100644 index 00000000000..8d189fbdf62 --- /dev/null +++ b/modules/markup/html_issue_test.go @@ -0,0 +1,72 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package markup_test + +import ( + "context" + "html/template" + "strings" + "testing" + + "code.gitea.io/gitea/modules/htmlutil" + "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/markup/markdown" + testModule "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRender_IssueList(t *testing.T) { + defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)() + markup.Init(&markup.RenderHelperFuncs{ + RenderRepoIssueIconTitle: func(ctx context.Context, opts markup.RenderIssueIconTitleOptions) (template.HTML, error) { + return htmlutil.HTMLFormat("
issue #%d
", opts.IssueIndex), nil + }, + }) + + test := func(input, expected string) { + rctx := markup.NewTestRenderContext(markup.TestAppURL, map[string]string{ + "user": "test-user", "repo": "test-repo", + "markupAllowShortIssuePattern": "true", + }) + out, err := markdown.RenderString(rctx, input) + require.NoError(t, err) + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(out))) + } + + t.Run("NormalIssueRef", func(t *testing.T) { + test( + "#12345", + `

#12345

`, + ) + }) + + t.Run("ListIssueRef", func(t *testing.T) { + test( + "* #12345", + `
    +
  • issue #12345
  • +
`, + ) + }) + + t.Run("ListIssueRefNormal", func(t *testing.T) { + test( + "* foo #12345 bar", + ``, + ) + }) + + t.Run("ListTodoIssueRef", func(t *testing.T) { + test( + "* [ ] #12345", + `
    +
  • issue #12345
  • +
`, + ) + }) +} diff --git a/modules/markup/html_link.go b/modules/markup/html_link.go index 5fd38b63cd3..0e7a988d368 100644 --- a/modules/markup/html_link.go +++ b/modules/markup/html_link.go @@ -9,6 +9,7 @@ import ( "strings" "code.gitea.io/gitea/modules/markup/common" + "code.gitea.io/gitea/modules/util" "golang.org/x/net/html" "golang.org/x/net/html/atom" @@ -171,6 +172,10 @@ func linkProcessor(ctx *RenderContext, node *html.Node) { } uri := node.Data[m[0]:m[1]] + remaining := node.Data[m[1]:] + if util.IsLikelyEllipsisLeftPart(remaining) { + return + } replaceContent(node, m[0], m[1], createLink(ctx, uri, uri, "" /*link*/)) node = node.NextSibling.NextSibling } diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go index 54bd91f3b3b..6d8f24184bd 100644 --- a/modules/markup/html_test.go +++ b/modules/markup/html_test.go @@ -206,6 +206,16 @@ func TestRender_links(t *testing.T) { test( "ftps://gitea.com", `

ftps://gitea.com

`) + + t.Run("LinkEllipsis", func(t *testing.T) { + input := util.EllipsisDisplayString("http://10.1.2.3", 12) + assert.Equal(t, "http://10…", input) + test(input, "

http://10…

") + + input = util.EllipsisDisplayString("http://10.1.2.3", 13) + assert.Equal(t, "http://10.…", input) + test(input, "

http://10.…

") + }) } func TestRender_email(t *testing.T) { diff --git a/modules/markup/internal/renderinternal.go b/modules/markup/internal/renderinternal.go index 4d58f160a92..7a3e37b120f 100644 --- a/modules/markup/internal/renderinternal.go +++ b/modules/markup/internal/renderinternal.go @@ -76,7 +76,7 @@ func (r *RenderInternal) ProtectSafeAttrs(content template.HTML) template.HTML { return template.HTML(reAttrClass().ReplaceAllString(string(content), `$1 data-attr-class="`+r.secureIDPrefix+`$2"$3`)) } -func (r *RenderInternal) FormatWithSafeAttrs(w io.Writer, fmt string, a ...any) error { +func (r *RenderInternal) FormatWithSafeAttrs(w io.Writer, fmt template.HTML, a ...any) error { _, err := w.Write([]byte(r.ProtectSafeAttrs(htmlutil.HTMLFormat(fmt, a...)))) return err } diff --git a/modules/markup/markdown/math/block_renderer.go b/modules/markup/markdown/math/block_renderer.go index c29f0618821..412e4d0dee6 100644 --- a/modules/markup/markdown/math/block_renderer.go +++ b/modules/markup/markdown/math/block_renderer.go @@ -4,6 +4,8 @@ package math import ( + "html/template" + "code.gitea.io/gitea/modules/markup/internal" giteaUtil "code.gitea.io/gitea/modules/util" @@ -50,7 +52,7 @@ func (r *BlockRenderer) renderBlock(w util.BufWriter, source []byte, node gast.N n := node.(*Block) if entering { code := giteaUtil.Iif(n.Inline, "", `
`) + ``
-		_ = r.renderInternal.FormatWithSafeAttrs(w, code)
+		_ = r.renderInternal.FormatWithSafeAttrs(w, template.HTML(code))
 		r.writeLines(w, source, n)
 	} else {
 		_, _ = w.WriteString(`` + giteaUtil.Iif(n.Inline, "", `
`) + "\n") diff --git a/modules/markup/orgmode/orgmode.go b/modules/markup/orgmode/orgmode.go index c6cc3340000..70d02c13216 100644 --- a/modules/markup/orgmode/orgmode.go +++ b/modules/markup/orgmode/orgmode.go @@ -147,7 +147,7 @@ func (r *orgWriter) resolveLink(kind, link string) string { func (r *orgWriter) WriteRegularLink(l org.RegularLink) { link := r.resolveLink(l.Kind(), l.URL) - printHTML := func(html string, a ...any) { + printHTML := func(html template.HTML, a ...any) { _, _ = fmt.Fprint(r, htmlutil.HTMLFormat(html, a...)) } // Inspired by https://github.com/niklasfasching/go-org/blob/6eb20dbda93cb88c3503f7508dc78cbbc639378f/org/html_writer.go#L406-L427 diff --git a/modules/markup/orgmode/orgmode_test.go b/modules/markup/orgmode/orgmode_test.go index e3cc05b4f02..de39bafebea 100644 --- a/modules/markup/orgmode/orgmode_test.go +++ b/modules/markup/orgmode/orgmode_test.go @@ -103,8 +103,8 @@ func HelloWorld() { } #+end_src `, `
-
// HelloWorld prints "Hello World"
-func HelloWorld() {
+
// HelloWorld prints "Hello World"
+func HelloWorld() {
 	fmt.Println("Hello World")
 }
`) diff --git a/modules/markup/render_helper.go b/modules/markup/render_helper.go index 82796ef2745..8ff0e7d6fb4 100644 --- a/modules/markup/render_helper.go +++ b/modules/markup/render_helper.go @@ -38,6 +38,7 @@ type RenderHelper interface { type RenderHelperFuncs struct { IsUsernameMentionable func(ctx context.Context, username string) bool RenderRepoFileCodePreview func(ctx context.Context, options RenderCodePreviewOptions) (template.HTML, error) + RenderRepoIssueIconTitle func(ctx context.Context, options RenderIssueIconTitleOptions) (template.HTML, error) } var DefaultRenderHelperFuncs *RenderHelperFuncs diff --git a/modules/markup/sanitizer_default.go b/modules/markup/sanitizer_default.go index 5eeafe940a4..14161eb5337 100644 --- a/modules/markup/sanitizer_default.go +++ b/modules/markup/sanitizer_default.go @@ -48,7 +48,7 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy { policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(unchecked|checked|indeterminate)$`)).OnElements("li") // Allow 'color' and 'background-color' properties for the style attribute on text elements. - policy.AllowStyles("color", "background-color").OnElements("span", "p") + policy.AllowStyles("color", "background-color").OnElements("div", "span", "p", "tr", "th", "td") policy.AllowAttrs("src", "autoplay", "controls").OnElements("video") diff --git a/modules/packages/maven/metadata.go b/modules/packages/maven/metadata.go index 42aa250718d..a61a62c0862 100644 --- a/modules/packages/maven/metadata.go +++ b/modules/packages/maven/metadata.go @@ -7,6 +7,7 @@ import ( "encoding/xml" "io" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/validation" "golang.org/x/net/html/charset" @@ -31,18 +32,27 @@ type Dependency struct { } type pomStruct struct { - XMLName xml.Name `xml:"project"` - GroupID string `xml:"groupId"` - ArtifactID string `xml:"artifactId"` - Version string `xml:"version"` - Name string `xml:"name"` - Description string `xml:"description"` - URL string `xml:"url"` - Licenses []struct { + XMLName xml.Name `xml:"project"` + + Parent struct { + GroupID string `xml:"groupId"` + ArtifactID string `xml:"artifactId"` + Version string `xml:"version"` + } `xml:"parent"` + + GroupID string `xml:"groupId"` + ArtifactID string `xml:"artifactId"` + Version string `xml:"version"` + Name string `xml:"name"` + Description string `xml:"description"` + URL string `xml:"url"` + + Licenses []struct { Name string `xml:"name"` URL string `xml:"url"` Distribution string `xml:"distribution"` } `xml:"licenses>license"` + Dependencies []struct { GroupID string `xml:"groupId"` ArtifactID string `xml:"artifactId"` @@ -81,8 +91,16 @@ func ParsePackageMetaData(r io.Reader) (*Metadata, error) { }) } + pomGroupID := pom.GroupID + if pomGroupID == "" { + // the current module could inherit parent: https://maven.apache.org/pom.html#Inheritance + pomGroupID = pom.Parent.GroupID + } + if pomGroupID == "" { + return nil, util.ErrInvalidArgument + } return &Metadata{ - GroupID: pom.GroupID, + GroupID: pomGroupID, ArtifactID: pom.ArtifactID, Name: pom.Name, Description: pom.Description, diff --git a/modules/packages/maven/metadata_test.go b/modules/packages/maven/metadata_test.go index e675467730c..2cff2908086 100644 --- a/modules/packages/maven/metadata_test.go +++ b/modules/packages/maven/metadata_test.go @@ -7,7 +7,10 @@ import ( "strings" "testing" + "code.gitea.io/gitea/modules/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "golang.org/x/text/encoding/charmap" ) @@ -86,4 +89,35 @@ func TestParsePackageMetaData(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, m) }) + + t.Run("ParentInherit", func(t *testing.T) { + pom := ` + + 4.0.0 + + com.mycompany.app + my-app + 1.0-SNAPSHOT + + submodule1 + +` + m, err := ParsePackageMetaData(strings.NewReader(pom)) + require.NoError(t, err) + require.NotNil(t, m) + + assert.Equal(t, "com.mycompany.app", m.GroupID) + assert.Equal(t, "submodule1", m.ArtifactID) + }) + + t.Run("ParentInherit", func(t *testing.T) { + pom := ` + + 4.0.0 + + +` + _, err := ParsePackageMetaData(strings.NewReader(pom)) + require.ErrorIs(t, err, util.ErrInvalidArgument) + }) } diff --git a/modules/packages/npm/creator.go b/modules/packages/npm/creator.go index 7d3d7cd6b55..8ba4dbfba71 100644 --- a/modules/packages/npm/creator.go +++ b/modules/packages/npm/creator.go @@ -81,6 +81,7 @@ type PackageMetadataVersion struct { BundleDependencies []string `json:"bundleDependencies,omitempty"` DevDependencies map[string]string `json:"devDependencies,omitempty"` PeerDependencies map[string]string `json:"peerDependencies,omitempty"` + PeerDependenciesMeta map[string]any `json:"peerDependenciesMeta,omitempty"` Bin map[string]string `json:"bin,omitempty"` OptionalDependencies map[string]string `json:"optionalDependencies,omitempty"` Readme string `json:"readme,omitempty"` @@ -222,6 +223,7 @@ func ParsePackage(r io.Reader) (*Package, error) { BundleDependencies: meta.BundleDependencies, DevelopmentDependencies: meta.DevDependencies, PeerDependencies: meta.PeerDependencies, + PeerDependenciesMeta: meta.PeerDependenciesMeta, OptionalDependencies: meta.OptionalDependencies, Bin: meta.Bin, Readme: meta.Readme, diff --git a/modules/packages/npm/metadata.go b/modules/packages/npm/metadata.go index 6bb77f302bb..d1d02633878 100644 --- a/modules/packages/npm/metadata.go +++ b/modules/packages/npm/metadata.go @@ -19,6 +19,7 @@ type Metadata struct { BundleDependencies []string `json:"bundleDependencies,omitempty"` DevelopmentDependencies map[string]string `json:"development_dependencies,omitempty"` PeerDependencies map[string]string `json:"peer_dependencies,omitempty"` + PeerDependenciesMeta map[string]any `json:"peer_dependencies_meta,omitempty"` OptionalDependencies map[string]string `json:"optional_dependencies,omitempty"` Bin map[string]string `json:"bin,omitempty"` Readme string `json:"readme,omitempty"` diff --git a/modules/references/references.go b/modules/references/references.go index dcb70a33d0f..a5b102b7f2e 100644 --- a/modules/references/references.go +++ b/modules/references/references.go @@ -330,22 +330,22 @@ func FindAllIssueReferences(content string) []IssueReference { } // FindRenderizableReferenceNumeric returns the first unvalidated reference found in a string. -func FindRenderizableReferenceNumeric(content string, prOnly, crossLinkOnly bool) (bool, *RenderizableReference) { +func FindRenderizableReferenceNumeric(content string, prOnly, crossLinkOnly bool) *RenderizableReference { var match []int if !crossLinkOnly { match = issueNumericPattern.FindStringSubmatchIndex(content) } if match == nil { if match = crossReferenceIssueNumericPattern.FindStringSubmatchIndex(content); match == nil { - return false, nil + return nil } } r := getCrossReference(util.UnsafeStringToBytes(content), match[2], match[3], false, prOnly) if r == nil { - return false, nil + return nil } - return true, &RenderizableReference{ + return &RenderizableReference{ Issue: r.issue, Owner: r.owner, Name: r.name, @@ -372,15 +372,14 @@ func FindRenderizableCommitCrossReference(content string) (bool, *RenderizableRe } // FindRenderizableReferenceRegexp returns the first regexp unvalidated references found in a string. -func FindRenderizableReferenceRegexp(content string, pattern *regexp.Regexp) (bool, *RenderizableReference) { +func FindRenderizableReferenceRegexp(content string, pattern *regexp.Regexp) *RenderizableReference { match := pattern.FindStringSubmatchIndex(content) if len(match) < 4 { - return false, nil + return nil } action, location := findActionKeywords([]byte(content), match[2]) - - return true, &RenderizableReference{ + return &RenderizableReference{ Issue: content[match[2]:match[3]], RefLocation: &RefSpan{Start: match[0], End: match[1]}, Action: action, @@ -390,15 +389,14 @@ func FindRenderizableReferenceRegexp(content string, pattern *regexp.Regexp) (bo } // FindRenderizableReferenceAlphanumeric returns the first alphanumeric unvalidated references found in a string. -func FindRenderizableReferenceAlphanumeric(content string) (bool, *RenderizableReference) { +func FindRenderizableReferenceAlphanumeric(content string) *RenderizableReference { match := issueAlphanumericPattern.FindStringSubmatchIndex(content) if match == nil { - return false, nil + return nil } action, location := findActionKeywords([]byte(content), match[2]) - - return true, &RenderizableReference{ + return &RenderizableReference{ Issue: content[match[2]:match[3]], RefLocation: &RefSpan{Start: match[2], End: match[3]}, Action: action, diff --git a/modules/references/references_test.go b/modules/references/references_test.go index 27803083c0b..1b6a968d6a1 100644 --- a/modules/references/references_test.go +++ b/modules/references/references_test.go @@ -249,11 +249,10 @@ func TestFindAllIssueReferences(t *testing.T) { } for _, fixture := range alnumFixtures { - found, ref := FindRenderizableReferenceAlphanumeric(fixture.input) + ref := FindRenderizableReferenceAlphanumeric(fixture.input) if fixture.issue == "" { - assert.False(t, found, "Failed to parse: {%s}", fixture.input) + assert.Nil(t, ref, "Failed to parse: {%s}", fixture.input) } else { - assert.True(t, found, "Failed to parse: {%s}", fixture.input) assert.Equal(t, fixture.issue, ref.Issue, "Failed to parse: {%s}", fixture.input) assert.Equal(t, fixture.refLocation, ref.RefLocation, "Failed to parse: {%s}", fixture.input) assert.Equal(t, fixture.action, ref.Action, "Failed to parse: {%s}", fixture.input) diff --git a/modules/repository/branch.go b/modules/repository/branch.go index 2bf9930f19f..4630e70aa8f 100644 --- a/modules/repository/branch.go +++ b/modules/repository/branch.go @@ -6,6 +6,7 @@ package repository import ( "context" "fmt" + "strings" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" @@ -51,6 +52,9 @@ func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository, { branches, _, err := gitRepo.GetBranchNames(0, 0) if err != nil { + if strings.Contains(err.Error(), "ref file is empty") { + return 0, nil + } return 0, err } log.Trace("SyncRepoBranches[%s]: branches[%d]: %v", repo.FullName(), len(branches), branches) diff --git a/modules/repository/init.go b/modules/repository/init.go index 5f500c5233f..24602ae0901 100644 --- a/modules/repository/init.go +++ b/modules/repository/init.go @@ -81,7 +81,7 @@ func LoadRepoConfig() error { if isDir, err := util.IsDir(customPath); err != nil { return fmt.Errorf("failed to check custom %s dir: %w", t, err) } else if isDir { - if typeFiles[i].custom, err = util.StatDir(customPath); err != nil { + if typeFiles[i].custom, err = util.ListDirRecursively(customPath, &util.ListDirOptions{SkipCommonHiddenNames: true}); err != nil { return fmt.Errorf("failed to list custom %s files: %w", t, err) } } diff --git a/modules/reqctx/datastore.go b/modules/reqctx/datastore.go new file mode 100644 index 00000000000..94232450f33 --- /dev/null +++ b/modules/reqctx/datastore.go @@ -0,0 +1,139 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package reqctx + +import ( + "context" + "io" + "sync" + + "code.gitea.io/gitea/modules/process" +) + +type ContextDataProvider interface { + GetData() ContextData +} + +type ContextData map[string]any + +func (ds ContextData) GetData() ContextData { + return ds +} + +func (ds ContextData) MergeFrom(other ContextData) ContextData { + for k, v := range other { + ds[k] = v + } + return ds +} + +// RequestDataStore is a short-lived context-related object that is used to store request-specific data. +type RequestDataStore interface { + GetData() ContextData + SetContextValue(k, v any) + GetContextValue(key any) any + AddCleanUp(f func()) + AddCloser(c io.Closer) +} + +type requestDataStoreKeyType struct{} + +var RequestDataStoreKey requestDataStoreKeyType + +type requestDataStore struct { + data ContextData + + mu sync.RWMutex + values map[any]any + cleanUpFuncs []func() +} + +func (r *requestDataStore) GetContextValue(key any) any { + if key == RequestDataStoreKey { + return r + } + r.mu.RLock() + defer r.mu.RUnlock() + return r.values[key] +} + +func (r *requestDataStore) SetContextValue(k, v any) { + r.mu.Lock() + r.values[k] = v + r.mu.Unlock() +} + +// GetData and the underlying ContextData are not thread-safe, callers should ensure thread-safety. +func (r *requestDataStore) GetData() ContextData { + if r.data == nil { + r.data = make(ContextData) + } + return r.data +} + +func (r *requestDataStore) AddCleanUp(f func()) { + r.mu.Lock() + r.cleanUpFuncs = append(r.cleanUpFuncs, f) + r.mu.Unlock() +} + +func (r *requestDataStore) AddCloser(c io.Closer) { + r.AddCleanUp(func() { _ = c.Close() }) +} + +func (r *requestDataStore) cleanUp() { + for _, f := range r.cleanUpFuncs { + f() + } +} + +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 + } + return nil +} + +type requestContext struct { + context.Context + RequestDataStore +} + +func (c *requestContext) Value(key any) any { + if v := c.GetContextValue(key); v != nil { + return v + } + return c.Context.Value(key) +} + +func NewRequestContext(parentCtx context.Context, profDesc string) (_ context.Context, finished func()) { + ctx, _, processFinished := process.GetManager().AddTypedContext(parentCtx, profDesc, process.RequestProcessType, true) + store := &requestDataStore{values: make(map[any]any)} + reqCtx := &requestContext{Context: ctx, RequestDataStore: store} + return reqCtx, func() { + store.cleanUp() + processFinished() + } +} + +// 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, RequestDataStore: &requestDataStore{values: make(map[any]any)}} +} diff --git a/modules/setting/config_env.go b/modules/setting/config_env.go index dfcb7db3c86..5d94a9641f7 100644 --- a/modules/setting/config_env.go +++ b/modules/setting/config_env.go @@ -166,3 +166,25 @@ func EnvironmentToConfig(cfg ConfigProvider, envs []string) (changed bool) { } return changed } + +// InitGiteaEnvVars initializes the environment variables for gitea +func InitGiteaEnvVars() { + // Ideally Gitea should only accept the environment variables which it clearly knows instead of unsetting the ones it doesn't want, + // but the ideal behavior would be a breaking change, and it seems not bringing enough benefits to end users, + // so at the moment we could still keep "unsetting the unnecessary environments" + + // HOME is managed by Gitea, Gitea's git should use "HOME/.gitconfig". + // But git would try "XDG_CONFIG_HOME/git/config" first if "HOME/.gitconfig" does not exist, + // then our git.InitFull would still write to "XDG_CONFIG_HOME/git/config" if XDG_CONFIG_HOME is set. + _ = os.Unsetenv("XDG_CONFIG_HOME") +} + +func InitGiteaEnvVarsForTesting() { + InitGiteaEnvVars() + _ = os.Unsetenv("GIT_AUTHOR_NAME") + _ = os.Unsetenv("GIT_AUTHOR_EMAIL") + _ = os.Unsetenv("GIT_AUTHOR_DATE") + _ = os.Unsetenv("GIT_COMMITTER_NAME") + _ = os.Unsetenv("GIT_COMMITTER_EMAIL") + _ = os.Unsetenv("GIT_COMMITTER_DATE") +} diff --git a/modules/setting/cors.go b/modules/setting/cors.go index 63daaad60ba..5260887d9d9 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 18585602c3d..e34baae012b 100644 --- a/modules/setting/indexer.go +++ b/modules/setting/indexer.go @@ -31,6 +31,8 @@ var Indexer = struct { IncludePatterns []*GlobMatcher ExcludePatterns []*GlobMatcher ExcludeVendored bool + + TypeBleveMaxFuzzniess int }{ IssueType: "bleve", IssuePath: "indexers/issues.bleve", @@ -88,6 +90,7 @@ func loadIndexerFrom(rootCfg ConfigProvider) { Indexer.ExcludeVendored = sec.Key("REPO_INDEXER_EXCLUDE_VENDORED").MustBool(true) Indexer.MaxIndexerFileSize = sec.Key("MAX_FILE_SIZE").MustInt64(1024 * 1024) Indexer.StartupTimeout = sec.Key("STARTUP_TIMEOUT").MustDuration(30 * time.Second) + Indexer.TypeBleveMaxFuzzniess = sec.Key("TYPE_BLEVE_MAX_FUZZINESS").MustInt(0) } // IndexerGlobFromString parses a comma separated list of patterns and returns a glob.Glob slice suited for repo indexing @@ -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 d4db55dc7bc..4c3dff68509 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 3d12fcf8d9f..2f798b75c7e 100644 --- a/modules/setting/security.go +++ b/modules/setting/security.go @@ -13,8 +13,9 @@ import ( "code.gitea.io/gitea/modules/log" ) +// Security settings + var ( - // Security settings InstallLock bool SecretKey string InternalToken string // internal access token @@ -27,7 +28,7 @@ var ( ReverseProxyTrustedProxies []string MinPasswordLength int ImportLocalPaths bool - DisableGitHooks bool + DisableGitHooks = true DisableWebhooks bool OnlyAllowPushIfGiteaEnvironmentSet bool PasswordComplexity []string diff --git a/modules/setting/session.go b/modules/setting/session.go index afe63bfdb78..19a05ce2c2a 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/setting.go b/modules/setting/setting.go index c93d199b1b6..20da796b58d 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -235,3 +235,9 @@ func checkOverlappedPath(name, path string) { } configuredPaths[path] = name } + +func PanicInDevOrTesting(msg string, a ...any) { + if !IsProd || IsInTesting { + panic(fmt.Sprintf(msg, a...)) + } +} diff --git a/modules/setting/time.go b/modules/setting/time.go index 39acba12ef1..97988211a98 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/setting/ui.go b/modules/setting/ui.go index db0fe9ef79a..20fc612b43a 100644 --- a/modules/setting/ui.go +++ b/modules/setting/ui.go @@ -63,6 +63,7 @@ var UI = struct { } `ini:"ui.admin"` User struct { RepoPagingNum int + OrgPagingNum int } `ini:"ui.user"` Meta struct { Author string @@ -127,8 +128,10 @@ var UI = struct { }, User: struct { RepoPagingNum int + OrgPagingNum int }{ RepoPagingNum: 15, + OrgPagingNum: 15, }, Meta: struct { Author string diff --git a/modules/storage/azureblob.go b/modules/storage/azureblob.go index 96c2525b29b..837afd0ba62 100644 --- a/modules/storage/azureblob.go +++ b/modules/storage/azureblob.go @@ -70,7 +70,7 @@ func (a *azureBlobObject) Seek(offset int64, whence int) (int64, error) { case io.SeekCurrent: offset += a.offset case io.SeekEnd: - offset = a.Size - offset + offset = a.Size + offset default: return 0, errors.New("Seek: invalid whence") } diff --git a/modules/storage/azureblob_test.go b/modules/storage/azureblob_test.go index 604870cb98e..6905db50081 100644 --- a/modules/storage/azureblob_test.go +++ b/modules/storage/azureblob_test.go @@ -4,6 +4,8 @@ package storage import ( + "bytes" + "io" "os" "testing" @@ -54,3 +56,46 @@ func TestAzureBlobStoragePath(t *testing.T) { assert.Equal(t, "base/a", m.buildAzureBlobPath("/a")) assert.Equal(t, "base/a/b", m.buildAzureBlobPath("/a/b/")) } + +func Test_azureBlobObject(t *testing.T) { + if os.Getenv("CI") == "" { + t.Skip("azureBlobStorage not present outside of CI") + return + } + + s, err := NewStorage(setting.AzureBlobStorageType, &setting.Storage{ + AzureBlobConfig: setting.AzureBlobStorageConfig{ + // https://learn.microsoft.com/azure/storage/common/storage-use-azurite?tabs=visual-studio-code#ip-style-url + Endpoint: "http://devstoreaccount1.azurite.local:10000", + // https://learn.microsoft.com/azure/storage/common/storage-use-azurite?tabs=visual-studio-code#well-known-storage-account-and-key + AccountName: "devstoreaccount1", + AccountKey: "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==", + Container: "test", + }, + }) + assert.NoError(t, err) + + data := "Q2xTckt6Y1hDOWh0" + _, err = s.Save("test.txt", bytes.NewBufferString(data), int64(len(data))) + assert.NoError(t, err) + obj, err := s.Open("test.txt") + assert.NoError(t, err) + offset, err := obj.Seek(2, io.SeekStart) + assert.NoError(t, err) + assert.EqualValues(t, 2, offset) + buf1 := make([]byte, 3) + read, err := obj.Read(buf1) + assert.NoError(t, err) + assert.EqualValues(t, 3, read) + assert.Equal(t, data[2:5], string(buf1)) + offset, err = obj.Seek(-5, io.SeekEnd) + assert.NoError(t, err) + assert.EqualValues(t, len(data)-5, offset) + buf2 := make([]byte, 4) + read, err = obj.Read(buf2) + assert.NoError(t, err) + assert.EqualValues(t, 4, read) + assert.Equal(t, data[11:15], string(buf2)) + assert.NoError(t, obj.Close()) + assert.NoError(t, s.Delete("test.txt")) +} diff --git a/modules/svg/processor.go b/modules/svg/processor.go index 82248fb0c12..4fcb11a57d0 100644 --- a/modules/svg/processor.go +++ b/modules/svg/processor.go @@ -10,7 +10,7 @@ import ( "sync" ) -type normalizeVarsStruct struct { +type globalVarsStruct struct { reXMLDoc, reComment, reAttrXMLNs, @@ -18,26 +18,23 @@ type normalizeVarsStruct struct { reAttrClassPrefix *regexp.Regexp } -var ( - normalizeVars *normalizeVarsStruct - normalizeVarsOnce sync.Once -) +var globalVars = sync.OnceValue(func() *globalVarsStruct { + return &globalVarsStruct{ + reXMLDoc: regexp.MustCompile(`(?s)<\?xml.*?>`), + reComment: regexp.MustCompile(`(?s)`), + + reAttrXMLNs: regexp.MustCompile(`(?s)\s+xmlns\s*=\s*"[^"]*"`), + reAttrSize: regexp.MustCompile(`(?s)\s+(width|height)\s*=\s*"[^"]+"`), + reAttrClassPrefix: regexp.MustCompile(`(?s)\s+class\s*=\s*"`), + } +}) // Normalize normalizes the SVG content: set default width/height, remove unnecessary tags/attributes // It's designed to work with valid SVG content. For invalid SVG content, the returned content is not guaranteed. func Normalize(data []byte, size int) []byte { - normalizeVarsOnce.Do(func() { - normalizeVars = &normalizeVarsStruct{ - reXMLDoc: regexp.MustCompile(`(?s)<\?xml.*?>`), - reComment: regexp.MustCompile(`(?s)`), - - reAttrXMLNs: regexp.MustCompile(`(?s)\s+xmlns\s*=\s*"[^"]*"`), - reAttrSize: regexp.MustCompile(`(?s)\s+(width|height)\s*=\s*"[^"]+"`), - reAttrClassPrefix: regexp.MustCompile(`(?s)\s+class\s*=\s*"`), - } - }) - data = normalizeVars.reXMLDoc.ReplaceAll(data, nil) - data = normalizeVars.reComment.ReplaceAll(data, nil) + vars := globalVars() + data = vars.reXMLDoc.ReplaceAll(data, nil) + data = vars.reComment.ReplaceAll(data, nil) data = bytes.TrimSpace(data) svgTag, svgRemaining, ok := bytes.Cut(data, []byte(">")) @@ -45,9 +42,9 @@ func Normalize(data []byte, size int) []byte { return data } normalized := bytes.Clone(svgTag) - normalized = normalizeVars.reAttrXMLNs.ReplaceAll(normalized, nil) - normalized = normalizeVars.reAttrSize.ReplaceAll(normalized, nil) - normalized = normalizeVars.reAttrClassPrefix.ReplaceAll(normalized, []byte(` class="`)) + normalized = vars.reAttrXMLNs.ReplaceAll(normalized, nil) + normalized = vars.reAttrSize.ReplaceAll(normalized, nil) + normalized = vars.reAttrClassPrefix.ReplaceAll(normalized, []byte(` class="`)) normalized = bytes.TrimSpace(normalized) normalized = fmt.Appendf(normalized, ` width="%d" height="%d"`, size, size) if !bytes.Contains(normalized, []byte(` class="`)) { diff --git a/modules/system/appstate_test.go b/modules/system/appstate_test.go index d4b9e167c2f..911319d00a5 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 880769dc655..609407d36b5 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -38,7 +38,7 @@ func NewFuncMap() template.FuncMap { "Iif": iif, "Eval": evalTokens, "SafeHTML": safeHTML, - "HTMLFormat": htmlutil.HTMLFormat, + "HTMLFormat": htmlFormat, "HTMLEscape": htmlEscape, "QueryEscape": queryEscape, "QueryBuild": QueryBuild, @@ -131,15 +131,9 @@ func NewFuncMap() template.FuncMap { "EnableTimetracking": func() bool { return setting.Service.EnableTimetracking }, - "DisableGitHooks": func() bool { - return setting.DisableGitHooks - }, "DisableWebhooks": func() bool { return setting.DisableWebhooks }, - "DisableImportLocal": func() bool { - return !setting.ImportLocalPaths - }, "UserThemeName": userThemeName, "NotificationSettings": func() map[string]any { return map[string]any{ @@ -213,6 +207,20 @@ func htmlEscape(s any) template.HTML { panic(fmt.Sprintf("unexpected type %T", s)) } +func htmlFormat(s any, args ...any) template.HTML { + if len(args) == 0 { + // to prevent developers from calling "HTMLFormat $userInput" by mistake which will lead to XSS + panic("missing arguments for HTMLFormat") + } + switch v := s.(type) { + case string: + return htmlutil.HTMLFormat(template.HTML(v), args...) + case template.HTML: + return htmlutil.HTMLFormat(v, args...) + } + panic(fmt.Sprintf("unexpected type %T", s)) +} + func jsEscapeSafe(s string) template.HTML { return template.HTML(template.JSEscapeString(s)) } @@ -264,22 +272,42 @@ func userThemeName(user *user_model.User) string { return setting.UI.DefaultTheme } +func isQueryParamEmpty(v any) bool { + return v == nil || v == false || v == 0 || v == int64(0) || v == "" +} + // QueryBuild builds a query string from a list of key-value pairs. -// It omits the nil and empty strings, but it doesn't omit other zero values, -// because the zero value of number types may have a meaning. +// It omits the nil, false, zero int/int64 and empty string values, +// because they are default empty values for "ctx.FormXxx" calls. +// If 0 or false need to be included, use string values: "0" and "false". +// Build rules: +// * Even parameters: always build as query string: a=b&c=d +// * Odd parameters: +// * * {"/anything", param-pairs...} => "/?param-paris" +// * * {"anything?old-params", new-param-pairs...} => "anything?old-params&new-param-paris" +// * * Otherwise: {"old¶ms", new-param-pairs...} => "old¶ms&new-param-paris" +// * * Other behaviors are undefined yet. func QueryBuild(a ...any) template.URL { - var s string + var reqPath, s string + hasTrailingSep := false if len(a)%2 == 1 { if v, ok := a[0].(string); ok { - if v == "" || (v[0] != '?' && v[0] != '&') { - panic("QueryBuild: invalid argument") - } s = v } else if v, ok := a[0].(template.URL); ok { s = string(v) } else { panic("QueryBuild: invalid argument") } + hasTrailingSep = s != "&" && strings.HasSuffix(s, "&") + if strings.HasPrefix(s, "/") || strings.Contains(s, "?") { + if s1, s2, ok := strings.Cut(s, "?"); ok { + reqPath = s1 + "?" + s = s2 + } else { + reqPath += s + "?" + s = "" + } + } } for i := len(a) % 2; i < len(a); i += 2 { k, ok := a[i].(string) @@ -290,19 +318,16 @@ func QueryBuild(a ...any) template.URL { if va, ok := a[i+1].(string); ok { v = va } else if a[i+1] != nil { - v = fmt.Sprint(a[i+1]) + if !isQueryParamEmpty(a[i+1]) { + v = fmt.Sprint(a[i+1]) + } } // pos1 to pos2 is the "k=v&" part, "&" is optional pos1 := strings.Index(s, "&"+k+"=") if pos1 != -1 { pos1++ - } else { - pos1 = strings.Index(s, "?"+k+"=") - if pos1 != -1 { - pos1++ - } else if strings.HasPrefix(s, k+"=") { - pos1 = 0 - } + } else if strings.HasPrefix(s, k+"=") { + pos1 = 0 } pos2 := len(s) if pos1 == -1 { @@ -315,7 +340,7 @@ func QueryBuild(a ...any) template.URL { } if v != "" { sep := "" - hasPrefixSep := pos1 == 0 || (pos1 <= len(s) && (s[pos1-1] == '?' || s[pos1-1] == '&')) + hasPrefixSep := pos1 == 0 || (pos1 <= len(s) && s[pos1-1] == '&') if !hasPrefixSep { sep = "&" } @@ -324,14 +349,25 @@ 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) } func panicIfDevOrTesting() { - if !setting.IsProd || setting.IsInTesting { - panic("legacy template functions are for backward compatibility only, do not use them in new code") - } + setting.PanicInDevOrTesting("legacy template functions are for backward compatibility only, do not use them in new code") } diff --git a/modules/templates/helper_test.go b/modules/templates/helper_test.go index a530d484bc4..5d7bc93622b 100644 --- a/modules/templates/helper_test.go +++ b/modules/templates/helper_test.go @@ -8,7 +8,6 @@ import ( "strings" "testing" - "code.gitea.io/gitea/modules/htmlutil" "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" @@ -88,7 +87,7 @@ func TestTemplateIif(t *testing.T) { func TestTemplateEscape(t *testing.T) { execTmpl := func(code string) string { tmpl := template.New("test") - tmpl.Funcs(template.FuncMap{"QueryBuild": QueryBuild, "HTMLFormat": htmlutil.HTMLFormat}) + tmpl.Funcs(template.FuncMap{"QueryBuild": QueryBuild, "HTMLFormat": htmlFormat}) template.Must(tmpl.Parse(code)) w := &strings.Builder{} assert.NoError(t, tmpl.Execute(w, nil)) @@ -118,3 +117,58 @@ func TestTemplateEscape(t *testing.T) { assert.Equal(t, `<>`, actual) }) } + +func TestQueryBuild(t *testing.T) { + t.Run("construct", func(t *testing.T) { + assert.Equal(t, "", string(QueryBuild())) + assert.Equal(t, "", string(QueryBuild("a", nil, "b", false, "c", 0, "d", ""))) + assert.Equal(t, "a=1&b=true", string(QueryBuild("a", 1, "b", "true"))) + + // path with query parameters + assert.Equal(t, "/?k=1", string(QueryBuild("/", "k", 1))) + assert.Equal(t, "/", string(QueryBuild("/?k=a", "k", 0))) + + // no path but question mark with query parameters + assert.Equal(t, "?k=1", string(QueryBuild("?", "k", 1))) + assert.Equal(t, "?", string(QueryBuild("?", "k", 0))) + assert.Equal(t, "path?k=1", string(QueryBuild("path?", "k", 1))) + assert.Equal(t, "path", string(QueryBuild("path?", "k", 0))) + + // only query parameters + assert.Equal(t, "&k=1", string(QueryBuild("&", "k", 1))) + assert.Equal(t, "", string(QueryBuild("&", "k", 0))) + assert.Equal(t, "", string(QueryBuild("&k=a", "k", 0))) + assert.Equal(t, "", string(QueryBuild("k=a&", "k", 0))) + assert.Equal(t, "a=1&b=2", string(QueryBuild("a=1", "b", 2))) + assert.Equal(t, "&a=1&b=2", string(QueryBuild("&a=1", "b", 2))) + assert.Equal(t, "a=1&b=2&", string(QueryBuild("a=1&", "b", 2))) + }) + + t.Run("replace", func(t *testing.T) { + assert.Equal(t, "a=1&c=d&e=f", string(QueryBuild("a=b&c=d&e=f", "a", 1))) + assert.Equal(t, "a=b&c=1&e=f", string(QueryBuild("a=b&c=d&e=f", "c", 1))) + assert.Equal(t, "a=b&c=d&e=1", string(QueryBuild("a=b&c=d&e=f", "e", 1))) + assert.Equal(t, "a=b&c=d&e=f&k=1", string(QueryBuild("a=b&c=d&e=f", "k", 1))) + }) + + t.Run("replace-&", func(t *testing.T) { + assert.Equal(t, "&a=1&c=d&e=f", string(QueryBuild("&a=b&c=d&e=f", "a", 1))) + assert.Equal(t, "&a=b&c=1&e=f", string(QueryBuild("&a=b&c=d&e=f", "c", 1))) + assert.Equal(t, "&a=b&c=d&e=1", string(QueryBuild("&a=b&c=d&e=f", "e", 1))) + assert.Equal(t, "&a=b&c=d&e=f&k=1", string(QueryBuild("&a=b&c=d&e=f", "k", 1))) + }) + + t.Run("delete", func(t *testing.T) { + assert.Equal(t, "c=d&e=f", string(QueryBuild("a=b&c=d&e=f", "a", ""))) + assert.Equal(t, "a=b&e=f", string(QueryBuild("a=b&c=d&e=f", "c", ""))) + assert.Equal(t, "a=b&c=d", string(QueryBuild("a=b&c=d&e=f", "e", ""))) + assert.Equal(t, "a=b&c=d&e=f", string(QueryBuild("a=b&c=d&e=f", "k", ""))) + }) + + t.Run("delete-&", func(t *testing.T) { + assert.Equal(t, "&c=d&e=f", string(QueryBuild("&a=b&c=d&e=f", "a", ""))) + assert.Equal(t, "&a=b&e=f", string(QueryBuild("&a=b&c=d&e=f", "c", ""))) + assert.Equal(t, "&a=b&c=d", string(QueryBuild("&a=b&c=d&e=f", "e", ""))) + assert.Equal(t, "&a=b&c=d&e=f", string(QueryBuild("&a=b&c=d&e=f", "k", ""))) + }) +} diff --git a/modules/templates/mailer.go b/modules/templates/mailer.go index ace81bf4a50..310d6453287 100644 --- a/modules/templates/mailer.go +++ b/modules/templates/mailer.go @@ -11,9 +11,9 @@ import ( "strings" texttmpl "text/template" - "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" ) var mailSubjectSplit = regexp.MustCompile(`(?m)^-{3,}\s*$`) @@ -24,7 +24,7 @@ func mailSubjectTextFuncMap() texttmpl.FuncMap { "dict": dict, "Eval": evalTokens, - "EllipsisString": base.EllipsisString, + "EllipsisString": util.EllipsisDisplayString, "AppName": func() string { return setting.AppName }, diff --git a/modules/templates/util_misc.go b/modules/templates/util_misc.go index d645fa013e8..2d42bc76b54 100644 --- a/modules/templates/util_misc.go +++ b/modules/templates/util_misc.go @@ -150,7 +150,7 @@ func mirrorRemoteAddress(ctx context.Context, m *repo_model.Repository, remoteNa return ret } - u, err := giturl.Parse(remoteURL) + u, err := giturl.ParseGitURL(remoteURL) if err != nil { log.Error("giturl.Parse %v", err) return ret diff --git a/modules/templates/util_string.go b/modules/templates/util_string.go index 382e2de13f1..683c77a8704 100644 --- a/modules/templates/util_string.go +++ b/modules/templates/util_string.go @@ -8,7 +8,7 @@ import ( "html/template" "strings" - "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/util" ) type StringUtils struct{} @@ -54,7 +54,7 @@ func (su *StringUtils) Cut(s, sep string) []any { } func (su *StringUtils) EllipsisString(s string, maxLength int) string { - return base.EllipsisString(s, maxLength) + return util.EllipsisDisplayString(s, maxLength) } func (su *StringUtils) ToUpper(s string) string { diff --git a/modules/util/path.go b/modules/util/path.go index 1272f5af2ee..d9f17bd124b 100644 --- a/modules/util/path.go +++ b/modules/util/path.go @@ -140,81 +140,51 @@ func IsExist(path string) (bool, error) { return false, err } -func statDir(dirPath, recPath string, includeDir, isDirOnly, followSymlinks bool) ([]string, error) { - dir, err := os.Open(dirPath) +func listDirRecursively(result *[]string, fsDir, recordParentPath string, opts *ListDirOptions) error { + dir, err := os.Open(fsDir) if err != nil { - return nil, err + return err } defer dir.Close() fis, err := dir.Readdir(0) if err != nil { - return nil, err + return err } - statList := make([]string, 0) for _, fi := range fis { - if CommonSkip(fi.Name()) { + if opts.SkipCommonHiddenNames && IsCommonHiddenFileName(fi.Name()) { continue } - - relPath := path.Join(recPath, fi.Name()) - curPath := path.Join(dirPath, fi.Name()) + relPath := path.Join(recordParentPath, fi.Name()) + curPath := filepath.Join(fsDir, fi.Name()) if fi.IsDir() { - if includeDir { - statList = append(statList, relPath+"/") - } - 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 opts.IncludeDir { + *result = append(*result, relPath+"/") } - 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. -// +type ListDirOptions struct { + IncludeDir bool // subdirectories are also included with suffix slash + SkipCommonHiddenNames bool +} + +// 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. -// If subdirectories is enabled, they will have suffix '/'. -func StatDir(rootPath string, includeDir ...bool) ([]string, error) { - if isDir, err := IsDir(rootPath); err != nil { +func ListDirRecursively(rootDir string, opts *ListDirOptions) (res []string, err error) { + if err = listDirRecursively(&res, rootDir, "", opts); err != nil { return nil, err - } else if !isDir { - return nil, errors.New("not a directory or does not exist: " + rootPath) - } - - isIncludeDir := false - if len(includeDir) != 0 { - isIncludeDir = includeDir[0] } - return statDir(rootPath, "", isIncludeDir, false, false) + return res, nil } func isOSWindows() bool { @@ -265,8 +235,8 @@ func HomeDir() (home string, err error) { return home, nil } -// CommonSkip will check a provided name to see if it represents file or directory that should not be watched -func CommonSkip(name string) bool { +// IsCommonHiddenFileName will check a provided name to see if it represents file or directory that should not be watched +func IsCommonHiddenFileName(name string) bool { if name == "" { return true } @@ -275,9 +245,9 @@ func CommonSkip(name string) bool { case '.': return true case 't', 'T': - return name[1:] == "humbs.db" + return name[1:] == "humbs.db" // macOS case 'd', 'D': - return name[1:] == "esktop.ini" + return name[1:] == "esktop.ini" // Windows } return false diff --git a/modules/util/path_test.go b/modules/util/path_test.go index 6a38bf4ace8..79c37e55f7a 100644 --- a/modules/util/path_test.go +++ b/modules/util/path_test.go @@ -5,10 +5,12 @@ package util import ( "net/url" + "os" "runtime" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestFileURLToPath(t *testing.T) { @@ -210,3 +212,21 @@ func TestCleanPath(t *testing.T) { assert.Equal(t, c.expected, FilePathJoinAbs(c.elems[0], c.elems[1:]...), "case: %v", c.elems) } } + +func TestListDirRecursively(t *testing.T) { + tmpDir := t.TempDir() + _ = os.WriteFile(tmpDir+"/.config", nil, 0o644) + _ = os.Mkdir(tmpDir+"/d1", 0o755) + _ = os.WriteFile(tmpDir+"/d1/f-d1", nil, 0o644) + _ = os.Mkdir(tmpDir+"/d1/s1", 0o755) + _ = os.WriteFile(tmpDir+"/d1/s1/f-d1s1", nil, 0o644) + _ = os.Mkdir(tmpDir+"/d2", 0o755) + + res, err := ListDirRecursively(tmpDir, &ListDirOptions{IncludeDir: true}) + require.NoError(t, err) + assert.ElementsMatch(t, []string{".config", "d1/", "d1/f-d1", "d1/s1/", "d1/s1/f-d1s1", "d2/"}, res) + + res, err = ListDirRecursively(tmpDir, &ListDirOptions{SkipCommonHiddenNames: true}) + require.NoError(t, err) + assert.ElementsMatch(t, []string{"d1/f-d1", "d1/s1/f-d1s1"}, res) +} diff --git a/modules/util/string.go b/modules/util/string.go index cf50f591c64..19cf75b8b35 100644 --- a/modules/util/string.go +++ b/modules/util/string.go @@ -3,7 +3,10 @@ package util -import "unsafe" +import ( + "strings" + "unsafe" +) func isSnakeCaseUpper(c byte) bool { return 'A' <= c && c <= 'Z' @@ -95,3 +98,15 @@ func UnsafeBytesToString(b []byte) string { func UnsafeStringToBytes(s string) []byte { return unsafe.Slice(unsafe.StringData(s), len(s)) } + +// SplitTrimSpace splits the string at given separator and trims leading and trailing space +func SplitTrimSpace(input, sep string) []string { + input = strings.TrimSpace(input) + var stringList []string + for _, s := range strings.Split(input, sep) { + if s = strings.TrimSpace(s); s != "" { + stringList = append(stringList, s) + } + } + return stringList +} diff --git a/modules/util/string_test.go b/modules/util/string_test.go index 0a4a8bbcfbf..ff67b5c7d40 100644 --- a/modules/util/string_test.go +++ b/modules/util/string_test.go @@ -45,3 +45,8 @@ func TestToSnakeCase(t *testing.T) { assert.Equal(t, expected, ToSnakeCase(input)) } } + +func TestSplitTrimSpace(t *testing.T) { + assert.Equal(t, []string{"a", "b", "c"}, SplitTrimSpace("a\nb\nc", "\n")) + assert.Equal(t, []string{"a", "b"}, SplitTrimSpace("\r\na\n\r\nb\n\n", "\n")) +} diff --git a/modules/util/truncate.go b/modules/util/truncate.go index f2edbdc6736..2bce2482813 100644 --- a/modules/util/truncate.go +++ b/modules/util/truncate.go @@ -5,6 +5,7 @@ package util import ( "strings" + "unicode" "unicode/utf8" ) @@ -14,43 +15,110 @@ const ( asciiEllipsis = "..." ) -// SplitStringAtByteN splits a string at byte n accounting for rune boundaries. (Combining characters are not accounted for.) -func SplitStringAtByteN(input string, n int) (left, right string) { - if len(input) <= n { - return input, "" +func IsLikelyEllipsisLeftPart(s string) bool { + return strings.HasSuffix(s, utf8Ellipsis) || strings.HasSuffix(s, asciiEllipsis) +} + +func ellipsisGuessDisplayWidth(r rune) int { + // To make the truncated string as long as possible, + // CJK/emoji chars are considered as 2-ASCII width but not 3-4 bytes width. + // Here we only make the best guess (better than counting them in bytes), + // it's impossible to 100% correctly determine the width of a rune without a real font and render. + // + // ATTENTION: the guessed width can't be zero, more details in ellipsisDisplayString's comment + if r <= 255 { + return 1 } - if !utf8.ValidString(input) { - if n-3 < 0 { - return input, "" + switch { + case r == '\u3000': /* ideographic (CJK) characters, still use 2 */ + return 2 + case unicode.Is(unicode.M, r), /* (Mark) */ + unicode.Is(unicode.Cf, r), /* (Other, format) */ + unicode.Is(unicode.Cs, r), /* (Other, surrogate) */ + unicode.Is(unicode.Z /* (Space) */, r): + return 1 + default: + return 2 + } +} + +// EllipsisDisplayString returns a truncated short string for display purpose. +// The length is the approximate number of ASCII-width in the string (CJK/emoji are 2-ASCII width) +// It appends "…" or "..." at the end of truncated string. +// It guarantees the length of the returned runes doesn't exceed the limit. +func EllipsisDisplayString(str string, limit int) string { + s, _, _, _ := ellipsisDisplayString(str, limit) + return s +} + +// EllipsisDisplayStringX works like EllipsisDisplayString while it also returns the right part +func EllipsisDisplayStringX(str string, limit int) (left, right string) { + left, offset, truncated, encounterInvalid := ellipsisDisplayString(str, limit) + if truncated { + right = str[offset:] + r, _ := utf8.DecodeRune(UnsafeStringToBytes(right)) + encounterInvalid = encounterInvalid || r == utf8.RuneError + ellipsis := utf8Ellipsis + if encounterInvalid { + ellipsis = asciiEllipsis } - return input[:n-3] + asciiEllipsis, asciiEllipsis + input[n-3:] + right = ellipsis + right } + return left, right +} - end := 0 - for end <= n-3 { - _, size := utf8.DecodeRuneInString(input[end:]) - if end+size > n-3 { +func ellipsisDisplayString(str string, limit int) (res string, offset int, truncated, encounterInvalid bool) { + if len(str) <= limit { + return str, len(str), false, false + } + + // To future maintainers: this logic must guarantee that the length of the returned runes doesn't exceed the limit, + // because the returned string will also be used as database value. UTF-8 VARCHAR(10) could store 10 rune characters, + // So each rune must be countered as at least 1 width. + // Even if there are some special Unicode characters (zero-width, combining, etc.), they should NEVER be counted as zero. + pos, used := 0, 0 + for i, r := range str { + encounterInvalid = encounterInvalid || r == utf8.RuneError + pos = i + runeWidth := ellipsisGuessDisplayWidth(r) + if used+runeWidth+3 > limit { break } - end += size + used += runeWidth + offset += utf8.RuneLen(r) } - return input[:end] + utf8Ellipsis, utf8Ellipsis + input[end:] + // if the remaining are fewer than 3 runes, then maybe we could add them, no need to ellipse + if len(str)-pos <= 12 { + var nextCnt, nextWidth int + for _, r := range str[pos:] { + if nextCnt >= 4 { + break + } + nextWidth += ellipsisGuessDisplayWidth(r) + nextCnt++ + } + if nextCnt <= 3 && used+nextWidth <= limit { + return str, len(str), false, false + } + } + if limit < 3 { + // if the limit is so small, do not add ellipsis + return str[:offset], offset, true, false + } + ellipsis := utf8Ellipsis + if encounterInvalid { + ellipsis = asciiEllipsis + } + return str[:offset] + ellipsis, offset, true, encounterInvalid } -// SplitTrimSpace splits the string at given separator and trims leading and trailing space -func SplitTrimSpace(input, sep string) []string { - // Trim initial leading & trailing space - input = strings.TrimSpace(input) - // replace CRLF with LF - input = strings.ReplaceAll(input, "\r\n", "\n") - - var stringList []string - for _, s := range strings.Split(input, sep) { - // trim leading and trailing space - stringList = append(stringList, strings.TrimSpace(s)) +// TruncateRunes returns a truncated string with given rune limit, +// it returns input string if its rune length doesn't exceed the limit. +func TruncateRunes(str string, limit int) string { + if utf8.RuneCountInString(str) < limit { + return str } - - return stringList + return string([]rune(str)[:limit]) } diff --git a/modules/util/truncate_test.go b/modules/util/truncate_test.go index dfe1230fd44..8789c824f50 100644 --- a/modules/util/truncate_test.go +++ b/modules/util/truncate_test.go @@ -4,43 +4,127 @@ package util import ( + "fmt" + "strings" "testing" "github.com/stretchr/testify/assert" ) -func TestSplitString(t *testing.T) { - type testCase struct { - input string - n int - leftSub string - ellipsis string +func TestEllipsisGuessDisplayWidth(t *testing.T) { + cases := []struct { + r string + want int + }{ + {r: "a", want: 1}, + {r: "é", want: 1}, + {r: "测", want: 2}, + {r: "âš½", want: 2}, + {r: "â˜ï¸", want: 3}, // 2 runes, it has a mark + {r: "\u200B", want: 1}, // ZWSP + {r: "\u3000", want: 2}, // ideographic space } - - test := func(tc []*testCase, f func(input string, n int) (left, right string)) { - for _, c := range tc { - l, r := f(c.input, c.n) - if c.ellipsis != "" { - assert.Equal(t, c.leftSub+c.ellipsis, l, "test split %q at %d, expected leftSub: %q", c.input, c.n, c.leftSub) - assert.Equal(t, c.ellipsis+c.input[len(c.leftSub):], r, "test split %s at %d, expected rightSub: %q", c.input, c.n, c.input[len(c.leftSub):]) - } else { - assert.Equal(t, c.leftSub, l, "test split %q at %d, expected leftSub: %q", c.input, c.n, c.leftSub) - assert.Empty(t, r, "test split %q at %d, expected rightSub: %q", c.input, c.n, "") + for _, c := range cases { + t.Run(c.r, func(t *testing.T) { + w := 0 + for _, r := range c.r { + w += ellipsisGuessDisplayWidth(r) } - } + assert.Equal(t, c.want, w, "hex=% x", []byte(c.r)) + }) } +} + +func TestEllipsisString(t *testing.T) { + cases := []struct { + limit int + + input, left, right string + }{ + {limit: 0, input: "abcde", left: "", right: "…abcde"}, + {limit: 1, input: "abcde", left: "", right: "…abcde"}, + {limit: 2, input: "abcde", left: "", right: "…abcde"}, + {limit: 3, input: "abcde", left: "…", right: "…abcde"}, + {limit: 4, input: "abcde", left: "a…", right: "…bcde"}, + {limit: 5, input: "abcde", left: "abcde", right: ""}, + {limit: 6, input: "abcde", left: "abcde", right: ""}, + {limit: 7, input: "abcde", left: "abcde", right: ""}, - tc := []*testCase{ - {"abc123xyz", 0, "", utf8Ellipsis}, - {"abc123xyz", 1, "", utf8Ellipsis}, - {"abc123xyz", 4, "a", utf8Ellipsis}, - {"å•Šbc123xyz", 4, "", utf8Ellipsis}, - {"å•Šbc123xyz", 6, "å•Š", utf8Ellipsis}, - {"å•Šbc", 5, "å•Šbc", ""}, - {"å•Šbc", 6, "å•Šbc", ""}, - {"abc\xef\x03\xfe", 3, "", asciiEllipsis}, - {"abc\xef\x03\xfe", 4, "a", asciiEllipsis}, - {"\xef\x03", 1, "\xef\x03", ""}, + // a CJK char or emoji is considered as 2-ASCII width, the ellipsis is 3-ASCII width + {limit: 0, input: "测试文本", left: "", right: "…测试文本"}, + {limit: 1, input: "测试文本", left: "", right: "…测试文本"}, + {limit: 2, input: "测试文本", left: "", right: "…测试文本"}, + {limit: 3, input: "测试文本", left: "…", right: "…测试文本"}, + {limit: 4, input: "测试文本", left: "…", right: "…测试文本"}, + {limit: 5, input: "测试文本", left: "测…", right: "…试文本"}, + {limit: 6, input: "测试文本", left: "测…", right: "…试文本"}, + {limit: 7, input: "测试文本", left: "测试…", right: "…文本"}, + {limit: 8, input: "测试文本", left: "测试文本", right: ""}, + {limit: 9, input: "测试文本", left: "测试文本", right: ""}, + + {limit: 6, input: "测试abc", left: "测…", right: "…试abc"}, + {limit: 7, input: "测试abc", left: "测试abc", right: ""}, // exactly 7-width + {limit: 8, input: "测试abc", left: "测试abc", right: ""}, + + {limit: 7, input: "测abc试啊", left: "测ab…", right: "…c试啊"}, + {limit: 8, input: "测abc试啊", left: "测abc…", right: "…试啊"}, + {limit: 9, input: "测abc试啊", left: "测abc试啊", right: ""}, // exactly 9-width + {limit: 10, input: "测abc试啊", left: "测abc试啊", right: ""}, } - test(tc, SplitStringAtByteN) + for _, c := range cases { + t.Run(fmt.Sprintf("%s(%d)", c.input, c.limit), func(t *testing.T) { + left, right := EllipsisDisplayStringX(c.input, c.limit) + assert.Equal(t, c.left, left, "left") + assert.Equal(t, c.right, right, "right") + }) + } + + t.Run("LongInput", func(t *testing.T) { + left, right := EllipsisDisplayStringX(strings.Repeat("abc", 240), 90) + assert.Equal(t, strings.Repeat("abc", 29)+"…", left) + assert.Equal(t, "…"+strings.Repeat("abc", 211), right) + }) + + t.Run("InvalidUtf8", func(t *testing.T) { + invalidCases := []struct { + limit int + left, right string + }{ + {limit: 0, left: "", right: "...\xef\x03\xfe\xef\x03\xfe"}, + {limit: 1, left: "", right: "...\xef\x03\xfe\xef\x03\xfe"}, + {limit: 2, left: "", right: "...\xef\x03\xfe\xef\x03\xfe"}, + {limit: 3, left: "...", right: "...\xef\x03\xfe\xef\x03\xfe"}, + {limit: 4, left: "...", right: "...\xef\x03\xfe\xef\x03\xfe"}, + {limit: 5, left: "\xef\x03\xfe...", right: "...\xef\x03\xfe"}, + {limit: 6, left: "\xef\x03\xfe\xef\x03\xfe", right: ""}, + {limit: 7, left: "\xef\x03\xfe\xef\x03\xfe", right: ""}, + } + for _, c := range invalidCases { + t.Run(fmt.Sprintf("%d", c.limit), func(t *testing.T) { + left, right := EllipsisDisplayStringX("\xef\x03\xfe\xef\x03\xfe", c.limit) + assert.Equal(t, c.left, left, "left") + assert.Equal(t, c.right, right, "right") + }) + } + }) + + t.Run("IsLikelyEllipsisLeftPart", func(t *testing.T) { + assert.True(t, IsLikelyEllipsisLeftPart("abcde…")) + assert.True(t, IsLikelyEllipsisLeftPart("abcde...")) + }) +} + +func TestTruncateRunes(t *testing.T) { + assert.Equal(t, "", TruncateRunes("", 0)) + assert.Equal(t, "", TruncateRunes("", 1)) + + assert.Equal(t, "", TruncateRunes("ab", 0)) + assert.Equal(t, "a", TruncateRunes("ab", 1)) + assert.Equal(t, "ab", TruncateRunes("ab", 2)) + assert.Equal(t, "ab", TruncateRunes("ab", 3)) + + assert.Equal(t, "", TruncateRunes("测试", 0)) + assert.Equal(t, "测", TruncateRunes("测试", 1)) + assert.Equal(t, "测试", TruncateRunes("测试", 2)) + assert.Equal(t, "测试", TruncateRunes("测试", 3)) } diff --git a/modules/web/handler.go b/modules/web/handler.go index 1812c664b34..9a3e4a7f176 100644 --- a/modules/web/handler.go +++ b/modules/web/handler.go @@ -4,7 +4,6 @@ package web import ( - goctx "context" "fmt" "net/http" "reflect" @@ -51,7 +50,6 @@ func (r *responseWriter) WriteHeader(statusCode int) { var ( httpReqType = reflect.TypeOf((*http.Request)(nil)) respWriterType = reflect.TypeOf((*http.ResponseWriter)(nil)).Elem() - cancelFuncType = reflect.TypeOf((*goctx.CancelFunc)(nil)).Elem() ) // preCheckHandler checks whether the handler is valid, developers could get first-time feedback, all mistakes could be found at startup @@ -65,11 +63,8 @@ func preCheckHandler(fn reflect.Value, argsIn []reflect.Value) { if !hasStatusProvider { panic(fmt.Sprintf("handler should have at least one ResponseStatusProvider argument, but got %s", fn.Type())) } - if fn.Type().NumOut() != 0 && fn.Type().NumIn() != 1 { - panic(fmt.Sprintf("handler should have no return value or only one argument, but got %s", fn.Type())) - } - if fn.Type().NumOut() == 1 && fn.Type().Out(0) != cancelFuncType { - panic(fmt.Sprintf("handler should return a cancel function, but got %s", fn.Type())) + if fn.Type().NumOut() != 0 { + panic(fmt.Sprintf("handler should have no return value other than registered ones, but got %s", fn.Type())) } } @@ -105,16 +100,10 @@ func prepareHandleArgsIn(resp http.ResponseWriter, req *http.Request, fn reflect return argsIn } -func handleResponse(fn reflect.Value, ret []reflect.Value) goctx.CancelFunc { - if len(ret) == 1 { - if cancelFunc, ok := ret[0].Interface().(goctx.CancelFunc); ok { - return cancelFunc - } - panic(fmt.Sprintf("unsupported return type: %s", ret[0].Type())) - } else if len(ret) > 1 { +func handleResponse(fn reflect.Value, ret []reflect.Value) { + if len(ret) != 0 { panic(fmt.Sprintf("unsupported return values: %s", fn.Type())) } - return nil } func hasResponseBeenWritten(argsIn []reflect.Value) bool { @@ -171,11 +160,8 @@ func toHandlerProvider(handler any) func(next http.Handler) http.Handler { routing.UpdateFuncInfo(req.Context(), funcInfo) ret := fn.Call(argsIn) - // handle the return value, and defer the cancel function if there is one - cancelFunc := handleResponse(fn, ret) - if cancelFunc != nil { - defer cancelFunc() - } + // handle the return value (no-op at the moment) + handleResponse(fn, ret) // if the response has not been written, call the next handler if next != nil && !hasResponseBeenWritten(argsIn) { diff --git a/modules/web/middleware/data.go b/modules/web/middleware/data.go index 08d83f94be7..a47da0f836b 100644 --- a/modules/web/middleware/data.go +++ b/modules/web/middleware/data.go @@ -7,46 +7,21 @@ import ( "context" "time" + "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" ) -// ContextDataStore represents a data store -type ContextDataStore interface { - GetData() ContextData -} - -type ContextData map[string]any - -func (ds ContextData) GetData() ContextData { - return ds -} - -func (ds ContextData) MergeFrom(other ContextData) ContextData { - for k, v := range other { - ds[k] = v - } - return ds -} - const ContextDataKeySignedUser = "SignedUser" -type contextDataKeyType struct{} - -var contextDataKey contextDataKeyType - -func WithContextData(c context.Context) context.Context { - return context.WithValue(c, contextDataKey, make(ContextData, 10)) -} - -func GetContextData(c context.Context) ContextData { - if ds, ok := c.Value(contextDataKey).(ContextData); ok { - return ds +func GetContextData(c context.Context) reqctx.ContextData { + if rc := reqctx.GetRequestDataStore(c); rc != nil { + return rc.GetData() } return nil } -func CommonTemplateContextData() ContextData { - return ContextData{ +func CommonTemplateContextData() reqctx.ContextData { + return reqctx.ContextData{ "IsLandingPageOrganizations": setting.LandingPageURL == setting.LandingPageOrganizations, "ShowRegistrationButton": setting.Service.ShowRegistrationButton, diff --git a/modules/web/middleware/flash.go b/modules/web/middleware/flash.go index 88da2049a41..0caaa8c0364 100644 --- a/modules/web/middleware/flash.go +++ b/modules/web/middleware/flash.go @@ -7,11 +7,13 @@ import ( "fmt" "html/template" "net/url" + + "code.gitea.io/gitea/modules/reqctx" ) // Flash represents a one time data transfer between two requests. type Flash struct { - DataStore ContextDataStore + DataStore reqctx.RequestDataStore url.Values ErrorMsg, WarningMsg, InfoMsg, SuccessMsg string } diff --git a/modules/web/route_test.go b/modules/web/route_test.go deleted file mode 100644 index 6e4c309293b..00000000000 --- a/modules/web/route_test.go +++ /dev/null @@ -1,222 +0,0 @@ -// Copyright 2021 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package web - -import ( - "bytes" - "net/http" - "net/http/httptest" - "strconv" - "testing" - - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/test" - - chi "github.com/go-chi/chi/v5" - "github.com/stretchr/testify/assert" -) - -func TestRoute1(t *testing.T) { - buff := bytes.NewBufferString("") - recorder := httptest.NewRecorder() - recorder.Body = buff - - r := NewRouter() - r.Get("/{username}/{reponame}/{type:issues|pulls}", func(resp http.ResponseWriter, req *http.Request) { - username := chi.URLParam(req, "username") - assert.EqualValues(t, "gitea", username) - reponame := chi.URLParam(req, "reponame") - assert.EqualValues(t, "gitea", reponame) - tp := chi.URLParam(req, "type") - assert.EqualValues(t, "issues", tp) - }) - - req, err := http.NewRequest("GET", "http://localhost:8000/gitea/gitea/issues", nil) - assert.NoError(t, err) - r.ServeHTTP(recorder, req) - assert.EqualValues(t, http.StatusOK, recorder.Code) -} - -func TestRoute2(t *testing.T) { - buff := bytes.NewBufferString("") - recorder := httptest.NewRecorder() - recorder.Body = buff - - hit := -1 - - r := NewRouter() - r.Group("/{username}/{reponame}", func() { - r.Group("", func() { - r.Get("/{type:issues|pulls}", func(resp http.ResponseWriter, req *http.Request) { - username := chi.URLParam(req, "username") - assert.EqualValues(t, "gitea", username) - reponame := chi.URLParam(req, "reponame") - assert.EqualValues(t, "gitea", reponame) - tp := chi.URLParam(req, "type") - assert.EqualValues(t, "issues", tp) - hit = 0 - }) - - r.Get("/{type:issues|pulls}/{index}", func(resp http.ResponseWriter, req *http.Request) { - username := chi.URLParam(req, "username") - assert.EqualValues(t, "gitea", username) - reponame := chi.URLParam(req, "reponame") - assert.EqualValues(t, "gitea", reponame) - tp := chi.URLParam(req, "type") - assert.EqualValues(t, "issues", tp) - index := chi.URLParam(req, "index") - assert.EqualValues(t, "1", index) - hit = 1 - }) - }, func(resp http.ResponseWriter, req *http.Request) { - if stop, err := strconv.Atoi(req.FormValue("stop")); err == nil { - hit = stop - resp.WriteHeader(http.StatusOK) - } - }) - - r.Group("/issues/{index}", func() { - r.Get("/view", func(resp http.ResponseWriter, req *http.Request) { - username := chi.URLParam(req, "username") - assert.EqualValues(t, "gitea", username) - reponame := chi.URLParam(req, "reponame") - assert.EqualValues(t, "gitea", reponame) - index := chi.URLParam(req, "index") - assert.EqualValues(t, "1", index) - hit = 2 - }) - }) - }) - - req, err := http.NewRequest("GET", "http://localhost:8000/gitea/gitea/issues", nil) - assert.NoError(t, err) - r.ServeHTTP(recorder, req) - assert.EqualValues(t, http.StatusOK, recorder.Code) - assert.EqualValues(t, 0, hit) - - req, err = http.NewRequest("GET", "http://localhost:8000/gitea/gitea/issues/1", nil) - assert.NoError(t, err) - r.ServeHTTP(recorder, req) - assert.EqualValues(t, http.StatusOK, recorder.Code) - assert.EqualValues(t, 1, hit) - - req, err = http.NewRequest("GET", "http://localhost:8000/gitea/gitea/issues/1?stop=100", nil) - assert.NoError(t, err) - r.ServeHTTP(recorder, req) - assert.EqualValues(t, http.StatusOK, recorder.Code) - assert.EqualValues(t, 100, hit) - - req, err = http.NewRequest("GET", "http://localhost:8000/gitea/gitea/issues/1/view", nil) - assert.NoError(t, err) - r.ServeHTTP(recorder, req) - assert.EqualValues(t, http.StatusOK, recorder.Code) - assert.EqualValues(t, 2, hit) -} - -func TestRoute3(t *testing.T) { - buff := bytes.NewBufferString("") - recorder := httptest.NewRecorder() - recorder.Body = buff - - hit := -1 - - m := NewRouter() - r := NewRouter() - r.Mount("/api/v1", m) - - m.Group("/repos", func() { - m.Group("/{username}/{reponame}", func() { - m.Group("/branch_protections", func() { - m.Get("", func(resp http.ResponseWriter, req *http.Request) { - hit = 0 - }) - m.Post("", func(resp http.ResponseWriter, req *http.Request) { - hit = 1 - }) - m.Group("/{name}", func() { - m.Get("", func(resp http.ResponseWriter, req *http.Request) { - hit = 2 - }) - m.Patch("", func(resp http.ResponseWriter, req *http.Request) { - hit = 3 - }) - m.Delete("", func(resp http.ResponseWriter, req *http.Request) { - hit = 4 - }) - }) - }) - }) - }) - - req, err := http.NewRequest("GET", "http://localhost:8000/api/v1/repos/gitea/gitea/branch_protections", nil) - assert.NoError(t, err) - r.ServeHTTP(recorder, req) - assert.EqualValues(t, http.StatusOK, recorder.Code) - assert.EqualValues(t, 0, hit) - - req, err = http.NewRequest("POST", "http://localhost:8000/api/v1/repos/gitea/gitea/branch_protections", nil) - assert.NoError(t, err) - r.ServeHTTP(recorder, req) - assert.EqualValues(t, http.StatusOK, recorder.Code, http.StatusOK) - assert.EqualValues(t, 1, hit) - - req, err = http.NewRequest("GET", "http://localhost:8000/api/v1/repos/gitea/gitea/branch_protections/master", nil) - assert.NoError(t, err) - r.ServeHTTP(recorder, req) - assert.EqualValues(t, http.StatusOK, recorder.Code) - assert.EqualValues(t, 2, hit) - - req, err = http.NewRequest("PATCH", "http://localhost:8000/api/v1/repos/gitea/gitea/branch_protections/master", nil) - assert.NoError(t, err) - r.ServeHTTP(recorder, req) - assert.EqualValues(t, http.StatusOK, recorder.Code) - assert.EqualValues(t, 3, hit) - - req, err = http.NewRequest("DELETE", "http://localhost:8000/api/v1/repos/gitea/gitea/branch_protections/master", nil) - assert.NoError(t, err) - r.ServeHTTP(recorder, req) - assert.EqualValues(t, http.StatusOK, recorder.Code) - assert.EqualValues(t, 4, hit) -} - -func TestRouteNormalizePath(t *testing.T) { - type paths struct { - EscapedPath, RawPath, Path string - } - testPath := func(reqPath string, expectedPaths paths) { - recorder := httptest.NewRecorder() - recorder.Body = bytes.NewBuffer(nil) - - actualPaths := paths{EscapedPath: "(none)", RawPath: "(none)", Path: "(none)"} - r := NewRouter() - r.Get("/*", func(resp http.ResponseWriter, req *http.Request) { - actualPaths.EscapedPath = req.URL.EscapedPath() - actualPaths.RawPath = req.URL.RawPath - actualPaths.Path = req.URL.Path - }) - - req, err := http.NewRequest("GET", reqPath, nil) - assert.NoError(t, err) - r.ServeHTTP(recorder, req) - assert.Equal(t, expectedPaths, actualPaths, "req path = %q", reqPath) - } - - // RawPath could be empty if the EscapedPath is the same as escape(Path) and it is already normalized - testPath("/", paths{EscapedPath: "/", RawPath: "", Path: "/"}) - testPath("//", paths{EscapedPath: "/", RawPath: "/", Path: "/"}) - testPath("/%2f", paths{EscapedPath: "/%2f", RawPath: "/%2f", Path: "//"}) - testPath("///a//b/", paths{EscapedPath: "/a/b", RawPath: "/a/b", Path: "/a/b"}) - - defer test.MockVariableValue(&setting.UseSubURLPath, true)() - defer test.MockVariableValue(&setting.AppSubURL, "/sub-path")() - testPath("/", paths{EscapedPath: "(none)", RawPath: "(none)", Path: "(none)"}) // 404 - testPath("/sub-path", paths{EscapedPath: "/", RawPath: "/", Path: "/"}) - testPath("/sub-path/", paths{EscapedPath: "/", RawPath: "/", Path: "/"}) - testPath("/sub-path//a/b///", paths{EscapedPath: "/a/b", RawPath: "/a/b", Path: "/a/b"}) - testPath("/sub-path/%2f/", paths{EscapedPath: "/%2f", RawPath: "/%2f", Path: "//"}) - // "/v2" is special for OCI container registry, it should always be in the root of the site - testPath("/v2", paths{EscapedPath: "/v2", RawPath: "/v2", Path: "/v2"}) - testPath("/v2/", paths{EscapedPath: "/v2", RawPath: "/v2", Path: "/v2"}) - testPath("/v2/%2f", paths{EscapedPath: "/v2/%2f", RawPath: "/v2/%2f", Path: "/v2//"}) -} diff --git a/modules/web/route.go b/modules/web/router.go similarity index 85% rename from modules/web/route.go rename to modules/web/router.go index 787521dfb07..da06b955b13 100644 --- a/modules/web/route.go +++ b/modules/web/router.go @@ -10,6 +10,7 @@ import ( "strings" "code.gitea.io/gitea/modules/htmlutil" + "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web/middleware" @@ -29,18 +30,18 @@ func Bind[T any](_ T) http.HandlerFunc { } // SetForm set the form object -func SetForm(dataStore middleware.ContextDataStore, obj any) { +func SetForm(dataStore reqctx.ContextDataProvider, obj any) { dataStore.GetData()["__form"] = obj } // GetForm returns the validate form information -func GetForm(dataStore middleware.ContextDataStore) any { +func GetForm(dataStore reqctx.RequestDataStore) any { return dataStore.GetData()["__form"] } // Router defines a route based on chi's router type Router struct { - chiRouter chi.Router + chiRouter *chi.Mux curGroupPrefix string curMiddlewares []any } @@ -92,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] @@ -116,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, ",") @@ -136,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) } @@ -242,39 +248,11 @@ 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 00000000000..4478689027f --- /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 00000000000..b59948581a9 --- /dev/null +++ b/modules/web/router_path.go @@ -0,0 +1,147 @@ +// 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 isValidMethod(name string) bool { + switch name { + case http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete, http.MethodHead, http.MethodOptions, http.MethodConnect, http.MethodTrace: + return true + } + return false +} + +func newRouterPathMatcher(methods, pattern string, h ...any) *routerPathMatcher { + middlewares, handlerFunc := wrapMiddlewareAndHandler(nil, h) + p := &routerPathMatcher{methods: make(container.Set[string]), middlewares: middlewares, handlerFunc: handlerFunc} + for _, method := range strings.Split(methods, ",") { + method = strings.TrimSpace(method) + if !isValidMethod(method) { + panic(fmt.Sprintf("invalid HTTP method: %s", method)) + } + p.methods.Add(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/router_test.go b/modules/web/router_test.go new file mode 100644 index 00000000000..582980a27ae --- /dev/null +++ b/modules/web/router_test.go @@ -0,0 +1,250 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package web + +import ( + "bytes" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + "code.gitea.io/gitea/modules/util" + + "github.com/go-chi/chi/v5" + "github.com/stretchr/testify/assert" +) + +func chiURLParamsToMap(chiCtx *chi.Context) map[string]string { + pathParams := chiCtx.URLParams + m := make(map[string]string, len(pathParams.Keys)) + for i, key := range pathParams.Keys { + if key == "*" && pathParams.Values[i] == "" { + continue // chi router will add an empty "*" key if there is a "Mount" + } + m[key] = pathParams.Values[i] + } + 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 := 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": ""}) + testProcess("//", "/a", map[string]string{"p1": "", "p2": "a"}) + testProcess("//", "/a/b", map[string]string{"p1": "a", "p2": "b"}) + testProcess("//", "/a/b/c", map[string]string{"p1": "a/b", "p2": "c"}) +} + +func TestRouter(t *testing.T) { + buff := bytes.NewBufferString("") + recorder := httptest.NewRecorder() + recorder.Body = buff + + type resultStruct struct { + method string + pathParams map[string]string + handlerMark string + } + var res resultStruct + + h := func(optMark ...string) func(resp http.ResponseWriter, req *http.Request) { + mark := util.OptionalArg(optMark, "") + return func(resp http.ResponseWriter, req *http.Request) { + res.method = req.Method + res.pathParams = chiURLParamsToMap(chi.RouteContext(req.Context())) + res.handlerMark = mark + } + } + + 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")) + }, 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() { + m.Group("/branches", func() { + m.Get("", h()) + m.Post("", h()) + m.Group("/{name}", func() { + m.Get("", h()) + m.Patch("", h()) + m.Delete("", h()) + }) + m.PathGroup("/*", func(g *RouterPathGroup) { + g.MatchPath("GET", `//`, stopMark("s2"), h("match-path")) + }, stopMark("s1")) + }) + }) + }) + + testRoute := func(t *testing.T, methodPath string, expected resultStruct) { + t.Run(methodPath, func(t *testing.T) { + res = resultStruct{} + methodPathFields := strings.Fields(methodPath) + req, err := http.NewRequest(methodPathFields[0], methodPathFields[1], nil) + assert.NoError(t, err) + r.ServeHTTP(recorder, req) + assert.EqualValues(t, expected, res) + }) + } + + 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(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(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(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", + }) + }) + + t.Run("Sub Router", func(t *testing.T) { + 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(t, "POST /api/v1/repos/the-user/the-repo/branches", resultStruct{ + method: "POST", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo"}, + }) + + 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(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(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) { + type paths struct { + EscapedPath, RawPath, Path string + } + testPath := func(reqPath string, expectedPaths paths) { + recorder := httptest.NewRecorder() + recorder.Body = bytes.NewBuffer(nil) + + actualPaths := paths{EscapedPath: "(none)", RawPath: "(none)", Path: "(none)"} + r := NewRouter() + r.Get("/*", func(resp http.ResponseWriter, req *http.Request) { + actualPaths.EscapedPath = req.URL.EscapedPath() + actualPaths.RawPath = req.URL.RawPath + actualPaths.Path = req.URL.Path + }) + + req, err := http.NewRequest("GET", reqPath, nil) + assert.NoError(t, err) + r.ServeHTTP(recorder, req) + assert.Equal(t, expectedPaths, actualPaths, "req path = %q", reqPath) + } + + // RawPath could be empty if the EscapedPath is the same as escape(Path) and it is already normalized + testPath("/", paths{EscapedPath: "/", RawPath: "", Path: "/"}) + testPath("//", paths{EscapedPath: "/", RawPath: "/", Path: "/"}) + testPath("/%2f", paths{EscapedPath: "/%2f", RawPath: "/%2f", Path: "//"}) + testPath("///a//b/", paths{EscapedPath: "/a/b", RawPath: "/a/b", Path: "/a/b"}) + + defer test.MockVariableValue(&setting.UseSubURLPath, true)() + defer test.MockVariableValue(&setting.AppSubURL, "/sub-path")() + testPath("/", paths{EscapedPath: "(none)", RawPath: "(none)", Path: "(none)"}) // 404 + testPath("/sub-path", paths{EscapedPath: "/", RawPath: "/", Path: "/"}) + testPath("/sub-path/", paths{EscapedPath: "/", RawPath: "/", Path: "/"}) + testPath("/sub-path//a/b///", paths{EscapedPath: "/a/b", RawPath: "/a/b", Path: "/a/b"}) + testPath("/sub-path/%2f/", paths{EscapedPath: "/%2f", RawPath: "/%2f", Path: "//"}) + // "/v2" is special for OCI container registry, it should always be in the root of the site + testPath("/v2", paths{EscapedPath: "/v2", RawPath: "/v2", Path: "/v2"}) + testPath("/v2/", paths{EscapedPath: "/v2", RawPath: "/v2", Path: "/v2"}) + testPath("/v2/%2f", paths{EscapedPath: "/v2/%2f", RawPath: "/v2/%2f", Path: "/v2//"}) +} diff --git a/modules/webhook/type.go b/modules/webhook/type.go index fbec8892722..aa4de45eb40 100644 --- a/modules/webhook/type.go +++ b/modules/webhook/type.go @@ -69,6 +69,10 @@ func (h HookEventType) Event() string { return "" } +func (h HookEventType) IsPullRequest() bool { + return h.Event() == "pull_request" +} + // HookType is the type of a webhook type HookType = string diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index 2abe3672cd8..beeb1dc3b89 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 @@ -3742,6 +3743,7 @@ variables.creation.success=PromÄ›nná „%s“ byla pÅ™idána. variables.update.failed=Úprava promÄ›nné se nezdaÅ™ila. variables.update.success=PromÄ›nná byla upravena. + [projects] deleted.display_name=OdstranÄ›ný projekt type-1.display_name=Samostatný projekt diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index c8dd3f71a23..ce2c43cb568 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,7 +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 193441828ae..31e57bbf978 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=Ταυτότητα & ΠÏόσβαση @@ -3439,6 +3440,7 @@ variables.creation.success=Η μεταβλητή "%s" έχει Ï€Ïοστεθε variables.update.failed=Αποτυχία επεξεÏγασίας μεταβλητής. variables.update.success=Η μεταβλητή έχει Ï„Ïοποποιηθεί. + [projects] type-1.display_name=Ατομικό ΈÏγο type-2.display_name=ΈÏγο ΑποθετηÏίου diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index ea4d36fb1ca..140e2efe571 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). @@ -649,6 +650,7 @@ joined_on = Joined on %s repositories = Repositories activity = Public Activity followers = Followers +show_more = Show More starred = Starred Repositories watched = Watched Repositories code = Code @@ -1014,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. @@ -1231,6 +1235,7 @@ create_new_repo_command = Creating a new repository on the command line push_exist_repo = Pushing an existing repository from the command line empty_message = This repository does not contain any content. broken_message = The Git data underlying this repository cannot be read. Contact the administrator of this instance or delete this repository. +no_branch = This repository doesn’t have any branches. code = Code code.desc = Access source code, files, commits and branches. @@ -2623,6 +2628,9 @@ diff.image.overlay = Overlay diff.has_escaped = This line has hidden Unicode characters diff.show_file_tree = Show file tree diff.hide_file_tree = Hide file tree +diff.submodule_added = Submodule %[1]s added at %[2]s +diff.submodule_deleted = Submodule %[1]s deleted from %[2]s +diff.submodule_updated = Submodule %[1]s updated: %[2]s releases.desc = Track project versions and downloads. release.releases = Releases @@ -2861,6 +2869,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 @@ -3530,6 +3542,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. @@ -3755,6 +3768,7 @@ workflow.not_found = Workflow '%s' not found. workflow.run_success = Workflow '%s' run successfully. workflow.from_ref = Use workflow from workflow.has_workflow_dispatch = This workflow has a workflow_dispatch event trigger. +workflow.has_no_workflow_dispatch = Workflow '%s' has no workflow_dispatch event trigger. need_approval_desc = Need approval to run workflows for fork pull request. diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index e95513766bb..cdfe1fb2e58 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 @@ -3415,6 +3416,7 @@ variables.creation.success=La variable "%s" ha sido añadida. variables.update.failed=Error al editar la variable. variables.update.success=La variable ha sido editada. + [projects] type-1.display_name=Proyecto individual type-2.display_name=Proyecto repositorio diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini index 640592f2bfb..4d90cf98762 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=حساب کاربران @@ -2529,6 +2530,7 @@ runs.commit=کامیت + [projects] [git.filemode] diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini index 375c7b11bf9..b5fa5c8afcd 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 @@ -1707,6 +1707,7 @@ runs.commit=Commit + [projects] [git.filemode] diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index d522abbd11c..becd8829fc7 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -244,6 +244,7 @@ license_desc=Venez récupérer documentation avant de modifier les paramètres. require_db_desc=Gitea nécessite MySQL, PostgreSQL, MSSQL, SQLite3 ou TiDB (avec le protocole MySQL). @@ -649,6 +650,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 +1016,9 @@ new_repo_helper=Un dépôt contient tous les fichiers d’un projet, ainsi que l owner=Propriétaire owner_helper=Certaines organisations peuvent ne pas apparaître dans la liste déroulante en raison d'une limite maximale du nombre de dépôts. repo_name=Nom du dépôt -repo_name_helper=Idéalement, le nom d'un dépôt devrait être court, mémorisable et unique. +repo_name_profile_public_hint=.profile est un dépôt spécial que vous pouvez utiliser pour ajouter un README.md à votre profil public d’organisation, visible à tout le monde. Assurez-vous qu’il soit public et initialisez-le avec un README dans le répertoire de profil pour commencer. +repo_name_profile_private_hint=.profile-private est un dépôt spécial que vous pouvez utiliser pour ajouter un README.md à votre profil d’organisation, visible uniquement aux membres de l’organisation. Assurez-vous qu’il soit privé et initialisez-le avec un README dans le répertoire de profil pour commencer. +repo_name_helper=Idéalement, le nom d’un dépôt devrait être court, mémorisable et unique. Vous pouvez personnaliser votre profil ou celui de votre organisation en créant un dépôt nommé « .profile » ou « .profile-private » et contenant un README.md. repo_size=Taille du dépôt template=Modèle template_select=Répliquer un modèle @@ -1231,6 +1235,7 @@ create_new_repo_command=Création d'un nouveau dépôt en ligne de commande push_exist_repo=Soumission d'un dépôt existant par ligne de commande empty_message=Ce dépôt n'a pas de contenu. broken_message=Les données git de ce dépôt ne peuvent pas être lues. Contactez l'administrateur de cette instance ou supprimez ce dépôt. +no_branch=Ce dépôt n’a aucune branche. code=Code code.desc=Accéder au code source, fichiers, révisions et branches. @@ -1945,6 +1950,8 @@ pulls.delete.title=Supprimer cette demande d'ajout ? pulls.delete.text=Voulez-vous vraiment supprimer cet demande d'ajout ? (Cela supprimera définitivement tout le contenu. Envisagez de le fermer à la place, si vous avez l'intention de le garder archivé) pulls.recently_pushed_new_branches=Vous avez soumis sur la branche %[1]s %[2]s +pulls.upstream_diverging_prompt_behind_1=Cette branche est en retard de %d révision sur %s +pulls.upstream_diverging_prompt_behind_n=Cette branche est en retard de %d révisions sur %s pulls.upstream_diverging_prompt_base_newer=La branche de base %s a de nouveaux changements pulls.upstream_diverging_merge=Synchroniser la bifurcation @@ -2858,6 +2865,10 @@ teams.invite.title=Vous avez été invité à rejoindre l'équipe %s/etc/apk/repositories : alpine.registry.key=Téléchargez la clé RSA publique du registre dans le dossier /etc/apk/keys/ pour vérifier la signature de l'index : alpine.registry.info=Choisissez $branch et $repository dans la liste ci-dessous. @@ -3719,6 +3731,7 @@ runners.status.active=Actif runners.status.offline=Hors-ligne runners.version=Version runners.reset_registration_token=Réinitialiser le jeton d'enregistrement +runners.reset_registration_token_confirm=Voulez-vous révoquer le jeton actuel et en générer un nouveau ? runners.reset_registration_token_success=Le jeton d’inscription de l’exécuteur a été réinitialisé avec succès runs.all_workflows=Tous les flux de travail @@ -3751,6 +3764,7 @@ workflow.not_found=Flux de travail « %s » introuvable. workflow.run_success=Le flux de travail « %s » s’est correctement exécuté. workflow.from_ref=Utiliser le flux de travail depuis workflow.has_workflow_dispatch=Ce flux de travail a un déclencheur d’événement workflow_dispatch. +workflow.has_no_workflow_dispatch=Le flux de travail %s n’a pas de déclencheur d’événement workflow_dispatch. need_approval_desc=Besoin d’approbation pour exécuter des flux de travail pour une demande d’ajout de bifurcation. @@ -3770,6 +3784,9 @@ variables.creation.success=La variable « %s » a été ajoutée. variables.update.failed=Impossible d’éditer la variable. variables.update.success=La variable a bien été modifiée. +logs.always_auto_scroll=Toujours faire défiler les journaux automatiquement +logs.always_expand_running=Toujours développer les journaux en cours + [projects] deleted.display_name=Projet supprimé type-1.display_name=Projet personnel diff --git a/options/locale/locale_ga-IE.ini b/options/locale/locale_ga-IE.ini index 6d989d33479..cf6c76a9dbf 100644 --- a/options/locale/locale_ga-IE.ini +++ b/options/locale/locale_ga-IE.ini @@ -649,6 +649,7 @@ joined_on=Cláraigh ar %s repositories=Stórais activity=Gníomhaíocht Phoiblí followers=Leantóirí +show_more=Taispeáin Tuilleadh starred=Stórais Réaltaithe watched=Stórais Breathnaithe code=Cód @@ -1014,7 +1015,6 @@ new_repo_helper=Tá gach comhad tionscadail i stór, lena n-áirítear stair ath owner=Úinéir owner_helper=B'fhéidir nach dtaispeánfar roinnt eagraíochtaí sa anuas mar gheall ar theorainn uasta comhaireamh stórais. repo_name=Ainm Stórais -repo_name_helper=Úsáideann dea-ainmneacha stórtha eochairfhocail ghearr, i gcuimhne agus uathúla. repo_size=Méid an Stóras template=Teimpléad template_select=Roghnaigh teimpléad. @@ -1945,6 +1945,8 @@ pulls.delete.title=Scrios an t-iarratas tarraingthe seo? pulls.delete.text=An bhfuil tú cinnte gur mhaith leat an t-iarratas tarraingthe seo a scriosadh? (Bainfidh sé seo an t-inneachar go léir go buan. Smaoinigh ar é a dhúnadh ina ionad sin, má tá sé i gceist agat é a choinneáil i gcartlann) pulls.recently_pushed_new_branches=Bhrúigh tú ar bhrainse %[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 @@ -2807,6 +2808,7 @@ runs.commit=Commit + [projects] [git.filemode] diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index bbd48989f7b..8ee9112c349 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 内ã®ãƒãƒ¼ãƒ  문서를 ì½ì–´ë³´ì„¸ìš”." +docker_helper=Gitea를 Dockerì—ì„œ 실행하려면 설정 ì „ì— ì´ ë¬¸ì„œë¥¼ ì½ì–´ë³´ì„¸ìš”. db_title=ë°ì´í„°ë² ì´ìŠ¤ 설정 db_type=ë°ì´í„°ë² ì´ìŠ¤ 유형 host=호스트 @@ -436,8 +436,8 @@ manage_gpg_keys=GPG 키 관리 add_key=키 추가 ssh_desc=ì´ëŸ¬í•œ SSH 공용 키는 ê·€í•˜ì˜ ê³„ì •ê³¼ ì—°ê²°ë˜ì–´ 있습니다. 해당 ê°œì¸ í‚¤ëŠ” ë‹¹ì‹ ì˜ ì €ìž¥ì†Œì— ëŒ€í•œ ì „ì²´ 액세스를 가능하게 합니다. gpg_desc=ì´ëŸ¬í•œ GPG 공개키는 ë‹¹ì‹ ì˜ ê³„ì •ê³¼ ì—°ê²°ë˜ì–´ìžˆìŠµë‹ˆë‹¤. ì»¤ë°‹ì´ ê²€ì¦ë  수 있ë„ë¡ ë‹¹ì‹ ì˜ ê°œì¸ í‚¤ë¥¼ 안전하게 유지하십시오. -ssh_helper="ë„ì›€ì´ í•„ìš”í•˜ì„¸ìš”? GitHubì˜ ì„¤ëª…ì„œë¥¼ 참조하시기 ë°”ëžë‹ˆë‹¤: SSH 키 ìƒì„±í•˜ê¸° ë˜ëŠ” SSH를 사용할 ë•Œ ì¼ë°˜ì ì¸ 문제" -gpg_helper="ë„ì›€ì´ í•„ìš”í•˜ì„¸ìš”? GitHubì˜ ì„¤ëª…ì„œë¥¼ 참조하시기 ë°”ëžë‹ˆë‹¤: GPGí‚¤ì— ëŒ€í•˜ì—¬." +ssh_helper=ë„ì›€ì´ í•„ìš”í•˜ì„¸ìš”? GitHubì˜ ì„¤ëª…ì„œë¥¼ 참조하시기 ë°”ëžë‹ˆë‹¤: SSH 키 ìƒì„±í•˜ê¸° ë˜ëŠ” SSH를 사용할 ë•Œ ì¼ë°˜ì ì¸ 문제 +gpg_helper=ë„ì›€ì´ í•„ìš”í•˜ì„¸ìš”? GitHubì˜ ì„¤ëª…ì„œë¥¼ 참조하시기 ë°”ëžë‹ˆë‹¤: GPGí‚¤ì— ëŒ€í•˜ì—¬. add_new_key=SSH 키 추가 add_new_gpg_key=GPG 키 추가 gpg_key_id_used=ê°™ì€ IDì˜ GPG 공개키가 ì´ë¯¸ 존재합니다. @@ -531,7 +531,6 @@ visibility.private=비공개 [repo] owner=ì†Œìœ ìž repo_name=저장소 ì´ë¦„ -repo_name_helper=ì¢‹ì€ ì €ìž¥ì†Œ ì´ë¦„ì€ ë³´í†µ 짧고 기억하기 ì¢‹ì€ íŠ¹ë³„í•œ 키워드로 ì´ë£¨ì–´ 집니다. repo_size=저장소 용량 template=템플릿 template_select=템플릿 고르기 @@ -539,7 +538,7 @@ template_helper=템플릿으로 저장소 만들기 visibility=가시성 visibility_helper_forced=사ì´íŠ¸ 관리ìžê°€ 새 ë ˆí¬ì§€í† ë¦¬ì— 대해 비공개로만 ìƒì„±ë˜ë„ë¡ í•˜ì˜€ìŠµë‹ˆë‹¤. visibility_fork_helper=(ë³€ê²½ì‚¬í•­ì„ ì ìš©í•˜ëŠ” 경우 모든 í¬í¬ê°€ ì˜í–¥ì„ 받게 ë©ë‹ˆë‹¤.) -clone_helper="í´ë¡ í•˜ëŠ”ë°ì— ë„ì›€ì´ í•„ìš”í•˜ë©´ Helpì— ë°©ë¬¸í•˜ì„¸ìš”." +clone_helper=í´ë¡ í•˜ëŠ”ë°ì— ë„ì›€ì´ í•„ìš”í•˜ë©´ Helpì— ë°©ë¬¸í•˜ì„¸ìš”. fork_repo=저장소 í¬í¬ fork_from=ì›ë³¸ 프로ì íŠ¸ : fork_visibility_helper=í¬í¬ëœ ì €ìž¥ì†Œì˜ ê°€ì‹œì„±ì€ ë³€ê²½í•˜ì‹¤ 수 없습니다. @@ -641,7 +640,7 @@ editor.or=í˜¹ì€ editor.cancel_lower=취소 editor.commit_changes=변경 ë‚´ìš©ì„ ì»¤ë°‹ editor.commit_message_desc=ì„ íƒì  확장 ì„¤ëª…ì„ ì¶”ê°€... -editor.commit_directly_to_this_branch="%s 브랜치ì—ì„œ ì§ì ‘ 커밋해주세요." +editor.commit_directly_to_this_branch=%s 브랜치ì—ì„œ ì§ì ‘ 커밋해주세요. editor.create_new_branch=ì´ ì»¤ë°‹ì— ëŒ€í•œ 새로운 브랜치를 만들고 ëŒì–´ì˜¤ê¸° ìš”ì²­ì„ ì‹œìž‘í•©ë‹ˆë‹¤. editor.new_branch_name_desc=새로운 브랜치 명... editor.cancel=취소 @@ -668,7 +667,7 @@ ext_issues.desc=외부 ì´ìŠˆ 트래커 ì—°ê²°. projects.description_placeholder=설명 projects.title=제목 projects.new=새 프로ì íŠ¸ -projects.template.desc="템플릿" +projects.template.desc=템플릿 projects.column.edit_title=ì´ë¦„ projects.column.new_title=ì´ë¦„ @@ -732,7 +731,7 @@ issues.action_milestone=마ì¼ìŠ¤í†¤ issues.action_milestone_no_select=마ì¼ìŠ¤í†¤ ì—†ìŒ issues.action_assignee=ë‹´ë‹¹ìž issues.action_assignee_no_select=ë‹´ë‹¹ìž ì—†ìŒ -issues.opened_by=" %[3]sê°€ %[1]sì„ ì˜¤í”ˆ" +issues.opened_by= %[3]sê°€ %[1]sì„ ì˜¤í”ˆ issues.previous=ì´ì „ issues.next=ë‹¤ìŒ issues.open_title=오픈 @@ -748,7 +747,7 @@ issues.create_comment=코멘트 issues.commit_ref_at=` 커밋 %[2]sì—ì„œ ì´ ì´ìŠˆ 언급` issues.role.owner=ì†Œìœ ìž issues.role.member=멤버 -issues.sign_in_require_desc="로그ì¸í•˜ì—¬ ì´ ëŒ€í™”ì— ì°¸ì—¬" +issues.sign_in_require_desc=로그ì¸í•˜ì—¬ ì´ ëŒ€í™”ì— ì°¸ì—¬ issues.edit=수정 issues.cancel=취소 issues.save=저장 @@ -781,9 +780,9 @@ issues.time_spent_total=ì´ ê²½ê³¼ëœ ì‹œê°„ issues.time_spent_from_all_authors=`ì´ ê²½ê³¼ëœ ì‹œê°„: %s` issues.due_date=마ê°ì¼ -issues.invalid_due_date_format="마ê°ì¼ì€ 반드시 'yyyy-mm-dd' 형ì‹ì´ì–´ì•¼ 합니다." -issues.error_modifying_due_date="마ê°ì¼ ìˆ˜ì •ì„ ì‹¤íŒ¨í•˜ì˜€ìŠµë‹ˆë‹¤." -issues.error_removing_due_date="마ê°ì¼ 삭제를 실패하였습니다." +issues.invalid_due_date_format=마ê°ì¼ì€ 반드시 'yyyy-mm-dd' 형ì‹ì´ì–´ì•¼ 합니다. +issues.error_modifying_due_date=마ê°ì¼ ìˆ˜ì •ì„ ì‹¤íŒ¨í•˜ì˜€ìŠµë‹ˆë‹¤. +issues.error_removing_due_date=마ê°ì¼ 삭제를 실패하였습니다. issues.due_date_form=yyyy-mm-dd issues.due_date_form_add=마ê°ì¼ 추가 issues.due_date_form_edit=편집 @@ -791,8 +790,8 @@ issues.due_date_form_remove=ì‚­ì œ issues.due_date_not_set=마ê°ì¼ì´ 설정ë˜ì§€ 않았습니다. issues.due_date_added=마ê°ì¼ %s 를 추가 %s issues.due_date_remove=%s %s 마ê°ì¼ì´ ì‚­ì œë˜ì—ˆìŠµë‹ˆë‹¤. -issues.due_date_overdue="기한 초과" -issues.due_date_invalid="ê¸°í•œì´ ì˜¬ë°”ë¥´ì§€ 않거나 범위를 벗어났습니다. 'yyyy-mm-dd'형ì‹ì„ 사용해주십시오." +issues.due_date_overdue=기한 초과 +issues.due_date_invalid=ê¸°í•œì´ ì˜¬ë°”ë¥´ì§€ 않거나 범위를 벗어났습니다. 'yyyy-mm-dd'형ì‹ì„ 사용해주십시오. issues.dependency.title=ì˜ì¡´ì„± issues.dependency.add=ì˜ì¡´ì„± 추가... issues.dependency.cancel=취소 @@ -810,7 +809,7 @@ issues.dependency.add_error_dep_exists=ì˜ì¡´ì„±ì´ ì´ë¯¸ 존재합니다. issues.dependency.add_error_dep_not_same_repo=ë‘ ì´ìŠˆëŠ” ê°™ì€ ë ˆí¬ì§€í† ë¦¬ ì•ˆì— ìžˆì–´ì•¼ 합니다. issues.review.self.approval=ìžì‹ ì˜ í’€ 리퀘스트를 승ì¸í•  수 없습니다. issues.review.self.rejection=ìžì‹ ì˜ í’€ ë¦¬í€˜ìŠ¤íŠ¸ì— ëŒ€í•œ ë³€ê²½ì„ ìš”ì²­í•  수 없습니다. -issues.review.approve="ì´ ë³€ê²½ì‚¬í•­ì„ ìŠ¹ì¸í•˜ì˜€ìŠµë‹ˆë‹¤. %s" +issues.review.approve=ì´ ë³€ê²½ì‚¬í•­ì„ ìŠ¹ì¸í•˜ì˜€ìŠµë‹ˆë‹¤. %s issues.review.pending=보류 issues.review.review=검토 issues.review.reviewers=리뷰어 @@ -825,7 +824,7 @@ pulls.compare_base=병합하기 pulls.compare_compare=다ìŒìœ¼ë¡œë¶€í„° í’€ pulls.filter_branch=Filter Branch pulls.create=í’€ 리퀘스트 ìƒì„± -pulls.title_desc="%[2]s ì—ì„œ %[3]s ë¡œ %[1]d commits 를 머지하려 합니다" +pulls.title_desc=%[2]s ì—ì„œ %[3]s ë¡œ %[1]d commits 를 머지하려 합니다 pulls.merged_title_desc=%[2]s ì—ì„œ %[3]s ë¡œ %[1]d commits 를 머지했습니다 %[4]s pulls.tab_conversation=대화 pulls.tab_commits=커밋 @@ -856,7 +855,7 @@ milestones.title=타ì´í‹€ milestones.desc=설명 milestones.due_date=기한 (ì„ íƒ ì‚¬í•­) milestones.clear=지우기 -milestones.invalid_due_date_format="마ê°ì¼ì€ 반드시 'yyyy-mm-dd' 형ì‹ì´ì–´ì•¼ 합니다." +milestones.invalid_due_date_format=마ê°ì¼ì€ 반드시 'yyyy-mm-dd' 형ì‹ì´ì–´ì•¼ 합니다. milestones.edit=마ì¼ìŠ¤í†¤ 편집 milestones.cancel=취소 milestones.modify=마ì¼ìŠ¤í†¤ 갱신 @@ -1191,6 +1190,7 @@ teams.repositories=팀 저장소 teams.add_duplicate_users=사용ìžê°€ ì´ë¯¸ 팀 멤버입니다. teams.members.none=ì´ íŒ€ì— ë©¤ë²„ê°€ 없습니다. + [admin] dashboard=대시보드 users=ì‚¬ìš©ìž ê³„ì • @@ -1563,6 +1563,7 @@ runs.commit=커밋 + [projects] [git.filemode] diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini index fd412b95b41..0dfc5683ecd 100644 --- a/options/locale/locale_lv-LV.ini +++ b/options/locale/locale_lv-LV.ini @@ -583,6 +583,7 @@ joined_on=PievienojÄs %s repositories=Repozitoriji activity=PubliskÄ aktivitÄte followers=SekotÄji +show_more=RÄdÄ«t vairÄk starred=AtzÄ«mÄ“ti repozitoriji watched=VÄ“rotie repozitoriji code=Kods @@ -912,7 +913,6 @@ new_repo_helper=Repozitorijs satur visus projekta failus, tajÄ skaitÄ izmaiņu owner=ĪpaÅ¡nieks owner_helper=Å…emot vÄ“rÄ maksimÄlÄ repozitoriju skaita ierobežojumu, ne visas organizÄcijas var tikt parÄdÄ«tas sarakstÄ. repo_name=Repozitorija nosaukums -repo_name_helper=Labi repozitorija nosaukumi ir Ä«si, unikÄli un tÄdi, ko viegli atcerÄ“ties. repo_size=Repozitorija izmÄ“rs template=Sagatave template_select=IzvÄ“lieties sagatavi. @@ -2595,6 +2595,7 @@ teams.invite.title=Tu esi uzaicinÄts pievienoties organizÄcijas %[2]s< teams.invite.by=UzaicinÄja %s teams.invite.description=Nospiediet pogu zemÄk, lai pievienotos komandai. + [admin] dashboard=Infopanelis self_check=PaÅ¡pÄrbaude @@ -3443,6 +3444,7 @@ variables.creation.success=MainÄ«gais "%s" tika pievienots. variables.update.failed=NeizdevÄs labot mainÄ«go. variables.update.success=MainÄ«gais tika labots. + [projects] type-1.display_name=IndividuÄlais projekts type-2.display_name=Repozitorija projekts diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini index a4da8177bce..8a6dabbceb2 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 @@ -2537,6 +2538,7 @@ runs.commit=Commit + [projects] [git.filemode] diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini index 22f701219d3..4d049c83d18 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 @@ -2430,6 +2430,7 @@ runs.commit=Commit + [projects] [git.filemode] diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index 8fb869898b5..f0c034a1331 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). @@ -494,7 +495,7 @@ register_notify.text_3=Se esta conta foi criada para si, defina a s reset_password=Recupere a sua conta reset_password.title=%s, você pediu para recuperar a sua conta -reset_password.text=Por favor clique na seguinte ligação para recuperar a sua conta em %s: +reset_password.text=Para recuperar a sua conta, clique na ligação seguinte (válida por %s): register_success=Inscrição bem sucedida @@ -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,9 @@ new_repo_helper=Um repositório contém todos os ficheiros do trabalho, incluind owner=Proprietário(a) owner_helper=Algumas organizações podem não aparecer na lista suspensa devido a um limite máximo de contagem de repositórios. repo_name=Nome do repositório -repo_name_helper=Um bom nome de repositório utiliza palavras curtas, memoráveis e únicas. +repo_name_profile_public_hint=.profile é um repositório especial que pode usar para adicionar README.md ao seu perfil público da organização, visível para qualquer pessoa. Certifique-se que é público e inicialize-o com um README na pasta do perfil para começar. +repo_name_profile_private_hint=.profile-private é um repositório especial que pode usar para adicionar um README.md ao seu perfil de membro da organização, visível apenas para membros da organização. Certifique-se que é privado e inicialize-o com um README na pasta de perfil para começar. +repo_name_helper=Bons nomes de repositórios usam palavras-chave curtas, memorizáveis e únicas. Um repositório chamado ".profile" ou ".profile-private" pode ser usado para adicionar um README.md ao perfil do utilizador ou da organização. repo_size=Tamanho do repositório template=Modelo template_select=Escolha um modelo. @@ -1231,6 +1235,7 @@ create_new_repo_command=Criando um novo repositório na linha de comandos push_exist_repo=Enviando, pela linha de comandos, um repositório existente empty_message=Este repositório não contém qualquer conteúdo. broken_message=Os dados Git subjacentes a este repositório não podem ser lidos. Contacte o administrador desta instância ou elimine este repositório. +no_branch=Este repositório não tem quaisquer ramos. code=Código code.desc=Aceder ao código fonte, ficheiros, cometimentos e ramos. @@ -1921,8 +1926,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 +1950,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 +2865,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 +3538,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. @@ -3719,6 +3731,7 @@ runners.status.active=Em funcionamento runners.status.offline=Desconectado runners.version=Versão runners.reset_registration_token=Repor código de registo +runners.reset_registration_token_confirm=Gostaria de invalidar o código vigente e gerar um novo? runners.reset_registration_token_success=O código de incrição do executor foi reposto com sucesso runs.all_workflows=Todas as sequências de trabalho @@ -3751,6 +3764,7 @@ workflow.not_found=A sequência de trabalho '%s' não foi encontrada. workflow.run_success=A sequência de trabalho '%s' foi executada com sucesso. workflow.from_ref=Usar sequência de trabalho de workflow.has_workflow_dispatch=Esta sequência de trabalho tem um despoletador de eventos workflow_dispatch. +workflow.has_no_workflow_dispatch=A sequência de trabalho '%s' não tem nenhum despoletador de eventos workflow_dispatch. need_approval_desc=É necessária aprovação para executar sequências de trabalho para a derivação do pedido de integração. @@ -3770,6 +3784,9 @@ variables.creation.success=A variável "%s" foi adicionada. variables.update.failed=Falha ao editar a variável. variables.update.success=A variável foi editada. +logs.always_auto_scroll=Rolar registos de forma automática e permanente +logs.always_expand_running=Expandir sempre os registos que vão rolando + [projects] deleted.display_name=Planeamento eliminado type-1.display_name=Planeamento individual diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini index 735077bd414..027a2cb19d3 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=Ð˜Ð´ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ñ Ð¸ доÑтуп @@ -3373,6 +3374,7 @@ variables.creation.success=ÐŸÐµÑ€ÐµÐ¼ÐµÐ½Ð½Ð°Ñ Â«%s» добавлена. variables.update.failed=Ðе удалоÑÑŒ изменить переменную. variables.update.success=ÐŸÐµÑ€ÐµÐ¼ÐµÐ½Ð½Ð°Ñ Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð°. + [projects] type-1.display_name=Индивидуальный проект type-2.display_name=Проект Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ð¸Ñ diff --git a/options/locale/locale_si-LK.ini b/options/locale/locale_si-LK.ini index 506fa5b4925..167ecaf24a3 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=පරිà·à·“ලක ගිණුම් @@ -2470,6 +2471,7 @@ runs.commit=කà·à¶´ + [projects] [git.filemode] diff --git a/options/locale/locale_sk-SK.ini b/options/locale/locale_sk-SK.ini index b4bf6fb552a..43b190098ff 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 @@ -1328,6 +1328,7 @@ runners.labels=Å títky + [projects] [git.filemode] diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini index fc138381db7..0315ebe9a11 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 @@ -2005,6 +2005,7 @@ runs.commit=Commit + [projects] [git.filemode] diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini index 9b7f2cb5c6d..fb30ab3b67b 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=Облікові запиÑи кориÑтувачів @@ -2538,6 +2539,7 @@ runs.commit=Коміт + [projects] [git.filemode] diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 60fb75df4f6..5e4723a4cd8 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=é€‰æ‹©æ¨¡æ¿ @@ -1945,6 +1945,8 @@ pulls.delete.title=删除此åˆå¹¶è¯·æ±‚? pulls.delete.text=你真的è¦åˆ é™¤è¿™ä¸ªåˆå¹¶è¯·æ±‚å—? (这将永久删除所有内容。如果你打算将内容存档,请考虑关闭它) pulls.recently_pushed_new_branches=您已ç»äºŽ%[2]s推é€äº†åˆ†æ”¯ %[1]s +pulls.upstream_diverging_prompt_behind_1=该分支è½åŽäºŽ %[2]s %[1]d 个æ交 +pulls.upstream_diverging_prompt_behind_n=该分支è½åŽäºŽ %[2]s %[1]d 个æ交 pulls.upstream_diverging_prompt_base_newer=基础分支 %s 有新的更改 pulls.upstream_diverging_merge=åŒæ­¥æ´¾ç”Ÿ @@ -2858,6 +2860,7 @@ teams.invite.title=您已被邀请加入组织 %s 中的团队 teams.invite.by=邀请人 %s teams.invite.description=请点击下é¢çš„按钮加入团队。 + [admin] maintenance=维护 dashboard=管ç†é¢æ¿ @@ -3770,6 +3773,7 @@ variables.creation.success=å˜é‡ “%s†添加æˆåŠŸã€‚ variables.update.failed=编辑å˜é‡å¤±è´¥ã€‚ variables.update.success=该å˜é‡å·²è¢«ç¼–辑。 + [projects] deleted.display_name=已删除项目 type-1.display_name=个人项目 diff --git a/options/locale/locale_zh-HK.ini b/options/locale/locale_zh-HK.ini index 6f37d30efcf..77f8d8a25d8 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=çµ„ç¹”ç®¡ç† @@ -975,6 +976,7 @@ runners.task_list.repository=儲存庫 + [projects] [git.filemode] diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini index 0bfc6d6365f..948b47bc9cb 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=è³‡è¨Šä¸»é  @@ -3763,6 +3764,7 @@ variables.creation.success=已新增變數「%sã€ã€‚ variables.update.failed=編輯變數失敗。 variables.update.success=已編輯變數。 + [projects] deleted.display_name=已刪除的專案 type-1.display_name=個人專案 diff --git a/package-lock.json b/package-lock.json index 60edfa95d07..1e3c5ab155d 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 9f5fbe3fdd8..6881ddb306c 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/actions/artifacts.go b/routers/api/actions/artifacts.go index 0a7f92ac40a..910edd6d589 100644 --- a/routers/api/actions/artifacts.go +++ b/routers/api/actions/artifacts.go @@ -126,11 +126,10 @@ func ArtifactsRoutes(prefix string) *web.Router { func ArtifactContexter() func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - base, baseCleanUp := context.NewBaseContext(resp, req) - defer baseCleanUp() + base := context.NewBaseContext(resp, req) ctx := &ArtifactContext{Base: base} - ctx.AppendContextValue(artifactContextKey, ctx) + ctx.SetContextValue(artifactContextKey, ctx) // action task call server api with Bearer ACTIONS_RUNTIME_TOKEN // we should verify the ACTIONS_RUNTIME_TOKEN diff --git a/routers/api/actions/artifactsv4.go b/routers/api/actions/artifactsv4.go index 6dd36888d24..8917a7a8a23 100644 --- a/routers/api/actions/artifactsv4.go +++ b/routers/api/actions/artifactsv4.go @@ -126,12 +126,9 @@ type artifactV4Routes struct { func ArtifactV4Contexter() func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - base, baseCleanUp := context.NewBaseContext(resp, req) - defer baseCleanUp() - + base := context.NewBaseContext(resp, req) ctx := &ArtifactContext{Base: base} - ctx.AppendContextValue(artifactContextKey, ctx) - + ctx.SetContextValue(artifactContextKey, ctx) next.ServeHTTP(ctx.Resp, ctx.Req) }) } diff --git a/routers/api/actions/runner/runner.go b/routers/api/actions/runner/runner.go index 8f365cc9267..c55b30f7ebb 100644 --- a/routers/api/actions/runner/runner.go +++ b/routers/api/actions/runner/runner.go @@ -69,7 +69,7 @@ func (s *Service) Register( labels := req.Msg.Labels // create new runner - name, _ := util.SplitStringAtByteN(req.Msg.Name, 255) + name := util.EllipsisDisplayString(req.Msg.Name, 255) runner := &actions_model.ActionRunner{ UUID: gouuid.New().String(), Name: name, diff --git a/routers/api/packages/alpine/alpine.go b/routers/api/packages/alpine/alpine.go index 4b652c9ecc3..f35cff3df23 100644 --- a/routers/api/packages/alpine/alpine.go +++ b/routers/api/packages/alpine/alpine.go @@ -114,7 +114,7 @@ func UploadPackageFile(ctx *context.Context) { pck, err := alpine_module.ParsePackage(buf) if err != nil { - if errors.Is(err, util.ErrInvalidArgument) || err == io.EOF { + if errors.Is(err, util.ErrInvalidArgument) || errors.Is(err, io.EOF) { apiError(ctx, http.StatusBadRequest, err) } else { apiError(ctx, http.StatusInternalServerError, err) diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index 47ea7137b82..41c3eb95e90 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -138,45 +138,11 @@ func CommonRoutes() *web.Router { }, reqPackageAccess(perm.AccessModeRead)) r.Group("/arch", func() { r.Methods("HEAD,GET", "/repository.key", arch.GetRepositoryKey) - - r.Methods("HEAD,GET,PUT,DELETE", "*", func(ctx *context.Context) { - path := strings.Trim(ctx.PathParam("*"), "/") - - if ctx.Req.Method == "PUT" { - reqPackageAccess(perm.AccessModeWrite)(ctx) - if ctx.Written() { - return - } - ctx.SetPathParam("repository", path) - arch.UploadPackageFile(ctx) - return - } - - pathFields := strings.Split(path, "/") - pathFieldsLen := len(pathFields) - - if (ctx.Req.Method == "HEAD" || ctx.Req.Method == "GET") && pathFieldsLen >= 2 { - ctx.SetPathParam("repository", strings.Join(pathFields[:pathFieldsLen-2], "/")) - ctx.SetPathParam("architecture", pathFields[pathFieldsLen-2]) - ctx.SetPathParam("filename", pathFields[pathFieldsLen-1]) - arch.GetPackageOrRepositoryFile(ctx) - return - } - - if ctx.Req.Method == "DELETE" && pathFieldsLen >= 3 { - reqPackageAccess(perm.AccessModeWrite)(ctx) - if ctx.Written() { - return - } - ctx.SetPathParam("repository", strings.Join(pathFields[:pathFieldsLen-3], "/")) - ctx.SetPathParam("name", pathFields[pathFieldsLen-3]) - ctx.SetPathParam("version", pathFields[pathFieldsLen-2]) - ctx.SetPathParam("architecture", pathFields[pathFieldsLen-1]) - arch.DeletePackageVersion(ctx) - return - } - - ctx.Status(http.StatusNotFound) + r.Methods("PUT", "" /* no repository */, reqPackageAccess(perm.AccessModeWrite), arch.UploadPackageFile) + r.PathGroup("/*", func(g *web.RouterPathGroup) { + g.MatchPath("PUT", "/", reqPackageAccess(perm.AccessModeWrite), arch.UploadPackageFile) + g.MatchPath("HEAD,GET", "///", arch.GetPackageOrRepositoryFile) + g.MatchPath("DELETE", "////", reqPackageAccess(perm.AccessModeWrite), arch.DeletePackageVersion) }) }, reqPackageAccess(perm.AccessModeRead)) r.Group("/cargo", func() { @@ -733,150 +699,28 @@ func ContainerRoutes() *web.Router { }) r.Get("/_catalog", container.ReqContainerAccess, container.GetRepositoryList) r.Group("/{username}", func() { - r.Group("/{image}", func() { - r.Group("/blobs/uploads", func() { - r.Post("", container.InitiateUploadBlob) - r.Group("/{uuid}", func() { - r.Get("", container.GetUploadBlob) - r.Patch("", container.UploadBlob) - r.Put("", container.EndUploadBlob) - r.Delete("", container.CancelUploadBlob) - }) - }, reqPackageAccess(perm.AccessModeWrite)) - r.Group("/blobs/{digest}", func() { - r.Head("", container.HeadBlob) - r.Get("", container.GetBlob) - r.Delete("", reqPackageAccess(perm.AccessModeWrite), container.DeleteBlob) - }) - r.Group("/manifests/{reference}", func() { - r.Put("", reqPackageAccess(perm.AccessModeWrite), container.UploadManifest) - r.Head("", container.HeadManifest) - r.Get("", container.GetManifest) - r.Delete("", reqPackageAccess(perm.AccessModeWrite), container.DeleteManifest) - }) - r.Get("/tags/list", container.GetTagList) - }, container.VerifyImageName) - - var ( - blobsUploadsPattern = regexp.MustCompile(`\A(.+)/blobs/uploads/([a-zA-Z0-9-_.=]+)\z`) - blobsPattern = regexp.MustCompile(`\A(.+)/blobs/([^/]+)\z`) - manifestsPattern = regexp.MustCompile(`\A(.+)/manifests/([^/]+)\z`) - ) - - // Manual mapping of routes because {image} can contain slashes which chi does not support - r.Methods("HEAD,GET,POST,PUT,PATCH,DELETE", "/*", func(ctx *context.Context) { - path := ctx.PathParam("*") - isHead := ctx.Req.Method == "HEAD" - isGet := ctx.Req.Method == "GET" - isPost := ctx.Req.Method == "POST" - isPut := ctx.Req.Method == "PUT" - isPatch := ctx.Req.Method == "PATCH" - isDelete := ctx.Req.Method == "DELETE" - - if isPost && strings.HasSuffix(path, "/blobs/uploads") { - reqPackageAccess(perm.AccessModeWrite)(ctx) - if ctx.Written() { - return - } - - ctx.SetPathParam("image", path[:len(path)-14]) - container.VerifyImageName(ctx) - if ctx.Written() { - return - } - - container.InitiateUploadBlob(ctx) - return - } - if isGet && strings.HasSuffix(path, "/tags/list") { - ctx.SetPathParam("image", path[:len(path)-10]) - container.VerifyImageName(ctx) - if ctx.Written() { - return - } - - container.GetTagList(ctx) - return - } - - m := blobsUploadsPattern.FindStringSubmatch(path) - if len(m) == 3 && (isGet || isPut || isPatch || isDelete) { - reqPackageAccess(perm.AccessModeWrite)(ctx) - if ctx.Written() { - return - } - - ctx.SetPathParam("image", m[1]) - container.VerifyImageName(ctx) - if ctx.Written() { - return - } - - ctx.SetPathParam("uuid", m[2]) - - if isGet { + r.PathGroup("/*", func(g *web.RouterPathGroup) { + g.MatchPath("POST", "//blobs/uploads", reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName, container.InitiateUploadBlob) + g.MatchPath("GET", "//tags/list", container.VerifyImageName, container.GetTagList) + g.MatchPath("GET,PATCH,PUT,DELETE", `//blobs/uploads/`, reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName, func(ctx *context.Context) { + if ctx.Req.Method == http.MethodGet { container.GetUploadBlob(ctx) - } else if isPatch { + } else if ctx.Req.Method == http.MethodPatch { container.UploadBlob(ctx) - } else if isPut { + } else if ctx.Req.Method == http.MethodPut { container.EndUploadBlob(ctx) - } else { + } else /* DELETE */ { container.CancelUploadBlob(ctx) } - return - } - m = blobsPattern.FindStringSubmatch(path) - if len(m) == 3 && (isHead || isGet || isDelete) { - ctx.SetPathParam("image", m[1]) - container.VerifyImageName(ctx) - if ctx.Written() { - return - } - - ctx.SetPathParam("digest", m[2]) - - if isHead { - container.HeadBlob(ctx) - } else if isGet { - container.GetBlob(ctx) - } else { - reqPackageAccess(perm.AccessModeWrite)(ctx) - if ctx.Written() { - return - } - container.DeleteBlob(ctx) - } - return - } - m = manifestsPattern.FindStringSubmatch(path) - if len(m) == 3 && (isHead || isGet || isPut || isDelete) { - ctx.SetPathParam("image", m[1]) - container.VerifyImageName(ctx) - if ctx.Written() { - return - } - - ctx.SetPathParam("reference", m[2]) - - if isHead { - container.HeadManifest(ctx) - } else if isGet { - container.GetManifest(ctx) - } else { - reqPackageAccess(perm.AccessModeWrite)(ctx) - if ctx.Written() { - return - } - if isPut { - container.UploadManifest(ctx) - } else { - container.DeleteManifest(ctx) - } - } - return - } - - ctx.Status(http.StatusNotFound) + }) + g.MatchPath("HEAD", `//blobs/`, container.VerifyImageName, container.HeadBlob) + g.MatchPath("GET", `//blobs/`, container.VerifyImageName, container.GetBlob) + g.MatchPath("DELETE", `//blobs/`, container.VerifyImageName, reqPackageAccess(perm.AccessModeWrite), container.DeleteBlob) + + g.MatchPath("HEAD", `//manifests/`, container.VerifyImageName, container.HeadManifest) + g.MatchPath("GET", `//manifests/`, container.VerifyImageName, container.GetManifest) + g.MatchPath("PUT", `//manifests/`, container.VerifyImageName, reqPackageAccess(perm.AccessModeWrite), container.UploadManifest) + g.MatchPath("DELETE", `//manifests/`, container.VerifyImageName, reqPackageAccess(perm.AccessModeWrite), container.DeleteManifest) }) }, container.ReqContainerAccess, context.UserAssignmentWeb(), context.PackageAssignment(), reqPackageAccess(perm.AccessModeRead)) diff --git a/routers/api/packages/arch/arch.go b/routers/api/packages/arch/arch.go index 573e93cfb01..f5dc6c1d019 100644 --- a/routers/api/packages/arch/arch.go +++ b/routers/api/packages/arch/arch.go @@ -62,7 +62,7 @@ func UploadPackageFile(ctx *context.Context) { pck, err := arch_module.ParsePackage(buf) if err != nil { - if errors.Is(err, util.ErrInvalidArgument) || err == io.EOF { + if errors.Is(err, util.ErrInvalidArgument) || errors.Is(err, io.EOF) { apiError(ctx, http.StatusBadRequest, err) } else { apiError(ctx, http.StatusInternalServerError, err) diff --git a/routers/api/packages/cargo/cargo.go b/routers/api/packages/cargo/cargo.go index 3d8407e6b60..42ef13476c1 100644 --- a/routers/api/packages/cargo/cargo.go +++ b/routers/api/packages/cargo/cargo.go @@ -181,7 +181,7 @@ func DownloadPackageFile(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -276,7 +276,7 @@ func UnyankPackage(ctx *context.Context) { func yankPackage(ctx *context.Context, yank bool) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeCargo, ctx.PathParam("package"), ctx.PathParam("version")) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } diff --git a/routers/api/packages/chef/chef.go b/routers/api/packages/chef/chef.go index b3cdf126977..a0c8c5696c6 100644 --- a/routers/api/packages/chef/chef.go +++ b/routers/api/packages/chef/chef.go @@ -216,7 +216,7 @@ func PackageVersionMetadata(ctx *context.Context) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeChef, packageName, packageVersion) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -327,7 +327,7 @@ func UploadPackage(ctx *context.Context) { func DownloadPackage(ctx *context.Context) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeChef, ctx.PathParam("name"), ctx.PathParam("version")) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -368,7 +368,7 @@ func DeletePackageVersion(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) diff --git a/routers/api/packages/composer/composer.go b/routers/api/packages/composer/composer.go index 40f72f64848..c6c14e5cf4f 100644 --- a/routers/api/packages/composer/composer.go +++ b/routers/api/packages/composer/composer.go @@ -176,7 +176,7 @@ func DownloadPackageFile(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } diff --git a/routers/api/packages/conan/conan.go b/routers/api/packages/conan/conan.go index 4a9f0a3ffc7..8019eee9f74 100644 --- a/routers/api/packages/conan/conan.go +++ b/routers/api/packages/conan/conan.go @@ -5,6 +5,7 @@ package conan import ( std_ctx "context" + "errors" "fmt" "io" "net/http" @@ -183,7 +184,7 @@ func serveSnapshot(ctx *context.Context, fileKey string) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeConan, rref.Name, rref.Version) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -244,7 +245,7 @@ func serveDownloadURLs(ctx *context.Context, fileKey, downloadURL string) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeConan, rref.Name, rref.Version) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -493,7 +494,7 @@ func downloadFile(ctx *context.Context, fileFilter container.Set[string], fileKe }, ) if err != nil { - if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -509,7 +510,7 @@ func DeleteRecipeV1(ctx *context.Context) { rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference) if err := deleteRecipeOrPackage(ctx, rref, true, nil, false); err != nil { - if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, conan_model.ErrPackageReferenceNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -524,7 +525,7 @@ func DeleteRecipeV2(ctx *context.Context) { rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference) if err := deleteRecipeOrPackage(ctx, rref, rref.Revision == "", nil, false); err != nil { - if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, conan_model.ErrPackageReferenceNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -571,7 +572,7 @@ func DeletePackageV1(ctx *context.Context) { for _, reference := range references { pref, _ := conan_module.NewPackageReference(currentRref, reference.Value, conan_module.DefaultRevision) if err := deleteRecipeOrPackage(ctx, currentRref, true, pref, true); err != nil { - if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, conan_model.ErrPackageReferenceNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -590,7 +591,7 @@ func DeletePackageV2(ctx *context.Context) { if pref != nil { // has package reference if err := deleteRecipeOrPackage(ctx, rref, false, pref, pref.Revision == ""); err != nil { - if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, conan_model.ErrPackageReferenceNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -615,7 +616,7 @@ func DeletePackageV2(ctx *context.Context) { pref, _ := conan_module.NewPackageReference(rref, reference.Value, conan_module.DefaultRevision) if err := deleteRecipeOrPackage(ctx, rref, false, pref, true); err != nil { - if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, conan_model.ErrPackageReferenceNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -749,7 +750,7 @@ func LatestRecipeRevision(ctx *context.Context) { revision, err := conan_model.GetLastRecipeRevision(ctx, ctx.Package.Owner.ID, rref) if err != nil { - if err == conan_model.ErrRecipeReferenceNotExist || err == conan_model.ErrPackageReferenceNotExist { + if errors.Is(err, conan_model.ErrRecipeReferenceNotExist) || errors.Is(err, conan_model.ErrPackageReferenceNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -766,7 +767,7 @@ func LatestPackageRevision(ctx *context.Context) { revision, err := conan_model.GetLastPackageRevision(ctx, ctx.Package.Owner.ID, pref) if err != nil { - if err == conan_model.ErrRecipeReferenceNotExist || err == conan_model.ErrPackageReferenceNotExist { + if errors.Is(err, conan_model.ErrRecipeReferenceNotExist) || errors.Is(err, conan_model.ErrPackageReferenceNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -796,7 +797,7 @@ func listRevisionFiles(ctx *context.Context, fileKey string) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeConan, rref.Name, rref.Version) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) diff --git a/routers/api/packages/conan/search.go b/routers/api/packages/conan/search.go index 7370c702cd6..0dbbd500d28 100644 --- a/routers/api/packages/conan/search.go +++ b/routers/api/packages/conan/search.go @@ -4,6 +4,7 @@ package conan import ( + "errors" "net/http" "strings" @@ -76,7 +77,7 @@ func searchPackages(ctx *context.Context, searchAllRevisions bool) { if !searchAllRevisions && rref.Revision == "" { lastRevision, err := conan_model.GetLastRecipeRevision(ctx, ctx.Package.Owner.ID, rref) if err != nil { - if err == conan_model.ErrRecipeReferenceNotExist { + if errors.Is(err, conan_model.ErrRecipeReferenceNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -87,7 +88,7 @@ func searchPackages(ctx *context.Context, searchAllRevisions bool) { } else { has, err := conan_model.RecipeExists(ctx, ctx.Package.Owner.ID, rref) if err != nil { - if err == conan_model.ErrRecipeReferenceNotExist { + if errors.Is(err, conan_model.ErrRecipeReferenceNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -119,7 +120,7 @@ func searchPackages(ctx *context.Context, searchAllRevisions bool) { } packageReferences, err := conan_model.GetPackageReferences(ctx, ctx.Package.Owner.ID, currentRef) if err != nil { - if err == conan_model.ErrRecipeReferenceNotExist { + if errors.Is(err, conan_model.ErrRecipeReferenceNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -133,7 +134,7 @@ func searchPackages(ctx *context.Context, searchAllRevisions bool) { pref, _ := conan_module.NewPackageReference(currentRef, packageReference.Value, "") lastPackageRevision, err := conan_model.GetLastPackageRevision(ctx, ctx.Package.Owner.ID, pref) if err != nil { - if err == conan_model.ErrPackageReferenceNotExist { + if errors.Is(err, conan_model.ErrPackageReferenceNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -143,7 +144,7 @@ func searchPackages(ctx *context.Context, searchAllRevisions bool) { pref = pref.WithRevision(lastPackageRevision.Value) infoRaw, err := conan_model.GetPackageInfo(ctx, ctx.Package.Owner.ID, pref) if err != nil { - if err == conan_model.ErrPackageReferenceNotExist { + if errors.Is(err, conan_model.ErrPackageReferenceNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) diff --git a/routers/api/packages/container/blob.go b/routers/api/packages/container/blob.go index 4595b9a33de..671803788aa 100644 --- a/routers/api/packages/container/blob.go +++ b/routers/api/packages/container/blob.go @@ -111,12 +111,11 @@ func getOrCreateUploadVersion(ctx context.Context, pi *packages_service.PackageI } var err error if p, err = packages_model.TryInsertPackage(ctx, p); err != nil { - if err == packages_model.ErrDuplicatePackage { - created = false - } else { + if !errors.Is(err, packages_model.ErrDuplicatePackage) { log.Error("Error inserting package: %v", err) return err } + created = false } if created { @@ -135,7 +134,7 @@ func getOrCreateUploadVersion(ctx context.Context, pi *packages_service.PackageI MetadataJSON: "null", } if pv, err = packages_model.GetOrInsertVersion(ctx, pv); err != nil { - if err != packages_model.ErrDuplicatePackageVersion { + if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) { log.Error("Error inserting package: %v", err) return err } @@ -161,7 +160,7 @@ func createFileForBlob(ctx context.Context, pv *packages_model.PackageVersion, p } var err error if pf, err = packages_model.TryInsertFile(ctx, pf); err != nil { - if err == packages_model.ErrDuplicatePackageFile { + if errors.Is(err, packages_model.ErrDuplicatePackageFile) { return nil } log.Error("Error inserting package file: %v", err) diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go index 3a470ad6852..bb14db9db79 100644 --- a/routers/api/packages/container/container.go +++ b/routers/api/packages/container/container.go @@ -324,7 +324,7 @@ func GetUploadBlob(ctx *context.Context) { upload, err := packages_model.GetBlobUploadByID(ctx, uuid) if err != nil { - if err == packages_model.ErrPackageBlobUploadNotExist { + if errors.Is(err, packages_model.ErrPackageBlobUploadNotExist) { apiErrorDefined(ctx, errBlobUploadUnknown) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -345,7 +345,7 @@ func UploadBlob(ctx *context.Context) { uploader, err := container_service.NewBlobUploader(ctx, ctx.PathParam("uuid")) if err != nil { - if err == packages_model.ErrPackageBlobUploadNotExist { + if errors.Is(err, packages_model.ErrPackageBlobUploadNotExist) { apiErrorDefined(ctx, errBlobUploadUnknown) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -396,7 +396,7 @@ func EndUploadBlob(ctx *context.Context) { uploader, err := container_service.NewBlobUploader(ctx, ctx.PathParam("uuid")) if err != nil { - if err == packages_model.ErrPackageBlobUploadNotExist { + if errors.Is(err, packages_model.ErrPackageBlobUploadNotExist) { apiErrorDefined(ctx, errBlobUploadUnknown) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -465,7 +465,7 @@ func CancelUploadBlob(ctx *context.Context) { _, err := packages_model.GetBlobUploadByID(ctx, uuid) if err != nil { - if err == packages_model.ErrPackageBlobUploadNotExist { + if errors.Is(err, packages_model.ErrPackageBlobUploadNotExist) { apiErrorDefined(ctx, errBlobUploadUnknown) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -501,7 +501,7 @@ func getBlobFromContext(ctx *context.Context) (*packages_model.PackageFileDescri func HeadBlob(ctx *context.Context) { blob, err := getBlobFromContext(ctx) if err != nil { - if err == container_model.ErrContainerBlobNotExist { + if errors.Is(err, container_model.ErrContainerBlobNotExist) { apiErrorDefined(ctx, errBlobUnknown) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -520,7 +520,7 @@ func HeadBlob(ctx *context.Context) { func GetBlob(ctx *context.Context) { blob, err := getBlobFromContext(ctx) if err != nil { - if err == container_model.ErrContainerBlobNotExist { + if errors.Is(err, container_model.ErrContainerBlobNotExist) { apiErrorDefined(ctx, errBlobUnknown) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -639,7 +639,7 @@ func getManifestFromContext(ctx *context.Context) (*packages_model.PackageFileDe func HeadManifest(ctx *context.Context) { manifest, err := getManifestFromContext(ctx) if err != nil { - if err == container_model.ErrContainerBlobNotExist { + if errors.Is(err, container_model.ErrContainerBlobNotExist) { apiErrorDefined(ctx, errManifestUnknown) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -659,7 +659,7 @@ func HeadManifest(ctx *context.Context) { func GetManifest(ctx *context.Context) { manifest, err := getManifestFromContext(ctx) if err != nil { - if err == container_model.ErrContainerBlobNotExist { + if errors.Is(err, container_model.ErrContainerBlobNotExist) { apiErrorDefined(ctx, errManifestUnknown) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -739,7 +739,7 @@ func GetTagList(ctx *context.Context) { image := ctx.PathParam("image") if _, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeContainer, image); err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiErrorDefined(ctx, errNameUnknown) } else { apiError(ctx, http.StatusInternalServerError, err) diff --git a/routers/api/packages/container/manifest.go b/routers/api/packages/container/manifest.go index 4a79a58f51f..ad035cf473e 100644 --- a/routers/api/packages/container/manifest.go +++ b/routers/api/packages/container/manifest.go @@ -240,7 +240,7 @@ func processImageManifestIndex(ctx context.Context, mci *manifestCreationInfo, b IsManifest: true, }) if err != nil { - if err == container_model.ErrContainerBlobNotExist { + if errors.Is(err, container_model.ErrContainerBlobNotExist) { return errManifestBlobUnknown } return err @@ -321,12 +321,11 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met } var err error if p, err = packages_model.TryInsertPackage(ctx, p); err != nil { - if err == packages_model.ErrDuplicatePackage { - created = false - } else { + if !errors.Is(err, packages_model.ErrDuplicatePackage) { log.Error("Error inserting package: %v", err) return nil, err } + created = false } if created { @@ -352,21 +351,23 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met } var pv *packages_model.PackageVersion if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil { - if err == packages_model.ErrDuplicatePackageVersion { - if err := packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil { - return nil, err - } + if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) { + log.Error("Error inserting package: %v", err) + return nil, err + } - // keep download count on overwrite - _pv.DownloadCount = pv.DownloadCount + if err = packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil { + return nil, err + } + + // keep download count on overwrite + _pv.DownloadCount = pv.DownloadCount - if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil { + if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil { + if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) { log.Error("Error inserting package: %v", err) return nil, err } - } else { - log.Error("Error inserting package: %v", err) - return nil, err } } @@ -417,7 +418,7 @@ func createFileFromBlobReference(ctx context.Context, pv, uploadVersion *package } var err error if pf, err = packages_model.TryInsertFile(ctx, pf); err != nil { - if err == packages_model.ErrDuplicatePackageFile { + if errors.Is(err, packages_model.ErrDuplicatePackageFile) { // Skip this blob because the manifest contains the same filesystem layer multiple times. return nil } diff --git a/routers/api/packages/debian/debian.go b/routers/api/packages/debian/debian.go index 162122ccbdc..fec34c91a6e 100644 --- a/routers/api/packages/debian/debian.go +++ b/routers/api/packages/debian/debian.go @@ -68,7 +68,7 @@ func GetRepositoryFile(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) diff --git a/routers/api/packages/generic/generic.go b/routers/api/packages/generic/generic.go index 868caf9cf06..0b5daa7334a 100644 --- a/routers/api/packages/generic/generic.go +++ b/routers/api/packages/generic/generic.go @@ -44,7 +44,7 @@ func DownloadPackageFile(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -155,7 +155,7 @@ func DeletePackage(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -182,7 +182,7 @@ func DeletePackageFile(ctx *context.Context) { return pv, pf, nil }() if err != nil { - if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } diff --git a/routers/api/packages/helm/helm.go b/routers/api/packages/helm/helm.go index cb30a20074e..fb12daaa46a 100644 --- a/routers/api/packages/helm/helm.go +++ b/routers/api/packages/helm/helm.go @@ -130,7 +130,7 @@ func DownloadPackageFile(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } diff --git a/routers/api/packages/maven/maven.go b/routers/api/packages/maven/maven.go index 9474b17bc79..4d04d4d1e9f 100644 --- a/routers/api/packages/maven/maven.go +++ b/routers/api/packages/maven/maven.go @@ -13,7 +13,7 @@ import ( "errors" "io" "net/http" - "path/filepath" + "path" "regexp" "sort" "strconv" @@ -25,6 +25,7 @@ import ( "code.gitea.io/gitea/modules/log" packages_module "code.gitea.io/gitea/modules/packages" maven_module "code.gitea.io/gitea/modules/packages/maven" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/api/packages/helper" "code.gitea.io/gitea/services/context" packages_service "code.gitea.io/gitea/services/packages" @@ -44,7 +45,7 @@ const ( var ( errInvalidParameters = errors.New("request parameters are invalid") - illegalCharacters = regexp.MustCompile(`[\\/:"<>|?\*]`) + illegalCharacters = regexp.MustCompile(`[\\/:"<>|?*]`) ) func apiError(ctx *context.Context, status int, obj any) { @@ -85,8 +86,10 @@ func handlePackageFile(ctx *context.Context, serveContent bool) { func serveMavenMetadata(ctx *context.Context, params parameters) { // /com/foo/project/maven-metadata.xml[.md5/.sha1/.sha256/.sha512] - packageName := params.GroupID + "-" + params.ArtifactID - pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, packageName) + pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageName()) + if errors.Is(err, util.ErrNotExist) { + pvs, err = packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageNameLegacy()) + } if err != nil { apiError(ctx, http.StatusInternalServerError, err) return @@ -116,10 +119,10 @@ func serveMavenMetadata(ctx *context.Context, params parameters) { latest := pds[len(pds)-1] // http.TimeFormat required a UTC time, refer to https://pkg.go.dev/net/http#TimeFormat - lastModifed := latest.Version.CreatedUnix.AsTime().UTC().Format(http.TimeFormat) - ctx.Resp.Header().Set("Last-Modified", lastModifed) + lastModified := latest.Version.CreatedUnix.AsTime().UTC().Format(http.TimeFormat) + ctx.Resp.Header().Set("Last-Modified", lastModified) - ext := strings.ToLower(filepath.Ext(params.Filename)) + ext := strings.ToLower(path.Ext(params.Filename)) if isChecksumExtension(ext) { var hash []byte switch ext { @@ -147,11 +150,12 @@ func serveMavenMetadata(ctx *context.Context, params parameters) { } func servePackageFile(ctx *context.Context, params parameters, serveContent bool) { - packageName := params.GroupID + "-" + params.ArtifactID - - pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, packageName, params.Version) + pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageName(), params.Version) + if errors.Is(err, util.ErrNotExist) { + pv, err = packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageNameLegacy(), params.Version) + } if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -161,14 +165,14 @@ func servePackageFile(ctx *context.Context, params parameters, serveContent bool filename := params.Filename - ext := strings.ToLower(filepath.Ext(filename)) + ext := strings.ToLower(path.Ext(filename)) if isChecksumExtension(ext) { filename = filename[:len(filename)-len(ext)] } pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, filename, packages_model.EmptyFileKey) if err != nil { - if err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -238,15 +242,17 @@ func UploadPackageFile(ctx *context.Context) { return } - log.Trace("Parameters: %+v", params) - // Ignore the package index //maven-metadata.xml if params.IsMeta && params.Version == "" { ctx.Status(http.StatusOK) return } - packageName := params.GroupID + "-" + params.ArtifactID + packageName := params.toInternalPackageName() + if ctx.FormBool("use_legacy_package_name") { + // for testing purpose only + packageName = params.toInternalPackageNameLegacy() + } // for the same package, only one upload at a time releaser, err := globallock.Lock(ctx, mavenPkgNameKey(packageName)) @@ -274,13 +280,26 @@ func UploadPackageFile(ctx *context.Context) { Creator: ctx.Doer, } - ext := filepath.Ext(params.Filename) + // old maven package uses "groupId-artifactId" as package name, so we need to update to the new format "groupId:artifactId" + legacyPackage, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageNameLegacy()) + if err != nil && !errors.Is(err, packages_model.ErrPackageNotExist) { + apiError(ctx, http.StatusInternalServerError, err) + return + } else if legacyPackage != nil { + err = packages_model.UpdatePackageNameByID(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, legacyPackage.ID, packageName) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + } + + ext := path.Ext(params.Filename) // Do not upload checksum files but compare the hashes. if isChecksumExtension(ext) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, pvci.Owner.ID, pvci.PackageType, pvci.Name, pvci.Version) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -289,7 +308,7 @@ func UploadPackageFile(ctx *context.Context) { } pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, params.Filename[:len(params.Filename)-len(ext)], packages_model.EmptyFileKey) if err != nil { - if err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -343,7 +362,7 @@ func UploadPackageFile(ctx *context.Context) { if pvci.Metadata != nil { pv, err := packages_model.GetVersionByNameAndVersion(ctx, pvci.Owner.ID, pvci.PackageType, pvci.Name, pvci.Version) - if err != nil && err != packages_model.ErrPackageNotExist { + if err != nil && !errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusInternalServerError, err) return } @@ -399,9 +418,26 @@ type parameters struct { IsMeta bool } +func (p *parameters) toInternalPackageName() string { + // there cuold be 2 choices: "/" or ":" + // Maven says: "groupId:artifactId:version" in their document: https://maven.apache.org/pom.html#Maven_Coordinates + // but it would be slightly ugly in URL: "/-/packages/maven/group-id%3Aartifact-id" + return p.GroupID + ":" + p.ArtifactID +} + +func (p *parameters) toInternalPackageNameLegacy() string { + return p.GroupID + "-" + p.ArtifactID +} + func extractPathParameters(ctx *context.Context) (parameters, error) { parts := strings.Split(ctx.PathParam("*"), "/") + // formats: + // * /com/group/id/artifactId/maven-metadata.xml[.md5|.sha1|.sha256|.sha512] + // * /com/group/id/artifactId/version-SNAPSHOT/maven-metadata.xml[.md5|.sha1|.sha256|.sha512] + // * /com/group/id/artifactId/version/any-file + // * /com/group/id/artifactId/version-SNAPSHOT/any-file + p := parameters{ Filename: parts[len(parts)-1], } diff --git a/routers/api/packages/npm/api.go b/routers/api/packages/npm/api.go index b4379f3f497..636680242a4 100644 --- a/routers/api/packages/npm/api.go +++ b/routers/api/packages/npm/api.go @@ -67,6 +67,7 @@ func createPackageMetadataVersion(registryURL string, pd *packages_model.Package BundleDependencies: metadata.BundleDependencies, DevDependencies: metadata.DevelopmentDependencies, PeerDependencies: metadata.PeerDependencies, + PeerDependenciesMeta: metadata.PeerDependenciesMeta, OptionalDependencies: metadata.OptionalDependencies, Readme: metadata.Readme, Bin: metadata.Bin, diff --git a/routers/api/packages/npm/npm.go b/routers/api/packages/npm/npm.go index 284723e0d7d..6ec46bcb365 100644 --- a/routers/api/packages/npm/npm.go +++ b/routers/api/packages/npm/npm.go @@ -98,7 +98,7 @@ func DownloadPackageFile(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -140,7 +140,7 @@ func DownloadPackageFileByName(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -163,7 +163,7 @@ func UploadPackage(ctx *context.Context) { return } - repo, err := repo_model.GetRepositoryByURL(ctx, npmPackage.Metadata.Repository.URL) + repo, err := repo_model.GetRepositoryByURLRelax(ctx, npmPackage.Metadata.Repository.URL) if err == nil { canWrite := repo.OwnerID == ctx.Doer.ID @@ -267,7 +267,7 @@ func DeletePackageVersion(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -341,7 +341,7 @@ func AddPackageTag(ctx *context.Context) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeNpm, packageName, version) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } diff --git a/routers/api/packages/nuget/nuget.go b/routers/api/packages/nuget/nuget.go index 70b95e6a774..07a8de0a684 100644 --- a/routers/api/packages/nuget/nuget.go +++ b/routers/api/packages/nuget/nuget.go @@ -259,7 +259,7 @@ func RegistrationLeafV2(ctx *context.Context) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeNuGet, packageName, packageVersion) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -288,7 +288,7 @@ func RegistrationLeafV3(ctx *context.Context) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeNuGet, packageName, packageVersion) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -418,7 +418,7 @@ func DownloadPackageFile(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -671,7 +671,7 @@ func DownloadSymbolFile(ctx *context.Context) { s, u, pf, err := packages_service.GetPackageFileStream(ctx, pfs[0]) if err != nil { - if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -699,7 +699,7 @@ func DeletePackage(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } diff --git a/routers/api/packages/pub/pub.go b/routers/api/packages/pub/pub.go index 2be27323fd1..e7b07aefd07 100644 --- a/routers/api/packages/pub/pub.go +++ b/routers/api/packages/pub/pub.go @@ -124,7 +124,7 @@ func PackageVersionMetadata(ctx *context.Context) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypePub, packageName, packageVersion) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -233,7 +233,7 @@ func FinalizePackage(ctx *context.Context) { _, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypePub, packageName, packageVersion) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -258,7 +258,7 @@ func DownloadPackageFile(ctx *context.Context) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypePub, packageName, packageVersion) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } diff --git a/routers/api/packages/pypi/pypi.go b/routers/api/packages/pypi/pypi.go index 5ea86071a9e..199f4e74787 100644 --- a/routers/api/packages/pypi/pypi.go +++ b/routers/api/packages/pypi/pypi.go @@ -5,11 +5,13 @@ package pypi import ( "encoding/hex" + "errors" "io" "net/http" "regexp" "sort" "strings" + "unicode" packages_model "code.gitea.io/gitea/models/packages" packages_module "code.gitea.io/gitea/modules/packages" @@ -93,7 +95,7 @@ func DownloadPackageFile(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -139,9 +141,30 @@ func UploadPackageFile(ctx *context.Context) { return } - projectURL := ctx.Req.FormValue("home_page") - if !validation.IsValidURL(projectURL) { - projectURL = "" + // Ensure ctx.Req.Form exists. + _ = ctx.Req.ParseForm() + + var homepageURL string + projectURLs := ctx.Req.Form["project_urls"] + for _, purl := range projectURLs { + label, url, found := strings.Cut(purl, ",") + if !found { + continue + } + if normalizeLabel(label) != "homepage" { + continue + } + homepageURL = strings.TrimSpace(url) + break + } + + if len(homepageURL) == 0 { + // TODO: Home-page is a deprecated metadata field. Remove this branch once it's no longer apart of the spec. + homepageURL = ctx.Req.FormValue("home_page") + } + + if !validation.IsValidURL(homepageURL) { + homepageURL = "" } _, _, err = packages_service.CreatePackageOrAddFileToExisting( @@ -160,7 +183,7 @@ func UploadPackageFile(ctx *context.Context) { Description: ctx.Req.FormValue("description"), LongDescription: ctx.Req.FormValue("long_description"), Summary: ctx.Req.FormValue("summary"), - ProjectURL: projectURL, + ProjectURL: homepageURL, License: ctx.Req.FormValue("license"), RequiresPython: ctx.Req.FormValue("requires_python"), }, @@ -189,6 +212,23 @@ func UploadPackageFile(ctx *context.Context) { ctx.Status(http.StatusCreated) } +// Normalizes a Project-URL label. +// See https://packaging.python.org/en/latest/specifications/well-known-project-urls/#label-normalization. +func normalizeLabel(label string) string { + var builder strings.Builder + + // "A label is normalized by deleting all ASCII punctuation and whitespace, and then converting the result + // to lowercase." + for _, r := range label { + if unicode.IsPunct(r) || unicode.IsSpace(r) { + continue + } + builder.WriteRune(unicode.ToLower(r)) + } + + return builder.String() +} + func isValidNameAndVersion(packageName, packageVersion string) bool { return nameMatcher.MatchString(packageName) && versionMatcher.MatchString(packageVersion) } diff --git a/routers/api/packages/pypi/pypi_test.go b/routers/api/packages/pypi/pypi_test.go index 3023692177f..786105693f1 100644 --- a/routers/api/packages/pypi/pypi_test.go +++ b/routers/api/packages/pypi/pypi_test.go @@ -36,3 +36,13 @@ func TestIsValidNameAndVersion(t *testing.T) { assert.False(t, isValidNameAndVersion("test-name", "1.0.1aa")) assert.False(t, isValidNameAndVersion("test-name", "1.0.0-alpha.beta")) } + +func TestNormalizeLabel(t *testing.T) { + // Cases fetched from https://packaging.python.org/en/latest/specifications/well-known-project-urls/#label-normalization. + assert.Equal(t, "homepage", normalizeLabel("Homepage")) + assert.Equal(t, "homepage", normalizeLabel("Home-page")) + assert.Equal(t, "homepage", normalizeLabel("Home page")) + assert.Equal(t, "changelog", normalizeLabel("Change_Log")) + assert.Equal(t, "whatsnew", normalizeLabel("What's New?")) + assert.Equal(t, "github", normalizeLabel("github")) +} diff --git a/routers/api/packages/rubygems/rubygems.go b/routers/api/packages/rubygems/rubygems.go index 958063e70ab..de8c7ef3ed0 100644 --- a/routers/api/packages/rubygems/rubygems.go +++ b/routers/api/packages/rubygems/rubygems.go @@ -185,7 +185,7 @@ func DownloadPackageFile(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -288,7 +288,7 @@ func DeletePackage(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } diff --git a/routers/api/packages/vagrant/vagrant.go b/routers/api/packages/vagrant/vagrant.go index 1daf2a0527a..3afaa5de1f3 100644 --- a/routers/api/packages/vagrant/vagrant.go +++ b/routers/api/packages/vagrant/vagrant.go @@ -4,6 +4,7 @@ package vagrant import ( + "errors" "fmt" "io" "net/http" @@ -230,7 +231,7 @@ func DownloadPackageFile(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } diff --git a/routers/api/v1/admin/adopt.go b/routers/api/v1/admin/adopt.go index 613d1234942..55ea8c6758c 100644 --- a/routers/api/v1/admin/adopt.go +++ b/routers/api/v1/admin/adopt.go @@ -80,8 +80,8 @@ func AdoptRepository(ctx *context.APIContext) { // "$ref": "#/responses/notFound" // "403": // "$ref": "#/responses/forbidden" - ownerName := ctx.PathParam(":username") - repoName := ctx.PathParam(":reponame") + ownerName := ctx.PathParam("username") + repoName := ctx.PathParam("reponame") ctxUser, err := user_model.GetUserByName(ctx, ownerName) if err != nil { @@ -142,8 +142,8 @@ func DeleteUnadoptedRepository(ctx *context.APIContext) { // "$ref": "#/responses/empty" // "403": // "$ref": "#/responses/forbidden" - ownerName := ctx.PathParam(":username") - repoName := ctx.PathParam(":reponame") + ownerName := ctx.PathParam("username") + repoName := ctx.PathParam("reponame") ctxUser, err := user_model.GetUserByName(ctx, ownerName) if err != nil { diff --git a/routers/api/v1/admin/cron.go b/routers/api/v1/admin/cron.go index fba9d33f25f..962e0077763 100644 --- a/routers/api/v1/admin/cron.go +++ b/routers/api/v1/admin/cron.go @@ -74,7 +74,7 @@ func PostCronTask(ctx *context.APIContext) { // "$ref": "#/responses/empty" // "404": // "$ref": "#/responses/notFound" - task := cron.GetTask(ctx.PathParam(":task")) + task := cron.GetTask(ctx.PathParam("task")) if task == nil { ctx.NotFound() return diff --git a/routers/api/v1/admin/email.go b/routers/api/v1/admin/email.go index 6fe418249ba..3de94d6868e 100644 --- a/routers/api/v1/admin/email.go +++ b/routers/api/v1/admin/email.go @@ -38,7 +38,7 @@ func GetAllEmails(ctx *context.APIContext) { listOptions := utils.GetListOptions(ctx) emails, maxResults, err := user_model.SearchEmails(ctx, &user_model.SearchEmailOptions{ - Keyword: ctx.PathParam(":email"), + Keyword: ctx.PathParam("email"), ListOptions: listOptions, }) if err != nil { @@ -82,6 +82,6 @@ func SearchEmail(ctx *context.APIContext) { // "403": // "$ref": "#/responses/forbidden" - ctx.SetPathParam(":email", ctx.FormTrim("q")) + ctx.SetPathParam("email", ctx.FormTrim("q")) GetAllEmails(ctx) } diff --git a/routers/api/v1/admin/hooks.go b/routers/api/v1/admin/hooks.go index db481fbf594..6b4689047b5 100644 --- a/routers/api/v1/admin/hooks.go +++ b/routers/api/v1/admin/hooks.go @@ -73,7 +73,7 @@ func GetHook(ctx *context.APIContext) { // "200": // "$ref": "#/responses/Hook" - hookID := ctx.PathParamInt64(":id") + hookID := ctx.PathParamInt64("id") hook, err := webhook.GetSystemOrDefaultWebhook(ctx, hookID) if err != nil { if errors.Is(err, util.ErrNotExist) { @@ -142,7 +142,7 @@ func EditHook(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.EditHookOption) // TODO in body params - hookID := ctx.PathParamInt64(":id") + hookID := ctx.PathParamInt64("id") utils.EditSystemHook(ctx, form, hookID) } @@ -164,7 +164,7 @@ func DeleteHook(ctx *context.APIContext) { // "204": // "$ref": "#/responses/empty" - hookID := ctx.PathParamInt64(":id") + hookID := ctx.PathParamInt64("id") if err := webhook.DeleteDefaultSystemWebhook(ctx, hookID); err != nil { if errors.Is(err, util.ErrNotExist) { ctx.NotFound() diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go index 3f4a73dcad5..21cb2f9ccdf 100644 --- a/routers/api/v1/admin/user.go +++ b/routers/api/v1/admin/user.go @@ -375,7 +375,7 @@ func DeleteUserPublicKey(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - if err := asymkey_service.DeletePublicKey(ctx, ctx.ContextUser, ctx.PathParamInt64(":id")); err != nil { + if err := asymkey_service.DeletePublicKey(ctx, ctx.ContextUser, ctx.PathParamInt64("id")); err != nil { if asymkey_model.IsErrKeyNotExist(err) { ctx.NotFound() } else if asymkey_model.IsErrKeyAccessDenied(err) { diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 96365e7c14d..2f943d306cc 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -596,12 +596,12 @@ func orgAssignment(args ...bool) func(ctx *context.APIContext) { var err error if assignOrg { - ctx.Org.Organization, err = organization.GetOrgByName(ctx, ctx.PathParam(":org")) + ctx.Org.Organization, err = organization.GetOrgByName(ctx, ctx.PathParam("org")) if err != nil { if organization.IsErrOrgNotExist(err) { - redirectUserID, err := user_model.LookupUserRedirect(ctx, ctx.PathParam(":org")) + redirectUserID, err := user_model.LookupUserRedirect(ctx, ctx.PathParam("org")) if err == nil { - context.RedirectToUser(ctx.Base, ctx.PathParam(":org"), redirectUserID) + context.RedirectToUser(ctx.Base, ctx.PathParam("org"), redirectUserID) } else if user_model.IsErrUserRedirectNotExist(err) { ctx.NotFound("GetOrgByName", err) } else { @@ -616,7 +616,7 @@ func orgAssignment(args ...bool) func(ctx *context.APIContext) { } if assignTeam { - ctx.Org.Team, err = organization.GetTeamByID(ctx, ctx.PathParamInt64(":teamid")) + ctx.Org.Team, err = organization.GetTeamByID(ctx, ctx.PathParamInt64("teamid")) if err != nil { if organization.IsErrTeamNotExist(err) { ctx.NotFound() diff --git a/routers/api/v1/notify/threads.go b/routers/api/v1/notify/threads.go index 0761e684a3f..58a38cfd18c 100644 --- a/routers/api/v1/notify/threads.go +++ b/routers/api/v1/notify/threads.go @@ -101,7 +101,7 @@ func ReadThread(ctx *context.APIContext) { } func getThread(ctx *context.APIContext) *activities_model.Notification { - n, err := activities_model.GetNotificationByID(ctx, ctx.PathParamInt64(":id")) + n, err := activities_model.GetNotificationByID(ctx, ctx.PathParamInt64("id")) if err != nil { if db.IsErrNotExist(err) { ctx.Error(http.StatusNotFound, "GetNotificationByID", err) diff --git a/routers/api/v1/org/label.go b/routers/api/v1/org/label.go index 24ee4ed6423..2a9bd92e878 100644 --- a/routers/api/v1/org/label.go +++ b/routers/api/v1/org/label.go @@ -139,7 +139,7 @@ func GetLabel(ctx *context.APIContext) { label *issues_model.Label err error ) - strID := ctx.PathParam(":id") + strID := ctx.PathParam("id") if intID, err2 := strconv.ParseInt(strID, 10, 64); err2 != nil { label, err = issues_model.GetLabelInOrgByName(ctx, ctx.Org.Organization.ID, strID) } else { @@ -190,7 +190,7 @@ func EditLabel(ctx *context.APIContext) { // "422": // "$ref": "#/responses/validationError" form := web.GetForm(ctx).(*api.EditLabelOption) - l, err := issues_model.GetLabelInOrgByID(ctx, ctx.Org.Organization.ID, ctx.PathParamInt64(":id")) + l, err := issues_model.GetLabelInOrgByID(ctx, ctx.Org.Organization.ID, ctx.PathParamInt64("id")) if err != nil { if issues_model.IsErrOrgLabelNotExist(err) { ctx.NotFound() @@ -249,7 +249,7 @@ func DeleteLabel(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - if err := issues_model.DeleteLabel(ctx, ctx.Org.Organization.ID, ctx.PathParamInt64(":id")); err != nil { + if err := issues_model.DeleteLabel(ctx, ctx.Org.Organization.ID, ctx.PathParamInt64("id")); err != nil { ctx.Error(http.StatusInternalServerError, "DeleteLabel", err) return } diff --git a/routers/api/v1/org/member.go b/routers/api/v1/org/member.go index 294d33014d1..23c7da3d968 100644 --- a/routers/api/v1/org/member.go +++ b/routers/api/v1/org/member.go @@ -143,7 +143,7 @@ func IsMember(ctx *context.APIContext) { // "404": // description: user is not a member - userToCheck := user.GetUserByParams(ctx) + userToCheck := user.GetContextUserByPathParam(ctx) if ctx.Written() { return } @@ -194,7 +194,7 @@ func IsPublicMember(ctx *context.APIContext) { // "404": // description: user is not a public member - userToCheck := user.GetUserByParams(ctx) + userToCheck := user.GetContextUserByPathParam(ctx) if ctx.Written() { return } @@ -236,7 +236,7 @@ func PublicizeMember(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - userToPublicize := user.GetUserByParams(ctx) + userToPublicize := user.GetContextUserByPathParam(ctx) if ctx.Written() { return } @@ -278,7 +278,7 @@ func ConcealMember(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - userToConceal := user.GetUserByParams(ctx) + userToConceal := user.GetContextUserByPathParam(ctx) if ctx.Written() { return } @@ -318,7 +318,7 @@ func DeleteMember(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - member := user.GetUserByParams(ctx) + member := user.GetContextUserByPathParam(ctx) if ctx.Written() { return } diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go index 3fb653bcb6d..d65f9224344 100644 --- a/routers/api/v1/org/org.go +++ b/routers/api/v1/org/org.go @@ -131,7 +131,7 @@ func GetUserOrgsPermissions(ctx *context.APIContext) { // "$ref": "#/responses/notFound" var o *user_model.User - if o = user.GetUserByParamsName(ctx, ":org"); o == nil { + if o = user.GetUserByPathParam(ctx, "org"); o == nil { return } diff --git a/routers/api/v1/org/team.go b/routers/api/v1/org/team.go index 8164d2cfe99..7f44f6ed95d 100644 --- a/routers/api/v1/org/team.go +++ b/routers/api/v1/org/team.go @@ -449,7 +449,7 @@ func GetTeamMember(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - u := user.GetUserByParams(ctx) + u := user.GetContextUserByPathParam(ctx) if ctx.Written() { return } @@ -492,7 +492,7 @@ func AddTeamMember(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - u := user.GetUserByParams(ctx) + u := user.GetContextUserByPathParam(ctx) if ctx.Written() { return } @@ -532,7 +532,7 @@ func RemoveTeamMember(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - u := user.GetUserByParams(ctx) + u := user.GetContextUserByPathParam(ctx) if ctx.Written() { return } @@ -645,7 +645,7 @@ func GetTeamRepo(ctx *context.APIContext) { // getRepositoryByParams get repository by a team's organization ID and repo name func getRepositoryByParams(ctx *context.APIContext) *repo_model.Repository { - repo, err := repo_model.GetRepositoryByName(ctx, ctx.Org.Team.OrgID, ctx.PathParam(":reponame")) + repo, err := repo_model.GetRepositoryByName(ctx, ctx.Org.Team.OrgID, ctx.PathParam("reponame")) if err != nil { if repo_model.IsErrRepoNotExist(err) { ctx.NotFound() diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index 9a31aec3144..2fcdd020587 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -487,7 +487,7 @@ func GetBranchProtection(ctx *context.APIContext) { // "$ref": "#/responses/notFound" repo := ctx.Repo.Repository - bpName := ctx.PathParam(":name") + bpName := ctx.PathParam("name") bp, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, bpName) if err != nil { ctx.Error(http.StatusInternalServerError, "GetProtectedBranchByID", err) @@ -729,15 +729,11 @@ func CreateBranchProtection(ctx *context.APIContext) { } else { if !isPlainRule { if ctx.Repo.GitRepo == nil { - ctx.Repo.GitRepo, err = gitrepo.OpenRepository(ctx, ctx.Repo.Repository) + ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository) if err != nil { ctx.Error(http.StatusInternalServerError, "OpenRepository", err) return } - defer func() { - ctx.Repo.GitRepo.Close() - ctx.Repo.GitRepo = nil - }() } // FIXME: since we only need to recheck files protected rules, we could improve this matchedBranches, err := git_model.FindAllMatchedBranches(ctx, ctx.Repo.Repository.ID, ruleName) @@ -809,7 +805,7 @@ func EditBranchProtection(ctx *context.APIContext) { // "$ref": "#/responses/repoArchivedError" form := web.GetForm(ctx).(*api.EditBranchProtectionOption) repo := ctx.Repo.Repository - bpName := ctx.PathParam(":name") + bpName := ctx.PathParam("name") protectBranch, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, bpName) if err != nil { ctx.Error(http.StatusInternalServerError, "GetProtectedBranchByID", err) @@ -1061,15 +1057,11 @@ func EditBranchProtection(ctx *context.APIContext) { } else { if !isPlainRule { if ctx.Repo.GitRepo == nil { - ctx.Repo.GitRepo, err = gitrepo.OpenRepository(ctx, ctx.Repo.Repository) + ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository) if err != nil { ctx.Error(http.StatusInternalServerError, "OpenRepository", err) return } - defer func() { - ctx.Repo.GitRepo.Close() - ctx.Repo.GitRepo = nil - }() } // FIXME: since we only need to recheck files protected rules, we could improve this @@ -1132,7 +1124,7 @@ func DeleteBranchProtection(ctx *context.APIContext) { // "$ref": "#/responses/notFound" repo := ctx.Repo.Repository - bpName := ctx.PathParam(":name") + bpName := ctx.PathParam("name") bp, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, bpName) if err != nil { ctx.Error(http.StatusInternalServerError, "GetProtectedBranchByID", err) diff --git a/routers/api/v1/repo/collaborators.go b/routers/api/v1/repo/collaborators.go index 0bbf5a1ea49..da3ee54e691 100644 --- a/routers/api/v1/repo/collaborators.go +++ b/routers/api/v1/repo/collaborators.go @@ -103,7 +103,7 @@ func IsCollaborator(ctx *context.APIContext) { // "422": // "$ref": "#/responses/validationError" - user, err := user_model.GetUserByName(ctx, ctx.PathParam(":collaborator")) + user, err := user_model.GetUserByName(ctx, ctx.PathParam("collaborator")) if err != nil { if user_model.IsErrUserNotExist(err) { ctx.Error(http.StatusUnprocessableEntity, "", err) @@ -163,7 +163,7 @@ func AddOrUpdateCollaborator(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.AddCollaboratorOption) - collaborator, err := user_model.GetUserByName(ctx, ctx.PathParam(":collaborator")) + collaborator, err := user_model.GetUserByName(ctx, ctx.PathParam("collaborator")) if err != nil { if user_model.IsErrUserNotExist(err) { ctx.Error(http.StatusUnprocessableEntity, "", err) @@ -226,7 +226,7 @@ func DeleteCollaborator(ctx *context.APIContext) { // "422": // "$ref": "#/responses/validationError" - collaborator, err := user_model.GetUserByName(ctx, ctx.PathParam(":collaborator")) + collaborator, err := user_model.GetUserByName(ctx, ctx.PathParam("collaborator")) if err != nil { if user_model.IsErrUserNotExist(err) { ctx.Error(http.StatusUnprocessableEntity, "", err) @@ -274,12 +274,12 @@ func GetRepoPermissions(ctx *context.APIContext) { // "403": // "$ref": "#/responses/forbidden" - if !ctx.Doer.IsAdmin && ctx.Doer.LoginName != ctx.PathParam(":collaborator") && !ctx.IsUserRepoAdmin() { + if !ctx.Doer.IsAdmin && ctx.Doer.LoginName != ctx.PathParam("collaborator") && !ctx.IsUserRepoAdmin() { ctx.Error(http.StatusForbidden, "User", "Only admins can query all permissions, repo admins can query all repo permissions, collaborators can query only their own") return } - collaborator, err := user_model.GetUserByName(ctx, ctx.PathParam(":collaborator")) + collaborator, err := user_model.GetUserByName(ctx, ctx.PathParam("collaborator")) if err != nil { if user_model.IsErrUserNotExist(err) { ctx.Error(http.StatusNotFound, "GetUserByName", err) diff --git a/routers/api/v1/repo/commits.go b/routers/api/v1/repo/commits.go index 788c75fab2f..3b144d0c437 100644 --- a/routers/api/v1/repo/commits.go +++ b/routers/api/v1/repo/commits.go @@ -63,7 +63,7 @@ func GetSingleCommit(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - sha := ctx.PathParam(":sha") + sha := ctx.PathParam("sha") if !git.IsValidRefPattern(sha) { ctx.Error(http.StatusUnprocessableEntity, "no valid ref or sha", fmt.Sprintf("no valid ref or sha: %s", sha)) return @@ -312,8 +312,8 @@ func DownloadCommitDiffOrPatch(ctx *context.APIContext) { // "$ref": "#/responses/string" // "404": // "$ref": "#/responses/notFound" - sha := ctx.PathParam(":sha") - diffType := git.RawDiffType(ctx.PathParam(":diffType")) + sha := ctx.PathParam("sha") + diffType := git.RawDiffType(ctx.PathParam("diffType")) if err := git.GetRawDiff(ctx.Repo.GitRepo, sha, diffType, ctx.Resp); err != nil { if git.IsErrNotExist(err) { diff --git a/routers/api/v1/repo/compare.go b/routers/api/v1/repo/compare.go index 1678bc033c6..a1813a8a765 100644 --- a/routers/api/v1/repo/compare.go +++ b/routers/api/v1/repo/compare.go @@ -44,13 +44,12 @@ func CompareDiff(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if ctx.Repo.GitRepo == nil { - gitRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository) + var err error + ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository) if err != nil { ctx.Error(http.StatusInternalServerError, "OpenRepository", err) return } - ctx.Repo.GitRepo = gitRepo - defer gitRepo.Close() } infoPath := ctx.PathParam("*") diff --git a/routers/api/v1/repo/download.go b/routers/api/v1/repo/download.go index 3620c1465fe..a8a23c4a8d4 100644 --- a/routers/api/v1/repo/download.go +++ b/routers/api/v1/repo/download.go @@ -28,13 +28,12 @@ func DownloadArchive(ctx *context.APIContext) { } if ctx.Repo.GitRepo == nil { - gitRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository) + var err error + ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository) if err != nil { ctx.Error(http.StatusInternalServerError, "OpenRepository", err) return } - ctx.Repo.GitRepo = gitRepo - defer gitRepo.Close() } r, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, ctx.PathParam("*"), tp) diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index 83848b7add5..7c7f53a5652 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" @@ -189,7 +188,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) { meta, err := git_model.GetLFSMetaObjectByOid(ctx, ctx.Repo.Repository.ID, pointer.Oid) // If there isn't one, just serve the data directly - if err == git_model.ErrLFSObjectNotExist { + if errors.Is(err, git_model.ErrLFSObjectNotExist) { // Handle caching for the blob SHA (not the LFS object OID) if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) { return @@ -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 @@ -287,13 +281,12 @@ func GetArchive(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if ctx.Repo.GitRepo == nil { - gitRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository) + var err error + ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository) if err != nil { ctx.Error(http.StatusInternalServerError, "OpenRepository", err) return } - ctx.Repo.GitRepo = gitRepo - defer gitRepo.Close() } archiveDownload(ctx) diff --git a/routers/api/v1/repo/git_hook.go b/routers/api/v1/repo/git_hook.go index 0887a90096f..868acf3d853 100644 --- a/routers/api/v1/repo/git_hook.go +++ b/routers/api/v1/repo/git_hook.go @@ -4,6 +4,7 @@ package repo import ( + "errors" "net/http" "code.gitea.io/gitea/modules/git" @@ -79,10 +80,10 @@ func GetGitHook(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - hookID := ctx.PathParam(":id") + hookID := ctx.PathParam("id") hook, err := ctx.Repo.GitRepo.GetHook(hookID) if err != nil { - if err == git.ErrNotValidHook { + if errors.Is(err, git.ErrNotValidHook) { ctx.NotFound() } else { ctx.Error(http.StatusInternalServerError, "GetHook", err) @@ -126,10 +127,10 @@ func EditGitHook(ctx *context.APIContext) { // "$ref": "#/responses/notFound" form := web.GetForm(ctx).(*api.EditGitHookOption) - hookID := ctx.PathParam(":id") + hookID := ctx.PathParam("id") hook, err := ctx.Repo.GitRepo.GetHook(hookID) if err != nil { - if err == git.ErrNotValidHook { + if errors.Is(err, git.ErrNotValidHook) { ctx.NotFound() } else { ctx.Error(http.StatusInternalServerError, "GetHook", err) @@ -175,10 +176,10 @@ func DeleteGitHook(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - hookID := ctx.PathParam(":id") + hookID := ctx.PathParam("id") hook, err := ctx.Repo.GitRepo.GetHook(hookID) if err != nil { - if err == git.ErrNotValidHook { + if errors.Is(err, git.ErrNotValidHook) { ctx.NotFound() } else { ctx.Error(http.StatusInternalServerError, "GetHook", err) diff --git a/routers/api/v1/repo/hook.go b/routers/api/v1/repo/hook.go index 9ef57da1b98..03143c8f99e 100644 --- a/routers/api/v1/repo/hook.go +++ b/routers/api/v1/repo/hook.go @@ -109,7 +109,7 @@ func GetHook(ctx *context.APIContext) { // "$ref": "#/responses/notFound" repo := ctx.Repo - hookID := ctx.PathParamInt64(":id") + hookID := ctx.PathParamInt64("id") hook, err := utils.GetRepoHook(ctx, repo.Repository.ID, hookID) if err != nil { return @@ -168,7 +168,7 @@ func TestHook(ctx *context.APIContext) { ref = r } - hookID := ctx.PathParamInt64(":id") + hookID := ctx.PathParamInt64("id") hook, err := utils.GetRepoHook(ctx, ctx.Repo.Repository.ID, hookID) if err != nil { return @@ -263,7 +263,7 @@ func EditHook(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" form := web.GetForm(ctx).(*api.EditHookOption) - hookID := ctx.PathParamInt64(":id") + hookID := ctx.PathParamInt64("id") utils.EditRepoHook(ctx, form, hookID) } @@ -296,7 +296,7 @@ func DeleteHook(ctx *context.APIContext) { // "$ref": "#/responses/empty" // "404": // "$ref": "#/responses/notFound" - if err := webhook.DeleteWebhookByRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":id")); err != nil { + if err := webhook.DeleteWebhookByRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id")); err != nil { if webhook.IsErrWebhookNotExist(err) { ctx.NotFound() } else { diff --git a/routers/api/v1/repo/hook_test.go b/routers/api/v1/repo/hook_test.go index c2f3a972ef6..c659a16f544 100644 --- a/routers/api/v1/repo/hook_test.go +++ b/routers/api/v1/repo/hook_test.go @@ -18,7 +18,7 @@ func TestTestHook(t *testing.T) { unittest.PrepareTestEnv(t) ctx, _ := contexttest.MockAPIContext(t, "user2/repo1/wiki/_pages") - ctx.SetPathParam(":id", "1") + ctx.SetPathParam("id", "1") contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepoCommit(t, ctx) contexttest.LoadUser(t, ctx, 2) diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index cbe709c030d..86dbcee5f7d 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -613,7 +613,7 @@ func GetIssue(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() @@ -733,7 +733,7 @@ func CreateIssue(ctx *context.APIContext) { } if form.Closed { - if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", true); err != nil { + if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil { if issues_model.IsErrDependenciesLeft(err) { ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies") return @@ -793,7 +793,7 @@ func EditIssue(ctx *context.APIContext) { // "$ref": "#/responses/error" form := web.GetForm(ctx).(*api.EditIssueOption) - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() @@ -912,27 +912,11 @@ func EditIssue(ctx *context.APIContext) { } } - var isClosed bool - switch state := api.StateType(*form.State); state { - case api.StateOpen: - isClosed = false - case api.StateClosed: - isClosed = true - default: - ctx.Error(http.StatusPreconditionFailed, "UnknownIssueStateError", fmt.Sprintf("unknown state: %s", state)) + state := api.StateType(*form.State) + closeOrReopenIssue(ctx, issue, state) + if ctx.Written() { return } - - if issue.IsClosed != isClosed { - if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", isClosed); err != nil { - if issues_model.IsErrDependenciesLeft(err) { - ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies") - return - } - ctx.Error(http.StatusInternalServerError, "ChangeStatus", err) - return - } - } } // Refetch from database to assign some automatic values @@ -976,7 +960,7 @@ func DeleteIssue(ctx *context.APIContext) { // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound(err) @@ -1032,7 +1016,7 @@ func UpdateIssueDeadline(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" form := web.GetForm(ctx).(*api.EditDeadlineOption) - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() @@ -1055,3 +1039,26 @@ func UpdateIssueDeadline(ctx *context.APIContext) { ctx.JSON(http.StatusCreated, api.IssueDeadline{Deadline: deadlineUnix.AsTimePtr()}) } + +func closeOrReopenIssue(ctx *context.APIContext, issue *issues_model.Issue, state api.StateType) { + if state != api.StateOpen && state != api.StateClosed { + ctx.Error(http.StatusPreconditionFailed, "UnknownIssueStateError", fmt.Sprintf("unknown state: %s", state)) + return + } + + if state == api.StateClosed && !issue.IsClosed { + if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil { + if issues_model.IsErrDependenciesLeft(err) { + ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue or pull request because it still has open dependencies") + return + } + ctx.Error(http.StatusInternalServerError, "CloseIssue", err) + return + } + } else if state == api.StateOpen && issue.IsClosed { + if err := issue_service.ReopenIssue(ctx, issue, ctx.Doer, ""); err != nil { + ctx.Error(http.StatusInternalServerError, "ReopenIssue", err) + return + } + } +} diff --git a/routers/api/v1/repo/issue_comment.go b/routers/api/v1/repo/issue_comment.go index f9b5aa816bc..96a61a527e9 100644 --- a/routers/api/v1/repo/issue_comment.go +++ b/routers/api/v1/repo/issue_comment.go @@ -68,7 +68,7 @@ func ListIssueComments(ctx *context.APIContext) { ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) return } - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { ctx.Error(http.StatusInternalServerError, "GetRawIssueByIndex", err) return @@ -172,7 +172,7 @@ func ListIssueCommentsAndTimeline(ctx *context.APIContext) { ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) return } - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { ctx.Error(http.StatusInternalServerError, "GetRawIssueByIndex", err) return @@ -380,7 +380,7 @@ func CreateIssueComment(ctx *context.APIContext) { // "$ref": "#/responses/repoArchivedError" form := web.GetForm(ctx).(*api.CreateIssueCommentOption) - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) return @@ -445,7 +445,7 @@ func GetIssueComment(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64(":id")) + comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id")) if err != nil { if issues_model.IsErrCommentNotExist(err) { ctx.NotFound(err) @@ -579,7 +579,7 @@ func EditIssueCommentDeprecated(ctx *context.APIContext) { } func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption) { - comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64(":id")) + comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id")) if err != nil { if issues_model.IsErrCommentNotExist(err) { ctx.NotFound(err) @@ -696,7 +696,7 @@ func DeleteIssueCommentDeprecated(ctx *context.APIContext) { } func deleteIssueComment(ctx *context.APIContext) { - comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64(":id")) + comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id")) if err != nil { if issues_model.IsErrCommentNotExist(err) { ctx.NotFound(err) diff --git a/routers/api/v1/repo/issue_dependency.go b/routers/api/v1/repo/issue_dependency.go index ae7502c661b..19dcf999b80 100644 --- a/routers/api/v1/repo/issue_dependency.go +++ b/routers/api/v1/repo/issue_dependency.go @@ -61,7 +61,7 @@ func GetIssueDependencies(ctx *context.APIContext) { return } - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound("IsErrIssueNotExist", err) @@ -499,7 +499,7 @@ func RemoveIssueBlocking(ctx *context.APIContext) { } func getParamsIssue(ctx *context.APIContext) *issues_model.Issue { - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound("IsErrIssueNotExist", err) diff --git a/routers/api/v1/repo/issue_label.go b/routers/api/v1/repo/issue_label.go index cc517619e97..ee1a842bc61 100644 --- a/routers/api/v1/repo/issue_label.go +++ b/routers/api/v1/repo/issue_label.go @@ -47,7 +47,7 @@ func ListIssueLabels(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() @@ -163,7 +163,7 @@ func DeleteIssueLabel(ctx *context.APIContext) { // "422": // "$ref": "#/responses/validationError" - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() @@ -178,7 +178,7 @@ func DeleteIssueLabel(ctx *context.APIContext) { return } - label, err := issues_model.GetLabelByID(ctx, ctx.PathParamInt64(":id")) + label, err := issues_model.GetLabelByID(ctx, ctx.PathParamInt64("id")) if err != nil { if issues_model.IsErrLabelNotExist(err) { ctx.Error(http.StatusUnprocessableEntity, "", err) @@ -285,7 +285,7 @@ func ClearIssueLabels(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() @@ -309,7 +309,7 @@ func ClearIssueLabels(ctx *context.APIContext) { } func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption) (*issues_model.Issue, []*issues_model.Label, error) { - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() @@ -335,6 +335,9 @@ func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption) labelIDs = append(labelIDs, int64(rv.Float())) case reflect.String: labelNames = append(labelNames, rv.String()) + default: + ctx.Error(http.StatusBadRequest, "InvalidLabel", "a label must be an integer or a string") + return nil, nil, fmt.Errorf("invalid label") } } if len(labelIDs) > 0 && len(labelNames) > 0 { @@ -342,11 +345,20 @@ func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption) return nil, nil, fmt.Errorf("invalid labels") } if len(labelNames) > 0 { - labelIDs, err = issues_model.GetLabelIDsInRepoByNames(ctx, ctx.Repo.Repository.ID, labelNames) + repoLabelIDs, err := issues_model.GetLabelIDsInRepoByNames(ctx, ctx.Repo.Repository.ID, labelNames) if err != nil { ctx.Error(http.StatusInternalServerError, "GetLabelIDsInRepoByNames", err) return nil, nil, err } + labelIDs = append(labelIDs, repoLabelIDs...) + if ctx.Repo.Owner.IsOrganization() { + orgLabelIDs, err := issues_model.GetLabelIDsInOrgByNames(ctx, ctx.Repo.Owner.ID, labelNames) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetLabelIDsInOrgByNames", err) + return nil, nil, err + } + labelIDs = append(labelIDs, orgLabelIDs...) + } } labels, err := issues_model.GetLabelsByIDs(ctx, labelIDs, "id", "repo_id", "org_id", "name", "exclusive") diff --git a/routers/api/v1/repo/issue_pin.go b/routers/api/v1/repo/issue_pin.go index 0ef90332915..388d4a3e99b 100644 --- a/routers/api/v1/repo/issue_pin.go +++ b/routers/api/v1/repo/issue_pin.go @@ -41,7 +41,7 @@ func PinIssue(ctx *context.APIContext) { // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() @@ -98,7 +98,7 @@ func UnpinIssue(ctx *context.APIContext) { // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() @@ -159,7 +159,7 @@ func MoveIssuePin(ctx *context.APIContext) { // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() @@ -169,7 +169,7 @@ func MoveIssuePin(ctx *context.APIContext) { return } - err = issue.MovePin(ctx, int(ctx.PathParamInt64(":position"))) + err = issue.MovePin(ctx, int(ctx.PathParamInt64("position"))) if err != nil { ctx.Error(http.StatusInternalServerError, "MovePin", err) return diff --git a/routers/api/v1/repo/issue_reaction.go b/routers/api/v1/repo/issue_reaction.go index 8d43cd518bc..ead86a717fa 100644 --- a/routers/api/v1/repo/issue_reaction.go +++ b/routers/api/v1/repo/issue_reaction.go @@ -51,7 +51,7 @@ func GetIssueCommentReactions(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64(":id")) + comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id")) if err != nil { if issues_model.IsErrCommentNotExist(err) { ctx.NotFound(err) @@ -188,7 +188,7 @@ func DeleteIssueCommentReaction(ctx *context.APIContext) { } func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOption, isCreateType bool) { - comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64(":id")) + comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id")) if err != nil { if issues_model.IsErrCommentNotExist(err) { ctx.NotFound(err) @@ -295,7 +295,7 @@ func GetIssueReactions(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() @@ -419,7 +419,7 @@ func DeleteIssueReaction(ctx *context.APIContext) { } func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, isCreateType bool) { - issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() diff --git a/routers/api/v1/repo/issue_stopwatch.go b/routers/api/v1/repo/issue_stopwatch.go index 4605ae21103..e7fba6d0ed1 100644 --- a/routers/api/v1/repo/issue_stopwatch.go +++ b/routers/api/v1/repo/issue_stopwatch.go @@ -161,7 +161,7 @@ func DeleteIssueStopwatch(ctx *context.APIContext) { } func prepareIssueStopwatch(ctx *context.APIContext, shouldExist bool) (*issues_model.Issue, error) { - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() diff --git a/routers/api/v1/repo/issue_subscription.go b/routers/api/v1/repo/issue_subscription.go index e51baad0b6c..4fb80b1ec4f 100644 --- a/routers/api/v1/repo/issue_subscription.go +++ b/routers/api/v1/repo/issue_subscription.go @@ -104,7 +104,7 @@ func DelIssueSubscription(ctx *context.APIContext) { } func setIssueSubscription(ctx *context.APIContext, watch bool) { - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() @@ -115,7 +115,7 @@ func setIssueSubscription(ctx *context.APIContext, watch bool) { return } - user, err := user_model.GetUserByName(ctx, ctx.PathParam(":user")) + user, err := user_model.GetUserByName(ctx, ctx.PathParam("user")) if err != nil { if user_model.IsErrUserNotExist(err) { ctx.NotFound() @@ -185,7 +185,7 @@ func CheckIssueSubscription(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() @@ -251,7 +251,7 @@ func GetIssueSubscribers(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() diff --git a/routers/api/v1/repo/issue_tracked_time.go b/routers/api/v1/repo/issue_tracked_time.go index 8d5e9fdad41..57961b06605 100644 --- a/routers/api/v1/repo/issue_tracked_time.go +++ b/routers/api/v1/repo/issue_tracked_time.go @@ -75,7 +75,7 @@ func ListTrackedTimes(ctx *context.APIContext) { ctx.NotFound("Timetracker is disabled") return } - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound(err) @@ -181,7 +181,7 @@ func AddTime(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" form := web.GetForm(ctx).(*api.AddTimeOption) - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound(err) @@ -264,7 +264,7 @@ func ResetIssueTime(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound(err) @@ -337,7 +337,7 @@ func DeleteTime(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound(err) @@ -356,7 +356,7 @@ func DeleteTime(ctx *context.APIContext) { return } - time, err := issues_model.GetTrackedTimeByID(ctx, ctx.PathParamInt64(":id")) + time, err := issues_model.GetTrackedTimeByID(ctx, ctx.PathParamInt64("id")) if err != nil { if db.IsErrNotExist(err) { ctx.NotFound(err) @@ -422,7 +422,7 @@ func ListTrackedTimesByUser(ctx *context.APIContext) { ctx.Error(http.StatusBadRequest, "", "time tracking disabled") return } - user, err := user_model.GetUserByName(ctx, ctx.PathParam(":timetrackingusername")) + user, err := user_model.GetUserByName(ctx, ctx.PathParam("timetrackingusername")) if err != nil { if user_model.IsErrUserNotExist(err) { ctx.NotFound(err) diff --git a/routers/api/v1/repo/key.go b/routers/api/v1/repo/key.go index 060694d085f..23cc9226287 100644 --- a/routers/api/v1/repo/key.go +++ b/routers/api/v1/repo/key.go @@ -143,7 +143,7 @@ func GetDeployKey(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - key, err := asymkey_model.GetDeployKeyByID(ctx, ctx.PathParamInt64(":id")) + key, err := asymkey_model.GetDeployKeyByID(ctx, ctx.PathParamInt64("id")) if err != nil { if asymkey_model.IsErrDeployKeyNotExist(err) { ctx.NotFound() @@ -279,7 +279,7 @@ func DeleteDeploykey(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - if err := asymkey_service.DeleteDeployKey(ctx, ctx.Repo.Repository, ctx.PathParamInt64(":id")); err != nil { + if err := asymkey_service.DeleteDeployKey(ctx, ctx.Repo.Repository, ctx.PathParamInt64("id")); err != nil { if asymkey_model.IsErrKeyAccessDenied(err) { ctx.Error(http.StatusForbidden, "", "You do not have access to this key") } else { diff --git a/routers/api/v1/repo/label.go b/routers/api/v1/repo/label.go index c2c43db6a4a..1ece2521e0f 100644 --- a/routers/api/v1/repo/label.go +++ b/routers/api/v1/repo/label.go @@ -99,7 +99,7 @@ func GetLabel(ctx *context.APIContext) { l *issues_model.Label err error ) - strID := ctx.PathParam(":id") + strID := ctx.PathParam("id") if intID, err2 := strconv.ParseInt(strID, 10, 64); err2 != nil { l, err = issues_model.GetLabelInRepoByName(ctx, ctx.Repo.Repository.ID, strID) } else { @@ -212,7 +212,7 @@ func EditLabel(ctx *context.APIContext) { // "$ref": "#/responses/validationError" form := web.GetForm(ctx).(*api.EditLabelOption) - l, err := issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":id")) + l, err := issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id")) if err != nil { if issues_model.IsErrRepoLabelNotExist(err) { ctx.NotFound() @@ -276,7 +276,7 @@ func DeleteLabel(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - if err := issues_model.DeleteLabel(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":id")); err != nil { + if err := issues_model.DeleteLabel(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id")); err != nil { ctx.Error(http.StatusInternalServerError, "DeleteLabel", err) return } diff --git a/routers/api/v1/repo/milestone.go b/routers/api/v1/repo/milestone.go index 78907c85a58..8d7516491ed 100644 --- a/routers/api/v1/repo/milestone.go +++ b/routers/api/v1/repo/milestone.go @@ -280,7 +280,7 @@ func DeleteMilestone(ctx *context.APIContext) { // getMilestoneByIDOrName get milestone by ID and if not available by name func getMilestoneByIDOrName(ctx *context.APIContext) *issues_model.Milestone { - mile := ctx.PathParam(":id") + mile := ctx.PathParam("id") mileID, _ := strconv.ParseInt(mile, 0, 64) if mileID != 0 { diff --git a/routers/api/v1/repo/mirror.go b/routers/api/v1/repo/mirror.go index 047203501e7..c911f6830cb 100644 --- a/routers/api/v1/repo/mirror.go +++ b/routers/api/v1/repo/mirror.go @@ -223,7 +223,7 @@ func GetPushMirrorByName(ctx *context.APIContext) { return } - mirrorName := ctx.PathParam(":name") + mirrorName := ctx.PathParam("name") // Get push mirror of a specific repo by remoteName pushMirror, exist, err := db.Get[repo_model.PushMirror](ctx, repo_model.PushMirrorOptions{ RepoID: ctx.Repo.Repository.ID, @@ -324,7 +324,7 @@ func DeletePushMirrorByRemoteName(ctx *context.APIContext) { return } - remoteName := ctx.PathParam(":name") + remoteName := ctx.PathParam("name") // Delete push mirror on repo by name. err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{RepoID: ctx.Repo.Repository.ID, RemoteName: remoteName}) if err != nil { diff --git a/routers/api/v1/repo/notes.go b/routers/api/v1/repo/notes.go index 8689d25e157..8fec844cc44 100644 --- a/routers/api/v1/repo/notes.go +++ b/routers/api/v1/repo/notes.go @@ -52,7 +52,7 @@ func GetNote(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - sha := ctx.PathParam(":sha") + sha := ctx.PathParam("sha") if !git.IsValidRefPattern(sha) { ctx.Error(http.StatusUnprocessableEntity, "no valid ref or sha", fmt.Sprintf("no valid ref or sha: %s", sha)) return diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 71c4c81b677..d0c3459b634 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -179,7 +179,7 @@ func GetPullRequest(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound() @@ -264,7 +264,7 @@ func GetPullRequestByBaseHead(ctx *context.APIContext) { headBranch = head } - pr, err := issues_model.GetPullRequestByBaseHeadInfo(ctx, ctx.Repo.Repository.ID, headRepoID, ctx.PathParam(":base"), headBranch) + pr, err := issues_model.GetPullRequestByBaseHeadInfo(ctx, ctx.Repo.Repository.ID, headRepoID, ctx.PathParam("base"), headBranch) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound() @@ -324,7 +324,7 @@ func DownloadPullDiffOrPatch(ctx *context.APIContext) { // "$ref": "#/responses/string" // "404": // "$ref": "#/responses/notFound" - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound() @@ -334,7 +334,7 @@ func DownloadPullDiffOrPatch(ctx *context.APIContext) { return } var patch bool - if ctx.PathParam(":diffType") == "diff" { + if ctx.PathParam("diffType") == "diff" { patch = false } else { patch = true @@ -603,7 +603,7 @@ func EditPullRequest(ctx *context.APIContext) { // "$ref": "#/responses/validationError" form := web.GetForm(ctx).(*api.EditPullRequestOption) - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound() @@ -728,27 +728,11 @@ func EditPullRequest(ctx *context.APIContext) { return } - var isClosed bool - switch state := api.StateType(*form.State); state { - case api.StateOpen: - isClosed = false - case api.StateClosed: - isClosed = true - default: - ctx.Error(http.StatusPreconditionFailed, "UnknownPRStateError", fmt.Sprintf("unknown state: %s", state)) + state := api.StateType(*form.State) + closeOrReopenIssue(ctx, issue, state) + if ctx.Written() { return } - - if issue.IsClosed != isClosed { - if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", isClosed); err != nil { - if issues_model.IsErrDependenciesLeft(err) { - ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this pull request because it still has open dependencies") - return - } - ctx.Error(http.StatusInternalServerError, "ChangeStatus", err) - return - } - } } // change pull target branch @@ -831,7 +815,7 @@ func IsPullRequestMerged(ctx *context.APIContext) { // "404": // description: pull request has not been merged - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound() @@ -889,7 +873,7 @@ func MergePullRequest(ctx *context.APIContext) { form := web.GetForm(ctx).(*forms.MergePullRequestForm) - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound("GetPullRequestByIndex", err) @@ -1256,7 +1240,7 @@ func UpdatePullRequest(ctx *context.APIContext) { // "422": // "$ref": "#/responses/validationError" - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound() @@ -1355,7 +1339,7 @@ func CancelScheduledAutoMerge(ctx *context.APIContext) { // "423": // "$ref": "#/responses/repoArchivedError" - pullIndex := ctx.PathParamInt64(":index") + pullIndex := ctx.PathParamInt64("index") pull, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, pullIndex) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { @@ -1441,7 +1425,7 @@ func GetPullRequestCommits(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound() @@ -1564,7 +1548,7 @@ func GetPullRequestFiles(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound() diff --git a/routers/api/v1/repo/pull_review.go b/routers/api/v1/repo/pull_review.go index def860eee8f..6d7a326370c 100644 --- a/routers/api/v1/repo/pull_review.go +++ b/routers/api/v1/repo/pull_review.go @@ -61,7 +61,7 @@ func ListPullReviews(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound("GetPullRequestByIndex", err) @@ -306,7 +306,7 @@ func CreatePullReview(ctx *context.APIContext) { // "$ref": "#/responses/validationError" opts := web.GetForm(ctx).(*api.CreatePullReviewOptions) - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound("GetPullRequestByIndex", err) @@ -533,7 +533,7 @@ func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest // prepareSingleReview return review, related pull and false or nil, nil and true if an error happen func prepareSingleReview(ctx *context.APIContext) (*issues_model.Review, *issues_model.PullRequest, bool) { - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound("GetPullRequestByIndex", err) @@ -543,7 +543,7 @@ func prepareSingleReview(ctx *context.APIContext) (*issues_model.Review, *issues return nil, nil, true } - review, err := issues_model.GetReviewByID(ctx, ctx.PathParamInt64(":id")) + review, err := issues_model.GetReviewByID(ctx, ctx.PathParamInt64("id")) if err != nil { if issues_model.IsErrReviewNotExist(err) { ctx.NotFound("GetReviewByID", err) @@ -698,7 +698,7 @@ func parseReviewersByNames(ctx *context.APIContext, reviewerNames, teamReviewerN } func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions, isAdd bool) { - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound("GetPullRequestByIndex", err) diff --git a/routers/api/v1/repo/release.go b/routers/api/v1/repo/release.go index 141f8121729..076f00f1d14 100644 --- a/routers/api/v1/repo/release.go +++ b/routers/api/v1/repo/release.go @@ -50,7 +50,7 @@ func GetRelease(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - id := ctx.PathParamInt64(":id") + id := ctx.PathParamInt64("id") release, err := repo_model.GetReleaseForRepoByID(ctx, ctx.Repo.Repository.ID, id) if err != nil && !repo_model.IsErrReleaseNotExist(err) { ctx.Error(http.StatusInternalServerError, "GetReleaseForRepoByID", err) @@ -319,7 +319,7 @@ func EditRelease(ctx *context.APIContext) { // "$ref": "#/responses/notFound" form := web.GetForm(ctx).(*api.EditReleaseOption) - id := ctx.PathParamInt64(":id") + id := ctx.PathParamInt64("id") rel, err := repo_model.GetReleaseForRepoByID(ctx, ctx.Repo.Repository.ID, id) if err != nil && !repo_model.IsErrReleaseNotExist(err) { ctx.Error(http.StatusInternalServerError, "GetReleaseForRepoByID", err) @@ -396,7 +396,7 @@ func DeleteRelease(ctx *context.APIContext) { // "422": // "$ref": "#/responses/validationError" - id := ctx.PathParamInt64(":id") + id := ctx.PathParamInt64("id") rel, err := repo_model.GetReleaseForRepoByID(ctx, ctx.Repo.Repository.ID, id) if err != nil && !repo_model.IsErrReleaseNotExist(err) { ctx.Error(http.StatusInternalServerError, "GetReleaseForRepoByID", err) diff --git a/routers/api/v1/repo/release_attachment.go b/routers/api/v1/repo/release_attachment.go index ed6cc8e1eaf..54ca1fc8431 100644 --- a/routers/api/v1/repo/release_attachment.go +++ b/routers/api/v1/repo/release_attachment.go @@ -72,12 +72,12 @@ func GetReleaseAttachment(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - releaseID := ctx.PathParamInt64(":id") + releaseID := ctx.PathParamInt64("id") if !checkReleaseMatchRepo(ctx, releaseID) { return } - attachID := ctx.PathParamInt64(":attachment_id") + attachID := ctx.PathParamInt64("attachment_id") attach, err := repo_model.GetAttachmentByID(ctx, attachID) if err != nil { if repo_model.IsErrAttachmentNotExist(err) { @@ -126,7 +126,7 @@ func ListReleaseAttachments(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - releaseID := ctx.PathParamInt64(":id") + releaseID := ctx.PathParamInt64("id") release, err := repo_model.GetReleaseByID(ctx, releaseID) if err != nil { if repo_model.IsErrReleaseNotExist(err) { @@ -199,7 +199,7 @@ func CreateReleaseAttachment(ctx *context.APIContext) { } // Check if release exists an load release - releaseID := ctx.PathParamInt64(":id") + releaseID := ctx.PathParamInt64("id") if !checkReleaseMatchRepo(ctx, releaseID) { return } @@ -299,12 +299,12 @@ func EditReleaseAttachment(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.EditAttachmentOptions) // Check if release exists an load release - releaseID := ctx.PathParamInt64(":id") + releaseID := ctx.PathParamInt64("id") if !checkReleaseMatchRepo(ctx, releaseID) { return } - attachID := ctx.PathParamInt64(":attachment_id") + attachID := ctx.PathParamInt64("attachment_id") attach, err := repo_model.GetAttachmentByID(ctx, attachID) if err != nil { if repo_model.IsErrAttachmentNotExist(err) { @@ -372,12 +372,12 @@ func DeleteReleaseAttachment(ctx *context.APIContext) { // "$ref": "#/responses/notFound" // Check if release exists an load release - releaseID := ctx.PathParamInt64(":id") + releaseID := ctx.PathParamInt64("id") if !checkReleaseMatchRepo(ctx, releaseID) { return } - attachID := ctx.PathParamInt64(":attachment_id") + attachID := ctx.PathParamInt64("attachment_id") attach, err := repo_model.GetAttachmentByID(ctx, attachID) if err != nil { if repo_model.IsErrAttachmentNotExist(err) { diff --git a/routers/api/v1/repo/release_tags.go b/routers/api/v1/repo/release_tags.go index 99f7a8cbf21..7380c5231c5 100644 --- a/routers/api/v1/repo/release_tags.go +++ b/routers/api/v1/repo/release_tags.go @@ -41,7 +41,7 @@ func GetReleaseByTag(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - tag := ctx.PathParam(":tag") + tag := ctx.PathParam("tag") release, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, tag) if err != nil { @@ -94,7 +94,7 @@ func DeleteReleaseByTag(ctx *context.APIContext) { // "422": // "$ref": "#/responses/validationError" - tag := ctx.PathParam(":tag") + tag := ctx.PathParam("tag") release, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, tag) if err != nil { diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 40990a28cbd..ce09e7fc0f9 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -495,7 +495,7 @@ func CreateOrgRepo(ctx *context.APIContext) { // "403": // "$ref": "#/responses/forbidden" opt := web.GetForm(ctx).(*api.CreateRepoOption) - org, err := organization.GetOrgByName(ctx, ctx.PathParam(":org")) + org, err := organization.GetOrgByName(ctx, ctx.PathParam("org")) if err != nil { if organization.IsErrOrgNotExist(err) { ctx.Error(http.StatusUnprocessableEntity, "", err) @@ -575,7 +575,7 @@ func GetByID(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - repo, err := repo_model.GetRepositoryByID(ctx, ctx.PathParamInt64(":id")) + repo, err := repo_model.GetRepositoryByID(ctx, ctx.PathParamInt64("id")) if err != nil { if repo_model.IsErrRepoNotExist(err) { ctx.NotFound() @@ -726,12 +726,11 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err if ctx.Repo.GitRepo == nil && !repo.IsEmpty { var err error - ctx.Repo.GitRepo, err = gitrepo.OpenRepository(ctx, repo) + ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo) if err != nil { ctx.Error(http.StatusInternalServerError, "Unable to OpenRepository", err) return err } - defer ctx.Repo.GitRepo.Close() } // Default branch only updated if changed and exist or the repository is empty diff --git a/routers/api/v1/repo/tag.go b/routers/api/v1/repo/tag.go index fe0910c735a..8447a8f1f22 100644 --- a/routers/api/v1/repo/tag.go +++ b/routers/api/v1/repo/tag.go @@ -357,7 +357,7 @@ func GetTagProtection(ctx *context.APIContext) { // "$ref": "#/responses/notFound" repo := ctx.Repo.Repository - id := ctx.PathParamInt64(":id") + id := ctx.PathParamInt64("id") pt, err := git_model.GetProtectedTagByID(ctx, id) if err != nil { ctx.Error(http.StatusInternalServerError, "GetProtectedTagByID", err) @@ -521,7 +521,7 @@ func EditTagProtection(ctx *context.APIContext) { repo := ctx.Repo.Repository form := web.GetForm(ctx).(*api.EditTagProtectionOption) - id := ctx.PathParamInt64(":id") + id := ctx.PathParamInt64("id") pt, err := git_model.GetProtectedTagByID(ctx, id) if err != nil { ctx.Error(http.StatusInternalServerError, "GetProtectedTagByID", err) @@ -616,7 +616,7 @@ func DeleteTagProtection(ctx *context.APIContext) { // "$ref": "#/responses/notFound" repo := ctx.Repo.Repository - id := ctx.PathParamInt64(":id") + id := ctx.PathParamInt64("id") pt, err := git_model.GetProtectedTagByID(ctx, id) if err != nil { ctx.Error(http.StatusInternalServerError, "GetProtectedTagByID", err) diff --git a/routers/api/v1/repo/teams.go b/routers/api/v1/repo/teams.go index 42fb0a1d753..e5a2d5c320b 100644 --- a/routers/api/v1/repo/teams.go +++ b/routers/api/v1/repo/teams.go @@ -221,7 +221,7 @@ func changeRepoTeam(ctx *context.APIContext, add bool) { } func getTeamByParam(ctx *context.APIContext) *organization.Team { - team, err := organization.GetTeam(ctx, ctx.Repo.Owner.ID, ctx.PathParam(":team")) + team, err := organization.GetTeam(ctx, ctx.Repo.Owner.ID, ctx.PathParam("team")) if err != nil { if organization.IsErrTeamNotExist(err) { ctx.Error(http.StatusNotFound, "TeamNotExit", err) diff --git a/routers/api/v1/repo/topic.go b/routers/api/v1/repo/topic.go index 6b9eedf6e0e..a1a15e7f46d 100644 --- a/routers/api/v1/repo/topic.go +++ b/routers/api/v1/repo/topic.go @@ -162,7 +162,7 @@ func AddTopic(ctx *context.APIContext) { // "422": // "$ref": "#/responses/invalidTopicsError" - topicName := strings.TrimSpace(strings.ToLower(ctx.PathParam(":topic"))) + topicName := strings.TrimSpace(strings.ToLower(ctx.PathParam("topic"))) if !repo_model.ValidateTopic(topicName) { ctx.JSON(http.StatusUnprocessableEntity, map[string]any{ @@ -229,7 +229,7 @@ func DeleteTopic(ctx *context.APIContext) { // "422": // "$ref": "#/responses/invalidTopicsError" - topicName := strings.TrimSpace(strings.ToLower(ctx.PathParam(":topic"))) + topicName := strings.TrimSpace(strings.ToLower(ctx.PathParam("topic"))) if !repo_model.ValidateTopic(topicName) { ctx.JSON(http.StatusUnprocessableEntity, map[string]any{ diff --git a/routers/api/v1/repo/transfer.go b/routers/api/v1/repo/transfer.go index 787ec34404f..b2090cac41c 100644 --- a/routers/api/v1/repo/transfer.go +++ b/routers/api/v1/repo/transfer.go @@ -100,7 +100,7 @@ func Transfer(ctx *context.APIContext) { } if ctx.Repo.GitRepo != nil { - ctx.Repo.GitRepo.Close() + _ = ctx.Repo.GitRepo.Close() ctx.Repo.GitRepo = nil } diff --git a/routers/api/v1/repo/tree.go b/routers/api/v1/repo/tree.go index efb247c19e2..768e5d41c1e 100644 --- a/routers/api/v1/repo/tree.go +++ b/routers/api/v1/repo/tree.go @@ -56,7 +56,7 @@ func GetTree(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - sha := ctx.PathParam(":sha") + sha := ctx.PathParam("sha") if len(sha) == 0 { ctx.Error(http.StatusBadRequest, "", "sha not provided") return diff --git a/routers/api/v1/repo/wiki.go b/routers/api/v1/repo/wiki.go index f9906ed250d..352d8f48fc0 100644 --- a/routers/api/v1/repo/wiki.go +++ b/routers/api/v1/repo/wiki.go @@ -136,7 +136,7 @@ func EditWikiPage(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.CreateWikiPageOptions) - oldWikiName := wiki_service.WebPathFromRequest(ctx.PathParamRaw(":pageName")) + oldWikiName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("pageName")) newWikiName := wiki_service.UserTitleToWebPath("", form.Title) if len(newWikiName) == 0 { @@ -242,7 +242,7 @@ func DeleteWikiPage(ctx *context.APIContext) { // "423": // "$ref": "#/responses/repoArchivedError" - wikiName := wiki_service.WebPathFromRequest(ctx.PathParamRaw(":pageName")) + wikiName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("pageName")) if err := wiki_service.DeleteWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName); err != nil { if err.Error() == "file does not exist" { @@ -370,7 +370,7 @@ func GetWikiPage(ctx *context.APIContext) { // "$ref": "#/responses/notFound" // get requested pagename - pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw(":pageName")) + pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("pageName")) wikiPage := getWikiPage(ctx, pageName) if !ctx.Written() { @@ -420,7 +420,7 @@ func ListPageRevisions(ctx *context.APIContext) { } // get requested pagename - pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw(":pageName")) + pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("pageName")) if len(pageName) == 0 { pageName = "Home" } diff --git a/routers/api/v1/user/app.go b/routers/api/v1/user/app.go index 9583bb548c7..bfbc2ba6220 100644 --- a/routers/api/v1/user/app.go +++ b/routers/api/v1/user/app.go @@ -165,7 +165,7 @@ func DeleteAccessToken(ctx *context.APIContext) { // "422": // "$ref": "#/responses/error" - token := ctx.PathParam(":id") + token := ctx.PathParam("id") tokenID, _ := strconv.ParseInt(token, 0, 64) if tokenID == 0 { @@ -306,7 +306,7 @@ func DeleteOauth2Application(ctx *context.APIContext) { // "$ref": "#/responses/empty" // "404": // "$ref": "#/responses/notFound" - appID := ctx.PathParamInt64(":id") + appID := ctx.PathParamInt64("id") if err := auth_model.DeleteOAuth2Application(ctx, appID, ctx.Doer.ID); err != nil { if auth_model.IsErrOAuthApplicationNotFound(err) { ctx.NotFound() @@ -338,7 +338,7 @@ func GetOauth2Application(ctx *context.APIContext) { // "$ref": "#/responses/OAuth2Application" // "404": // "$ref": "#/responses/notFound" - appID := ctx.PathParamInt64(":id") + appID := ctx.PathParamInt64("id") app, err := auth_model.GetOAuth2ApplicationByID(ctx, appID) if err != nil { if auth_model.IsErrOauthClientIDInvalid(err) || auth_model.IsErrOAuthApplicationNotFound(err) { @@ -382,7 +382,7 @@ func UpdateOauth2Application(ctx *context.APIContext) { // "$ref": "#/responses/OAuth2Application" // "404": // "$ref": "#/responses/notFound" - appID := ctx.PathParamInt64(":id") + appID := ctx.PathParamInt64("id") data := web.GetForm(ctx).(*api.CreateOAuth2ApplicationOptions) diff --git a/routers/api/v1/user/follower.go b/routers/api/v1/user/follower.go index 6abb70de194..8f46808f9ec 100644 --- a/routers/api/v1/user/follower.go +++ b/routers/api/v1/user/follower.go @@ -201,7 +201,7 @@ func CheckFollowing(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - target := GetUserByParamsName(ctx, ":target") + target := GetUserByPathParam(ctx, "target") // FIXME: it is not right to call this function, it should load the "target" directly if ctx.Written() { return } diff --git a/routers/api/v1/user/gpg_key.go b/routers/api/v1/user/gpg_key.go index ba5c0fdc454..ef667a18832 100644 --- a/routers/api/v1/user/gpg_key.go +++ b/routers/api/v1/user/gpg_key.go @@ -116,7 +116,7 @@ func GetGPGKey(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - key, err := asymkey_model.GetGPGKeyForUserByID(ctx, ctx.Doer.ID, ctx.PathParamInt64(":id")) + key, err := asymkey_model.GetGPGKeyForUserByID(ctx, ctx.Doer.ID, ctx.PathParamInt64("id")) if err != nil { if asymkey_model.IsErrGPGKeyNotExist(err) { ctx.NotFound() @@ -280,7 +280,7 @@ func DeleteGPGKey(ctx *context.APIContext) { return } - if err := asymkey_model.DeleteGPGKey(ctx, ctx.Doer, ctx.PathParamInt64(":id")); err != nil { + if err := asymkey_model.DeleteGPGKey(ctx, ctx.Doer, ctx.PathParamInt64("id")); err != nil { if asymkey_model.IsErrGPGKeyAccessDenied(err) { ctx.Error(http.StatusForbidden, "", "You do not have access to this key") } else { diff --git a/routers/api/v1/user/helper.go b/routers/api/v1/user/helper.go index 23a526cd676..9a6f305700d 100644 --- a/routers/api/v1/user/helper.go +++ b/routers/api/v1/user/helper.go @@ -10,8 +10,9 @@ import ( "code.gitea.io/gitea/services/context" ) -// GetUserByParamsName get user by name -func GetUserByParamsName(ctx *context.APIContext, name string) *user_model.User { +// GetUserByPathParam get user by the path param name +// it will redirect to the user's new name if the user's name has been changed +func GetUserByPathParam(ctx *context.APIContext, name string) *user_model.User { username := ctx.PathParam(name) user, err := user_model.GetUserByName(ctx, username) if err != nil { @@ -29,7 +30,7 @@ func GetUserByParamsName(ctx *context.APIContext, name string) *user_model.User return user } -// GetUserByParams returns user whose name is presented in URL (":username"). -func GetUserByParams(ctx *context.APIContext) *user_model.User { - return GetUserByParamsName(ctx, ":username") +// GetContextUserByPathParam returns user whose name is presented in URL (path param "username"). +func GetContextUserByPathParam(ctx *context.APIContext) *user_model.User { + return GetUserByPathParam(ctx, "username") } diff --git a/routers/api/v1/user/key.go b/routers/api/v1/user/key.go index e4278c2ec0e..5a9125b4f30 100644 --- a/routers/api/v1/user/key.go +++ b/routers/api/v1/user/key.go @@ -179,7 +179,7 @@ func GetPublicKey(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - key, err := asymkey_model.GetPublicKeyByID(ctx, ctx.PathParamInt64(":id")) + key, err := asymkey_model.GetPublicKeyByID(ctx, ctx.PathParamInt64("id")) if err != nil { if asymkey_model.IsErrKeyNotExist(err) { ctx.NotFound() @@ -274,7 +274,7 @@ func DeletePublicKey(ctx *context.APIContext) { return } - id := ctx.PathParamInt64(":id") + id := ctx.PathParamInt64("id") externallyManaged, err := asymkey_model.PublicKeyIsExternallyManaged(ctx, id) if err != nil { if asymkey_model.IsErrKeyNotExist(err) { diff --git a/routers/api/v1/user/user.go b/routers/api/v1/user/user.go index e668326861e..43dabe1b60c 100644 --- a/routers/api/v1/user/user.go +++ b/routers/api/v1/user/user.go @@ -121,7 +121,7 @@ func GetInfo(ctx *context.APIContext) { if !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) { // fake ErrUserNotExist error message to not leak information about existence - ctx.NotFound("GetUserByName", user_model.ErrUserNotExist{Name: ctx.PathParam(":username")}) + ctx.NotFound("GetUserByName", user_model.ErrUserNotExist{Name: ctx.PathParam("username")}) return } ctx.JSON(http.StatusOK, convert.ToUser(ctx, ctx.ContextUser, ctx.Doer)) diff --git a/routers/common/codesearch.go b/routers/common/codesearch.go new file mode 100644 index 00000000000..a14af126e55 --- /dev/null +++ b/routers/common/codesearch.go @@ -0,0 +1,39 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package common + +import ( + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/services/context" +) + +func PrepareCodeSearch(ctx *context.Context) (ret struct { + Keyword string + Language string + IsFuzzy bool +}, +) { + ret.Language = ctx.FormTrim("l") + ret.Keyword = ctx.FormTrim("q") + + fuzzyDefault := setting.Indexer.RepoIndexerEnabled + fuzzyAllow := true + if setting.Indexer.RepoType == "bleve" && setting.Indexer.TypeBleveMaxFuzzniess == 0 { + fuzzyDefault = false + fuzzyAllow = false + } + isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(fuzzyDefault) + if isFuzzy && !fuzzyAllow { + ctx.Flash.Info("Fuzzy search is disabled by default due to performance reasons") + isFuzzy = false + } + + ctx.Data["IsBleveFuzzyDisabled"] = true + ctx.Data["Keyword"] = ret.Keyword + ctx.Data["Language"] = ret.Language + ctx.Data["IsFuzzy"] = isFuzzy + + ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled + return ret +} diff --git a/routers/common/errpage_test.go b/routers/common/errpage_test.go index 4fd63ba49e7..dfea55f510b 100644 --- a/routers/common/errpage_test.go +++ b/routers/common/errpage_test.go @@ -12,8 +12,8 @@ import ( "testing" "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/test" - "code.gitea.io/gitea/modules/web/middleware" "github.com/stretchr/testify/assert" ) @@ -21,7 +21,7 @@ import ( func TestRenderPanicErrorPage(t *testing.T) { w := httptest.NewRecorder() req := &http.Request{URL: &url.URL{}} - req = req.WithContext(middleware.WithContextData(context.Background())) + req = req.WithContext(reqctx.NewRequestContextForTest(context.Background())) RenderPanicErrorPage(w, req, errors.New("fake panic error (for test only)")) respContent := w.Body.String() assert.Contains(t, respContent, `class="page-content status-page-500"`) diff --git a/routers/common/middleware.go b/routers/common/middleware.go index 51e42d87a0b..12b0c67b018 100644 --- a/routers/common/middleware.go +++ b/routers/common/middleware.go @@ -4,16 +4,14 @@ package common import ( - go_context "context" "fmt" "net/http" "strings" "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/httplib" - "code.gitea.io/gitea/modules/process" + "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/routing" "code.gitea.io/gitea/services/context" @@ -24,65 +22,76 @@ import ( // ProtocolMiddlewares returns HTTP protocol related middlewares, and it provides a global panic recovery func ProtocolMiddlewares() (handlers []any) { - // make sure chi uses EscapedPath(RawPath) as RoutePath, then "%2f" could be handled correctly - handlers = append(handlers, func(next http.Handler) http.Handler { - return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - ctx := chi.RouteContext(req.Context()) - if req.URL.RawPath == "" { - ctx.RoutePath = req.URL.EscapedPath() - } else { - ctx.RoutePath = req.URL.RawPath - } - next.ServeHTTP(resp, req) - }) - }) + // the order is important + handlers = append(handlers, ChiRoutePathHandler()) // make sure chi has correct paths + handlers = append(handlers, RequestContextHandler()) // prepare the context and panic recovery - // prepare the ContextData and panic recovery - handlers = append(handlers, func(next http.Handler) http.Handler { + if setting.ReverseProxyLimit > 0 && len(setting.ReverseProxyTrustedProxies) > 0 { + handlers = append(handlers, ForwardedHeadersHandler(setting.ReverseProxyLimit, setting.ReverseProxyTrustedProxies)) + } + + if setting.IsRouteLogEnabled() { + handlers = append(handlers, routing.NewLoggerHandler()) + } + + if setting.IsAccessLogEnabled() { + handlers = append(handlers, context.AccessLogger()) + } + + return handlers +} + +func RequestContextHandler() func(h http.Handler) http.Handler { + return func(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + profDesc := fmt.Sprintf("%s: %s", req.Method, req.RequestURI) + ctx, finished := reqctx.NewRequestContext(req.Context(), profDesc) + defer finished() + defer func() { if err := recover(); err != nil { RenderPanicErrorPage(resp, req, err) // it should never panic } }() - req = req.WithContext(middleware.WithContextData(req.Context())) - req = req.WithContext(go_context.WithValue(req.Context(), httplib.RequestContextKey, req)) - next.ServeHTTP(resp, req) - }) - }) - // wrap the request and response, use the process context and add it to the process manager - handlers = append(handlers, func(next http.Handler) http.Handler { - return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - ctx, _, finished := process.GetManager().AddTypedContext(req.Context(), fmt.Sprintf("%s: %s", req.Method, req.RequestURI), process.RequestProcessType, true) - defer finished() - next.ServeHTTP(context.WrapResponseWriter(resp), req.WithContext(cache.WithCacheContext(ctx))) + ds := reqctx.GetRequestDataStore(ctx) + req = req.WithContext(cache.WithCacheContext(ctx)) + ds.SetContextValue(httplib.RequestContextKey, req) + ds.AddCleanUp(func() { + if req.MultipartForm != nil { + _ = req.MultipartForm.RemoveAll() // remove the temp files buffered to tmp directory + } + }) + next.ServeHTTP(context.WrapResponseWriter(resp), req) }) - }) + } +} - if setting.ReverseProxyLimit > 0 { - opt := proxy.NewForwardedHeadersOptions(). - WithForwardLimit(setting.ReverseProxyLimit). - ClearTrustedProxies() - for _, n := range setting.ReverseProxyTrustedProxies { - if !strings.Contains(n, "/") { - opt.AddTrustedProxy(n) +func ChiRoutePathHandler() func(h http.Handler) http.Handler { + // make sure chi uses EscapedPath(RawPath) as RoutePath, then "%2f" could be handled correctly + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + ctx := chi.RouteContext(req.Context()) + if req.URL.RawPath == "" { + ctx.RoutePath = req.URL.EscapedPath() } else { - opt.AddTrustedNetwork(n) + ctx.RoutePath = req.URL.RawPath } - } - handlers = append(handlers, proxy.ForwardedHeaders(opt)) - } - - if setting.IsRouteLogEnabled() { - handlers = append(handlers, routing.NewLoggerHandler()) + next.ServeHTTP(resp, req) + }) } +} - if setting.IsAccessLogEnabled() { - handlers = append(handlers, context.AccessLogger()) +func ForwardedHeadersHandler(limit int, trustedProxies []string) func(h http.Handler) http.Handler { + opt := proxy.NewForwardedHeadersOptions().WithForwardLimit(limit).ClearTrustedProxies() + for _, n := range trustedProxies { + if !strings.Contains(n, "/") { + opt.AddTrustedProxy(n) + } else { + opt.AddTrustedNetwork(n) + } } - - return handlers + return proxy.ForwardedHeaders(opt) } func Sessioner() func(next http.Handler) http.Handler { diff --git a/routers/init.go b/routers/init.go index 98ce1bc4c98..e7aa765bf03 100644 --- a/routers/init.go +++ b/routers/init.go @@ -133,7 +133,7 @@ func InitWebInstalled(ctx context.Context) { highlight.NewContext() external.RegisterRenderers() - markup.Init(markup_service.ProcessorHelper()) + markup.Init(markup_service.FormalRenderHelperFuncs()) if setting.EnableSQLite3 { log.Info("SQLite3 support is enabled") diff --git a/routers/install/install.go b/routers/install/install.go index 1819bafc628..8a1d57aa0b3 100644 --- a/routers/install/install.go +++ b/routers/install/install.go @@ -25,6 +25,7 @@ import ( "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/timeutil" @@ -61,15 +62,11 @@ func Contexter() func(next http.Handler) http.Handler { envConfigKeys := setting.CollectEnvConfigKeys() return func(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - base, baseCleanUp := context.NewBaseContext(resp, req) - defer baseCleanUp() - + base := context.NewBaseContext(resp, req) ctx := context.NewWebContext(base, rnd, session.GetSession(req)) - ctx.AppendContextValue(context.WebContextKey, ctx) + ctx.SetContextValue(context.WebContextKey, ctx) ctx.Data.MergeFrom(middleware.CommonTemplateContextData()) - ctx.Data.MergeFrom(middleware.ContextData{ - "Context": ctx, // TODO: use "ctx" in template and remove this - "locale": ctx.Locale, + ctx.Data.MergeFrom(reqctx.ContextData{ "Title": ctx.Locale.Tr("install.install"), "PageIsInstall": true, "DbTypeNames": dbTypeNames, diff --git a/routers/private/default_branch.go b/routers/private/default_branch.go index 8f6e9084df7..c375d70dc6f 100644 --- a/routers/private/default_branch.go +++ b/routers/private/default_branch.go @@ -16,9 +16,9 @@ import ( // SetDefaultBranch updates the default branch func SetDefaultBranch(ctx *gitea_context.PrivateContext) { - ownerName := ctx.PathParam(":owner") - repoName := ctx.PathParam(":repo") - branch := ctx.PathParam(":branch") + ownerName := ctx.PathParam("owner") + repoName := ctx.PathParam("repo") + branch := ctx.PathParam("branch") ctx.Repo.Repository.DefaultBranch = branch if err := gitrepo.SetDefaultBranch(ctx, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch); err != nil { diff --git a/routers/private/hook_post_receive.go b/routers/private/hook_post_receive.go index 8d12b7a9536..dba6aef9a32 100644 --- a/routers/private/hook_post_receive.go +++ b/routers/private/hook_post_receive.go @@ -8,11 +8,9 @@ import ( "fmt" "net/http" - "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" access_model "code.gitea.io/gitea/models/perm/access" - pull_model "code.gitea.io/gitea/models/pull" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/cache" @@ -22,7 +20,7 @@ import ( "code.gitea.io/gitea/modules/private" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" - timeutil "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" gitea_context "code.gitea.io/gitea/services/context" @@ -40,8 +38,8 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) { // b) our update function will likely change the repository in the db so we will need to refresh it // c) we don't always need the repo - ownerName := ctx.PathParam(":owner") - repoName := ctx.PathParam(":repo") + ownerName := ctx.PathParam("owner") + repoName := ctx.PathParam("repo") // defer getting the repository at this point - as we should only retrieve it if we're going to call update var ( @@ -359,21 +357,9 @@ func handlePullRequestMerging(ctx *gitea_context.PrivateContext, opts *private.H return } - pr.MergedCommitID = updates[len(updates)-1].NewCommitID - pr.MergedUnix = timeutil.TimeStampNow() - pr.Merger = pusher - pr.MergerID = pusher.ID - err = db.WithTx(ctx, func(ctx context.Context) error { - // Removing an auto merge pull and ignore if not exist - if err := pull_model.DeleteScheduledAutoMerge(ctx, pr.ID); err != nil && !db.IsErrNotExist(err) { - return fmt.Errorf("DeleteScheduledAutoMerge[%d]: %v", opts.PullRequestID, err) - } - if _, err := pr.SetMerged(ctx); err != nil { - return fmt.Errorf("SetMerged failed: %s/%s Error: %v", ownerName, repoName, err) - } - return nil - }) - if err != nil { + // FIXME: Maybe we need a `PullRequestStatusMerged` status for PRs that are merged, currently we use the previous status + // here to keep it as before, that maybe PullRequestStatusMergeable + if _, err := pull_service.SetMerged(ctx, pr, updates[len(updates)-1].NewCommitID, timeutil.TimeStampNow(), pusher, pr.Status); err != nil { log.Error("Failed to update PR to merged: %v", err) ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "Failed to update PR to merged"}) } diff --git a/routers/private/internal.go b/routers/private/internal.go index 1fb72f13d9c..a78c76f8970 100644 --- a/routers/private/internal.go +++ b/routers/private/internal.go @@ -63,8 +63,8 @@ func Routes() *web.Router { r.Post("/ssh/{id}/update/{repoid}", UpdatePublicKeyInRepo) r.Post("/ssh/log", bind(private.SSHLogOption{}), SSHLog) r.Post("/hook/pre-receive/{owner}/{repo}", RepoAssignment, bind(private.HookOptions{}), HookPreReceive) - r.Post("/hook/post-receive/{owner}/{repo}", context.OverrideContext, bind(private.HookOptions{}), HookPostReceive) - r.Post("/hook/proc-receive/{owner}/{repo}", context.OverrideContext, RepoAssignment, bind(private.HookOptions{}), HookProcReceive) + r.Post("/hook/post-receive/{owner}/{repo}", context.OverrideContext(), bind(private.HookOptions{}), HookPostReceive) + r.Post("/hook/proc-receive/{owner}/{repo}", context.OverrideContext(), RepoAssignment, bind(private.HookOptions{}), HookProcReceive) r.Post("/hook/set-default-branch/{owner}/{repo}/{branch}", RepoAssignment, SetDefaultBranch) r.Get("/serv/none/{keyid}", ServNoCommand) r.Get("/serv/command/{keyid}/{owner}/{repo}", ServCommand) @@ -88,7 +88,7 @@ func Routes() *web.Router { // Fortunately, the LFS handlers are able to handle requests without a complete web context common.AddOwnerRepoGitLFSRoutes(r, func(ctx *context.PrivateContext) { webContext := &context.Context{Base: ctx.Base} - ctx.AppendContextValue(context.WebContextKey, webContext) + ctx.SetContextValue(context.WebContextKey, webContext) }) }) diff --git a/routers/private/internal_repo.go b/routers/private/internal_repo.go index aad0a3fb1aa..e111d6689e0 100644 --- a/routers/private/internal_repo.go +++ b/routers/private/internal_repo.go @@ -4,7 +4,6 @@ package private import ( - "context" "fmt" "net/http" @@ -17,40 +16,29 @@ import ( // This file contains common functions relating to setting the Repository for the internal routes -// RepoAssignment assigns the repository and gitrepository to the private context -func RepoAssignment(ctx *gitea_context.PrivateContext) context.CancelFunc { - ownerName := ctx.PathParam(":owner") - repoName := ctx.PathParam(":repo") +// RepoAssignment assigns the repository and git repository to the private context +func RepoAssignment(ctx *gitea_context.PrivateContext) { + ownerName := ctx.PathParam("owner") + repoName := ctx.PathParam("repo") repo := loadRepository(ctx, ownerName, repoName) if ctx.Written() { // Error handled in loadRepository - return nil + return } - gitRepo, err := gitrepo.OpenRepository(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{ Err: fmt.Sprintf("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err), }) - return nil + return } - ctx.Repo = &gitea_context.Repository{ Repository: repo, GitRepo: gitRepo, } - - // We opened it, we should close it - cancel := func() { - // If it's been set to nil then assume someone else has closed it. - if ctx.Repo.GitRepo != nil { - ctx.Repo.GitRepo.Close() - } - } - - return cancel } func loadRepository(ctx *gitea_context.PrivateContext, ownerName, repoName string) *repo_model.Repository { diff --git a/routers/private/key.go b/routers/private/key.go index 063db76520f..9fd0a16c074 100644 --- a/routers/private/key.go +++ b/routers/private/key.go @@ -14,8 +14,8 @@ import ( // UpdatePublicKeyInRepo update public key and deploy key updates func UpdatePublicKeyInRepo(ctx *context.PrivateContext) { - keyID := ctx.PathParamInt64(":id") - repoID := ctx.PathParamInt64(":repoid") + keyID := ctx.PathParamInt64("id") + repoID := ctx.PathParamInt64("repoid") if err := asymkey_model.UpdatePublicKeyUpdated(ctx, keyID); err != nil { ctx.JSON(http.StatusInternalServerError, private.Response{ Err: err.Error(), diff --git a/routers/private/serv.go b/routers/private/serv.go index 4dd7d06fb36..ecff3b7a531 100644 --- a/routers/private/serv.go +++ b/routers/private/serv.go @@ -25,7 +25,7 @@ import ( // ServNoCommand returns information about the provided keyid func ServNoCommand(ctx *context.PrivateContext) { - keyID := ctx.PathParamInt64(":keyid") + keyID := ctx.PathParamInt64("keyid") if keyID <= 0 { ctx.JSON(http.StatusBadRequest, private.Response{ UserMsg: fmt.Sprintf("Bad key id: %d", keyID), @@ -77,9 +77,9 @@ func ServNoCommand(ctx *context.PrivateContext) { // ServCommand returns information about the provided keyid func ServCommand(ctx *context.PrivateContext) { - keyID := ctx.PathParamInt64(":keyid") - ownerName := ctx.PathParam(":owner") - repoName := ctx.PathParam(":repo") + keyID := ctx.PathParamInt64("keyid") + ownerName := ctx.PathParam("owner") + repoName := ctx.PathParam("repo") mode := perm.AccessMode(ctx.FormInt("mode")) // Set the basic parts of the results to return diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go index 6a65cfa697c..249347e8352 100644 --- a/routers/web/admin/auths.go +++ b/routers/web/admin/auths.go @@ -337,7 +337,7 @@ func EditAuthSource(ctx *context.Context) { oauth2providers := oauth2.GetSupportedOAuth2Providers() ctx.Data["OAuth2Providers"] = oauth2providers - source, err := auth.GetSourceByID(ctx, ctx.PathParamInt64(":authid")) + source, err := auth.GetSourceByID(ctx, ctx.PathParamInt64("authid")) if err != nil { ctx.ServerError("auth.GetSourceByID", err) return @@ -371,7 +371,7 @@ func EditAuthSourcePost(ctx *context.Context) { oauth2providers := oauth2.GetSupportedOAuth2Providers() ctx.Data["OAuth2Providers"] = oauth2providers - source, err := auth.GetSourceByID(ctx, ctx.PathParamInt64(":authid")) + source, err := auth.GetSourceByID(ctx, ctx.PathParamInt64("authid")) if err != nil { ctx.ServerError("auth.GetSourceByID", err) return @@ -442,7 +442,7 @@ func EditAuthSourcePost(ctx *context.Context) { // DeleteAuthSource response for deleting an auth source func DeleteAuthSource(ctx *context.Context) { - source, err := auth.GetSourceByID(ctx, ctx.PathParamInt64(":authid")) + source, err := auth.GetSourceByID(ctx, ctx.PathParamInt64("authid")) if err != nil { ctx.ServerError("auth.GetSourceByID", err) return @@ -454,7 +454,7 @@ func DeleteAuthSource(ctx *context.Context) { } else { ctx.Flash.Error(fmt.Sprintf("auth_service.DeleteSource: %v", err)) } - ctx.JSONRedirect(setting.AppSubURL + "/-/admin/auths/" + url.PathEscape(ctx.PathParam(":authid"))) + ctx.JSONRedirect(setting.AppSubURL + "/-/admin/auths/" + url.PathEscape(ctx.PathParam("authid"))) return } log.Trace("Authentication deleted by admin(%s): %d", ctx.Doer.Name, source.ID) diff --git a/routers/web/admin/emails.go b/routers/web/admin/emails.go index e925de89372..23ddfa583a1 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 da345f2f89b..51223422595 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 27d39dcf4be..1bc8abb88cc 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 cf158f7aa96..f6a3af1c866 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) } @@ -219,7 +214,7 @@ func NewUserPost(ctx *context.Context) { } func prepareUserInfo(ctx *context.Context) *user_model.User { - u, err := user_model.GetUserByID(ctx, ctx.PathParamInt64(":userid")) + u, err := user_model.GetUserByID(ctx, ctx.PathParamInt64("userid")) if err != nil { if user_model.IsErrUserNotExist(err) { ctx.Redirect(setting.AppSubURL + "/-/admin/users") @@ -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) } @@ -481,12 +478,12 @@ func EditUserPost(ctx *context.Context) { } ctx.Flash.Success(ctx.Tr("admin.users.update_profile_success")) - ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam(":userid"))) + ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam("userid"))) } // DeleteUser response for deleting a user func DeleteUser(ctx *context.Context) { - u, err := user_model.GetUserByID(ctx, ctx.PathParamInt64(":userid")) + u, err := user_model.GetUserByID(ctx, ctx.PathParamInt64("userid")) if err != nil { ctx.ServerError("GetUserByID", err) return @@ -495,7 +492,7 @@ func DeleteUser(ctx *context.Context) { // admin should not delete themself if u.ID == ctx.Doer.ID { ctx.Flash.Error(ctx.Tr("admin.users.cannot_delete_self")) - ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam(":userid"))) + ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam("userid"))) return } @@ -503,16 +500,16 @@ func DeleteUser(ctx *context.Context) { switch { case repo_model.IsErrUserOwnRepos(err): ctx.Flash.Error(ctx.Tr("admin.users.still_own_repo")) - ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam(":userid"))) + ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam("userid"))) case org_model.IsErrUserHasOrgs(err): ctx.Flash.Error(ctx.Tr("admin.users.still_has_org")) - ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam(":userid"))) + ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam("userid"))) case packages_model.IsErrUserOwnPackages(err): ctx.Flash.Error(ctx.Tr("admin.users.still_own_packages")) - ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam(":userid"))) + ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam("userid"))) case user_model.IsErrDeleteLastAdminUser(err): ctx.Flash.Error(ctx.Tr("auth.last_admin")) - ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam(":userid"))) + ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam("userid"))) default: ctx.ServerError("DeleteUser", err) } diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 42736a423f8..3fe1d5970e8 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/oauth.go b/routers/web/auth/oauth.go index 32c30c71e8e..7a9721cf56f 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -34,7 +34,7 @@ import ( // SignInOAuth handles the OAuth2 login buttons func SignInOAuth(ctx *context.Context) { - provider := ctx.PathParam(":provider") + provider := ctx.PathParam("provider") authSource, err := auth.GetActiveOAuth2SourceByName(ctx, provider) if err != nil { @@ -73,7 +73,7 @@ func SignInOAuth(ctx *context.Context) { // SignInOAuthCallback handles the callback from the given provider func SignInOAuthCallback(ctx *context.Context) { - provider := ctx.PathParam(":provider") + provider := ctx.PathParam("provider") if ctx.Req.FormValue("error") != "" { var errorKeyValues []string diff --git a/routers/web/auth/password.go b/routers/web/auth/password.go index 3812d582e53..614e086f773 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 0bc84d2d1e1..1ea1398173e 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 4df89253b4c..ae5ff3db761 100644 --- a/routers/web/explore/code.go +++ b/routers/web/explore/code.go @@ -11,6 +11,7 @@ import ( code_indexer "code.gitea.io/gitea/modules/indexer/code" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/services/context" ) @@ -32,18 +33,10 @@ func Code(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("explore") ctx.Data["PageIsExplore"] = true ctx.Data["PageIsExploreCode"] = true - - language := ctx.FormTrim("l") - keyword := ctx.FormTrim("q") - - isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true) - - ctx.Data["Keyword"] = keyword - ctx.Data["Language"] = language - ctx.Data["IsFuzzy"] = isFuzzy ctx.Data["PageIsViewCode"] = true - if keyword == "" { + prepareSearch := common.PrepareCodeSearch(ctx) + if prepareSearch.Keyword == "" { ctx.HTML(http.StatusOK, tplExploreCode) return } @@ -80,9 +73,9 @@ func Code(ctx *context.Context) { if (len(repoIDs) > 0) || isAdmin { total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{ RepoIDs: repoIDs, - Keyword: keyword, - IsKeywordFuzzy: isFuzzy, - Language: language, + Keyword: prepareSearch.Keyword, + IsKeywordFuzzy: prepareSearch.IsFuzzy, + Language: prepareSearch.Language, Paginator: &db.ListOptions{ Page: page, PageSize: setting.UI.RepoSearchPagingNum, @@ -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 c421aea7151..cf3128314bd 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 ef103af8cf5..b14272c76aa 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/feed/render.go b/routers/web/feed/render.go index f975fc7cb2f..462ebb97b5a 100644 --- a/routers/web/feed/render.go +++ b/routers/web/feed/render.go @@ -9,7 +9,7 @@ import ( // RenderBranchFeed render format for branch or file func RenderBranchFeed(ctx *context.Context) { - _, _, showFeedType := GetFeedType(ctx.PathParam(":reponame"), ctx.Req) + _, _, showFeedType := GetFeedType(ctx.PathParam("reponame"), ctx.Req) if ctx.Repo.TreePath == "" { ShowBranchFeed(ctx, ctx.Repo.Repository, showFeedType) } else { diff --git a/routers/web/goget.go b/routers/web/goget.go index 3714dd8eb0f..79d5c2b2076 100644 --- a/routers/web/goget.go +++ b/routers/web/goget.go @@ -69,9 +69,9 @@ func goGet(ctx *context.Context) { var cloneURL string if setting.Repository.GoGetCloneURLProtocol == "ssh" { - cloneURL = repo_model.ComposeSSHCloneURL(ownerName, repoName) + cloneURL = repo_model.ComposeSSHCloneURL(ctx.Doer, ownerName, repoName) } else { - cloneURL = repo_model.ComposeHTTPSCloneURL(ownerName, repoName) + cloneURL = repo_model.ComposeHTTPSCloneURL(ctx, ownerName, repoName) } goImportContent := fmt.Sprintf("%s git %s", goGetImport, cloneURL /*CloneLink*/) goSourceContent := fmt.Sprintf("%s _ %s %s", goGetImport, prefix+"{/dir}" /*GoDocDirectory*/, prefix+"{/dir}/{file}#L{line}" /*GoDocFile*/) diff --git a/routers/web/org/home.go b/routers/web/org/home.go index 7122aff6bd7..277adb60ca0 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,20 +22,18 @@ 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) { - uname := ctx.PathParam(":username") + uname := ctx.PathParam("username") if strings.HasSuffix(uname, ".keys") || strings.HasSuffix(uname, ".gpg") { ctx.NotFound("", nil) return } - ctx.SetPathParam(":org", uname) + ctx.SetPathParam("org", uname) context.HandleOrgAssignment(ctx) if ctx.Written() { return @@ -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 - - if profileGitRepo == nil || profileReadme == nil || viewRepositories { - return false - } +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 bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil { - log.Error("failed to GetBlobContent: %v", err) + 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 { - 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) + if prepareResult.ProfilePublicReadmeBlob != nil { + profileRepo, readmeBlob = prepareResult.ProfilePublicRepo, prepareResult.ProfilePublicReadmeBlob } else { - ctx.Data["ProfileReadme"] = profileContent + profileRepo, readmeBlob = prepareResult.ProfilePrivateRepo, prepareResult.ProfilePrivateReadmeBlob + viewAsMember = true } } + if readmeBlob == nil { + return false + } + + 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 f91062957e2..1665a123025 100644 --- a/routers/web/org/members.go +++ b/routers/web/org/members.go @@ -54,9 +54,9 @@ func Members(ctx *context.Context) { return } - err = shared_user.RenderOrgHeader(ctx) + _, err = shared_user.PrepareOrgHeader(ctx) if err != nil { - ctx.ServerError("RenderOrgHeader", err) + ctx.ServerError("PrepareOrgHeader", err) return } @@ -90,7 +90,7 @@ func MembersAction(ctx *context.Context) { org := ctx.Org.Organization - switch ctx.PathParam(":action") { + switch ctx.PathParam("action") { case "private": if ctx.Doer.ID != member.ID && !ctx.Org.IsOwner { ctx.Error(http.StatusNotFound) @@ -131,7 +131,7 @@ func MembersAction(ctx *context.Context) { } if err != nil { - log.Error("Action(%s): %v", ctx.PathParam(":action"), err) + log.Error("Action(%s): %v", ctx.PathParam("action"), err) ctx.JSON(http.StatusOK, map[string]any{ "ok": false, "err": err.Error(), @@ -140,7 +140,7 @@ func MembersAction(ctx *context.Context) { } redirect := ctx.Org.OrgLink + "/members" - if ctx.PathParam(":action") == "leave" { + if ctx.PathParam("action") == "leave" { redirect = setting.AppSubURL + "/" } diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index 08201e5eaa3..32da1b41d16 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) @@ -196,7 +196,7 @@ func NewProjectPost(ctx *context.Context) { // ChangeProjectStatus updates the status of a project between "open" and "close" func ChangeProjectStatus(ctx *context.Context) { var toClose bool - switch ctx.PathParam(":action") { + switch ctx.PathParam("action") { case "open": toClose = false case "close": @@ -205,18 +205,18 @@ func ChangeProjectStatus(ctx *context.Context) { ctx.JSONRedirect(ctx.ContextUser.HomeLink() + "/-/projects") return } - id := ctx.PathParamInt64(":id") + id := ctx.PathParamInt64("id") if err := project_model.ChangeProjectStatusByRepoIDAndID(ctx, 0, id, toClose); err != nil { 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 func DeleteProject(ctx *context.Context) { - p, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) + p, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) if err != nil { ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) return @@ -245,7 +245,7 @@ func RenderEditProject(ctx *context.Context) { shared_user.RenderUserHeader(ctx) - p, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) + p, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) if err != nil { ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) return @@ -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) } @@ -269,13 +269,13 @@ func RenderEditProject(ctx *context.Context) { // EditProjectPost response for editing a project func EditProjectPost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.CreateProjectForm) - projectID := ctx.PathParamInt64(":id") + projectID := ctx.PathParamInt64("id") ctx.Data["Title"] = ctx.Tr("repo.projects.edit") ctx.Data["PageIsEditProjects"] = true 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) @@ -318,7 +318,7 @@ func EditProjectPost(ctx *context.Context) { // ViewProject renders the project with board view for a project func ViewProject(ctx *context.Context) { - project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) + project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) if err != nil { ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) return @@ -447,18 +447,18 @@ func DeleteProjectColumn(ctx *context.Context) { return } - project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) + project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) if err != nil { ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) return } - pb, err := project_model.GetColumn(ctx, ctx.PathParamInt64(":columnID")) + pb, err := project_model.GetColumn(ctx, ctx.PathParamInt64("columnID")) if err != nil { ctx.ServerError("GetProjectColumn", err) return } - if pb.ProjectID != ctx.PathParamInt64(":id") { + if pb.ProjectID != ctx.PathParamInt64("id") { ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ "message": fmt.Sprintf("ProjectColumn[%d] is not in Project[%d] as expected", pb.ID, project.ID), }) @@ -472,7 +472,7 @@ func DeleteProjectColumn(ctx *context.Context) { return } - if err := project_model.DeleteColumnByID(ctx, ctx.PathParamInt64(":columnID")); err != nil { + if err := project_model.DeleteColumnByID(ctx, ctx.PathParamInt64("columnID")); err != nil { ctx.ServerError("DeleteProjectColumnByID", err) return } @@ -484,7 +484,7 @@ func DeleteProjectColumn(ctx *context.Context) { func AddColumnToProjectPost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.EditProjectColumnForm) - project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) + project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) if err != nil { ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) return @@ -512,18 +512,18 @@ func CheckProjectColumnChangePermissions(ctx *context.Context) (*project_model.P return nil, nil } - project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) + project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) if err != nil { ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) return nil, nil } - column, err := project_model.GetColumn(ctx, ctx.PathParamInt64(":columnID")) + column, err := project_model.GetColumn(ctx, ctx.PathParamInt64("columnID")) if err != nil { ctx.ServerError("GetProjectColumn", err) return nil, nil } - if column.ProjectID != ctx.PathParamInt64(":id") { + if column.ProjectID != ctx.PathParamInt64("id") { ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ "message": fmt.Sprintf("ProjectColumn[%d] is not in Project[%d] as expected", column.ID, project.ID), }) @@ -587,7 +587,7 @@ func MoveIssues(ctx *context.Context) { return } - project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) + project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) if err != nil { ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) return @@ -597,7 +597,7 @@ func MoveIssues(ctx *context.Context) { return } - column, err := project_model.GetColumn(ctx, ctx.PathParamInt64(":columnID")) + column, err := project_model.GetColumn(ctx, ctx.PathParamInt64("columnID")) if err != nil { ctx.NotFoundOrServerError("GetProjectColumn", project_model.IsErrProjectColumnNotExist, err) return diff --git a/routers/web/org/projects_test.go b/routers/web/org/projects_test.go index c52cb7ed4ce..c3a769e621e 100644 --- a/routers/web/org/projects_test.go +++ b/routers/web/org/projects_test.go @@ -18,8 +18,8 @@ func TestCheckProjectColumnChangePermissions(t *testing.T) { ctx, _ := contexttest.MockContext(t, "user2/-/projects/4/4") contexttest.LoadUser(t, ctx, 2) ctx.ContextUser = ctx.Doer // user2 - ctx.SetPathParam(":id", "4") - ctx.SetPathParam(":columnID", "4") + ctx.SetPathParam("id", "4") + ctx.SetPathParam("columnID", "4") project, column := org.CheckProjectColumnChangePermissions(ctx) assert.NotNil(t, project) diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go index 7414a11308b..26031029d66 100644 --- a/routers/web/org/teams.go +++ b/routers/web/org/teams.go @@ -58,9 +58,9 @@ func Teams(ctx *context.Context) { } ctx.Data["Teams"] = ctx.Org.Teams - err := shared_user.RenderOrgHeader(ctx) + _, err := shared_user.PrepareOrgHeader(ctx) if err != nil { - ctx.ServerError("RenderOrgHeader", err) + ctx.ServerError("PrepareOrgHeader", err) return } @@ -71,7 +71,7 @@ func Teams(ctx *context.Context) { func TeamsAction(ctx *context.Context) { page := ctx.FormString("page") var err error - switch ctx.PathParam(":action") { + switch ctx.PathParam("action") { case "join": if !ctx.Org.IsOwner { ctx.Error(http.StatusNotFound) @@ -84,7 +84,7 @@ func TeamsAction(ctx *context.Context) { if org_model.IsErrLastOrgOwner(err) { ctx.Flash.Error(ctx.Tr("form.last_org_owner")) } else { - log.Error("Action(%s): %v", ctx.PathParam(":action"), err) + log.Error("Action(%s): %v", ctx.PathParam("action"), err) ctx.JSON(http.StatusOK, map[string]any{ "ok": false, "err": err.Error(), @@ -111,7 +111,7 @@ func TeamsAction(ctx *context.Context) { if org_model.IsErrLastOrgOwner(err) { ctx.Flash.Error(ctx.Tr("form.last_org_owner")) } else { - log.Error("Action(%s): %v", ctx.PathParam(":action"), err) + log.Error("Action(%s): %v", ctx.PathParam("action"), err) ctx.JSON(http.StatusOK, map[string]any{ "ok": false, "err": err.Error(), @@ -178,7 +178,7 @@ func TeamsAction(ctx *context.Context) { } if err := org_model.RemoveInviteByID(ctx, iid, ctx.Org.Team.ID); err != nil { - log.Error("Action(%s): %v", ctx.PathParam(":action"), err) + log.Error("Action(%s): %v", ctx.PathParam("action"), err) ctx.ServerError("RemoveInviteByID", err) return } @@ -192,7 +192,7 @@ func TeamsAction(ctx *context.Context) { } else if errors.Is(err, user_model.ErrBlockedUser) { ctx.Flash.Error(ctx.Tr("org.teams.members.blocked_user")) } else { - log.Error("Action(%s): %v", ctx.PathParam(":action"), err) + log.Error("Action(%s): %v", ctx.PathParam("action"), err) ctx.JSON(http.StatusOK, map[string]any{ "ok": false, "err": err.Error(), @@ -233,7 +233,7 @@ func TeamsRepoAction(ctx *context.Context) { } var err error - action := ctx.PathParam(":action") + action := ctx.PathParam("action") switch action { case "add": repoName := path.Base(ctx.FormString("repo_name")) @@ -258,7 +258,7 @@ func TeamsRepoAction(ctx *context.Context) { } if err != nil { - log.Error("Action(%s): '%s' %v", ctx.PathParam(":action"), ctx.Org.Team.Name, err) + log.Error("Action(%s): '%s' %v", ctx.PathParam("action"), ctx.Org.Team.Name, err) ctx.ServerError("TeamsRepoAction", err) return } diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go index 099593bff08..539c4b6ed00 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" @@ -33,8 +32,9 @@ import ( ) const ( - tplListActions templates.TplName = "repo/actions/list" - tplViewActions templates.TplName = "repo/actions/view" + tplListActions templates.TplName = "repo/actions/list" + tplDispatchInputsActions templates.TplName = "repo/actions/workflow_dispatch_inputs" + tplViewActions templates.TplName = "repo/actions/view" ) type Workflow struct { @@ -65,107 +65,143 @@ func MustEnableActions(ctx *context.Context) { func List(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("actions.actions") ctx.Data["PageIsActions"] = true + + commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) + if err != nil { + ctx.ServerError("GetBranchCommit", err) + return + } + + workflows := prepareWorkflowDispatchTemplate(ctx, commit) + if ctx.Written() { + return + } + + prepareWorkflowList(ctx, workflows) + if ctx.Written() { + return + } + + ctx.HTML(http.StatusOK, tplListActions) +} + +func WorkflowDispatchInputs(ctx *context.Context) { + ref := ctx.FormString("ref") + if ref == "" { + ctx.NotFound("WorkflowDispatchInputs: no ref", nil) + return + } + // get target commit of run from specified ref + refName := git.RefName(ref) + var commit *git.Commit + var err error + if refName.IsTag() { + commit, err = ctx.Repo.GitRepo.GetTagCommit(refName.TagName()) + } else if refName.IsBranch() { + commit, err = ctx.Repo.GitRepo.GetBranchCommit(refName.BranchName()) + } else { + ctx.ServerError("UnsupportedRefType", nil) + return + } + if err != nil { + ctx.ServerError("GetTagCommit/GetBranchCommit", err) + return + } + prepareWorkflowDispatchTemplate(ctx, commit) + if ctx.Written() { + return + } + ctx.HTML(http.StatusOK, tplDispatchInputsActions) +} + +func prepareWorkflowDispatchTemplate(ctx *context.Context, commit *git.Commit) (workflows []Workflow) { workflowID := ctx.FormString("workflow") - actorID := ctx.FormInt64("actor") - status := ctx.FormInt("status") ctx.Data["CurWorkflow"] = workflowID + ctx.Data["CurWorkflowExists"] = false - var workflows []Workflow var curWorkflow *model.Workflow - if empty, err := ctx.Repo.GitRepo.IsEmpty(); err != nil { - ctx.ServerError("IsEmpty", err) - return - } else if !empty { - commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) - if err != nil { - ctx.ServerError("GetBranchCommit", err) - return - } - entries, err := actions.ListWorkflows(commit) - if err != nil { - ctx.ServerError("ListWorkflows", err) - return - } - // Get all runner labels - runners, err := db.Find[actions_model.ActionRunner](ctx, actions_model.FindRunnerOptions{ - RepoID: ctx.Repo.Repository.ID, - IsOnline: optional.Some(true), - WithAvailable: true, - }) + entries, err := actions.ListWorkflows(commit) + if err != nil { + ctx.ServerError("ListWorkflows", err) + return nil + } + + // Get all runner labels + runners, err := db.Find[actions_model.ActionRunner](ctx, actions_model.FindRunnerOptions{ + RepoID: ctx.Repo.Repository.ID, + IsOnline: optional.Some(true), + WithAvailable: true, + }) + if err != nil { + ctx.ServerError("FindRunners", err) + return nil + } + allRunnerLabels := make(container.Set[string]) + for _, r := range runners { + allRunnerLabels.AddMultiple(r.AgentLabels...) + } + + workflows = make([]Workflow, 0, len(entries)) + for _, entry := range entries { + workflow := Workflow{Entry: *entry} + content, err := actions.GetContentFromEntry(entry) if err != nil { - ctx.ServerError("FindRunners", err) - return + ctx.ServerError("GetContentFromEntry", err) + return nil } - allRunnerLabels := make(container.Set[string]) - for _, r := range runners { - allRunnerLabels.AddMultiple(r.AgentLabels...) + wf, err := model.ReadWorkflow(bytes.NewReader(content)) + if err != nil { + workflow.ErrMsg = ctx.Locale.TrString("actions.runs.invalid_workflow_helper", err.Error()) + workflows = append(workflows, workflow) + continue } - - workflows = make([]Workflow, 0, len(entries)) - for _, entry := range entries { - workflow := Workflow{Entry: *entry} - content, err := actions.GetContentFromEntry(entry) - if err != nil { - ctx.ServerError("GetContentFromEntry", err) - return - } - wf, err := model.ReadWorkflow(bytes.NewReader(content)) - if err != nil { - workflow.ErrMsg = ctx.Locale.TrString("actions.runs.invalid_workflow_helper", err.Error()) - workflows = append(workflows, workflow) + // The workflow must contain at least one job without "needs". Otherwise, a deadlock will occur and no jobs will be able to run. + hasJobWithoutNeeds := false + // Check whether you have matching runner and a job without "needs" + emptyJobsNumber := 0 + for _, j := range wf.Jobs { + if j == nil { + emptyJobsNumber++ continue } - // The workflow must contain at least one job without "needs". Otherwise, a deadlock will occur and no jobs will be able to run. - hasJobWithoutNeeds := false - // Check whether have matching runner and a job without "needs" - emptyJobsNumber := 0 - for _, j := range wf.Jobs { - if j == nil { - emptyJobsNumber++ + if !hasJobWithoutNeeds && len(j.Needs()) == 0 { + hasJobWithoutNeeds = true + } + runsOnList := j.RunsOn() + for _, ro := range runsOnList { + if strings.Contains(ro, "${{") { + // Skip if it contains expressions. + // The expressions could be very complex and could not be evaluated here, + // so just skip it, it's OK since it's just a tooltip message. continue } - if !hasJobWithoutNeeds && len(j.Needs()) == 0 { - hasJobWithoutNeeds = true - } - runsOnList := j.RunsOn() - for _, ro := range runsOnList { - if strings.Contains(ro, "${{") { - // Skip if it contains expressions. - // The expressions could be very complex and could not be evaluated here, - // so just skip it, it's OK since it's just a tooltip message. - continue - } - if !allRunnerLabels.Contains(ro) { - workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_matching_online_runner_helper", ro) - break - } - } - if workflow.ErrMsg != "" { + if !allRunnerLabels.Contains(ro) { + workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_matching_online_runner_helper", ro) break } } - if !hasJobWithoutNeeds { - workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job_without_needs") - } - if emptyJobsNumber == len(wf.Jobs) { - workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job") + if workflow.ErrMsg != "" { + break } - workflows = append(workflows, workflow) + } + if !hasJobWithoutNeeds { + workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job_without_needs") + } + if emptyJobsNumber == len(wf.Jobs) { + workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job") + } + workflows = append(workflows, workflow) - if workflow.Entry.Name() == workflowID { - curWorkflow = wf - } + if workflow.Entry.Name() == workflowID { + curWorkflow = wf + ctx.Data["CurWorkflowExists"] = true } } + ctx.Data["workflows"] = workflows ctx.Data["RepoLink"] = ctx.Repo.Repository.Link() - page := ctx.FormInt("page") - if page <= 0 { - page = 1 - } - actionsConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions).ActionsConfig() ctx.Data["ActionsConfig"] = actionsConfig @@ -189,7 +225,7 @@ func List(ctx *context.Context) { branches, err := git_model.FindBranchNames(ctx, branchOpts) if err != nil { ctx.ServerError("FindBranchNames", err) - return + return nil } // always put default branch on the top if it exists if slices.Contains(branches, ctx.Repo.Repository.DefaultBranch) { @@ -201,12 +237,23 @@ func List(ctx *context.Context) { tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID) if err != nil { ctx.ServerError("GetTagNamesByRepoID", err) - return + return nil } ctx.Data["Tags"] = tags } } } + return workflows +} + +func prepareWorkflowList(ctx *context.Context, workflows []Workflow) { + actorID := ctx.FormInt64("actor") + status := ctx.FormInt("status") + workflowID := ctx.FormString("workflow") + page := ctx.FormInt("page") + if page <= 0 { + page = 1 + } // if status or actor query param is not given to frontend href, (href="//actions") // they will be 0 by default, which indicates get all status or actors @@ -262,14 +309,9 @@ 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 - - ctx.HTML(http.StatusOK, tplListActions) } // loadIsRefDeleted loads the IsRefDeleted field for each run in the list. diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index ba17fa427d1..9a18ca53058 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -812,13 +812,8 @@ func Run(ctx *context_module.Context) { return } - // get workflow entry from default branch commit - defaultBranchCommit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) - if err != nil { - ctx.Error(http.StatusInternalServerError, err.Error()) - return - } - entries, err := actions.ListWorkflows(defaultBranchCommit) + // get workflow entry from runTargetCommit + entries, err := actions.ListWorkflows(runTargetCommit) if err != nil { ctx.Error(http.StatusInternalServerError, err.Error()) return diff --git a/routers/web/repo/attachment.go b/routers/web/repo/attachment.go index 04f480f6110..f8e51521be5 100644 --- a/routers/web/repo/attachment.go +++ b/routers/web/repo/attachment.go @@ -154,5 +154,5 @@ func ServeAttachment(ctx *context.Context, uuid string) { // GetAttachment serve attachments func GetAttachment(ctx *context.Context) { - ServeAttachment(ctx, ctx.PathParam(":uuid")) + ServeAttachment(ctx, ctx.PathParam("uuid")) } diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go index 72fd958e28b..2bcd7821b47 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/cherry_pick.go b/routers/web/repo/cherry_pick.go index 30f4c8a90ef..35f158df525 100644 --- a/routers/web/repo/cherry_pick.go +++ b/routers/web/repo/cherry_pick.go @@ -24,8 +24,8 @@ var tplCherryPick templates.TplName = "repo/editor/cherry_pick" // CherryPick handles cherrypick GETs func CherryPick(ctx *context.Context) { - ctx.Data["SHA"] = ctx.PathParam(":sha") - cherryPickCommit, err := ctx.Repo.GitRepo.GetCommit(ctx.PathParam(":sha")) + ctx.Data["SHA"] = ctx.PathParam("sha") + cherryPickCommit, err := ctx.Repo.GitRepo.GetCommit(ctx.PathParam("sha")) if err != nil { if git.IsErrNotExist(err) { ctx.NotFound("Missing Commit", err) @@ -37,7 +37,7 @@ func CherryPick(ctx *context.Context) { if ctx.FormString("cherry-pick-type") == "revert" { ctx.Data["CherryPickType"] = "revert" - ctx.Data["commit_summary"] = "revert " + ctx.PathParam(":sha") + ctx.Data["commit_summary"] = "revert " + ctx.PathParam("sha") ctx.Data["commit_message"] = "revert " + cherryPickCommit.Message() } else { ctx.Data["CherryPickType"] = "cherry-pick" @@ -66,7 +66,7 @@ func CherryPick(ctx *context.Context) { func CherryPickPost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.CherryPickForm) - sha := ctx.PathParam(":sha") + sha := ctx.PathParam("sha") ctx.Data["SHA"] = sha if form.Revert { ctx.Data["CherryPickType"] = "revert" @@ -140,7 +140,7 @@ func CherryPickPost(ctx *context.Context) { if form.Revert { if err := git.GetReverseRawDiff(ctx, ctx.Repo.Repository.RepoPath(), sha, buf); err != nil { if git.IsErrNotExist(err) { - ctx.NotFound("GetRawDiff", errors.New("commit "+ctx.PathParam(":sha")+" does not exist.")) + ctx.NotFound("GetRawDiff", errors.New("commit "+ctx.PathParam("sha")+" does not exist.")) return } ctx.ServerError("GetRawDiff", err) @@ -149,7 +149,7 @@ func CherryPickPost(ctx *context.Context) { } else { if err := git.GetRawDiff(ctx.Repo.GitRepo, sha, git.RawDiffType("patch"), buf); err != nil { if git.IsErrNotExist(err) { - ctx.NotFound("GetRawDiff", errors.New("commit "+ctx.PathParam(":sha")+" does not exist.")) + ctx.NotFound("GetRawDiff", errors.New("commit "+ctx.PathParam("sha")+" does not exist.")) return } ctx.ServerError("GetRawDiff", err) diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index 1447b17a366..3655233312c 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) } @@ -282,7 +274,7 @@ func Diff(ctx *context.Context) { userName := ctx.Repo.Owner.Name repoName := ctx.Repo.Repository.Name - commitID := ctx.PathParam(":sha") + commitID := ctx.PathParam("sha") var ( gitRepo *git.Repository err error @@ -427,13 +419,13 @@ func RawDiff(ctx *context.Context) { } if err := git.GetRawDiff( gitRepo, - ctx.PathParam(":sha"), - git.RawDiffType(ctx.PathParam(":ext")), + ctx.PathParam("sha"), + git.RawDiffType(ctx.PathParam("ext")), ctx.Resp, ); err != nil { if git.IsErrNotExist(err) { ctx.NotFound("GetRawDiff", - errors.New("commit "+ctx.PathParam(":sha")+" does not exist.")) + errors.New("commit "+ctx.PathParam("sha")+" does not exist.")) return } ctx.ServerError("GetRawDiff", err) diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index 6c59421bda6..b3c1eb7cb08 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -664,7 +664,7 @@ func PrepareCompareDiff( } if len(title) > 255 { var trailer string - title, trailer = util.SplitStringAtByteN(title, 255) + title, trailer = util.EllipsisDisplayStringX(title, 255) if len(trailer) > 0 { if ctx.Data["content"] != nil { ctx.Data["content"] = fmt.Sprintf("%s\n\n%s", trailer, ctx.Data["content"]) @@ -864,7 +864,7 @@ func ExcerptBlob(ctx *context.Context) { direction := ctx.FormString("direction") filePath := ctx.FormString("path") gitRepo := ctx.Repo.GitRepo - if ctx.FormBool("wiki") { + if ctx.Data["PageIsWiki"] == true { var err error gitRepo, err = gitrepo.OpenWikiRepository(ctx, ctx.Repo.Repository) if err != nil { diff --git a/routers/web/repo/download.go b/routers/web/repo/download.go index 1ed907b2f97..cb1163c70b7 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/editor_test.go b/routers/web/repo/editor_test.go index 68d69408ac8..566db316935 100644 --- a/routers/web/repo/editor_test.go +++ b/routers/web/repo/editor_test.go @@ -43,7 +43,7 @@ func TestCleanUploadName(t *testing.T) { func TestGetUniquePatchBranchName(t *testing.T) { unittest.PrepareTestEnv(t) ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetPathParam(":id", "1") + ctx.SetPathParam("id", "1") contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepoCommit(t, ctx) contexttest.LoadUser(t, ctx, 2) @@ -58,7 +58,7 @@ func TestGetUniquePatchBranchName(t *testing.T) { func TestGetClosestParentWithFiles(t *testing.T) { unittest.PrepareTestEnv(t) ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetPathParam(":id", "1") + ctx.SetPathParam("id", "1") contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepoCommit(t, ctx) contexttest.LoadUser(t, ctx, 2) diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go index a8a7a4bd79f..6b2a7fd0766 100644 --- a/routers/web/repo/githttp.go +++ b/routers/web/repo/githttp.go @@ -57,8 +57,8 @@ func CorsHandler() func(next http.Handler) http.Handler { // httpBase implementation git smart HTTP protocol func httpBase(ctx *context.Context) *serviceHandler { - username := ctx.PathParam(":username") - reponame := strings.TrimSuffix(ctx.PathParam(":reponame"), ".git") + username := ctx.PathParam("username") + reponame := strings.TrimSuffix(ctx.PathParam("reponame"), ".git") if ctx.FormString("go-get") == "1" { context.EarlyResponseForGoGetMeta(ctx) diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 23012dda3d4..f150897a2d7 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -181,7 +181,7 @@ func retrieveProjectsInternal(ctx *context.Context, repo *repo_model.Repository) // GetActionIssue will return the issue which is used in the context. func GetActionIssue(ctx *context.Context) *issues_model.Issue { - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { ctx.NotFoundOrServerError("GetIssueByIndex", issues_model.IsErrIssueNotExist, err) return nil @@ -246,7 +246,7 @@ func getActionIssues(ctx *context.Context) issues_model.IssueList { // GetIssueInfo get an issue of a repository func GetIssueInfo(ctx *context.Context) { - issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.Error(http.StatusNotFound) @@ -379,7 +379,7 @@ func UpdateIssueContent(ctx *context.Context) { // UpdateIssueDeadline updates an issue deadline func UpdateIssueDeadline(ctx *context.Context) { - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound("GetIssueByIndex", err) @@ -506,7 +506,7 @@ func ChangeIssueReaction(ctx *context.Context) { return } - switch ctx.PathParam(":action") { + switch ctx.PathParam("action") { case "react": reaction, err := issue_service.CreateIssueReaction(ctx, ctx.Doer, issue, form.Content) if err != nil { @@ -540,7 +540,7 @@ func ChangeIssueReaction(ctx *context.Context) { log.Trace("Reaction for issue removed: %d/%d", ctx.Repo.Repository.ID, issue.ID) default: - ctx.NotFound(fmt.Sprintf("Unknown action %s", ctx.PathParam(":action")), nil) + ctx.NotFound(fmt.Sprintf("Unknown action %s", ctx.PathParam("action")), nil) return } @@ -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_comment.go b/routers/web/repo/issue_comment.go index 6b7b29d9d71..8564c613de0 100644 --- a/routers/web/repo/issue_comment.go +++ b/routers/web/repo/issue_comment.go @@ -154,25 +154,28 @@ func NewComment(ctx *context.Context) { if pr != nil { ctx.Flash.Info(ctx.Tr("repo.pulls.open_unmerged_pull_exists", pr.Index)) } else { - isClosed := form.Status == "close" - if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", isClosed); err != nil { - log.Error("ChangeStatus: %v", err) - - if issues_model.IsErrDependenciesLeft(err) { - if issue.IsPull { - ctx.JSONError(ctx.Tr("repo.issues.dependency.pr_close_blocked")) - } else { - ctx.JSONError(ctx.Tr("repo.issues.dependency.issue_close_blocked")) + if form.Status == "close" && !issue.IsClosed { + if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil { + log.Error("CloseIssue: %v", err) + if issues_model.IsErrDependenciesLeft(err) { + if issue.IsPull { + ctx.JSONError(ctx.Tr("repo.issues.dependency.pr_close_blocked")) + } else { + ctx.JSONError(ctx.Tr("repo.issues.dependency.issue_close_blocked")) + } + return } - return + } else { + if err := stopTimerIfAvailable(ctx, ctx.Doer, issue); err != nil { + ctx.ServerError("stopTimerIfAvailable", err) + return + } + log.Trace("Issue [%d] status changed to closed: %v", issue.ID, issue.IsClosed) } - } else { - if err := stopTimerIfAvailable(ctx, ctx.Doer, issue); err != nil { - ctx.ServerError("CreateOrStopIssueStopwatch", err) - return + } else if form.Status == "reopen" && issue.IsClosed { + if err := issue_service.ReopenIssue(ctx, issue, ctx.Doer, ""); err != nil { + log.Error("ReopenIssue: %v", err) } - - log.Trace("Issue [%d] status changed to closed: %v", issue.ID, issue.IsClosed) } } } @@ -209,7 +212,7 @@ func NewComment(ctx *context.Context) { // UpdateCommentContent change comment of issue's content func UpdateCommentContent(ctx *context.Context) { - comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64(":id")) + comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id")) if err != nil { ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err) return @@ -287,7 +290,7 @@ func UpdateCommentContent(ctx *context.Context) { // DeleteComment delete comment of issue func DeleteComment(ctx *context.Context) { - comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64(":id")) + comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id")) if err != nil { ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err) return @@ -322,7 +325,7 @@ func DeleteComment(ctx *context.Context) { // ChangeCommentReaction create a reaction for comment func ChangeCommentReaction(ctx *context.Context) { form := web.GetForm(ctx).(*forms.ReactionForm) - comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64(":id")) + comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id")) if err != nil { ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err) return @@ -366,7 +369,7 @@ func ChangeCommentReaction(ctx *context.Context) { return } - switch ctx.PathParam(":action") { + switch ctx.PathParam("action") { case "react": reaction, err := issue_service.CreateCommentReaction(ctx, ctx.Doer, comment, form.Content) if err != nil { @@ -400,7 +403,7 @@ func ChangeCommentReaction(ctx *context.Context) { log.Trace("Reaction for comment removed: %d/%d/%d", ctx.Repo.Repository.ID, comment.Issue.ID, comment.ID) default: - ctx.NotFound(fmt.Sprintf("Unknown action %s", ctx.PathParam(":action")), nil) + ctx.NotFound(fmt.Sprintf("Unknown action %s", ctx.PathParam("action")), nil) return } @@ -427,7 +430,7 @@ func ChangeCommentReaction(ctx *context.Context) { // GetCommentAttachments returns attachments for the comment func GetCommentAttachments(ctx *context.Context) { - comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64(":id")) + comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id")) if err != nil { ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err) return diff --git a/routers/web/repo/issue_label_test.go b/routers/web/repo/issue_label_test.go index c86a03da51a..8a613e2c7e5 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_list.go b/routers/web/repo/issue_list.go index ff98bf8ec8e..2f615a100e3 100644 --- a/routers/web/repo/issue_list.go +++ b/routers/web/repo/issue_list.go @@ -418,14 +418,11 @@ func UpdateIssueStatus(ctx *context.Context) { return } - var isClosed bool - switch action := ctx.FormString("action"); action { - case "open": - isClosed = false - case "close": - isClosed = true - default: + action := ctx.FormString("action") + if action != "open" && action != "close" { log.Warn("Unrecognized action: %s", action) + ctx.JSONOK() + return } if _, err := issues.LoadRepositories(ctx); err != nil { @@ -441,15 +438,20 @@ func UpdateIssueStatus(ctx *context.Context) { if issue.IsPull && issue.PullRequest.HasMerged { continue } - if issue.IsClosed != isClosed { - if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", isClosed); err != nil { + if action == "close" && !issue.IsClosed { + if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil { if issues_model.IsErrDependenciesLeft(err) { ctx.JSON(http.StatusPreconditionFailed, map[string]any{ "error": ctx.Tr("repo.issues.dependency.issue_batch_close_blocked", issue.Index), }) return } - ctx.ServerError("ChangeStatus", err) + ctx.ServerError("CloseIssue", err) + return + } + } else if action == "open" && issue.IsClosed { + if err := issue_service.ReopenIssue(ctx, issue, ctx.Doer, ""); err != nil { + ctx.ServerError("ReopenIssue", err) return } } @@ -748,7 +750,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt // Issues render issues page func Issues(ctx *context.Context) { - isPullList := ctx.PathParam(":type") == "pulls" + isPullList := ctx.PathParam("type") == "pulls" if isPullList { MustAllowPulls(ctx) if ctx.Written() { diff --git a/routers/web/repo/issue_new.go b/routers/web/repo/issue_new.go index 9a941ce8571..32daa3e48fa 100644 --- a/routers/web/repo/issue_new.go +++ b/routers/web/repo/issue_new.go @@ -396,8 +396,15 @@ func NewIssuePost(ctx *context.Context) { log.Trace("Issue created: %d/%d", repo.ID, issue.ID) if ctx.FormString("redirect_after_creation") == "project" && projectID > 0 { - ctx.JSONRedirect(ctx.Repo.RepoLink + "/projects/" + strconv.FormatInt(projectID, 10)) - } else { - ctx.JSONRedirect(issue.Link()) + project, err := project_model.GetProjectByID(ctx, projectID) + if err == nil { + if project.Type == project_model.TypeOrganization { + ctx.JSONRedirect(project_model.ProjectLinkForOrg(ctx.Repo.Owner, project.ID)) + } else { + ctx.JSONRedirect(project_model.ProjectLinkForRepo(repo, project.ID)) + } + return + } } + ctx.JSONRedirect(issue.Link()) } diff --git a/routers/web/repo/issue_page_meta.go b/routers/web/repo/issue_page_meta.go index b536b04d7c6..272343f460f 100644 --- a/routers/web/repo/issue_page_meta.go +++ b/routers/web/repo/issue_page_meta.go @@ -193,6 +193,7 @@ func (d *IssuePageMetaData) retrieveReviewersData(ctx *context.Context) { var posterID int64 var isClosed bool var reviews issues_model.ReviewList + var err error if d.Issue == nil { if ctx.Doer != nil { @@ -206,14 +207,7 @@ func (d *IssuePageMetaData) retrieveReviewersData(ctx *context.Context) { isClosed = d.Issue.IsClosed || d.Issue.PullRequest.HasMerged - originalAuthorReviews, err := issues_model.GetReviewersFromOriginalAuthorsByIssueID(ctx, d.Issue.ID) - if err != nil { - ctx.ServerError("GetReviewersFromOriginalAuthorsByIssueID", err) - return - } - data.OriginalReviews = originalAuthorReviews - - reviews, err = issues_model.GetReviewsByIssueID(ctx, d.Issue.ID) + reviews, data.OriginalReviews, err = issues_model.GetReviewsByIssueID(ctx, d.Issue.ID) if err != nil { ctx.ServerError("GetReviewersByIssueID", err) return diff --git a/routers/web/repo/issue_pin.go b/routers/web/repo/issue_pin.go index 0074e31f038..d7d3205c378 100644 --- a/routers/web/repo/issue_pin.go +++ b/routers/web/repo/issue_pin.go @@ -39,7 +39,7 @@ func IssuePinOrUnpin(ctx *context.Context) { // IssueUnpin unpins a Issue func IssueUnpin(ctx *context.Context) { - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { ctx.Status(http.StatusInternalServerError) log.Error(err.Error()) diff --git a/routers/web/repo/issue_timetrack.go b/routers/web/repo/issue_timetrack.go index 88c539488e5..36e931a48fb 100644 --- a/routers/web/repo/issue_timetrack.go +++ b/routers/web/repo/issue_timetrack.go @@ -60,7 +60,7 @@ func DeleteTime(c *context.Context) { return } - t, err := issues_model.GetTrackedTimeByID(c, c.PathParamInt64(":timeid")) + t, err := issues_model.GetTrackedTimeByID(c, c.PathParamInt64("timeid")) if err != nil { if db.IsErrNotExist(err) { c.NotFound("time not found", err) diff --git a/routers/web/repo/issue_view.go b/routers/web/repo/issue_view.go index 09b57f4e783..61e75e211bd 100644 --- a/routers/web/repo/issue_view.go +++ b/routers/web/repo/issue_view.go @@ -265,13 +265,13 @@ func combineLabelComments(issue *issues_model.Issue) { // ViewIssue render issue view page func ViewIssue(ctx *context.Context) { - if ctx.PathParam(":type") == "issues" { + if ctx.PathParam("type") == "issues" { // If issue was requested we check if repo has external tracker and redirect extIssueUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalTracker) if err == nil && extIssueUnit != nil { if extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == markup.IssueNameStyleNumeric || extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == "" { metas := ctx.Repo.Repository.ComposeMetas(ctx) - metas["index"] = ctx.PathParam(":index") + metas["index"] = ctx.PathParam("index") res, err := vars.Expand(extIssueUnit.ExternalTrackerConfig().ExternalTrackerFormat, metas) if err != nil { log.Error("unable to expand template vars for issue url. issue: %s, err: %v", metas["index"], err) @@ -287,7 +287,7 @@ func ViewIssue(ctx *context.Context) { } } - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound("GetIssueByIndex", err) @@ -301,10 +301,10 @@ func ViewIssue(ctx *context.Context) { } // Make sure type and URL matches. - if ctx.PathParam(":type") == "issues" && issue.IsPull { + if ctx.PathParam("type") == "issues" && issue.IsPull { ctx.Redirect(issue.Link()) return - } else if ctx.PathParam(":type") == "pulls" && !issue.IsPull { + } else if ctx.PathParam("type") == "pulls" && !issue.IsPull { ctx.Redirect(issue.Link()) return } diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go index d6e41a89b2c..6a0e6b25a9a 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) @@ -147,7 +145,7 @@ func EditMilestone(ctx *context.Context) { ctx.Data["PageIsMilestones"] = true ctx.Data["PageIsEditMilestone"] = true - m, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":id")) + m, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id")) if err != nil { if issues_model.IsErrMilestoneNotExist(err) { ctx.NotFound("", nil) @@ -183,7 +181,7 @@ func EditMilestonePost(ctx *context.Context) { return } - m, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":id")) + m, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id")) if err != nil { if issues_model.IsErrMilestoneNotExist(err) { ctx.NotFound("", nil) @@ -207,7 +205,7 @@ func EditMilestonePost(ctx *context.Context) { // ChangeMilestoneStatus response for change a milestone's status func ChangeMilestoneStatus(ctx *context.Context) { var toClose bool - switch ctx.PathParam(":action") { + switch ctx.PathParam("action") { case "open": toClose = false case "close": @@ -216,7 +214,7 @@ func ChangeMilestoneStatus(ctx *context.Context) { ctx.JSONRedirect(ctx.Repo.RepoLink + "/milestones") return } - id := ctx.PathParamInt64(":id") + id := ctx.PathParamInt64("id") if err := issues_model.ChangeMilestoneStatusByRepoIDAndID(ctx, ctx.Repo.Repository.ID, id, toClose); err != nil { if issues_model.IsErrMilestoneNotExist(err) { @@ -226,7 +224,7 @@ func ChangeMilestoneStatus(ctx *context.Context) { } return } - ctx.JSONRedirect(ctx.Repo.RepoLink + "/milestones?state=" + url.QueryEscape(ctx.PathParam(":action"))) + ctx.JSONRedirect(ctx.Repo.RepoLink + "/milestones?state=" + url.QueryEscape(ctx.PathParam("action"))) } // DeleteMilestone delete a milestone @@ -242,7 +240,7 @@ func DeleteMilestone(ctx *context.Context) { // MilestoneIssuesAndPulls lists all the issues and pull requests of the milestone func MilestoneIssuesAndPulls(ctx *context.Context) { - milestoneID := ctx.PathParamInt64(":id") + milestoneID := ctx.PathParamInt64("id") projectID := ctx.FormInt64("project") milestone, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, milestoneID) if err != nil { diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go index c8d3719bc04..5dcfd0454c8 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 92227e3f3e0..346132102f6 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) @@ -166,7 +166,7 @@ func NewProjectPost(ctx *context.Context) { // ChangeProjectStatus updates the status of a project between "open" and "close" func ChangeProjectStatus(ctx *context.Context) { var toClose bool - switch ctx.PathParam(":action") { + switch ctx.PathParam("action") { case "open": toClose = false case "close": @@ -175,18 +175,18 @@ func ChangeProjectStatus(ctx *context.Context) { ctx.JSONRedirect(ctx.Repo.RepoLink + "/projects") return } - id := ctx.PathParamInt64(":id") + id := ctx.PathParamInt64("id") if err := project_model.ChangeProjectStatusByRepoIDAndID(ctx, ctx.Repo.Repository.ID, id, toClose); err != nil { 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 func DeleteProject(ctx *context.Context) { - p, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) + p, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) if err != nil { if project_model.IsErrProjectNotExist(err) { ctx.NotFound("", nil) @@ -216,7 +216,7 @@ func RenderEditProject(ctx *context.Context) { ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) ctx.Data["CardTypes"] = project_model.GetCardConfig() - p, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) + p, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) if err != nil { if project_model.IsErrProjectNotExist(err) { ctx.NotFound("", nil) @@ -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) } @@ -243,13 +243,13 @@ func RenderEditProject(ctx *context.Context) { // EditProjectPost response for editing a project func EditProjectPost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.CreateProjectForm) - projectID := ctx.PathParamInt64(":id") + projectID := ctx.PathParamInt64("id") ctx.Data["Title"] = ctx.Tr("repo.projects.edit") 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) @@ -288,7 +288,7 @@ func EditProjectPost(ctx *context.Context) { // ViewProject renders the project with board view func ViewProject(ctx *context.Context) { - project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) + project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) if err != nil { if project_model.IsErrProjectNotExist(err) { ctx.NotFound("", nil) @@ -468,7 +468,7 @@ func DeleteProjectColumn(ctx *context.Context) { return } - project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) + project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) if err != nil { if project_model.IsErrProjectNotExist(err) { ctx.NotFound("", nil) @@ -478,12 +478,12 @@ func DeleteProjectColumn(ctx *context.Context) { return } - pb, err := project_model.GetColumn(ctx, ctx.PathParamInt64(":columnID")) + pb, err := project_model.GetColumn(ctx, ctx.PathParamInt64("columnID")) if err != nil { ctx.ServerError("GetProjectColumn", err) return } - if pb.ProjectID != ctx.PathParamInt64(":id") { + if pb.ProjectID != ctx.PathParamInt64("id") { ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ "message": fmt.Sprintf("ProjectColumn[%d] is not in Project[%d] as expected", pb.ID, project.ID), }) @@ -497,7 +497,7 @@ func DeleteProjectColumn(ctx *context.Context) { return } - if err := project_model.DeleteColumnByID(ctx, ctx.PathParamInt64(":columnID")); err != nil { + if err := project_model.DeleteColumnByID(ctx, ctx.PathParamInt64("columnID")); err != nil { ctx.ServerError("DeleteProjectColumnByID", err) return } @@ -515,7 +515,7 @@ func AddColumnToProjectPost(ctx *context.Context) { return } - project, err := project_model.GetProjectForRepoByID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":id")) + project, err := project_model.GetProjectForRepoByID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id")) if err != nil { if project_model.IsErrProjectNotExist(err) { ctx.NotFound("", nil) @@ -553,7 +553,7 @@ func checkProjectColumnChangePermissions(ctx *context.Context) (*project_model.P return nil, nil } - project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) + project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) if err != nil { if project_model.IsErrProjectNotExist(err) { ctx.NotFound("", nil) @@ -563,12 +563,12 @@ func checkProjectColumnChangePermissions(ctx *context.Context) (*project_model.P return nil, nil } - column, err := project_model.GetColumn(ctx, ctx.PathParamInt64(":columnID")) + column, err := project_model.GetColumn(ctx, ctx.PathParamInt64("columnID")) if err != nil { ctx.ServerError("GetProjectColumn", err) return nil, nil } - if column.ProjectID != ctx.PathParamInt64(":id") { + if column.ProjectID != ctx.PathParamInt64("id") { ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ "message": fmt.Sprintf("ProjectColumn[%d] is not in Project[%d] as expected", column.ID, project.ID), }) @@ -639,7 +639,7 @@ func MoveIssues(ctx *context.Context) { return } - project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) + project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) if err != nil { if project_model.IsErrProjectNotExist(err) { ctx.NotFound("ProjectNotExist", nil) @@ -653,7 +653,7 @@ func MoveIssues(ctx *context.Context) { return } - column, err := project_model.GetColumn(ctx, ctx.PathParamInt64(":columnID")) + column, err := project_model.GetColumn(ctx, ctx.PathParamInt64("columnID")) if err != nil { if project_model.IsErrProjectColumnNotExist(err) { ctx.NotFound("ProjectColumnNotExist", nil) diff --git a/routers/web/repo/projects_test.go b/routers/web/repo/projects_test.go index 1a42c615abc..d0690d9a4de 100644 --- a/routers/web/repo/projects_test.go +++ b/routers/web/repo/projects_test.go @@ -17,8 +17,8 @@ func TestCheckProjectColumnChangePermissions(t *testing.T) { ctx, _ := contexttest.MockContext(t, "user2/repo1/projects/1/2") contexttest.LoadUser(t, ctx, 2) contexttest.LoadRepo(t, ctx, 1) - ctx.SetPathParam(":id", "1") - ctx.SetPathParam(":columnID", "2") + ctx.SetPathParam("id", "1") + ctx.SetPathParam("columnID", "2") project, column := checkProjectColumnChangePermissions(ctx) assert.NotNil(t, project) diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 0948282ca2f..9f3d1c1b7c0 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -108,7 +108,7 @@ func getRepository(ctx *context.Context, repoID int64) *repo_model.Repository { } func getPullInfo(ctx *context.Context) (issue *issues_model.Issue, ok bool) { - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound("GetIssueByIndex", err) @@ -1544,7 +1544,7 @@ func DownloadPullPatch(ctx *context.Context) { // DownloadPullDiffOrPatch render a pull's raw diff or patch func DownloadPullDiffOrPatch(ctx *context.Context, patch bool) { - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound("GetPullRequestByIndex", err) @@ -1637,7 +1637,7 @@ func UpdatePullRequestTarget(ctx *context.Context) { func SetAllowEdits(ctx *context.Context) { form := web.GetForm(ctx).(*forms.UpdateAllowEditsForm) - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound("GetPullRequestByIndex", err) diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go index b8176cb70b9..5b099fe8d4f 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 6df7d78f7a7..0f408b22e09 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -147,27 +147,33 @@ func getRepoPrivate(ctx *context.Context) bool { } } -// Create render creating repository page -func Create(ctx *context.Context) { +func createCommon(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("new_repo") - - // Give default value for template to render. ctx.Data["Gitignores"] = repo_module.Gitignores ctx.Data["LabelTemplateFiles"] = repo_module.LabelTemplateFiles ctx.Data["Licenses"] = repo_module.Licenses ctx.Data["Readmes"] = repo_module.Readmes - ctx.Data["readme"] = "Default" - ctx.Data["private"] = getRepoPrivate(ctx) ctx.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate - ctx.Data["default_branch"] = setting.Repository.DefaultBranch + ctx.Data["CanCreateRepo"] = ctx.Doer.CanCreateRepo() + ctx.Data["MaxCreationLimit"] = ctx.Doer.MaxCreationLimit() + ctx.Data["SupportedObjectFormats"] = git.DefaultFeatures().SupportedObjectFormats + ctx.Data["DefaultObjectFormat"] = git.Sha1ObjectFormat +} +// Create render creating repository page +func Create(ctx *context.Context) { + createCommon(ctx) ctxUser := checkContextUser(ctx, ctx.FormInt64("org")) if ctx.Written() { return } ctx.Data["ContextUser"] = ctxUser + ctx.Data["readme"] = "Default" + ctx.Data["private"] = getRepoPrivate(ctx) + ctx.Data["default_branch"] = setting.Repository.DefaultBranch ctx.Data["repo_template_name"] = ctx.Tr("repo.template_select") + templateID := ctx.FormInt64("template_id") if templateID > 0 { templateRepo, err := repo_model.GetRepositoryByID(ctx, templateID) @@ -177,11 +183,6 @@ func Create(ctx *context.Context) { } } - ctx.Data["CanCreateRepo"] = ctx.Doer.CanCreateRepo() - ctx.Data["MaxCreationLimit"] = ctx.Doer.MaxCreationLimit() - ctx.Data["SupportedObjectFormats"] = git.DefaultFeatures().SupportedObjectFormats - ctx.Data["DefaultObjectFormat"] = git.Sha1ObjectFormat - ctx.HTML(http.StatusOK, tplCreate) } @@ -219,16 +220,8 @@ func handleCreateError(ctx *context.Context, owner *user_model.User, err error, // CreatePost response for creating repository func CreatePost(ctx *context.Context) { + createCommon(ctx) form := web.GetForm(ctx).(*forms.CreateRepoForm) - ctx.Data["Title"] = ctx.Tr("new_repo") - - ctx.Data["Gitignores"] = repo_module.Gitignores - ctx.Data["LabelTemplateFiles"] = repo_module.LabelTemplateFiles - ctx.Data["Licenses"] = repo_module.Licenses - ctx.Data["Readmes"] = repo_module.Readmes - - ctx.Data["CanCreateRepo"] = ctx.Doer.CanCreateRepo() - ctx.Data["MaxCreationLimit"] = ctx.Doer.MaxCreationLimit() ctxUser := checkContextUser(ctx, form.UID) if ctx.Written() { @@ -236,6 +229,14 @@ func CreatePost(ctx *context.Context) { } ctx.Data["ContextUser"] = ctxUser + if form.RepoTemplate > 0 { + templateRepo, err := repo_model.GetRepositoryByID(ctx, form.RepoTemplate) + if err == nil && access_model.CheckRepoUnitUser(ctx, templateRepo, ctxUser, unit.TypeCode) { + ctx.Data["repo_template"] = form.RepoTemplate + ctx.Data["repo_template_name"] = templateRepo.Name + } + } + if ctx.HasError() { ctx.HTML(http.StatusOK, tplCreate) return @@ -311,7 +312,7 @@ const ( // Action response for actions to a repository func Action(ctx *context.Context) { var err error - switch ctx.PathParam(":action") { + switch ctx.PathParam("action") { case "watch": err = repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, true) case "unwatch": @@ -339,12 +340,12 @@ func Action(ctx *context.Context) { if errors.Is(err, user_model.ErrBlockedUser) { ctx.Flash.Error(ctx.Tr("repo.action.blocked_user")) } else { - ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.PathParam(":action")), err) + ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.PathParam("action")), err) return } } - switch ctx.PathParam(":action") { + switch ctx.PathParam("action") { case "watch", "unwatch": ctx.Data["IsWatchingRepo"] = repo_model.IsWatching(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID) case "star", "unstar": @@ -354,17 +355,17 @@ func Action(ctx *context.Context) { // see the `hx-trigger="refreshUserCards ..."` comments in tmpl ctx.RespHeader().Add("hx-trigger", "refreshUserCards") - switch ctx.PathParam(":action") { + switch ctx.PathParam("action") { case "watch", "unwatch", "star", "unstar": // we have to reload the repository because NumStars or NumWatching (used in the templates) has just changed ctx.Data["Repository"], err = repo_model.GetRepositoryByName(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.Name) if err != nil { - ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.PathParam(":action")), err) + ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.PathParam("action")), err) return } } - switch ctx.PathParam(":action") { + switch ctx.PathParam("action") { case "watch", "unwatch": ctx.HTML(http.StatusOK, tplWatchUnwatch) return diff --git a/routers/web/repo/search.go b/routers/web/repo/search.go index a037a348333..c60301475f3 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/setting/git_hooks.go b/routers/web/repo/setting/git_hooks.go index 2e9caa4c868..1d922113039 100644 --- a/routers/web/repo/setting/git_hooks.go +++ b/routers/web/repo/setting/git_hooks.go @@ -30,7 +30,7 @@ func GitHooksEdit(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.settings.githooks") ctx.Data["PageIsSettingsGitHooks"] = true - name := ctx.PathParam(":name") + name := ctx.PathParam("name") hook, err := ctx.Repo.GitRepo.GetHook(name) if err != nil { if err == git.ErrNotValidHook { @@ -46,7 +46,7 @@ func GitHooksEdit(ctx *context.Context) { // GitHooksEditPost response for editing a git hook of a repository func GitHooksEditPost(ctx *context.Context) { - name := ctx.PathParam(":name") + name := ctx.PathParam("name") hook, err := ctx.Repo.GitRepo.GetHook(name) if err != nil { if err == git.ErrNotValidHook { diff --git a/routers/web/repo/setting/protected_tag.go b/routers/web/repo/setting/protected_tag.go index 1c24c8a7ba5..1730ad4a8be 100644 --- a/routers/web/repo/setting/protected_tag.go +++ b/routers/web/repo/setting/protected_tag.go @@ -170,7 +170,7 @@ func setTagsContext(ctx *context.Context) error { func selectProtectedTagByContext(ctx *context.Context) *git_model.ProtectedTag { id := ctx.FormInt64("id") if id == 0 { - id = ctx.PathParamInt64(":id") + id = ctx.PathParamInt64("id") } tag, err := git_model.GetProtectedTagByID(ctx, id) diff --git a/routers/web/repo/setting/runners.go b/routers/web/repo/setting/runners.go index ec037baec1a..94f2ae7a0c0 100644 --- a/routers/web/repo/setting/runners.go +++ b/routers/web/repo/setting/runners.go @@ -147,7 +147,7 @@ func RunnersEdit(ctx *context.Context) { } actions_shared.RunnerDetails(ctx, page, - ctx.PathParamInt64(":runnerid"), rCtx.OwnerID, rCtx.RepoID, + ctx.PathParamInt64("runnerid"), rCtx.OwnerID, rCtx.RepoID, ) ctx.HTML(http.StatusOK, rCtx.RunnerEditTemplate) } @@ -158,9 +158,9 @@ func RunnersEditPost(ctx *context.Context) { ctx.ServerError("getRunnersCtx", err) return } - actions_shared.RunnerDetailsEditPost(ctx, ctx.PathParamInt64(":runnerid"), + actions_shared.RunnerDetailsEditPost(ctx, ctx.PathParamInt64("runnerid"), rCtx.OwnerID, rCtx.RepoID, - rCtx.RedirectLink+url.PathEscape(ctx.PathParam(":runnerid"))) + rCtx.RedirectLink+url.PathEscape(ctx.PathParam("runnerid"))) } func ResetRunnerRegistrationToken(ctx *context.Context) { @@ -179,7 +179,7 @@ func RunnerDeletePost(ctx *context.Context) { ctx.ServerError("getRunnersCtx", err) return } - actions_shared.RunnerDeletePost(ctx, ctx.PathParamInt64(":runnerid"), rCtx.RedirectLink, rCtx.RedirectLink+url.PathEscape(ctx.PathParam(":runnerid"))) + actions_shared.RunnerDeletePost(ctx, ctx.PathParamInt64("runnerid"), rCtx.RedirectLink, rCtx.RedirectLink+url.PathEscape(ctx.PathParam("runnerid"))) } func RedirectToDefaultSetting(ctx *context.Context) { diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go index bcf8a7eac08..1b0ba83af4d 100644 --- a/routers/web/repo/setting/webhook.go +++ b/routers/web/repo/setting/webhook.go @@ -99,9 +99,9 @@ func getOwnerRepoCtx(ctx *context.Context) (*ownerRepoCtx, error) { if ctx.Data["PageIsAdmin"] == true { return &ownerRepoCtx{ IsAdmin: true, - IsSystemWebhook: ctx.PathParam(":configType") == "system-hooks", + IsSystemWebhook: ctx.PathParam("configType") == "system-hooks", Link: path.Join(setting.AppSubURL, "/-/admin/hooks"), - LinkNew: path.Join(setting.AppSubURL, "/-/admin/", ctx.PathParam(":configType")), + LinkNew: path.Join(setting.AppSubURL, "/-/admin/", ctx.PathParam("configType")), NewTemplate: tplAdminHookNew, }, nil } @@ -110,7 +110,7 @@ func getOwnerRepoCtx(ctx *context.Context) (*ownerRepoCtx, error) { } func checkHookType(ctx *context.Context) string { - hookType := strings.ToLower(ctx.PathParam(":type")) + hookType := strings.ToLower(ctx.PathParam("type")) if !util.SliceContainsString(setting.Webhook.Types, hookType, true) { ctx.NotFound("checkHookType", nil) return "" @@ -592,11 +592,11 @@ func checkWebhook(ctx *context.Context) (*ownerRepoCtx, *webhook.Webhook) { var w *webhook.Webhook if orCtx.RepoID > 0 { - w, err = webhook.GetWebhookByRepoID(ctx, orCtx.RepoID, ctx.PathParamInt64(":id")) + w, err = webhook.GetWebhookByRepoID(ctx, orCtx.RepoID, ctx.PathParamInt64("id")) } else if orCtx.OwnerID > 0 { - w, err = webhook.GetWebhookByOwnerID(ctx, orCtx.OwnerID, ctx.PathParamInt64(":id")) + w, err = webhook.GetWebhookByOwnerID(ctx, orCtx.OwnerID, ctx.PathParamInt64("id")) } else if orCtx.IsAdmin { - w, err = webhook.GetSystemOrDefaultWebhook(ctx, ctx.PathParamInt64(":id")) + w, err = webhook.GetSystemOrDefaultWebhook(ctx, ctx.PathParamInt64("id")) } if err != nil || w == nil { if webhook.IsErrWebhookNotExist(err) { @@ -645,7 +645,7 @@ func WebHooksEdit(ctx *context.Context) { // TestWebhook test if web hook is work fine func TestWebhook(ctx *context.Context) { - hookID := ctx.PathParamInt64(":id") + hookID := ctx.PathParamInt64("id") w, err := webhook.GetWebhookByRepoID(ctx, ctx.Repo.Repository.ID, hookID) if err != nil { ctx.Flash.Error("GetWebhookByRepoID: " + err.Error()) @@ -706,7 +706,7 @@ func TestWebhook(ctx *context.Context) { // ReplayWebhook replays a webhook func ReplayWebhook(ctx *context.Context) { - hookTaskUUID := ctx.PathParam(":uuid") + hookTaskUUID := ctx.PathParam("uuid") orCtx, w := checkWebhook(ctx) if ctx.Written() { diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 14fc9038f3e..9fe2b58ebc6 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -309,7 +309,6 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri } ctx.Data["TreeLink"] = treeLink - ctx.Data["SSHDomain"] = setting.SSH.Domain return allEntries } diff --git a/routers/web/repo/view_home.go b/routers/web/repo/view_home.go index b318c4a621a..8c9f54656b0 100644 --- a/routers/web/repo/view_home.go +++ b/routers/web/repo/view_home.go @@ -223,16 +223,41 @@ func prepareRecentlyPushedNewBranches(ctx *context.Context) { } } +func updateContextRepoEmptyAndStatus(ctx *context.Context, empty bool, status repo_model.RepositoryStatus) { + if ctx.Repo.Repository.IsEmpty == empty && ctx.Repo.Repository.Status == status { + return + } + ctx.Repo.Repository.IsEmpty = empty + if ctx.Repo.Repository.Status == repo_model.RepositoryReady || ctx.Repo.Repository.Status == repo_model.RepositoryBroken { + ctx.Repo.Repository.Status = status // only handle ready and broken status, leave other status as-is + } + if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, ctx.Repo.Repository, "is_empty", "status"); err != nil { + ctx.ServerError("updateContextRepoEmptyAndStatus: UpdateRepositoryCols", err) + return + } +} + func handleRepoEmptyOrBroken(ctx *context.Context) { showEmpty := true - var err error if ctx.Repo.GitRepo != nil { - showEmpty, err = ctx.Repo.GitRepo.IsEmpty() + reallyEmpty, err := ctx.Repo.GitRepo.IsEmpty() if err != nil { + showEmpty = true // the repo is broken + updateContextRepoEmptyAndStatus(ctx, true, repo_model.RepositoryBroken) log.Error("GitRepo.IsEmpty: %v", err) - ctx.Repo.Repository.Status = repo_model.RepositoryBroken - showEmpty = true ctx.Flash.Error(ctx.Tr("error.occurred"), true) + } else if reallyEmpty { + showEmpty = true // the repo is really empty + updateContextRepoEmptyAndStatus(ctx, true, repo_model.RepositoryReady) + } else if ctx.Repo.Commit == nil { + showEmpty = true // it is not really empty, but there is no branch + // at the moment, other repo units like "actions" are not able to handle such case, + // so we just mark the repo as empty to prevent from displaying these units. + ctx.Data["RepoHasContentsWithoutBranch"] = true + updateContextRepoEmptyAndStatus(ctx, true, repo_model.RepositoryReady) + } else { + // the repo is actually not empty and has branches, need to update the database later + showEmpty = false } } if showEmpty { @@ -240,18 +265,11 @@ func handleRepoEmptyOrBroken(ctx *context.Context) { return } - // the repo is not really empty, so we should update the modal in database - // such problem may be caused by: - // 1) an error occurs during pushing/receiving. 2) the user replaces an empty git repo manually - // and even more: the IsEmpty flag is deeply broken and should be removed with the UI changed to manage to cope with empty repos. - // it's possible for a repository to be non-empty by that flag but still 500 - // because there are no branches - only tags -or the default branch is non-extant as it has been 0-pushed. - ctx.Repo.Repository.IsEmpty = false - if err = repo_model.UpdateRepositoryCols(ctx, ctx.Repo.Repository, "is_empty"); err != nil { - ctx.ServerError("UpdateRepositoryCols", err) - return - } - if err = repo_module.UpdateRepoSize(ctx, ctx.Repo.Repository); err != nil { + // The repo is not really empty, so we should update the model in database, such problem may be caused by: + // 1) an error occurs during pushing/receiving. + // 2) the user replaces an empty git repo manually. + updateContextRepoEmptyAndStatus(ctx, false, repo_model.RepositoryReady) + if err := repo_module.UpdateRepoSize(ctx, ctx.Repo.Repository); err != nil { ctx.ServerError("UpdateRepoSize", err) return } @@ -276,7 +294,7 @@ func prepareToRenderDirOrFile(entry *git.TreeEntry) func(ctx *context.Context) { func handleRepoHomeFeed(ctx *context.Context) bool { if setting.Other.EnableFeed { - isFeed, _, showFeedType := feed.GetFeedType(ctx.PathParam(":reponame"), ctx.Req) + isFeed, _, showFeedType := feed.GetFeedType(ctx.PathParam("reponame"), ctx.Req) if isFeed { switch { case ctx.Link == fmt.Sprintf("%s.%s", ctx.Repo.RepoLink, showFeedType): diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index 3fca7cebea5..19366c01045 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/actions/variables.go b/routers/web/shared/actions/variables.go index 5c5768243ab..f895475748a 100644 --- a/routers/web/shared/actions/variables.go +++ b/routers/web/shared/actions/variables.go @@ -40,7 +40,7 @@ func CreateVariable(ctx *context.Context, ownerID, repoID int64, redirectURL str } func UpdateVariable(ctx *context.Context, redirectURL string) { - id := ctx.PathParamInt64(":variable_id") + id := ctx.PathParamInt64("variable_id") form := web.GetForm(ctx).(*forms.EditVariableForm) if ok, err := actions_service.UpdateVariable(ctx, id, form.Name, form.Data); err != nil || !ok { @@ -53,7 +53,7 @@ func UpdateVariable(ctx *context.Context, redirectURL string) { } func DeleteVariable(ctx *context.Context, redirectURL string) { - id := ctx.PathParamInt64(":variable_id") + id := ctx.PathParamInt64("variable_id") if err := actions_service.DeleteVariableByID(ctx, id); err != nil { log.Error("Delete variable [%d] failed: %v", id, err) diff --git a/routers/web/shared/project/column.go b/routers/web/shared/project/column.go index ba1f527beaa..141c80716f5 100644 --- a/routers/web/shared/project/column.go +++ b/routers/web/shared/project/column.go @@ -11,7 +11,7 @@ import ( // MoveColumns moves or keeps columns in a project and sorts them inside that project func MoveColumns(ctx *context.Context) { - project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) + project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) if err != nil { ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) return diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go index 4cb0592b4b7..62b146c7f32 100644 --- a/routers/web/shared/user/header.go +++ b/routers/web/shared/user/header.go @@ -20,6 +20,7 @@ import ( "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" ) @@ -61,11 +62,20 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) { orgs, err := db.Find[organization.Organization](ctx, organization.FindOrgOptions{ UserID: ctx.ContextUser.ID, IncludePrivate: showPrivate, + ListOptions: db.ListOptions{ + Page: 1, + // query one more results (without a separate counting) to see whether we need to add the "show more orgs" link + PageSize: setting.UI.User.OrgPagingNum + 1, + }, }) if err != nil { ctx.ServerError("FindOrgs", err) return } + if len(orgs) > setting.UI.User.OrgPagingNum { + orgs = orgs[:setting.UI.User.OrgPagingNum] + ctx.Data["ShowMoreOrgs"] = true + } ctx.Data["Orgs"] = orgs ctx.Data["HasOrgsVisible"] = organization.HasOrgsVisible(ctx, orgs, ctx.Doer) @@ -93,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 { @@ -160,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" +) - _, _, profileReadmeBlob, profileClose := FindUserProfileReadme(ctx, ctx.Doer) - defer profileClose() - ctx.Data["HasProfileReadme"] = profileReadmeBlob != nil +type PrepareOrgHeaderResult struct { + ProfilePublicRepo *repo_model.Repository + ProfilePublicReadmeBlob *git.Blob + ProfilePrivateRepo *repo_model.Repository + ProfilePrivateReadmeBlob *git.Blob + HasOrgProfileReadme bool +} - return nil +func PrepareOrgHeader(ctx *context.Context) (result *PrepareOrgHeaderResult, err error) { + if err = LoadHeaderCount(ctx); err != nil { + return nil, err + } + + result = &PrepareOrgHeaderResult{} + result.ProfilePublicRepo, result.ProfilePublicReadmeBlob = FindOwnerProfileReadme(ctx, ctx.Doer) + result.ProfilePrivateRepo, result.ProfilePrivateReadmeBlob = FindOwnerProfileReadme(ctx, ctx.Doer, RepoNameProfilePrivate) + result.HasOrgProfileReadme = result.ProfilePublicReadmeBlob != nil || result.ProfilePrivateReadmeBlob != nil + ctx.Data["HasOrgProfileReadme"] = result.HasOrgProfileReadme // many pages need it to show the "overview" tab + return result, nil } diff --git a/routers/web/user/avatar.go b/routers/web/user/avatar.go index 7000e257785..f77bd602b3d 100644 --- a/routers/web/user/avatar.go +++ b/routers/web/user/avatar.go @@ -23,8 +23,8 @@ func cacheableRedirect(ctx *context.Context, location string) { // AvatarByUserName redirect browser to user avatar of requested size func AvatarByUserName(ctx *context.Context) { - userName := ctx.PathParam(":username") - size := int(ctx.PathParamInt64(":size")) + userName := ctx.PathParam("username") + size := int(ctx.PathParamInt64("size")) var user *user_model.User if strings.ToLower(userName) != user_model.GhostUserLowerName { @@ -46,7 +46,7 @@ func AvatarByUserName(ctx *context.Context) { // AvatarByEmailHash redirects the browser to the email avatar link func AvatarByEmailHash(ctx *context.Context) { - hash := ctx.PathParam(":hash") + hash := ctx.PathParam("hash") email, err := avatars.GetEmailForHash(ctx, hash) if err != nil { ctx.ServerError("invalid avatar hash: "+hash, err) diff --git a/routers/web/user/code.go b/routers/web/user/code.go index f805f2b0281..665ce1a6a6f 100644 --- a/routers/web/user/code.go +++ b/routers/web/user/code.go @@ -11,6 +11,7 @@ import ( code_indexer "code.gitea.io/gitea/modules/indexer/code" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/routers/common" shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/context" ) @@ -34,20 +35,11 @@ func CodeSearch(ctx *context.Context) { } ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled - ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled ctx.Data["Title"] = ctx.Tr("explore.code") - - language := ctx.FormTrim("l") - keyword := ctx.FormTrim("q") - - isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true) - - ctx.Data["Keyword"] = keyword - ctx.Data["Language"] = language - ctx.Data["IsFuzzy"] = isFuzzy ctx.Data["IsCodePage"] = true - if keyword == "" { + prepareSearch := common.PrepareCodeSearch(ctx) + if prepareSearch.Keyword == "" { ctx.HTML(http.StatusOK, tplUserCode) return } @@ -77,9 +69,9 @@ func CodeSearch(ctx *context.Context) { if len(repoIDs) > 0 { total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{ RepoIDs: repoIDs, - Keyword: keyword, - IsKeywordFuzzy: isFuzzy, - Language: language, + Keyword: prepareSearch.Keyword, + IsKeywordFuzzy: prepareSearch.IsFuzzy, + Language: prepareSearch.Language, Paginator: &db.ListOptions{ Page: page, PageSize: setting.UI.RepoSearchPagingNum, @@ -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 e118aa051e8..c79648a4559 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -56,7 +56,7 @@ const ( // getDashboardContextUser finds out which context user dashboard is being viewed as . func getDashboardContextUser(ctx *context.Context) *user_model.User { ctxUser := ctx.Doer - orgName := ctx.PathParam(":org") + orgName := ctx.PathParam("org") if len(orgName) > 0 { ctxUser = ctx.Org.Organization.AsUser() ctx.Data["Teams"] = ctx.Org.Teams @@ -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 732fac25958..1c91ff63643 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 d5aac513b6e..1f75faf1c64 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) @@ -502,7 +493,7 @@ func PackageSettingsPost(ctx *context.Context) { // DownloadPackageFile serves the content of a package file func DownloadPackageFile(ctx *context.Context) { - pf, err := packages_model.GetFileForVersionByID(ctx, ctx.Package.Descriptor.Version.ID, ctx.PathParamInt64(":fileid")) + pf, err := packages_model.GetFileForVersionByID(ctx, ctx.Package.Descriptor.Version.ID, ctx.PathParamInt64("fileid")) if err != nil { if err == packages_model.ErrPackageFileNotExist { ctx.NotFound("", err) diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index 44824de7521..006ffdcf7e0 100644 --- a/routers/web/user/profile.go +++ b/routers/web/user/profile.go @@ -12,6 +12,7 @@ import ( activities_model "code.gitea.io/gitea/models/activities" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/renderhelper" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" @@ -73,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) @@ -95,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 { @@ -253,9 +253,24 @@ 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": + orgs, count, err := db.FindAndCount[organization.Organization](ctx, organization.FindOrgOptions{ + UserID: ctx.ContextUser.ID, + IncludePrivate: showPrivate, + ListOptions: db.ListOptions{ + Page: page, + PageSize: pagingNum, + }, + }) + if err != nil { + ctx.ServerError("GetUserOrganizations", err) + return + } + ctx.Data["Cards"] = orgs + total = int(count) default: // default to "repositories" repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ @@ -294,31 +309,7 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb } pager := context.NewPagination(total, pagingNum, page, 5) - pager.SetDefaultParams(ctx) - pager.AddParamString("tab", tab) - if tab != "followers" && tab != "following" && tab != "activity" && tab != "projects" { - pager.AddParamString("language", language) - } - if tab == "activity" { - if ctx.Data["Date"] != nil { - pager.AddParamString("date", fmt.Sprint(ctx.Data["Date"])) - } - } - 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 } diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go index 4b3c214096c..ebf682bf588 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/routers/web/web.go b/routers/web/web.go index e1005aae440..32d65865ac0 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -4,7 +4,6 @@ package web import ( - gocontext "context" "net/http" "strings" @@ -1167,7 +1166,7 @@ func registerRoutes(m *web.Router) { Get(repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff). Post(reqSignIn, context.RepoMustNotBeArchived(), reqRepoPullsReader, repo.MustAllowPulls, web.Bind(forms.CreateIssueForm{}), repo.SetWhitespaceBehavior, repo.CompareAndPullRequestPost) }, optSignIn, context.RepoAssignment, reqRepoCodeReader) - // end "/{username}/{reponame}": find, compare, list (code related) + // end "/{username}/{reponame}": repo code: find, compare, list m.Group("/{username}/{reponame}", func() { m.Get("/issues/posters", repo.IssuePosters) // it can't use {type:issues|pulls} because it would conflict with other routes like "/pulls/{index}" @@ -1413,6 +1412,7 @@ func registerRoutes(m *web.Router) { m.Post("/disable", reqRepoAdmin, actions.DisableWorkflowFile) m.Post("/enable", reqRepoAdmin, actions.EnableWorkflowFile) m.Post("/run", reqRepoActionsWriter, actions.Run) + m.Get("/workflow-dispatch-inputs", reqRepoActionsWriter, actions.WorkflowDispatchInputs) m.Group("/runs/{run}", func() { m.Combo(""). @@ -1434,7 +1434,7 @@ func registerRoutes(m *web.Router) { m.Group("/workflows/{workflow_name}", func() { m.Get("/badge.svg", actions.GetWorkflowBadge) }) - }, optSignIn, context.RepoAssignment, reqRepoActionsReader, actions.MustEnableActions) + }, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoActionsReader, actions.MustEnableActions) // end "/{username}/{reponame}/actions" m.Group("/{username}/{reponame}/wiki", func() { @@ -1444,12 +1444,13 @@ func registerRoutes(m *web.Router) { m.Combo("/*"). Get(repo.Wiki). Post(context.RepoMustNotBeArchived(), reqSignIn, reqRepoWikiWriter, web.Bind(forms.NewWikiForm{}), repo.WikiPost) + m.Get("/blob_excerpt/{sha}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.ExcerptBlob) m.Get("/commit/{sha:[a-f0-9]{7,64}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff) m.Get("/commit/{sha:[a-f0-9]{7,64}}.{ext:patch|diff}", repo.RawDiff) m.Get("/raw/*", repo.WikiRaw) }, optSignIn, context.RepoAssignment, repo.MustEnableWiki, reqRepoWikiReader, func(ctx *context.Context) { ctx.Data["PageIsWiki"] = true - ctx.Data["CloneButtonOriginLink"] = ctx.Repo.Repository.WikiCloneLink() + ctx.Data["CloneButtonOriginLink"] = ctx.Repo.Repository.WikiCloneLink(ctx, ctx.Doer) }) // end "/{username}/{reponame}/wiki" @@ -1519,28 +1520,6 @@ func registerRoutes(m *web.Router) { m.Get("", repo.Branches) }, repo.MustBeNotEmpty, context.RepoRef()) - m.Group("/blob_excerpt", func() { - m.Get("/{sha}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.ExcerptBlob) - }, func(ctx *context.Context) gocontext.CancelFunc { - // FIXME: refactor this function, use separate routes for wiki/code - if ctx.FormBool("wiki") { - ctx.Data["PageIsWiki"] = true - repo.MustEnableWiki(ctx) - return nil - } - - if ctx.Written() { - return nil - } - cancel := context.RepoRef()(ctx) - if ctx.Written() { - return cancel - } - - repo.MustBeNotEmpty(ctx) - return cancel - }) - m.Group("/media", func() { m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.SingleDownloadOrLFS) m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.SingleDownloadOrLFS) @@ -1580,6 +1559,8 @@ func registerRoutes(m *web.Router) { m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.RefBlame) }, repo.MustBeNotEmpty) + m.Get("/blob_excerpt/{sha}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.ExcerptBlob) + m.Group("", func() { m.Get("/graph", repo.Graph) m.Get("/commit/{sha:([a-f0-9]{7,64})$}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff) diff --git a/services/actions/commit_status.go b/services/actions/commit_status.go index 7f52c9d31b5..94ab89a3b7d 100644 --- a/services/actions/commit_status.go +++ b/services/actions/commit_status.go @@ -54,7 +54,13 @@ func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er return fmt.Errorf("head commit is missing in event payload") } sha = payload.HeadCommit.ID - case webhook_module.HookEventPullRequest, webhook_module.HookEventPullRequestSync: + case // pull_request + webhook_module.HookEventPullRequest, + webhook_module.HookEventPullRequestSync, + webhook_module.HookEventPullRequestAssign, + webhook_module.HookEventPullRequestLabel, + webhook_module.HookEventPullRequestReviewRequest, + webhook_module.HookEventPullRequestMilestone: if run.TriggerEvent == actions_module.GithubEventPullRequestTarget { event = "pull_request_target" } else { diff --git a/services/actions/notifier.go b/services/actions/notifier.go index a4ebdf9e888..67e33e7cce0 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/auth/interface.go b/services/auth/interface.go index ece28af12d1..275b4dd56ce 100644 --- a/services/auth/interface.go +++ b/services/auth/interface.go @@ -8,12 +8,11 @@ import ( "net/http" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/session" - "code.gitea.io/gitea/modules/web/middleware" ) -// DataStore represents a data store -type DataStore middleware.ContextDataStore +type DataStore = reqctx.ContextDataProvider // SessionStore represents a session store type SessionStore session.Store diff --git a/services/auth/oauth2_test.go b/services/auth/oauth2_test.go index b706847e8e1..0d9e793cf3a 100644 --- a/services/auth/oauth2_test.go +++ b/services/auth/oauth2_test.go @@ -9,7 +9,7 @@ import ( "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/web/middleware" + "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/services/actions" "github.com/stretchr/testify/assert" @@ -23,7 +23,7 @@ func TestUserIDFromToken(t *testing.T) { token, err := actions.CreateAuthorizationToken(RunningTaskID, 1, 2) assert.NoError(t, err) - ds := make(middleware.ContextData) + ds := make(reqctx.ContextData) o := OAuth2{} uid := o.userIDFromToken(context.Background(), token, ds) diff --git a/services/context/api.go b/services/context/api.go index b45e80a3297..bda705cb483 100644 --- a/services/context/api.go +++ b/services/context/api.go @@ -5,7 +5,6 @@ package context import ( - "context" "fmt" "net/http" "net/url" @@ -212,17 +211,15 @@ func (ctx *APIContext) SetLinkHeader(total, pageSize int) { func APIContexter() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - base, baseCleanUp := NewBaseContext(w, req) + base := NewBaseContext(w, req) ctx := &APIContext{ Base: base, Cache: cache.GetCache(), Repo: &Repository{PullRequest: &PullRequest{}}, Org: &APIOrganization{}, } - defer baseCleanUp() - ctx.Base.AppendContextValue(apiContextKey, ctx) - ctx.Base.AppendContextValueFunc(gitrepo.RepositoryContextKey, func() any { return ctx.Repo.GitRepo }) + ctx.SetContextValue(apiContextKey, ctx) // If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid. if ctx.Req.Method == "POST" && strings.Contains(ctx.Req.Header.Get("Content-Type"), "multipart/form-data") { @@ -267,31 +264,22 @@ func (ctx *APIContext) NotFound(objs ...any) { // ReferencesGitRepo injects the GitRepo into the Context // you can optional skip the IsEmpty check -func ReferencesGitRepo(allowEmpty ...bool) func(ctx *APIContext) (cancel context.CancelFunc) { - return func(ctx *APIContext) (cancel context.CancelFunc) { +func ReferencesGitRepo(allowEmpty ...bool) func(ctx *APIContext) { + return func(ctx *APIContext) { // Empty repository does not have reference information. if ctx.Repo.Repository.IsEmpty && !(len(allowEmpty) != 0 && allowEmpty[0]) { - return nil + return } // For API calls. if ctx.Repo.GitRepo == nil { - gitRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository) + var err error + 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 cancel - } - ctx.Repo.GitRepo = gitRepo - // We opened it, we should close it - return func() { - // If it's been set to nil then assume someone else has closed it. - if ctx.Repo.GitRepo != nil { - _ = ctx.Repo.GitRepo.Close() - } + return } } - - return cancel } } diff --git a/services/context/base.go b/services/context/base.go index d6270955843..7a39353e09e 100644 --- a/services/context/base.go +++ b/services/context/base.go @@ -9,81 +9,36 @@ import ( "html/template" "io" "net/http" - "net/url" - "strconv" "strings" - "time" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/web/middleware" - - "github.com/go-chi/chi/v5" ) -type contextValuePair struct { - key any - valueFn func() any -} - type BaseContextKeyType struct{} var BaseContextKey BaseContextKeyType type Base struct { - originCtx context.Context - contextValues []contextValuePair + context.Context + reqctx.RequestDataStore Resp ResponseWriter Req *http.Request // Data is prepared by ContextDataStore middleware, this field only refers to the pre-created/prepared ContextData. // Although it's mainly used for MVC templates, sometimes it's also used to pass data between middlewares/handler - Data middleware.ContextData + Data reqctx.ContextData // Locale is mainly for Web context, although the API context also uses it in some cases: message response, form validation Locale translation.Locale } -func (b *Base) Deadline() (deadline time.Time, ok bool) { - return b.originCtx.Deadline() -} - -func (b *Base) Done() <-chan struct{} { - return b.originCtx.Done() -} - -func (b *Base) Err() error { - return b.originCtx.Err() -} - -func (b *Base) Value(key any) any { - for _, pair := range b.contextValues { - if pair.key == key { - return pair.valueFn() - } - } - return b.originCtx.Value(key) -} - -func (b *Base) AppendContextValueFunc(key any, valueFn func() any) any { - b.contextValues = append(b.contextValues, contextValuePair{key, valueFn}) - return b -} - -func (b *Base) AppendContextValue(key, value any) any { - b.contextValues = append(b.contextValues, contextValuePair{key, func() any { return value }}) - return b -} - -func (b *Base) GetData() middleware.ContextData { - return b.Data -} - // AppendAccessControlExposeHeaders append headers by name to "Access-Control-Expose-Headers" header func (b *Base) AppendAccessControlExposeHeaders(names ...string) { val := b.RespHeader().Get("Access-Control-Expose-Headers") @@ -147,93 +102,6 @@ func (b *Base) RemoteAddr() string { return b.Req.RemoteAddr } -// PathParam returns the param in request path, eg: "/{var}" => "/a%2fb", then `var == "a/b"` -func (b *Base) PathParam(name string) string { - s, err := url.PathUnescape(b.PathParamRaw(name)) - if err != nil && !setting.IsProd { - panic("Failed to unescape path param: " + err.Error() + ", there seems to be a double-unescaping bug") - } - return s -} - -// PathParamRaw returns the raw param in request path, eg: "/{var}" => "/a%2fb", then `var == "a%2fb"` -func (b *Base) PathParamRaw(name string) string { - return chi.URLParam(b.Req, strings.TrimPrefix(name, ":")) -} - -// PathParamInt64 returns the param in request path as int64 -func (b *Base) PathParamInt64(p string) int64 { - v, _ := strconv.ParseInt(b.PathParam(p), 10, 64) - return v -} - -// SetPathParam set request path params into routes -func (b *Base) SetPathParam(k, v string) { - chiCtx := chi.RouteContext(b) - chiCtx.URLParams.Add(strings.TrimPrefix(k, ":"), url.PathEscape(v)) -} - -// 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) -} - -// FormStrings returns a string slice for the provided key from the form -func (b *Base) FormStrings(key string) []string { - if b.Req.Form == nil { - if err := b.Req.ParseMultipartForm(32 << 20); err != nil { - return nil - } - } - if v, ok := b.Req.Form[key]; ok { - return v - } - return nil -} - -// FormTrim returns the first value for the provided key in the form as a space trimmed string -func (b *Base) FormTrim(key string) string { - return strings.TrimSpace(b.Req.FormValue(key)) -} - -// FormInt returns the first value for the provided key in the form as an int -func (b *Base) FormInt(key string) int { - v, _ := strconv.Atoi(b.Req.FormValue(key)) - return v -} - -// FormInt64 returns the first value for the provided key in the form as an int64 -func (b *Base) FormInt64(key string) int64 { - v, _ := strconv.ParseInt(b.Req.FormValue(key), 10, 64) - return v -} - -// FormBool returns true if the value for the provided key in the form is "1", "true" or "on" -func (b *Base) FormBool(key string) bool { - s := b.Req.FormValue(key) - v, _ := strconv.ParseBool(s) - v = v || strings.EqualFold(s, "on") - return v -} - -// FormOptionalBool returns an optional.Some(true) or optional.Some(false) if the value -// for the provided key exists in the form else it returns optional.None[bool]() -func (b *Base) FormOptionalBool(key string) optional.Option[bool] { - value := b.Req.FormValue(key) - if len(value) == 0 { - return optional.None[bool]() - } - s := b.Req.FormValue(key) - v, _ := strconv.ParseBool(s) - v = v || strings.EqualFold(s, "on") - return optional.Some(v) -} - -func (b *Base) SetFormString(key, value string) { - _ = b.Req.FormValue(key) // force parse form - b.Req.Form.Set(key, value) -} - // PlainTextBytes renders bytes as plain text func (b *Base) plainTextInternal(skip, status int, bs []byte) { statusPrefix := status / 100 @@ -295,13 +163,6 @@ func (b *Base) ServeContent(r io.ReadSeeker, opts *ServeHeaderOptions) { http.ServeContent(b.Resp, b.Req, opts.Filename, opts.LastModified, r) } -// Close frees all resources hold by Context -func (b *Base) cleanUp() { - if b.Req != nil && b.Req.MultipartForm != nil { - _ = b.Req.MultipartForm.RemoveAll() // remove the temp files buffered to tmp directory - } -} - func (b *Base) Tr(msg string, args ...any) template.HTML { return b.Locale.Tr(msg, args...) } @@ -310,17 +171,28 @@ func (b *Base) TrN(cnt any, key1, keyN string, args ...any) template.HTML { return b.Locale.TrN(cnt, key1, keyN, args...) } -func NewBaseContext(resp http.ResponseWriter, req *http.Request) (b *Base, closeFunc func()) { - b = &Base{ - originCtx: req.Context(), - Req: req, - Resp: WrapResponseWriter(resp), - Locale: middleware.Locale(resp, req), - Data: middleware.GetContextData(req.Context()), +func NewBaseContext(resp http.ResponseWriter, req *http.Request) *Base { + ds := reqctx.GetRequestDataStore(req.Context()) + b := &Base{ + Context: req.Context(), + RequestDataStore: ds, + Req: req, + Resp: WrapResponseWriter(resp), + Locale: middleware.Locale(resp, req), + Data: ds.GetData(), } b.Req = b.Req.WithContext(b) - b.AppendContextValue(BaseContextKey, b) - b.AppendContextValue(translation.ContextKey, b.Locale) - b.AppendContextValue(httplib.RequestContextKey, b.Req) - return b, b.cleanUp + ds.SetContextValue(BaseContextKey, b) + ds.SetContextValue(translation.ContextKey, b.Locale) + ds.SetContextValue(httplib.RequestContextKey, b.Req) + return b +} + +func NewBaseContextForTest(resp http.ResponseWriter, req *http.Request) *Base { + if !setting.IsInTesting { + panic("This function is only for testing") + } + ctx := reqctx.NewRequestContextForTest(req.Context()) + *req = *req.WithContext(ctx) + return NewBaseContext(resp, req) } diff --git a/services/context/base_form.go b/services/context/base_form.go new file mode 100644 index 00000000000..5b8cae9e998 --- /dev/null +++ b/services/context/base_form.go @@ -0,0 +1,77 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package context + +import ( + "strconv" + "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, 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 +func (b *Base) FormStrings(key string) []string { + if b.Req.Form == nil { + if err := b.Req.ParseMultipartForm(32 << 20); err != nil { + return nil + } + } + if v, ok := b.Req.Form[key]; ok { + return v + } + return nil +} + +// FormTrim returns the first value for the provided key in the form as a space trimmed string +func (b *Base) FormTrim(key string) string { + return strings.TrimSpace(b.Req.FormValue(key)) +} + +// FormInt returns the first value for the provided key in the form as an int +func (b *Base) FormInt(key string) int { + v, _ := strconv.Atoi(b.Req.FormValue(key)) + return v +} + +// FormInt64 returns the first value for the provided key in the form as an int64 +func (b *Base) FormInt64(key string) int64 { + v, _ := strconv.ParseInt(b.Req.FormValue(key), 10, 64) + return v +} + +// FormBool returns true if the value for the provided key in the form is "1", "true" or "on" +func (b *Base) FormBool(key string) bool { + s := b.Req.FormValue(key) + v, _ := strconv.ParseBool(s) + v = v || strings.EqualFold(s, "on") + return v +} + +// FormOptionalBool returns an optional.Some(true) or optional.Some(false) if the value +// for the provided key exists in the form else it returns optional.None[bool]() +func (b *Base) FormOptionalBool(key string) optional.Option[bool] { + value := b.Req.FormValue(key) + if len(value) == 0 { + return optional.None[bool]() + } + s := b.Req.FormValue(key) + v, _ := strconv.ParseBool(s) + v = v || strings.EqualFold(s, "on") + return optional.Some(v) +} + +func (b *Base) SetFormString(key, value string) { + _ = b.Req.FormValue(key) // force parse form + b.Req.Form.Set(key, value) +} diff --git a/services/context/base_path.go b/services/context/base_path.go new file mode 100644 index 00000000000..3678deaff98 --- /dev/null +++ b/services/context/base_path.go @@ -0,0 +1,47 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package context + +import ( + "net/url" + "strconv" + "strings" + + "code.gitea.io/gitea/modules/setting" + + "github.com/go-chi/chi/v5" +) + +// PathParam returns the param in request path, eg: "/{var}" => "/a%2fb", then `var == "a/b"` +func (b *Base) PathParam(name string) string { + s, err := url.PathUnescape(b.PathParamRaw(name)) + if err != nil && !setting.IsProd { + panic("Failed to unescape path param: " + err.Error() + ", there seems to be a double-unescaping bug") + } + return s +} + +// PathParamRaw returns the raw param in request path, eg: "/{var}" => "/a%2fb", then `var == "a%2fb"` +func (b *Base) PathParamRaw(name string) string { + if strings.HasPrefix(name, ":") { + setting.PanicInDevOrTesting("path param should not start with ':'") + name = name[1:] + } + return chi.URLParam(b.Req, name) +} + +// PathParamInt64 returns the param in request path as int64 +func (b *Base) PathParamInt64(p string) int64 { + v, _ := strconv.ParseInt(b.PathParam(p), 10, 64) + return v +} + +// SetPathParam set request path params into routes +func (b *Base) SetPathParam(name, value string) { + if strings.HasPrefix(name, ":") { + setting.PanicInDevOrTesting("path param should not start with ':'") + name = name[1:] + } + chi.RouteContext(b).URLParams.Add(name, url.PathEscape(value)) +} diff --git a/services/context/base_test.go b/services/context/base_test.go index 823f20e00bc..b936b76f58b 100644 --- a/services/context/base_test.go +++ b/services/context/base_test.go @@ -14,6 +14,7 @@ import ( ) func TestRedirect(t *testing.T) { + setting.IsInTesting = true req, _ := http.NewRequest("GET", "/", nil) cases := []struct { @@ -28,10 +29,9 @@ func TestRedirect(t *testing.T) { } for _, c := range cases { resp := httptest.NewRecorder() - b, cleanup := NewBaseContext(resp, req) + b := NewBaseContextForTest(resp, req) resp.Header().Add("Set-Cookie", (&http.Cookie{Name: setting.SessionConfig.CookieName, Value: "dummy"}).String()) b.Redirect(c.url) - cleanup() has := resp.Header().Get("Set-Cookie") == "i_like_gitea=dummy" assert.Equal(t, c.keep, has, "url = %q", c.url) } @@ -39,9 +39,8 @@ func TestRedirect(t *testing.T) { req, _ = http.NewRequest("GET", "/", nil) resp := httptest.NewRecorder() req.Header.Add("HX-Request", "true") - b, cleanup := NewBaseContext(resp, req) + b := NewBaseContextForTest(resp, req) b.Redirect("/other") - cleanup() assert.Equal(t, "/other", resp.Header().Get("HX-Redirect")) assert.Equal(t, http.StatusNoContent, resp.Code) } diff --git a/services/context/context.go b/services/context/context.go index 0d5429e366c..6715c5663dc 100644 --- a/services/context/context.go +++ b/services/context/context.go @@ -18,7 +18,6 @@ import ( "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/cache" - "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/httpcache" "code.gitea.io/gitea/modules/session" "code.gitea.io/gitea/modules/setting" @@ -153,14 +152,9 @@ func Contexter() func(next http.Handler) http.Handler { } return func(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - base, baseCleanUp := NewBaseContext(resp, req) - defer baseCleanUp() + base := NewBaseContext(resp, req) ctx := NewWebContext(base, rnd, session.GetContextSession(req)) - ctx.Data.MergeFrom(middleware.CommonTemplateContextData()) - if setting.IsProd && !setting.IsInTesting { - ctx.Data["Context"] = ctx // TODO: use "ctx" in template and remove this - } ctx.Data["CurrentURL"] = setting.AppSubURL + req.URL.RequestURI() ctx.Data["Link"] = ctx.Link @@ -168,9 +162,7 @@ func Contexter() func(next http.Handler) http.Handler { ctx.PageData = map[string]any{} ctx.Data["PageData"] = ctx.PageData - ctx.Base.AppendContextValue(WebContextKey, ctx) - ctx.Base.AppendContextValueFunc(gitrepo.RepositoryContextKey, func() any { return ctx.Repo.GitRepo }) - + ctx.Base.SetContextValue(WebContextKey, ctx) ctx.Csrf = NewCSRFProtector(csrfOpts) // Get the last flash message from cookie diff --git a/services/context/context_response.go b/services/context/context_response.go index 4c086ea9f57..c7044791eb6 100644 --- a/services/context/context_response.go +++ b/services/context/context_response.go @@ -106,7 +106,7 @@ func (ctx *Context) JSONTemplate(tmpl templates.TplName) { } // RenderToHTML renders the template content to a HTML string -func (ctx *Context) RenderToHTML(name templates.TplName, data map[string]any) (template.HTML, error) { +func (ctx *Context) RenderToHTML(name templates.TplName, data any) (template.HTML, error) { var buf strings.Builder err := ctx.Render.HTML(&buf, 0, name, data, ctx.TemplateContext) return template.HTML(buf.String()), err diff --git a/services/context/context_test.go b/services/context/context_test.go index 984593398d4..54044644f07 100644 --- a/services/context/context_test.go +++ b/services/context/context_test.go @@ -26,6 +26,7 @@ func TestRemoveSessionCookieHeader(t *testing.T) { } func TestRedirectToCurrentSite(t *testing.T) { + setting.IsInTesting = true defer test.MockVariableValue(&setting.AppURL, "http://localhost:3000/sub/")() defer test.MockVariableValue(&setting.AppSubURL, "/sub")() cases := []struct { @@ -40,8 +41,7 @@ func TestRedirectToCurrentSite(t *testing.T) { t.Run(c.location, func(t *testing.T) { req := &http.Request{URL: &url.URL{Path: "/"}} resp := httptest.NewRecorder() - base, baseCleanUp := NewBaseContext(resp, req) - defer baseCleanUp() + base := NewBaseContextForTest(resp, req) ctx := NewWebContext(base, nil, nil) ctx.RedirectToCurrentSite(c.location) redirect := test.RedirectURL(resp) diff --git a/services/context/org.go b/services/context/org.go index bf482fa7543..be87cef7a31 100644 --- a/services/context/org.go +++ b/services/context/org.go @@ -40,7 +40,7 @@ func (org *Organization) CanReadUnit(ctx *Context, unitType unit.Type) bool { } func GetOrganizationByParams(ctx *Context) { - orgName := ctx.PathParam(":org") + orgName := ctx.PathParam("org") var err error @@ -220,7 +220,7 @@ func HandleOrgAssignment(ctx *Context, args ...bool) { ctx.Data["NumTeams"] = len(ctx.Org.Teams) } - teamName := ctx.PathParam(":team") + teamName := ctx.PathParam("team") if len(teamName) > 0 { teamExists := false for _, team := range ctx.Org.Teams { diff --git a/services/context/package.go b/services/context/package.go index 271b61e99c3..e98e01acbb0 100644 --- a/services/context/package.go +++ b/services/context/package.go @@ -153,12 +153,10 @@ func PackageContexter() func(next http.Handler) http.Handler { renderer := templates.HTMLRenderer() return func(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - base, baseCleanUp := NewBaseContext(resp, req) - defer baseCleanUp() - + base := NewBaseContext(resp, req) // it is still needed when rendering 500 page in a package handler ctx := NewWebContext(base, renderer, nil) - ctx.Base.AppendContextValue(WebContextKey, ctx) + ctx.SetContextValue(WebContextKey, ctx) next.ServeHTTP(ctx.Resp, ctx.Req) }) } diff --git a/services/context/pagination.go b/services/context/pagination.go index 42117cf96de..d33dd217d0e 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/private.go b/services/context/private.go index 8b41949f604..51857da8fe5 100644 --- a/services/context/private.go +++ b/services/context/private.go @@ -64,11 +64,9 @@ func GetPrivateContext(req *http.Request) *PrivateContext { func PrivateContexter() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - base, baseCleanUp := NewBaseContext(w, req) + base := NewBaseContext(w, req) ctx := &PrivateContext{Base: base} - defer baseCleanUp() - ctx.Base.AppendContextValue(privateContextKey, ctx) - + ctx.SetContextValue(privateContextKey, ctx) next.ServeHTTP(ctx.Resp, ctx.Req) }) } @@ -78,8 +76,15 @@ func PrivateContexter() func(http.Handler) http.Handler { // This function should be used when there is a need for work to continue even if the request has been cancelled. // Primarily this affects hook/post-receive and hook/proc-receive both of which need to continue working even if // the underlying request has timed out from the ssh/http push -func OverrideContext(ctx *PrivateContext) (cancel context.CancelFunc) { - // We now need to override the request context as the base for our work because even if the request is cancelled we have to continue this work - ctx.Override, _, cancel = process.GetManager().AddTypedContext(graceful.GetManager().HammerContext(), fmt.Sprintf("PrivateContext: %s", ctx.Req.RequestURI), process.RequestProcessType, true) - return cancel +func OverrideContext() func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + // We now need to override the request context as the base for our work because even if the request is cancelled we have to continue this work + ctx := GetPrivateContext(req) + var finished func() + ctx.Override, _, finished = process.GetManager().AddTypedContext(graceful.GetManager().HammerContext(), fmt.Sprintf("PrivateContext: %s", ctx.Req.RequestURI), process.RequestProcessType, true) + defer finished() + next.ServeHTTP(ctx.Resp, ctx.Req) + }) + } } diff --git a/services/context/repo.go b/services/context/repo.go index e96916ca421..4de905ef2cb 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -316,8 +316,8 @@ func ComposeGoGetImport(ctx context.Context, owner, repo string) string { // This is particular a workaround for "go get" command which does not respect // .netrc file. func EarlyResponseForGoGetMeta(ctx *Context) { - username := ctx.PathParam(":username") - reponame := strings.TrimSuffix(ctx.PathParam(":reponame"), ".git") + username := ctx.PathParam("username") + reponame := strings.TrimSuffix(ctx.PathParam("reponame"), ".git") if username == "" || reponame == "" { ctx.PlainText(http.StatusBadRequest, "invalid repository path") return @@ -325,9 +325,9 @@ func EarlyResponseForGoGetMeta(ctx *Context) { var cloneURL string if setting.Repository.GoGetCloneURLProtocol == "ssh" { - cloneURL = repo_model.ComposeSSHCloneURL(username, reponame) + cloneURL = repo_model.ComposeSSHCloneURL(ctx.Doer, username, reponame) } else { - cloneURL = repo_model.ComposeHTTPSCloneURL(username, reponame) + cloneURL = repo_model.ComposeHTTPSCloneURL(ctx, username, reponame) } goImportContent := fmt.Sprintf("%s git %s", ComposeGoGetImport(ctx, username, reponame), cloneURL) htmlMeta := fmt.Sprintf(``, html.EscapeString(goImportContent)) @@ -336,8 +336,8 @@ func EarlyResponseForGoGetMeta(ctx *Context) { // RedirectToRepo redirect to a differently-named repository func RedirectToRepo(ctx *Base, redirectRepoID int64) { - ownerName := ctx.PathParam(":username") - previousRepoName := ctx.PathParam(":reponame") + ownerName := ctx.PathParam("username") + previousRepoName := ctx.PathParam("reponame") repo, err := repo_model.GetRepositoryByID(ctx, redirectRepoID) if err != nil { @@ -397,11 +397,13 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) { } // RepoAssignment returns a middleware to handle repository assignment -func RepoAssignment(ctx *Context) context.CancelFunc { +func RepoAssignment(ctx *Context) { if _, repoAssignmentOnce := ctx.Data["repoAssignmentExecuted"]; repoAssignmentOnce { // FIXME: it should panic in dev/test modes to have a clear behavior - log.Trace("RepoAssignment was exec already, skipping second call ...") - return nil + if !setting.IsProd || setting.IsInTesting { + panic("RepoAssignment should not be executed twice") + } + return } ctx.Data["repoAssignmentExecuted"] = true @@ -410,8 +412,8 @@ func RepoAssignment(ctx *Context) context.CancelFunc { err error ) - userName := ctx.PathParam(":username") - repoName := ctx.PathParam(":reponame") + userName := ctx.PathParam("username") + repoName := ctx.PathParam("reponame") repoName = strings.TrimSuffix(repoName, ".git") if setting.Other.EnableFeed { repoName = strings.TrimSuffix(repoName, ".rss") @@ -429,7 +431,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc { // https://github.com/golang/go/issues/19760 if ctx.FormString("go-get") == "1" { EarlyResponseForGoGetMeta(ctx) - return nil + return } if redirectUserID, err := user_model.LookupUserRedirect(ctx, userName); err == nil { @@ -442,7 +444,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc { } else { ctx.ServerError("GetUserByName", err) } - return nil + return } } ctx.Repo.Owner = owner @@ -454,7 +456,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc { if strings.HasSuffix(repoName, ".wiki") { // ctx.Req.URL.Path does not have the preceding appSubURL - any redirect must have this added // Now we happen to know that all of our paths are: /:username/:reponame/whatever_else - originalRepoName := ctx.PathParam(":reponame") + originalRepoName := ctx.PathParam("reponame") redirectRepoName := strings.TrimSuffix(repoName, ".wiki") redirectRepoName += originalRepoName[len(redirectRepoName)+5:] redirectPath := strings.Replace( @@ -467,7 +469,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc { redirectPath += "?" + ctx.Req.URL.RawQuery } ctx.Redirect(path.Join(setting.AppSubURL, redirectPath)) - return nil + return } // Get repository. @@ -480,7 +482,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc { } else if repo_model.IsErrRedirectNotExist(err) { if ctx.FormString("go-get") == "1" { EarlyResponseForGoGetMeta(ctx) - return nil + return } ctx.NotFound("GetRepositoryByName", nil) } else { @@ -489,13 +491,13 @@ func RepoAssignment(ctx *Context) context.CancelFunc { } else { ctx.ServerError("GetRepositoryByName", err) } - return nil + return } repo.Owner = owner repoAssignment(ctx, repo) if ctx.Written() { - return nil + return } ctx.Repo.RepoLink = repo.Link() @@ -520,7 +522,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc { }) if err != nil { ctx.ServerError("GetReleaseCountByRepoID", err) - return nil + return } ctx.Data["NumReleases"], err = db.Count[repo_model.Release](ctx, repo_model.FindReleasesOptions{ // only show draft releases for users who can write, read-only users shouldn't see draft releases. @@ -529,7 +531,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc { }) if err != nil { ctx.ServerError("GetReleaseCountByRepoID", err) - return nil + return } ctx.Data["Title"] = owner.Name + "/" + repo.Name @@ -546,14 +548,14 @@ func RepoAssignment(ctx *Context) context.CancelFunc { canSignedUserFork, err := repo_module.CanUserForkRepo(ctx, ctx.Doer, ctx.Repo.Repository) if err != nil { ctx.ServerError("CanUserForkRepo", err) - return nil + return } ctx.Data["CanSignedUserFork"] = canSignedUserFork userAndOrgForks, err := repo_model.GetForksByUserAndOrgs(ctx, ctx.Doer, ctx.Repo.Repository) if err != nil { ctx.ServerError("GetForksByUserAndOrgs", err) - return nil + return } ctx.Data["UserAndOrgForks"] = userAndOrgForks @@ -562,7 +564,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc { // If multiple forks are available or if the user can fork to another account, but there is already a fork: open selection dialog ctx.Data["ShowForkModal"] = len(userAndOrgForks) > 1 || (canSignedUserFork && len(userAndOrgForks) > 0) - ctx.Data["RepoCloneLink"] = repo.CloneLink() + ctx.Data["RepoCloneLink"] = repo.CloneLink(ctx, ctx.Doer) cloneButtonShowHTTPS := !setting.Repository.DisableHTTPGit cloneButtonShowSSH := !setting.SSH.Disabled && (ctx.IsSigned || setting.SSH.ExposeAnonymous) @@ -587,14 +589,14 @@ func RepoAssignment(ctx *Context) context.CancelFunc { if repo.IsFork { RetrieveBaseRepo(ctx, repo) if ctx.Written() { - return nil + return } } if repo.IsGenerated() { RetrieveTemplateRepo(ctx, repo) if ctx.Written() { - return nil + return } } @@ -609,10 +611,18 @@ func RepoAssignment(ctx *Context) context.CancelFunc { if !isHomeOrSettings { ctx.Redirect(ctx.Repo.RepoLink) } - return nil + return } - gitRepo, err := gitrepo.OpenRepository(ctx, repo) + if ctx.Repo.GitRepo != nil { + if !setting.IsProd || setting.IsInTesting { + panic("RepoAssignment: GitRepo should be nil") + } + _ = ctx.Repo.GitRepo.Close() + ctx.Repo.GitRepo = nil + } + + 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) @@ -622,28 +632,16 @@ func RepoAssignment(ctx *Context) context.CancelFunc { if !isHomeOrSettings { ctx.Redirect(ctx.Repo.RepoLink) } - return nil + return } ctx.ServerError("RepoAssignment Invalid repo "+repo.FullName(), err) - return nil - } - if ctx.Repo.GitRepo != nil { - ctx.Repo.GitRepo.Close() - } - ctx.Repo.GitRepo = gitRepo - - // We opened it, we should close it - cancel := func() { - // If it's been set to nil then assume someone else has closed it. - if ctx.Repo.GitRepo != nil { - ctx.Repo.GitRepo.Close() - } + return } // Stop at this point when the repo is empty. if ctx.Repo.Repository.IsEmpty { ctx.Data["BranchName"] = ctx.Repo.Repository.DefaultBranch - return cancel + return } branchOpts := git_model.FindBranchOptions{ @@ -654,7 +652,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc { branchesTotal, err := db.Count[git_model.Branch](ctx, branchOpts) if err != nil { ctx.ServerError("CountBranches", err) - return cancel + return } // non-empty repo should have at least 1 branch, so this repository's branches haven't been synced yet @@ -662,7 +660,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc { branchesTotal, err = repo_module.SyncRepoBranches(ctx, ctx.Repo.Repository.ID, 0) if err != nil { ctx.ServerError("SyncRepoBranches", err) - return cancel + return } } @@ -670,7 +668,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc { // If no branch is set in the request URL, try to guess a default one. if len(ctx.Repo.BranchName) == 0 { - if len(ctx.Repo.Repository.DefaultBranch) > 0 && gitRepo.IsBranchExist(ctx.Repo.Repository.DefaultBranch) { + if len(ctx.Repo.Repository.DefaultBranch) > 0 && ctx.Repo.GitRepo.IsBranchExist(ctx.Repo.Repository.DefaultBranch) { ctx.Repo.BranchName = ctx.Repo.Repository.DefaultBranch } else { ctx.Repo.BranchName, _ = gitrepo.GetDefaultBranch(ctx, ctx.Repo.Repository) @@ -711,12 +709,12 @@ func RepoAssignment(ctx *Context) context.CancelFunc { repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository) if err != nil { ctx.ServerError("GetPendingRepositoryTransfer", err) - return cancel + return } if err := repoTransfer.LoadAttributes(ctx); err != nil { ctx.ServerError("LoadRecipient", err) - return cancel + return } ctx.Data["RepoTransfer"] = repoTransfer @@ -731,7 +729,6 @@ func RepoAssignment(ctx *Context) context.CancelFunc { ctx.Data["GoDocDirectory"] = fullURLPrefix + "{/dir}" ctx.Data["GoDocFile"] = fullURLPrefix + "{/dir}/{file}#L{line}" } - return cancel } // RepoRefType type of repo reference @@ -750,7 +747,7 @@ const headRefName = "HEAD" // RepoRef handles repository reference names when the ref name is not // explicitly given -func RepoRef() func(*Context) context.CancelFunc { +func RepoRef() func(*Context) { // since no ref name is explicitly specified, ok to just use branch return RepoRefByType(RepoRefBranch) } @@ -865,9 +862,9 @@ type RepoRefByTypeOptions struct { // RepoRefByType handles repository reference name for a specific type // of repository reference -func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func(*Context) context.CancelFunc { +func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func(*Context) { opt := util.OptionalArg(opts) - return func(ctx *Context) (cancel context.CancelFunc) { + return func(ctx *Context) { refType := detectRefType // Empty repository does not have reference information. if ctx.Repo.Repository.IsEmpty { @@ -875,7 +872,7 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func ctx.Repo.IsViewBranch = true ctx.Repo.BranchName = ctx.Repo.Repository.DefaultBranch ctx.Data["TreePath"] = "" - return nil + return } var ( @@ -884,17 +881,10 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func ) if ctx.Repo.GitRepo == nil { - ctx.Repo.GitRepo, err = gitrepo.OpenRepository(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 nil - } - // We opened it, we should close it - cancel = func() { - // If it's been set to nil then assume someone else has closed it. - if ctx.Repo.GitRepo != nil { - ctx.Repo.GitRepo.Close() - } + return } } @@ -907,10 +897,8 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func refName = brs[0].Name } else if len(brs) == 0 { log.Error("No branches in non-empty repository %s", ctx.Repo.GitRepo.Path) - ctx.Repo.Repository.MarkAsBrokenEmpty() } else { log.Error("GetBranches error: %v", err) - ctx.Repo.Repository.MarkAsBrokenEmpty() } } ctx.Repo.RefName = refName @@ -921,10 +909,9 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func } else if strings.Contains(err.Error(), "fatal: not a git repository") || strings.Contains(err.Error(), "object does not exist") { // if the repository is broken, we can continue to the handler code, to show "Settings -> Delete Repository" for end users log.Error("GetBranchCommit: %v", err) - ctx.Repo.Repository.MarkAsBrokenEmpty() } else { ctx.ServerError("GetBranchCommit", err) - return cancel + return } ctx.Repo.IsViewBranch = true } else { @@ -941,7 +928,7 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func ctx.Flash.Info(ctx.Tr("repo.branch.renamed", refName, renamedBranchName)) link := setting.AppSubURL + strings.Replace(ctx.Req.URL.EscapedPath(), util.PathEscapeSegments(refName), util.PathEscapeSegments(renamedBranchName), 1) ctx.Redirect(link) - return cancel + return } if refType == RepoRefBranch && ctx.Repo.GitRepo.IsBranchExist(refName) { @@ -951,7 +938,7 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(refName) if err != nil { ctx.ServerError("GetBranchCommit", err) - return cancel + return } ctx.Repo.CommitID = ctx.Repo.Commit.ID.String() } else if refType == RepoRefTag && ctx.Repo.GitRepo.IsTagExist(refName) { @@ -962,10 +949,10 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func if err != nil { if git.IsErrNotExist(err) { ctx.NotFound("GetTagCommit", err) - return cancel + return } ctx.ServerError("GetTagCommit", err) - return cancel + return } ctx.Repo.CommitID = ctx.Repo.Commit.ID.String() } else if git.IsStringLikelyCommitID(ctx.Repo.GetObjectFormat(), refName, 7) { @@ -975,7 +962,7 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetCommit(refName) if err != nil { ctx.NotFound("GetCommit", err) - return cancel + return } // If short commit ID add canonical link header if len(refName) < ctx.Repo.GetObjectFormat().FullLength() { @@ -984,10 +971,10 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func } } else { if opt.IgnoreNotExistErr { - return cancel + return } ctx.NotFound("RepoRef invalid repo", fmt.Errorf("branch or tag not exist: %s", refName)) - return cancel + return } if guessLegacyPath { @@ -999,7 +986,7 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func ctx.Repo.BranchNameSubURL(), util.PathEscapeSegments(ctx.Repo.TreePath)) ctx.Redirect(redirect) - return cancel + return } } @@ -1017,12 +1004,10 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount() if err != nil { ctx.ServerError("GetCommitsCount", err) - return cancel + return } ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount ctx.Repo.GitRepo.LastCommitCache = git.NewLastCommitCache(ctx.Repo.CommitsCount, ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, cache.GetCache()) - - return cancel } } diff --git a/services/context/upload/upload.go b/services/context/upload/upload.go index cefd13ebb60..da4370a4336 100644 --- a/services/context/upload/upload.go +++ b/services/context/upload/upload.go @@ -97,8 +97,8 @@ func AddUploadContext(ctx *context.Context, uploadType string) { } else if uploadType == "comment" { ctx.Data["UploadUrl"] = ctx.Repo.RepoLink + "/issues/attachments" ctx.Data["UploadRemoveUrl"] = ctx.Repo.RepoLink + "/issues/attachments/remove" - if len(ctx.PathParam(":index")) > 0 { - ctx.Data["UploadLinkUrl"] = ctx.Repo.RepoLink + "/issues/" + url.PathEscape(ctx.PathParam(":index")) + "/attachments" + if len(ctx.PathParam("index")) > 0 { + ctx.Data["UploadLinkUrl"] = ctx.Repo.RepoLink + "/issues/" + url.PathEscape(ctx.PathParam("index")) + "/attachments" } else { ctx.Data["UploadLinkUrl"] = ctx.Repo.RepoLink + "/issues/attachments" } diff --git a/services/context/user.go b/services/context/user.go index b0e855e9231..dbc35e198d4 100644 --- a/services/context/user.go +++ b/services/context/user.go @@ -33,7 +33,7 @@ func UserAssignmentWeb() func(ctx *Context) { // UserIDAssignmentAPI returns a middleware to handle context-user assignment for api routes func UserIDAssignmentAPI() func(ctx *APIContext) { return func(ctx *APIContext) { - userID := ctx.PathParamInt64(":user-id") + userID := ctx.PathParamInt64("user-id") if ctx.IsSigned && ctx.Doer.ID == userID { ctx.ContextUser = ctx.Doer @@ -59,7 +59,7 @@ func UserAssignmentAPI() func(ctx *APIContext) { } func userAssignment(ctx *Base, doer *user_model.User, errCb func(int, string, any)) (contextUser *user_model.User) { - username := ctx.PathParam(":username") + username := ctx.PathParam("username") if doer != nil && doer.LowerName == strings.ToLower(username) { contextUser = doer diff --git a/services/contexttest/context_tests.go b/services/contexttest/context_tests.go index 39ad5a362f2..b0f71cad20e 100644 --- a/services/contexttest/context_tests.go +++ b/services/contexttest/context_tests.go @@ -21,6 +21,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/gitrepo" + "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/session" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/translation" @@ -40,7 +41,7 @@ func mockRequest(t *testing.T, reqPath string) *http.Request { requestURL, err := url.Parse(path) assert.NoError(t, err) req := &http.Request{Method: method, Host: requestURL.Host, URL: requestURL, Form: maps.Clone(requestURL.Query()), Header: http.Header{}} - req = req.WithContext(middleware.WithContextData(req.Context())) + req = req.WithContext(reqctx.NewRequestContextForTest(req.Context())) return req } @@ -60,17 +61,16 @@ func MockContext(t *testing.T, reqPath string, opts ...MockContextOption) (*cont } resp := httptest.NewRecorder() req := mockRequest(t, reqPath) - base, baseCleanUp := context.NewBaseContext(resp, req) - _ = baseCleanUp // during test, it doesn't need to do clean up. TODO: this can be improved later + base := context.NewBaseContext(resp, req) base.Data = middleware.GetContextData(req.Context()) base.Locale = &translation.MockLocale{} chiCtx := chi.NewRouteContext() ctx := context.NewWebContext(base, opt.Render, nil) - ctx.AppendContextValue(context.WebContextKey, ctx) - ctx.AppendContextValue(chi.RouteCtxKey, chiCtx) + ctx.SetContextValue(context.WebContextKey, ctx) + ctx.SetContextValue(chi.RouteCtxKey, chiCtx) if opt.SessionStore != nil { - ctx.AppendContextValue(session.MockStoreContextKey, opt.SessionStore) + ctx.SetContextValue(session.MockStoreContextKey, opt.SessionStore) ctx.Session = opt.SessionStore } ctx.Cache = cache.GetCache() @@ -83,27 +83,24 @@ func MockContext(t *testing.T, reqPath string, opts ...MockContextOption) (*cont func MockAPIContext(t *testing.T, reqPath string) (*context.APIContext, *httptest.ResponseRecorder) { resp := httptest.NewRecorder() req := mockRequest(t, reqPath) - base, baseCleanUp := context.NewBaseContext(resp, req) + base := context.NewBaseContext(resp, req) base.Data = middleware.GetContextData(req.Context()) base.Locale = &translation.MockLocale{} ctx := &context.APIContext{Base: base} - _ = baseCleanUp // during test, it doesn't need to do clean up. TODO: this can be improved later - chiCtx := chi.NewRouteContext() - ctx.Base.AppendContextValue(chi.RouteCtxKey, chiCtx) + ctx.SetContextValue(chi.RouteCtxKey, chiCtx) return ctx, resp } func MockPrivateContext(t *testing.T, reqPath string) (*context.PrivateContext, *httptest.ResponseRecorder) { resp := httptest.NewRecorder() req := mockRequest(t, reqPath) - base, baseCleanUp := context.NewBaseContext(resp, req) + base := context.NewBaseContext(resp, req) base.Data = middleware.GetContextData(req.Context()) base.Locale = &translation.MockLocale{} ctx := &context.PrivateContext{Base: base} - _ = baseCleanUp // during test, it doesn't need to do clean up. TODO: this can be improved later chiCtx := chi.NewRouteContext() - ctx.Base.AppendContextValue(chi.RouteCtxKey, chiCtx) + ctx.SetContextValue(chi.RouteCtxKey, chiCtx) return ctx, resp } diff --git a/services/convert/repository.go b/services/convert/repository.go index 88ccd88fcfe..632b6392d57 100644 --- a/services/convert/repository.go +++ b/services/convert/repository.go @@ -33,7 +33,9 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR permissionInRepo.SetUnitsWithDefaultAccessMode(repo.Units, permissionInRepo.AccessMode) } - cloneLink := repo.CloneLink() + // TODO: ideally we should pass "doer" into "ToRepo" to to make CloneLink could generate user-related links + // And passing "doer" in will also fix other FIXMEs in this file. + cloneLink := repo.CloneLinkGeneral(ctx) // no doer at the moment permission := &api.Permission{ Admin: permissionInRepo.AccessMode >= perm.AccessModeAdmin, Push: permissionInRepo.UnitAccessMode(unit_model.TypeCode) >= perm.AccessModeWrite, diff --git a/services/convert/utils.go b/services/convert/utils.go index 5e9d32cc8ed..b59884ec508 100644 --- a/services/convert/utils.go +++ b/services/convert/utils.go @@ -36,6 +36,8 @@ func ToGitServiceType(value string) structs.GitServiceType { return structs.OneDevService case "gitbucket": return structs.GitBucketService + case "codebase": + return structs.CodebaseService case "codecommit": return structs.CodeCommitService default: diff --git a/services/convert/utils_test.go b/services/convert/utils_test.go index 1ac03a30975..a8363ec6bde 100644 --- a/services/convert/utils_test.go +++ b/services/convert/utils_test.go @@ -21,6 +21,8 @@ func TestToGitServiceType(t *testing.T) { typ string enum int }{{ + typ: "trash", enum: 1, + }, { typ: "github", enum: 2, }, { typ: "gitea", enum: 3, @@ -29,7 +31,13 @@ func TestToGitServiceType(t *testing.T) { }, { typ: "gogs", enum: 5, }, { - typ: "trash", enum: 1, + typ: "onedev", enum: 6, + }, { + typ: "gitbucket", enum: 7, + }, { + typ: "codebase", enum: 8, + }, { + typ: "codecommit", enum: 9, }} for _, test := range tc { assert.EqualValues(t, test.enum, ToGitServiceType(test.typ)) diff --git a/services/feed/notifier.go b/services/feed/notifier.go index d941027c352..702eb5ad533 100644 --- a/services/feed/notifier.go +++ b/services/feed/notifier.go @@ -109,7 +109,7 @@ func (a *actionNotifier) CreateIssueComment(ctx context.Context, doer *user_mode IsPrivate: issue.Repo.IsPrivate, } - truncatedContent, truncatedRight := util.SplitStringAtByteN(comment.Content, 200) + truncatedContent, truncatedRight := util.EllipsisDisplayStringX(comment.Content, 200) if truncatedRight != "" { // in case the content is in a Latin family language, we remove the last broken word. lastSpaceIdx := strings.LastIndex(truncatedContent, " ") diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index 0b5a855d42f..f42686bb718 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -360,7 +360,6 @@ type DiffFile struct { IsLFSFile bool IsRenamed bool IsAmbiguous bool - IsSubmodule bool Sections []*DiffSection IsIncomplete bool IsIncompleteLineTooLong bool @@ -372,6 +371,9 @@ type DiffFile struct { Language string Mode string OldMode string + + IsSubmodule bool // if IsSubmodule==true, then there must be a SubmoduleDiffInfo + SubmoduleDiffInfo *SubmoduleDiffInfo } // GetType returns type of diff file. @@ -609,9 +611,8 @@ parsingLoop: if strings.HasPrefix(line, "new mode ") { curFile.Mode = prepareValue(line, "new mode ") } - if strings.HasSuffix(line, " 160000\n") { - curFile.IsSubmodule = true + curFile.IsSubmodule, curFile.SubmoduleDiffInfo = true, &SubmoduleDiffInfo{} } case strings.HasPrefix(line, "rename from "): curFile.IsRenamed = true @@ -646,17 +647,17 @@ parsingLoop: curFile.Mode = prepareValue(line, "new file mode ") } if strings.HasSuffix(line, " 160000\n") { - curFile.IsSubmodule = true + curFile.IsSubmodule, curFile.SubmoduleDiffInfo = true, &SubmoduleDiffInfo{} } case strings.HasPrefix(line, "deleted"): curFile.Type = DiffFileDel curFile.IsDeleted = true if strings.HasSuffix(line, " 160000\n") { - curFile.IsSubmodule = true + curFile.IsSubmodule, curFile.SubmoduleDiffInfo = true, &SubmoduleDiffInfo{} } case strings.HasPrefix(line, "index"): if strings.HasSuffix(line, " 160000\n") { - curFile.IsSubmodule = true + curFile.IsSubmodule, curFile.SubmoduleDiffInfo = true, &SubmoduleDiffInfo{} } case strings.HasPrefix(line, "similarity index 100%"): curFile.Type = DiffFileRename @@ -915,6 +916,13 @@ func parseHunks(ctx context.Context, curFile *DiffFile, maxLines, maxLineCharact } } curSection.Lines = append(curSection.Lines, diffLine) + + // Parse submodule additions + if curFile.SubmoduleDiffInfo != nil { + if ref, found := bytes.CutPrefix(lineBytes, []byte("+Subproject commit ")); found { + curFile.SubmoduleDiffInfo.NewRefID = string(bytes.TrimSpace(ref)) + } + } case '-': curFileLinesCount++ curFile.Deletion++ @@ -936,6 +944,13 @@ func parseHunks(ctx context.Context, curFile *DiffFile, maxLines, maxLineCharact lastLeftIdx = len(curSection.Lines) } curSection.Lines = append(curSection.Lines, diffLine) + + // Parse submodule deletion + if curFile.SubmoduleDiffInfo != nil { + if ref, found := bytes.CutPrefix(lineBytes, []byte("-Subproject commit ")); found { + curFile.SubmoduleDiffInfo.PreviousRefID = string(bytes.TrimSpace(ref)) + } + } case ' ': curFileLinesCount++ if maxLines > -1 && curFileLinesCount >= maxLines { @@ -1195,6 +1210,11 @@ func GetDiff(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, fi } } + // Populate Submodule URLs + if diffFile.SubmoduleDiffInfo != nil { + diffFile.SubmoduleDiffInfo.PopulateURL(diffFile, beforeCommit, commit) + } + if !isVendored.Has() { isVendored = optional.Some(analyze.IsVendor(diffFile.Name)) } diff --git a/services/gitdiff/submodule.go b/services/gitdiff/submodule.go new file mode 100644 index 00000000000..02ca666544c --- /dev/null +++ b/services/gitdiff/submodule.go @@ -0,0 +1,65 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitdiff + +import ( + "context" + "html/template" + + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/htmlutil" + "code.gitea.io/gitea/modules/log" +) + +type SubmoduleDiffInfo struct { + SubmoduleName string + SubmoduleFile *git.CommitSubmoduleFile // it might be nil if the submodule is not found or unable to parse + NewRefID string + PreviousRefID string +} + +func (si *SubmoduleDiffInfo) PopulateURL(diffFile *DiffFile, leftCommit, rightCommit *git.Commit) { + si.SubmoduleName = diffFile.Name + submoduleCommit := rightCommit // If the submodule is added or updated, check at the right commit + if diffFile.IsDeleted { + submoduleCommit = leftCommit // If the submodule is deleted, check at the left commit + } + if submoduleCommit == nil { + return + } + + submodule, err := submoduleCommit.GetSubModule(diffFile.GetDiffFileName()) + if err != nil { + log.Error("Unable to PopulateURL for submodule %q: GetSubModule: %v", diffFile.GetDiffFileName(), err) + return // ignore the error, do not cause 500 errors for end users + } + if submodule != nil { + si.SubmoduleFile = git.NewCommitSubmoduleFile(submodule.URL, submoduleCommit.ID.String()) + } +} + +func (si *SubmoduleDiffInfo) CommitRefIDLinkHTML(ctx context.Context, commitID string) template.HTML { + webLink := si.SubmoduleFile.SubmoduleWebLink(ctx, commitID) + if webLink == nil { + return htmlutil.HTMLFormat("%s", base.ShortSha(commitID)) + } + return htmlutil.HTMLFormat(`%s`, webLink.CommitWebLink, base.ShortSha(commitID)) +} + +func (si *SubmoduleDiffInfo) CompareRefIDLinkHTML(ctx context.Context) template.HTML { + webLink := si.SubmoduleFile.SubmoduleWebLink(ctx, si.PreviousRefID, si.NewRefID) + if webLink == nil { + return htmlutil.HTMLFormat("%s...%s", base.ShortSha(si.PreviousRefID), base.ShortSha(si.NewRefID)) + } + return htmlutil.HTMLFormat(`%s...%s`, webLink.CommitWebLink, base.ShortSha(si.PreviousRefID), base.ShortSha(si.NewRefID)) +} + +func (si *SubmoduleDiffInfo) SubmoduleRepoLinkHTML(ctx context.Context) template.HTML { + webLink := si.SubmoduleFile.SubmoduleWebLink(ctx) + if webLink == nil { + return htmlutil.HTMLFormat("%s", si.SubmoduleName) + } + return htmlutil.HTMLFormat(`%s`, webLink.RepoWebLink, si.SubmoduleName) +} diff --git a/services/gitdiff/submodule_test.go b/services/gitdiff/submodule_test.go new file mode 100644 index 00000000000..89f32c0e0c8 --- /dev/null +++ b/services/gitdiff/submodule_test.go @@ -0,0 +1,236 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitdiff + +import ( + "context" + "strings" + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/setting" + + "github.com/stretchr/testify/assert" +) + +func TestParseSubmoduleInfo(t *testing.T) { + type testcase struct { + name string + gitdiff string + infos map[int]SubmoduleDiffInfo + } + + tests := []testcase{ + { + name: "added", + gitdiff: `diff --git a/.gitmodules b/.gitmodules +new file mode 100644 +index 0000000..4ac13c1 +--- /dev/null ++++ b/.gitmodules +@@ -0,0 +1,3 @@ ++[submodule "gitea-mirror"] ++ path = gitea-mirror ++ url = https://gitea.com/gitea/gitea-mirror +diff --git a/gitea-mirror b/gitea-mirror +new file mode 160000 +index 0000000..68972a9 +--- /dev/null ++++ b/gitea-mirror +@@ -0,0 +1 @@ ++Subproject commit 68972a994719ae5c74e28d8fa82fa27c23399bc8 +`, + infos: map[int]SubmoduleDiffInfo{ + 1: {NewRefID: "68972a994719ae5c74e28d8fa82fa27c23399bc8"}, + }, + }, + { + name: "updated", + gitdiff: `diff --git a/gitea-mirror b/gitea-mirror +index 68972a9..c8ffe77 160000 +--- a/gitea-mirror ++++ b/gitea-mirror +@@ -1 +1 @@ +-Subproject commit 68972a994719ae5c74e28d8fa82fa27c23399bc8 ++Subproject commit c8ffe777cf9c5bb47a38e3e0b3a3b5de6cd8813d +`, + infos: map[int]SubmoduleDiffInfo{ + 0: { + PreviousRefID: "68972a994719ae5c74e28d8fa82fa27c23399bc8", + NewRefID: "c8ffe777cf9c5bb47a38e3e0b3a3b5de6cd8813d", + }, + }, + }, + { + name: "rename", + gitdiff: `diff --git a/.gitmodules b/.gitmodules +index 4ac13c1..0510edd 100644 +--- a/.gitmodules ++++ b/.gitmodules +@@ -1,3 +1,3 @@ + [submodule "gitea-mirror"] +- path = gitea-mirror ++ path = gitea + url = https://gitea.com/gitea/gitea-mirror +diff --git a/gitea-mirror b/gitea +similarity index 100% +rename from gitea-mirror +rename to gitea +`, + }, + { + name: "deleted", + gitdiff: `diff --git a/.gitmodules b/.gitmodules +index 0510edd..e69de29 100644 +--- a/.gitmodules ++++ b/.gitmodules +@@ -1,3 +0,0 @@ +-[submodule "gitea-mirror"] +- path = gitea +- url = https://gitea.com/gitea/gitea-mirror +diff --git a/gitea b/gitea +deleted file mode 160000 +index c8ffe77..0000000 +--- a/gitea ++++ /dev/null +@@ -1 +0,0 @@ +-Subproject commit c8ffe777cf9c5bb47a38e3e0b3a3b5de6cd8813d +`, + infos: map[int]SubmoduleDiffInfo{ + 1: { + PreviousRefID: "c8ffe777cf9c5bb47a38e3e0b3a3b5de6cd8813d", + }, + }, + }, + { + name: "moved and updated", + gitdiff: `diff --git a/.gitmodules b/.gitmodules +index 0510edd..bced3d8 100644 +--- a/.gitmodules ++++ b/.gitmodules +@@ -1,3 +1,3 @@ + [submodule "gitea-mirror"] +- path = gitea ++ path = gitea-1.22 + url = https://gitea.com/gitea/gitea-mirror +diff --git a/gitea b/gitea +deleted file mode 160000 +index c8ffe77..0000000 +--- a/gitea ++++ /dev/null +@@ -1 +0,0 @@ +-Subproject commit c8ffe777cf9c5bb47a38e3e0b3a3b5de6cd8813d +diff --git a/gitea-1.22 b/gitea-1.22 +new file mode 160000 +index 0000000..8eefa1f +--- /dev/null ++++ b/gitea-1.22 +@@ -0,0 +1 @@ ++Subproject commit 8eefa1f6dedf2488db2c9e12c916e8e51f673160 +`, + infos: map[int]SubmoduleDiffInfo{ + 1: { + PreviousRefID: "c8ffe777cf9c5bb47a38e3e0b3a3b5de6cd8813d", + }, + 2: { + NewRefID: "8eefa1f6dedf2488db2c9e12c916e8e51f673160", + }, + }, + }, + { + name: "converted to file", + gitdiff: `diff --git a/.gitmodules b/.gitmodules +index 0510edd..e69de29 100644 +--- a/.gitmodules ++++ b/.gitmodules +@@ -1,3 +0,0 @@ +-[submodule "gitea-mirror"] +- path = gitea +- url = https://gitea.com/gitea/gitea-mirror +diff --git a/gitea b/gitea +deleted file mode 160000 +index c8ffe77..0000000 +--- a/gitea ++++ /dev/null +@@ -1 +0,0 @@ +-Subproject commit c8ffe777cf9c5bb47a38e3e0b3a3b5de6cd8813d +diff --git a/gitea b/gitea +new file mode 100644 +index 0000000..33a9488 +--- /dev/null ++++ b/gitea +@@ -0,0 +1 @@ ++example +`, + infos: map[int]SubmoduleDiffInfo{ + 1: { + PreviousRefID: "c8ffe777cf9c5bb47a38e3e0b3a3b5de6cd8813d", + }, + }, + }, + { + name: "converted to submodule", + gitdiff: `diff --git a/.gitmodules b/.gitmodules +index e69de29..14ee267 100644 +--- a/.gitmodules ++++ b/.gitmodules +@@ -0,0 +1,3 @@ ++[submodule "gitea"] ++ path = gitea ++ url = https://gitea.com/gitea/gitea-mirror +diff --git a/gitea b/gitea +deleted file mode 100644 +index 33a9488..0000000 +--- a/gitea ++++ /dev/null +@@ -1 +0,0 @@ +-example +diff --git a/gitea b/gitea +new file mode 160000 +index 0000000..68972a9 +--- /dev/null ++++ b/gitea +@@ -0,0 +1 @@ ++Subproject commit 68972a994719ae5c74e28d8fa82fa27c23399bc8 +`, + infos: map[int]SubmoduleDiffInfo{ + 2: { + NewRefID: "68972a994719ae5c74e28d8fa82fa27c23399bc8", + }, + }, + }, + } + + for _, testcase := range tests { + testcase := testcase + t.Run(testcase.name, func(t *testing.T) { + diff, err := ParsePatch(db.DefaultContext, setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, strings.NewReader(testcase.gitdiff), "") + assert.NoError(t, err) + + for i, expected := range testcase.infos { + actual := diff.Files[i] + assert.NotNil(t, actual) + assert.Equal(t, expected, *actual.SubmoduleDiffInfo) + } + }) + } +} + +func TestSubmoduleInfo(t *testing.T) { + sdi := &SubmoduleDiffInfo{ + SubmoduleName: "name", + PreviousRefID: "aaaa", + NewRefID: "bbbb", + } + ctx := context.Background() + assert.EqualValues(t, "1111", sdi.CommitRefIDLinkHTML(ctx, "1111")) + assert.EqualValues(t, "aaaa...bbbb", sdi.CompareRefIDLinkHTML(ctx)) + assert.EqualValues(t, "name", sdi.SubmoduleRepoLinkHTML(ctx)) + + sdi.SubmoduleFile = git.NewCommitSubmoduleFile("https://github.com/owner/repo", "1234") + assert.EqualValues(t, `1111`, sdi.CommitRefIDLinkHTML(ctx, "1111")) + assert.EqualValues(t, `aaaa...bbbb`, sdi.CompareRefIDLinkHTML(ctx)) + assert.EqualValues(t, `name`, sdi.SubmoduleRepoLinkHTML(ctx)) +} diff --git a/services/issue/commit.go b/services/issue/commit.go index 0579e0f5c53..963d0359fd3 100644 --- a/services/issue/commit.go +++ b/services/issue/commit.go @@ -188,15 +188,19 @@ func UpdateIssuesCommit(ctx context.Context, doer *user_model.User, repo *repo_m continue } } - isClosed := ref.Action == references.XRefActionCloses - if isClosed && len(ref.TimeLog) > 0 { - if err := issueAddTime(ctx, refIssue, doer, c.Timestamp, ref.TimeLog); err != nil { + + refIssue.Repo = refRepo + if ref.Action == references.XRefActionCloses && !refIssue.IsClosed { + if len(ref.TimeLog) > 0 { + if err := issueAddTime(ctx, refIssue, doer, c.Timestamp, ref.TimeLog); err != nil { + return err + } + } + if err := CloseIssue(ctx, refIssue, doer, c.Sha1); err != nil { return err } - } - if isClosed != refIssue.IsClosed { - refIssue.Repo = refRepo - if err := ChangeStatus(ctx, refIssue, doer, c.Sha1, isClosed); err != nil { + } else if ref.Action == references.XRefActionReopens && refIssue.IsClosed { + if err := ReopenIssue(ctx, refIssue, doer, c.Sha1); err != nil { return err } } diff --git a/services/issue/milestone.go b/services/issue/milestone.go index ff645744a7d..beb6f131a90 100644 --- a/services/issue/milestone.go +++ b/services/issue/milestone.go @@ -59,6 +59,10 @@ func changeMilestoneAssign(ctx context.Context, doer *user_model.User, issue *is } } + if issue.MilestoneID == 0 { + issue.Milestone = nil + } + return nil } diff --git a/services/issue/milestone_test.go b/services/issue/milestone_test.go index 42b910166f5..bf5abc85b70 100644 --- a/services/issue/milestone_test.go +++ b/services/issue/milestone_test.go @@ -23,6 +23,7 @@ func TestChangeMilestoneAssign(t *testing.T) { oldMilestoneID := issue.MilestoneID issue.MilestoneID = 2 + assert.NoError(t, issue.LoadMilestone(db.DefaultContext)) assert.NoError(t, ChangeMilestoneAssign(db.DefaultContext, issue, doer, oldMilestoneID)) unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ IssueID: issue.ID, @@ -31,4 +32,11 @@ func TestChangeMilestoneAssign(t *testing.T) { OldMilestoneID: oldMilestoneID, }) unittest.CheckConsistencyFor(t, &issues_model.Milestone{}, &issues_model.Issue{}) + assert.NotNil(t, issue.Milestone) + + oldMilestoneID = issue.MilestoneID + issue.MilestoneID = 0 + assert.NoError(t, ChangeMilestoneAssign(db.DefaultContext, issue, doer, oldMilestoneID)) + assert.EqualValues(t, 0, issue.MilestoneID) + assert.Nil(t, issue.Milestone) } diff --git a/services/issue/status.go b/services/issue/status.go index 967c29bd222..e18b891175a 100644 --- a/services/issue/status.go +++ b/services/issue/status.go @@ -6,34 +6,54 @@ package issue import ( "context" + "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" notify_service "code.gitea.io/gitea/services/notify" ) -// ChangeStatus changes issue status to open or closed. -// closed means the target status -// Fix me: you should check whether the current issue status is same to the target status before call this function -// as in function changeIssueStatus we will return WasClosedError, even the issue status and target status are both open -func ChangeStatus(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, commitID string, closed bool) error { - comment, err := issues_model.ChangeIssueStatus(ctx, issue, doer, closed) +// CloseIssue close an issue. +func CloseIssue(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, commitID string) error { + dbCtx, committer, err := db.TxContext(ctx) if err != nil { - if issues_model.IsErrDependenciesLeft(err) && closed { - if err := issues_model.FinishIssueStopwatchIfPossible(ctx, doer, issue); err != nil { + return err + } + defer committer.Close() + + comment, err := issues_model.CloseIssue(dbCtx, issue, doer) + if err != nil { + if issues_model.IsErrDependenciesLeft(err) { + if err := issues_model.FinishIssueStopwatchIfPossible(dbCtx, doer, issue); err != nil { log.Error("Unable to stop stopwatch for issue[%d]#%d: %v", issue.ID, issue.Index, err) } } return err } - if closed { - if err := issues_model.FinishIssueStopwatchIfPossible(ctx, doer, issue); err != nil { - return err - } + if err := issues_model.FinishIssueStopwatchIfPossible(dbCtx, doer, issue); err != nil { + return err + } + + if err := committer.Commit(); err != nil { + return err + } + committer.Close() + + notify_service.IssueChangeStatus(ctx, doer, commitID, issue, comment, true) + + return nil +} + +// ReopenIssue reopen an issue. +// FIXME: If some issues dependent this one are closed, should we also reopen them? +func ReopenIssue(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, commitID string) error { + comment, err := issues_model.ReopenIssue(ctx, issue, doer) + if err != nil { + return err } - notify_service.IssueChangeStatus(ctx, doer, commitID, issue, comment, closed) + notify_service.IssueChangeStatus(ctx, doer, commitID, issue, comment, false) return nil } diff --git a/services/mailer/mail.go b/services/mailer/mail.go index a6763e4f03c..52e19bde6f2 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/mailer/sender/message.go b/services/mailer/sender/message.go index db20675572d..55f03e4f7ec 100644 --- a/services/mailer/sender/message.go +++ b/services/mailer/sender/message.go @@ -10,9 +10,9 @@ import ( "strings" "time" - "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" "github.com/jaytaylor/html2text" gomail "github.com/wneessen/go-mail" @@ -54,7 +54,7 @@ func (m *Message) ToMessage() *gomail.Msg { plainBody, err := html2text.FromString(m.Body) if err != nil || setting.MailService.SendAsPlainText { - if strings.Contains(base.TruncateString(m.Body, 100), "") { + if strings.Contains(util.TruncateRunes(m.Body, 100), "") { log.Warn("Mail contains HTML but configured to send as plain text.") } msg.SetBodyString("text/plain", plainBody) diff --git a/services/markup/main_test.go b/services/markup/main_test.go index 5553ebc0589..d04a18bfa17 100644 --- a/services/markup/main_test.go +++ b/services/markup/main_test.go @@ -11,6 +11,6 @@ import ( func TestMain(m *testing.M) { unittest.MainTest(m, &unittest.TestOptions{ - FixtureFiles: []string{"user.yml", "repository.yml", "access.yml", "repo_unit.yml"}, + FixtureFiles: []string{"user.yml", "repository.yml", "access.yml", "repo_unit.yml", "issue.yml"}, }) } diff --git a/services/markup/processorhelper.go b/services/markup/renderhelper.go similarity index 88% rename from services/markup/processorhelper.go rename to services/markup/renderhelper.go index 1f1abf496a3..4b9852b48bf 100644 --- a/services/markup/processorhelper.go +++ b/services/markup/renderhelper.go @@ -11,9 +11,10 @@ import ( gitea_context "code.gitea.io/gitea/services/context" ) -func ProcessorHelper() *markup.RenderHelperFuncs { +func FormalRenderHelperFuncs() *markup.RenderHelperFuncs { return &markup.RenderHelperFuncs{ RenderRepoFileCodePreview: renderRepoFileCodePreview, + RenderRepoIssueIconTitle: renderRepoIssueIconTitle, IsUsernameMentionable: func(ctx context.Context, username string) bool { mentionedUser, err := user.GetUserByName(ctx, username) if err != nil { diff --git a/services/markup/processorhelper_codepreview.go b/services/markup/renderhelper_codepreview.go similarity index 97% rename from services/markup/processorhelper_codepreview.go rename to services/markup/renderhelper_codepreview.go index 0500e57e461..170c70c4098 100644 --- a/services/markup/processorhelper_codepreview.go +++ b/services/markup/renderhelper_codepreview.go @@ -18,6 +18,7 @@ import ( "code.gitea.io/gitea/modules/indexer/code" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" gitea_context "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/repository/files" ) @@ -46,7 +47,7 @@ func renderRepoFileCodePreview(ctx context.Context, opts markup.RenderCodePrevie return "", err } if !perms.CanRead(unit.TypeCode) { - return "", fmt.Errorf("no permission") + return "", util.ErrPermissionDenied } gitRepo, err := gitrepo.OpenRepository(ctx, dbRepo) diff --git a/services/markup/processorhelper_codepreview_test.go b/services/markup/renderhelper_codepreview_test.go similarity index 95% rename from services/markup/processorhelper_codepreview_test.go rename to services/markup/renderhelper_codepreview_test.go index 154e4e8e441..ea945584b42 100644 --- a/services/markup/processorhelper_codepreview_test.go +++ b/services/markup/renderhelper_codepreview_test.go @@ -9,12 +9,13 @@ import ( "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/contexttest" "github.com/stretchr/testify/assert" ) -func TestProcessorHelperCodePreview(t *testing.T) { +func TestRenderHelperCodePreview(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) ctx, _ := contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()}) @@ -79,5 +80,5 @@ func TestProcessorHelperCodePreview(t *testing.T) { LineStart: 1, LineStop: 10, }) - assert.ErrorContains(t, err, "no permission") + assert.ErrorIs(t, err, util.ErrPermissionDenied) } diff --git a/services/markup/renderhelper_issueicontitle.go b/services/markup/renderhelper_issueicontitle.go new file mode 100644 index 00000000000..53a508e9081 --- /dev/null +++ b/services/markup/renderhelper_issueicontitle.go @@ -0,0 +1,66 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package markup + +import ( + "context" + "fmt" + "html/template" + + "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/perm/access" + "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/htmlutil" + "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/util" + gitea_context "code.gitea.io/gitea/services/context" +) + +func renderRepoIssueIconTitle(ctx context.Context, opts markup.RenderIssueIconTitleOptions) (_ template.HTML, err error) { + webCtx, ok := ctx.Value(gitea_context.WebContextKey).(*gitea_context.Context) + if !ok { + return "", fmt.Errorf("context is not a web context") + } + + textIssueIndex := fmt.Sprintf("(#%d)", opts.IssueIndex) + dbRepo := webCtx.Repo.Repository + if opts.OwnerName != "" { + dbRepo, err = repo.GetRepositoryByOwnerAndName(ctx, opts.OwnerName, opts.RepoName) + if err != nil { + return "", err + } + textIssueIndex = fmt.Sprintf("(%s/%s#%d)", dbRepo.OwnerName, dbRepo.Name, opts.IssueIndex) + } + if dbRepo == nil { + return "", nil + } + + issue, err := issues.GetIssueByIndex(ctx, dbRepo.ID, opts.IssueIndex) + if err != nil { + return "", err + } + + if webCtx.Repo.Repository == nil || dbRepo.ID != webCtx.Repo.Repository.ID { + perms, err := access.GetUserRepoPermission(ctx, dbRepo, webCtx.Doer) + if err != nil { + return "", err + } + if !perms.CanReadIssuesOrPulls(issue.IsPull) { + return "", util.ErrPermissionDenied + } + } + + if issue.IsPull { + if err = issue.LoadPullRequest(ctx); err != nil { + return "", err + } + } + + htmlIcon, err := webCtx.RenderToHTML("shared/issueicon", issue) + if err != nil { + return "", err + } + + return htmlutil.HTMLFormat(`%s %s %s`, opts.LinkHref, htmlIcon, issue.Title, textIssueIndex), nil +} diff --git a/services/markup/renderhelper_issueicontitle_test.go b/services/markup/renderhelper_issueicontitle_test.go new file mode 100644 index 00000000000..adce8401e07 --- /dev/null +++ b/services/markup/renderhelper_issueicontitle_test.go @@ -0,0 +1,49 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package markup + +import ( + "testing" + + "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/contexttest" + + "github.com/stretchr/testify/assert" +) + +func TestRenderHelperIssueIconTitle(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + ctx, _ := contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()}) + ctx.Repo.Repository = unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1}) + htm, err := renderRepoIssueIconTitle(ctx, markup.RenderIssueIconTitleOptions{ + LinkHref: "/link", + IssueIndex: 1, + }) + assert.NoError(t, err) + assert.Equal(t, `octicon-issue-opened(16/text green) issue1 (#1)`, string(htm)) + + ctx, _ = contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()}) + htm, err = renderRepoIssueIconTitle(ctx, markup.RenderIssueIconTitleOptions{ + OwnerName: "user2", + RepoName: "repo1", + LinkHref: "/link", + IssueIndex: 1, + }) + assert.NoError(t, err) + assert.Equal(t, `octicon-issue-opened(16/text green) issue1 (user2/repo1#1)`, string(htm)) + + ctx, _ = contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()}) + _, err = renderRepoIssueIconTitle(ctx, markup.RenderIssueIconTitleOptions{ + OwnerName: "user2", + RepoName: "repo2", + LinkHref: "/link", + IssueIndex: 2, + }) + assert.ErrorIs(t, err, util.ErrPermissionDenied) +} diff --git a/services/markup/processorhelper_test.go b/services/markup/renderhelper_mention_test.go similarity index 57% rename from services/markup/processorhelper_test.go rename to services/markup/renderhelper_mention_test.go index 170edae0e0b..c244fa3d21d 100644 --- a/services/markup/processorhelper_test.go +++ b/services/markup/renderhelper_mention_test.go @@ -18,7 +18,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestProcessorHelper(t *testing.T) { +func TestRenderHelperMention(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) userPublic := "user1" @@ -32,23 +32,22 @@ func TestProcessorHelper(t *testing.T) { unittest.AssertCount(t, &user.User{Name: userNoSuch}, 0) // when using general context, use user's visibility to check - assert.True(t, ProcessorHelper().IsUsernameMentionable(context.Background(), userPublic)) - assert.False(t, ProcessorHelper().IsUsernameMentionable(context.Background(), userLimited)) - assert.False(t, ProcessorHelper().IsUsernameMentionable(context.Background(), userPrivate)) - assert.False(t, ProcessorHelper().IsUsernameMentionable(context.Background(), userNoSuch)) + assert.True(t, FormalRenderHelperFuncs().IsUsernameMentionable(context.Background(), userPublic)) + assert.False(t, FormalRenderHelperFuncs().IsUsernameMentionable(context.Background(), userLimited)) + assert.False(t, FormalRenderHelperFuncs().IsUsernameMentionable(context.Background(), userPrivate)) + assert.False(t, FormalRenderHelperFuncs().IsUsernameMentionable(context.Background(), userNoSuch)) // when using web context, use user.IsUserVisibleToViewer to check req, err := http.NewRequest("GET", "/", nil) assert.NoError(t, err) - base, baseCleanUp := gitea_context.NewBaseContext(httptest.NewRecorder(), req) - defer baseCleanUp() + base := gitea_context.NewBaseContextForTest(httptest.NewRecorder(), req) giteaCtx := gitea_context.NewWebContext(base, &contexttest.MockRender{}, nil) - assert.True(t, ProcessorHelper().IsUsernameMentionable(giteaCtx, userPublic)) - assert.False(t, ProcessorHelper().IsUsernameMentionable(giteaCtx, userPrivate)) + assert.True(t, FormalRenderHelperFuncs().IsUsernameMentionable(giteaCtx, userPublic)) + assert.False(t, FormalRenderHelperFuncs().IsUsernameMentionable(giteaCtx, userPrivate)) giteaCtx.Doer, err = user.GetUserByName(db.DefaultContext, userPrivate) assert.NoError(t, err) - assert.True(t, ProcessorHelper().IsUsernameMentionable(giteaCtx, userPublic)) - assert.True(t, ProcessorHelper().IsUsernameMentionable(giteaCtx, userPrivate)) + assert.True(t, FormalRenderHelperFuncs().IsUsernameMentionable(giteaCtx, userPublic)) + assert.True(t, FormalRenderHelperFuncs().IsUsernameMentionable(giteaCtx, userPrivate)) } diff --git a/services/migrations/codecommit.go b/services/migrations/codecommit.go index ccda62fc3d9..fead527f5b9 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/migrations/gitea_downloader_test.go b/services/migrations/gitea_downloader_test.go index c37c70947e3..d04e12abdbc 100644 --- a/services/migrations/gitea_downloader_test.go +++ b/services/migrations/gitea_downloader_test.go @@ -30,10 +30,10 @@ func TestGiteaDownloadRepo(t *testing.T) { downloader, err := NewGiteaDownloader(context.Background(), "https://gitea.com", "gitea/test_repo", "", "", giteaToken) if downloader == nil { - t.Fatal("NewGitlabDownloader is nil") + t.Fatal("NewGiteaDownloader is nil") } if !assert.NoError(t, err) { - t.Fatal("NewGitlabDownloader error occur") + t.Fatal("NewGiteaDownloader error occur") } repo, err := downloader.GetRepoInfo() diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go index eb21b6534b8..9e06b77b66c 100644 --- a/services/migrations/gitea_uploader.go +++ b/services/migrations/gitea_uploader.go @@ -19,7 +19,6 @@ import ( issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" - base_module "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/label" @@ -409,7 +408,7 @@ func (g *GiteaLocalUploader) CreateIssues(issues ...*base.Issue) error { RepoID: g.repo.ID, Repo: g.repo, Index: issue.Number, - Title: base_module.TruncateString(issue.Title, 255), + Title: util.TruncateRunes(issue.Title, 255), Content: issue.Content, Ref: issue.Ref, IsClosed: issue.State == "closed", diff --git a/services/migrations/gitlab.go b/services/migrations/gitlab.go index 295bc7c29f5..07d5040b5b3 100644 --- a/services/migrations/gitlab.go +++ b/services/migrations/gitlab.go @@ -21,7 +21,7 @@ import ( base "code.gitea.io/gitea/modules/migration" "code.gitea.io/gitea/modules/structs" - "github.com/xanzy/go-gitlab" + gitlab "gitlab.com/gitlab-org/api/client-go" ) var ( diff --git a/services/migrations/gitlab_test.go b/services/migrations/gitlab_test.go index eccfc4def1f..556fe771c5d 100644 --- a/services/migrations/gitlab_test.go +++ b/services/migrations/gitlab_test.go @@ -17,7 +17,7 @@ import ( base "code.gitea.io/gitea/modules/migration" "github.com/stretchr/testify/assert" - "github.com/xanzy/go-gitlab" + gitlab "gitlab.com/gitlab-org/api/client-go" ) func TestGitlabDownloadRepo(t *testing.T) { diff --git a/services/mirror/mirror_pull.go b/services/mirror/mirror_pull.go index 22d380e8e68..948222a436c 100644 --- a/services/mirror/mirror_pull.go +++ b/services/mirror/mirror_pull.go @@ -32,7 +32,7 @@ const gitShortEmptySha = "0000000" // UpdateAddress writes new address to Git repository and database func UpdateAddress(ctx context.Context, m *repo_model.Mirror, addr string) error { - u, err := giturl.Parse(addr) + u, err := giturl.ParseGitURL(addr) if err != nil { return fmt.Errorf("invalid addr: %v", err) } @@ -41,13 +41,13 @@ func UpdateAddress(ctx context.Context, m *repo_model.Mirror, addr string) error repoPath := m.GetRepository(ctx).RepoPath() // Remove old remote _, _, err = git.NewCommand(ctx, "remote", "rm").AddDynamicArguments(remoteName).RunStdString(&git.RunOpts{Dir: repoPath}) - if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") { + if err != nil && !git.IsRemoteNotExistError(err) { return err } cmd := git.NewCommand(ctx, "remote", "add").AddDynamicArguments(remoteName).AddArguments("--mirror=fetch").AddDynamicArguments(addr) _, _, err = cmd.RunStdString(&git.RunOpts{Dir: repoPath}) - if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") { + if err != nil && !git.IsRemoteNotExistError(err) { return err } @@ -56,13 +56,13 @@ func UpdateAddress(ctx context.Context, m *repo_model.Mirror, addr string) error wikiRemotePath := repo_module.WikiRemoteURL(ctx, addr) // Remove old remote of wiki _, _, err = git.NewCommand(ctx, "remote", "rm").AddDynamicArguments(remoteName).RunStdString(&git.RunOpts{Dir: wikiPath}) - if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") { + if err != nil && !git.IsRemoteNotExistError(err) { return err } cmd = git.NewCommand(ctx, "remote", "add").AddDynamicArguments(remoteName).AddArguments("--mirror=fetch").AddDynamicArguments(wikiRemotePath) _, _, err = cmd.RunStdString(&git.RunOpts{Dir: wikiPath}) - if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") { + if err != nil && !git.IsRemoteNotExistError(err) { return err } } diff --git a/services/org/user_test.go b/services/org/user_test.go index 56d01a3b63a..96d1a1c8cac 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/packages/packages.go b/services/packages/packages.go index 55351afce29..bd1d460fd3b 100644 --- a/services/packages/packages.go +++ b/services/packages/packages.go @@ -132,12 +132,11 @@ func createPackageAndVersion(ctx context.Context, pvci *PackageCreationInfo, all } var err error if p, err = packages_model.TryInsertPackage(ctx, p); err != nil { - if err == packages_model.ErrDuplicatePackage { - packageCreated = false - } else { + if !errors.Is(err, packages_model.ErrDuplicatePackage) { log.Error("Error inserting package: %v", err) return nil, false, err } + packageCreated = false } if packageCreated { @@ -163,11 +162,10 @@ func createPackageAndVersion(ctx context.Context, pvci *PackageCreationInfo, all MetadataJSON: string(metadataJSON), } if pv, err = packages_model.GetOrInsertVersion(ctx, pv); err != nil { - if err == packages_model.ErrDuplicatePackageVersion { + if errors.Is(err, packages_model.ErrDuplicatePackageVersion) && allowDuplicate { versionCreated = false - } - if err != packages_model.ErrDuplicatePackageVersion || !allowDuplicate { - log.Error("Error inserting package: %v", err) + } else { + log.Error("Error inserting package: %v", err) // other error, or disallowing duplicates return nil, false, err } } @@ -433,7 +431,7 @@ func GetOrCreateInternalPackageVersion(ctx context.Context, ownerID int64, packa } var err error if p, err = packages_model.TryInsertPackage(ctx, p); err != nil { - if err != packages_model.ErrDuplicatePackage { + if !errors.Is(err, packages_model.ErrDuplicatePackage) { log.Error("Error inserting package: %v", err) return err } diff --git a/services/pull/check.go b/services/pull/check.go index bffca394a8b..e1adc3ca3bf 100644 --- a/services/pull/check.go +++ b/services/pull/check.go @@ -282,9 +282,6 @@ func manuallyMerged(ctx context.Context, pr *issues_model.PullRequest) bool { return false } - pr.MergedCommitID = commit.ID.String() - pr.MergedUnix = timeutil.TimeStamp(commit.Author.When.Unix()) - pr.Status = issues_model.PullRequestStatusManuallyMerged merger, _ := user_model.GetUserByEmail(ctx, commit.Author.Email) // When the commit author is unknown set the BaseRepo owner as merger @@ -297,10 +294,8 @@ func manuallyMerged(ctx context.Context, pr *issues_model.PullRequest) bool { } merger = pr.BaseRepo.Owner } - pr.Merger = merger - pr.MergerID = merger.ID - if merged, err := pr.SetMerged(ctx); err != nil { + if merged, err := SetMerged(ctx, pr, commit.ID.String(), timeutil.TimeStamp(commit.Author.When.Unix()), merger, issues_model.PullRequestStatusManuallyMerged); err != nil { log.Error("%-v setMerged : %v", pr, err) return false } else if !merged { diff --git a/services/pull/merge.go b/services/pull/merge.go index fba85f1e519..9c909ef7958 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -17,6 +17,7 @@ import ( git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" access_model "code.gitea.io/gitea/models/perm/access" + pull_model "code.gitea.io/gitea/models/pull" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" @@ -263,14 +264,17 @@ func handleCloseCrossReferences(ctx context.Context, pr *issues_model.PullReques if err = ref.Issue.LoadRepo(ctx); err != nil { return err } - isClosed := ref.RefAction == references.XRefActionCloses - if isClosed != ref.Issue.IsClosed { - if err = issue_service.ChangeStatus(ctx, ref.Issue, doer, pr.MergedCommitID, isClosed); err != nil { + if ref.RefAction == references.XRefActionCloses && !ref.Issue.IsClosed { + if err = issue_service.CloseIssue(ctx, ref.Issue, doer, pr.MergedCommitID); err != nil { // Allow ErrDependenciesLeft if !issues_model.IsErrDependenciesLeft(err) { return err } } + } else if ref.RefAction == references.XRefActionReopens && ref.Issue.IsClosed { + if err = issue_service.ReopenIssue(ctx, ref.Issue, doer, pr.MergedCommitID); err != nil { + return err + } } } return nil @@ -629,14 +633,8 @@ func MergedManually(ctx context.Context, pr *issues_model.PullRequest, doer *use return fmt.Errorf("Wrong commit ID") } - pr.MergedCommitID = commitID - pr.MergedUnix = timeutil.TimeStamp(commit.Author.When.Unix()) - pr.Status = issues_model.PullRequestStatusManuallyMerged - pr.Merger = doer - pr.MergerID = doer.ID - var merged bool - if merged, err = pr.SetMerged(ctx); err != nil { + if merged, err = SetMerged(ctx, pr, commitID, timeutil.TimeStamp(commit.Author.When.Unix()), doer, issues_model.PullRequestStatusManuallyMerged); err != nil { return err } else if !merged { return fmt.Errorf("SetMerged failed") @@ -653,3 +651,68 @@ 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, mergedCommitID string, mergedTimeStamp timeutil.TimeStamp, merger *user_model.User, mergeStatus issues_model.PullRequestStatus) (bool, error) { + if pr.HasMerged { + return false, fmt.Errorf("PullRequest[%d] already merged", pr.Index) + } + + pr.HasMerged = true + pr.MergedCommitID = mergedCommitID + pr.MergedUnix = mergedTimeStamp + pr.Merger = merger + pr.MergerID = merger.ID + pr.Status = mergeStatus + // reset the conflicted files as there cannot be any if we're merged + pr.ConflictedFiles = []string{} + + if pr.MergedCommitID == "" || pr.MergedUnix == 0 || pr.Merger == nil { + return false, fmt.Errorf("unable to merge PullRequest[%d], some required fields are empty", pr.Index) + } + + ctx, committer, err := db.TxContext(ctx) + if err != nil { + return false, err + } + defer committer.Close() + + pr.Issue = nil + if err := pr.LoadIssue(ctx); err != nil { + return false, err + } + + if err := pr.Issue.LoadRepo(ctx); err != nil { + return false, err + } + + if err := pr.Issue.Repo.LoadOwner(ctx); err != nil { + return false, err + } + + // Removing an auto merge pull and ignore if not exist + if err := pull_model.DeleteScheduledAutoMerge(ctx, pr.ID); err != nil && !db.IsErrNotExist(err) { + return false, fmt.Errorf("DeleteScheduledAutoMerge[%d]: %v", pr.ID, err) + } + + // Set issue as closed + if _, err := issues_model.SetIssueAsClosed(ctx, pr.Issue, pr.Merger, true); err != nil { + return false, fmt.Errorf("ChangeIssueStatus: %w", err) + } + + // 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 cnt, err := db.GetEngine(ctx).Where("id = ?", pr.ID). + And("has_merged = ?", false). + Cols("has_merged, status, merge_base, merged_commit_id, merger_id, merged_unix, conflicted_files"). + Update(pr); err != nil { + return false, fmt.Errorf("failed to update pr[%d]: %w", pr.ID, err) + } else if cnt != 1 { + return false, issues_model.ErrIssueAlreadyChanged + } + + if err := committer.Commit(); err != nil { + return false, err + } + + return true, nil +} diff --git a/services/pull/merge_squash.go b/services/pull/merge_squash.go index 197d8102dd9..7258671888c 100644 --- a/services/pull/merge_squash.go +++ b/services/pull/merge_squash.go @@ -5,12 +5,12 @@ package pull import ( "fmt" + "strings" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" ) @@ -25,12 +25,12 @@ func getAuthorSignatureSquash(ctx *mergeContext) (*git.Signature, error) { // Try to get an signature from the same user in one of the commits, as the // poster email might be private or commits might have a different signature // than the primary email address of the poster. - gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpenPath(ctx, ctx.tmpBasePath) + gitRepo, err := git.OpenRepository(ctx, ctx.tmpBasePath) if err != nil { log.Error("%-v Unable to open base repository: %v", ctx.pr, err) return nil, err } - defer closer.Close() + defer gitRepo.Close() commits, err := gitRepo.CommitsBetweenIDs(trackingBranch, "HEAD") if err != nil { @@ -66,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/patch.go b/services/pull/patch.go index 36ca9dbdb67..13623d73c66 100644 --- a/services/pull/patch.go +++ b/services/pull/patch.go @@ -41,9 +41,19 @@ func DownloadDiffOrPatch(ctx context.Context, pr *issues_model.PullRequest, w io } defer closer.Close() - if err := gitRepo.GetDiffOrPatch(pr.MergeBase, pr.GetGitRefName(), w, patch, binary); err != nil { - log.Error("Unable to get patch file from %s to %s in %s Error: %v", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err) - return fmt.Errorf("Unable to get patch file from %s to %s in %s Error: %w", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err) + compareArg := pr.MergeBase + "..." + pr.GetGitRefName() + switch { + case patch: + err = gitRepo.GetPatch(compareArg, w) + case binary: + err = gitRepo.GetDiffBinary(compareArg, w) + default: + err = gitRepo.GetDiff(compareArg, w) + } + + if err != nil { + log.Error("unable to get patch file from %s to %s in %s Error: %v", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err) + return fmt.Errorf("unable to get patch file from %s to %s in %s Error: %w", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err) } return nil } @@ -354,7 +364,7 @@ func checkConflicts(ctx context.Context, pr *issues_model.PullRequest, gitRepo * _ = util.Remove(tmpPatchFile.Name()) }() - if err := gitRepo.GetDiffBinary(pr.MergeBase, "tracking", tmpPatchFile); err != nil { + if err := gitRepo.GetDiffBinary(pr.MergeBase+"...tracking", tmpPatchFile); err != nil { tmpPatchFile.Close() log.Error("Unable to get patch file from %s to %s in %s Error: %v", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err) return false, fmt.Errorf("unable to get patch file from %s to %s in %s Error: %w", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err) diff --git a/services/pull/pull.go b/services/pull/pull.go index 0256c2c3f63..52abf35cec4 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 @@ -264,6 +265,7 @@ func ChangeTargetBranch(ctx context.Context, pr *issues_model.PullRequest, doer ID: pr.Issue.ID, RepoID: pr.Issue.RepoID, Index: pr.Issue.Index, + IsPull: true, } } @@ -706,7 +708,7 @@ func CloseBranchPulls(ctx context.Context, doer *user_model.User, repoID int64, var errs errlist for _, pr := range prs { - if err = issue_service.ChangeStatus(ctx, pr.Issue, doer, "", true); err != nil && !issues_model.IsErrPullWasClosed(err) && !issues_model.IsErrDependenciesLeft(err) { + if err = issue_service.CloseIssue(ctx, pr.Issue, doer, ""); err != nil && !issues_model.IsErrIssueIsClosed(err) && !issues_model.IsErrDependenciesLeft(err) { errs = append(errs, err) } } @@ -740,7 +742,7 @@ func CloseRepoBranchesPulls(ctx context.Context, doer *user_model.User, repo *re if pr.BaseRepoID == repo.ID { continue } - if err = issue_service.ChangeStatus(ctx, pr.Issue, doer, "", true); err != nil && !issues_model.IsErrPullWasClosed(err) { + if err = issue_service.CloseIssue(ctx, pr.Issue, doer, ""); err != nil && !issues_model.IsErrIssueIsClosed(err) { errs = append(errs, err) } } diff --git a/services/release/release.go b/services/release/release.go index 84c60a105a4..835a5943b14 100644 --- a/services/release/release.go +++ b/services/release/release.go @@ -179,7 +179,7 @@ func CreateRelease(gitRepo *git.Repository, rel *repo_model.Release, attachmentU return err } - rel.Title, _ = util.SplitStringAtByteN(rel.Title, 255) + rel.Title = util.EllipsisDisplayString(rel.Title, 255) rel.LowerTagName = strings.ToLower(rel.TagName) if err = db.Insert(gitRepo.Ctx, rel); err != nil { return err diff --git a/services/repository/create.go b/services/repository/create.go index a3199f2a40a..23aacd6f958 100644 --- a/services/repository/create.go +++ b/services/repository/create.go @@ -79,7 +79,7 @@ func prepareRepoCommit(ctx context.Context, repo *repo_model.Repository, tmpDir, return fmt.Errorf("GetRepoInitFile[%s]: %w", opts.Readme, err) } - cloneLink := repo.CloneLink() + cloneLink := repo.CloneLink(ctx, nil /* no doer so do not generate user-related SSH link */) match := map[string]string{ "Name": repo.Name, "Description": repo.Description, diff --git a/services/repository/files/content_test.go b/services/repository/files/content_test.go index a899be70e33..7cb46c0bb67 100644 --- a/services/repository/files/content_test.go +++ b/services/repository/files/content_test.go @@ -53,7 +53,7 @@ func getExpectedReadmeContentsResponse() *api.ContentsResponse { func TestGetContents(t *testing.T) { unittest.PrepareTestEnv(t) ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetPathParam(":id", "1") + ctx.SetPathParam("id", "1") contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepoCommit(t, ctx) contexttest.LoadUser(t, ctx, 2) @@ -81,7 +81,7 @@ func TestGetContents(t *testing.T) { func TestGetContentsOrListForDir(t *testing.T) { unittest.PrepareTestEnv(t) ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetPathParam(":id", "1") + ctx.SetPathParam("id", "1") contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepoCommit(t, ctx) contexttest.LoadUser(t, ctx, 2) @@ -116,7 +116,7 @@ func TestGetContentsOrListForDir(t *testing.T) { func TestGetContentsOrListForFile(t *testing.T) { unittest.PrepareTestEnv(t) ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetPathParam(":id", "1") + ctx.SetPathParam("id", "1") contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepoCommit(t, ctx) contexttest.LoadUser(t, ctx, 2) @@ -144,7 +144,7 @@ func TestGetContentsOrListForFile(t *testing.T) { func TestGetContentsErrors(t *testing.T) { unittest.PrepareTestEnv(t) ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetPathParam(":id", "1") + ctx.SetPathParam("id", "1") contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepoCommit(t, ctx) contexttest.LoadUser(t, ctx, 2) @@ -175,7 +175,7 @@ func TestGetContentsErrors(t *testing.T) { func TestGetContentsOrListErrors(t *testing.T) { unittest.PrepareTestEnv(t) ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetPathParam(":id", "1") + ctx.SetPathParam("id", "1") contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepoCommit(t, ctx) contexttest.LoadUser(t, ctx, 2) @@ -206,7 +206,7 @@ func TestGetContentsOrListErrors(t *testing.T) { func TestGetContentsOrListOfEmptyRepos(t *testing.T) { unittest.PrepareTestEnv(t) ctx, _ := contexttest.MockContext(t, "user30/empty") - ctx.SetPathParam(":id", "52") + ctx.SetPathParam("id", "52") contexttest.LoadRepo(t, ctx, 52) contexttest.LoadUser(t, ctx, 30) contexttest.LoadGitRepo(t, ctx) @@ -231,15 +231,15 @@ func TestGetBlobBySHA(t *testing.T) { defer ctx.Repo.GitRepo.Close() sha := "65f1bf27bc3bf70f64657658635e66094edbcb4d" - ctx.SetPathParam(":id", "1") - ctx.SetPathParam(":sha", sha) + ctx.SetPathParam("id", "1") + ctx.SetPathParam("sha", sha) gitRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository) if err != nil { t.Fail() } - gbr, err := GetBlobBySHA(ctx, ctx.Repo.Repository, gitRepo, ctx.PathParam(":sha")) + gbr, err := GetBlobBySHA(ctx, ctx.Repo.Repository, gitRepo, ctx.PathParam("sha")) expectedGBR := &api.GitBlobResponse{ Content: "dHJlZSAyYTJmMWQ0NjcwNzI4YTJlMTAwNDllMzQ1YmQ3YTI3NjQ2OGJlYWI2CmF1dGhvciB1c2VyMSA8YWRkcmVzczFAZXhhbXBsZS5jb20+IDE0ODk5NTY0NzkgLTA0MDAKY29tbWl0dGVyIEV0aGFuIEtvZW5pZyA8ZXRoYW50a29lbmlnQGdtYWlsLmNvbT4gMTQ4OTk1NjQ3OSAtMDQwMAoKSW5pdGlhbCBjb21taXQK", Encoding: "base64", diff --git a/services/repository/files/diff_test.go b/services/repository/files/diff_test.go index ea6ffe60c3f..b7bdcd8ecf4 100644 --- a/services/repository/files/diff_test.go +++ b/services/repository/files/diff_test.go @@ -18,7 +18,7 @@ import ( func TestGetDiffPreview(t *testing.T) { unittest.PrepareTestEnv(t) ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetPathParam(":id", "1") + ctx.SetPathParam("id", "1") contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepoCommit(t, ctx) contexttest.LoadUser(t, ctx, 2) @@ -140,7 +140,7 @@ func TestGetDiffPreview(t *testing.T) { func TestGetDiffPreviewErrors(t *testing.T) { unittest.PrepareTestEnv(t) ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetPathParam(":id", "1") + ctx.SetPathParam("id", "1") contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepoCommit(t, ctx) contexttest.LoadUser(t, ctx, 2) diff --git a/services/repository/files/file_test.go b/services/repository/files/file_test.go index b2f51e3c820..52c05748834 100644 --- a/services/repository/files/file_test.go +++ b/services/repository/files/file_test.go @@ -99,7 +99,7 @@ func getExpectedFileResponse() *api.FileResponse { func TestGetFileResponseFromCommit(t *testing.T) { unittest.PrepareTestEnv(t) ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetPathParam(":id", "1") + ctx.SetPathParam("id", "1") contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepoCommit(t, ctx) contexttest.LoadUser(t, ctx, 2) diff --git a/services/repository/files/tree_test.go b/services/repository/files/tree_test.go index 786bc15857c..0c60fddf7b4 100644 --- a/services/repository/files/tree_test.go +++ b/services/repository/files/tree_test.go @@ -25,10 +25,10 @@ func TestGetTreeBySHA(t *testing.T) { sha := ctx.Repo.Repository.DefaultBranch page := 1 perPage := 10 - ctx.SetPathParam(":id", "1") - ctx.SetPathParam(":sha", sha) + ctx.SetPathParam("id", "1") + ctx.SetPathParam("sha", sha) - tree, err := GetTreeBySHA(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.PathParam(":sha"), page, perPage, true) + tree, err := GetTreeBySHA(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.PathParam("sha"), page, perPage, true) assert.NoError(t, err) expectedTree := &api.GitTreeResponse{ SHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d", diff --git a/services/repository/generate.go b/services/repository/generate.go index 24cf9d1b9bf..d5c07e98005 100644 --- a/services/repository/generate.go +++ b/services/repository/generate.go @@ -9,7 +9,6 @@ import ( "context" "fmt" "os" - "path" "path/filepath" "regexp" "strconv" @@ -52,7 +51,7 @@ var defaultTransformers = []transformer{ {Name: "TITLE", Transform: util.ToTitleCase}, } -func generateExpansion(src string, templateRepo, generateRepo *repo_model.Repository, sanitizeFileName bool) string { +func generateExpansion(ctx context.Context, src string, templateRepo, generateRepo *repo_model.Repository, sanitizeFileName bool) string { year, month, day := time.Now().Date() expansions := []expansion{ {Name: "YEAR", Value: strconv.Itoa(year), Transformers: nil}, @@ -67,10 +66,10 @@ func generateExpansion(src string, templateRepo, generateRepo *repo_model.Reposi {Name: "TEMPLATE_OWNER", Value: templateRepo.OwnerName, Transformers: defaultTransformers}, {Name: "REPO_LINK", Value: generateRepo.Link(), Transformers: nil}, {Name: "TEMPLATE_LINK", Value: templateRepo.Link(), Transformers: nil}, - {Name: "REPO_HTTPS_URL", Value: generateRepo.CloneLink().HTTPS, Transformers: nil}, - {Name: "TEMPLATE_HTTPS_URL", Value: templateRepo.CloneLink().HTTPS, Transformers: nil}, - {Name: "REPO_SSH_URL", Value: generateRepo.CloneLink().SSH, Transformers: nil}, - {Name: "TEMPLATE_SSH_URL", Value: templateRepo.CloneLink().SSH, Transformers: nil}, + {Name: "REPO_HTTPS_URL", Value: generateRepo.CloneLinkGeneral(ctx).HTTPS, Transformers: nil}, + {Name: "TEMPLATE_HTTPS_URL", Value: templateRepo.CloneLinkGeneral(ctx).HTTPS, Transformers: nil}, + {Name: "REPO_SSH_URL", Value: generateRepo.CloneLinkGeneral(ctx).SSH, Transformers: nil}, + {Name: "TEMPLATE_SSH_URL", Value: templateRepo.CloneLinkGeneral(ctx).SSH, Transformers: nil}, } expansionMap := make(map[string]string) @@ -123,7 +122,7 @@ func (gt *GiteaTemplate) Globs() []glob.Glob { return gt.globs } -func checkGiteaTemplate(tmpDir string) (*GiteaTemplate, error) { +func readGiteaTemplateFile(tmpDir string) (*GiteaTemplate, error) { gtPath := filepath.Join(tmpDir, ".gitea", "template") if _, err := os.Stat(gtPath); os.IsNotExist(err) { return nil, nil @@ -136,12 +135,55 @@ func checkGiteaTemplate(tmpDir string) (*GiteaTemplate, error) { return nil, err } - gt := &GiteaTemplate{ - Path: gtPath, - Content: content, + return &GiteaTemplate{Path: gtPath, Content: content}, nil +} + +func processGiteaTemplateFile(ctx context.Context, tmpDir string, templateRepo, generateRepo *repo_model.Repository, giteaTemplateFile *GiteaTemplate) error { + if err := util.Remove(giteaTemplateFile.Path); err != nil { + return fmt.Errorf("remove .giteatemplate: %w", err) } + if len(giteaTemplateFile.Globs()) == 0 { + return nil // Avoid walking tree if there are no globs + } + tmpDirSlash := strings.TrimSuffix(filepath.ToSlash(tmpDir), "/") + "/" + return filepath.WalkDir(tmpDirSlash, func(path string, d os.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + + if d.IsDir() { + return nil + } + + base := strings.TrimPrefix(filepath.ToSlash(path), tmpDirSlash) + for _, g := range giteaTemplateFile.Globs() { + if g.Match(base) { + content, err := os.ReadFile(path) + if err != nil { + return err + } - return gt, nil + generatedContent := []byte(generateExpansion(ctx, string(content), templateRepo, generateRepo, false)) + if err := os.WriteFile(path, generatedContent, 0o644); err != nil { + return err + } + + substPath := filepath.FromSlash(filepath.Join(tmpDirSlash, generateExpansion(ctx, base, templateRepo, generateRepo, true))) + + // Create parent subdirectories if needed or continue silently if it exists + if err = os.MkdirAll(filepath.Dir(substPath), 0o755); err != nil { + return err + } + + // Substitute filename variables + if err = os.Rename(path, substPath); err != nil { + return err + } + break + } + } + return nil + }) // end: WalkDir } func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *repo_model.Repository, tmpDir string) error { @@ -167,81 +209,43 @@ func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *r return fmt.Errorf("git clone: %w", err) } - if err := util.RemoveAll(path.Join(tmpDir, ".git")); err != nil { + // Get active submodules from the template + submodules, err := git.GetTemplateSubmoduleCommits(ctx, tmpDir) + if err != nil { + return fmt.Errorf("GetTemplateSubmoduleCommits: %w", err) + } + + if err = util.RemoveAll(filepath.Join(tmpDir, ".git")); err != nil { return fmt.Errorf("remove git dir: %w", err) } // Variable expansion - gt, err := checkGiteaTemplate(tmpDir) + giteaTemplateFile, err := readGiteaTemplateFile(tmpDir) if err != nil { - return fmt.Errorf("checkGiteaTemplate: %w", err) + return fmt.Errorf("readGiteaTemplateFile: %w", err) } - if gt != nil { - if err := util.Remove(gt.Path); err != nil { - return fmt.Errorf("remove .giteatemplate: %w", err) - } - - // Avoid walking tree if there are no globs - if len(gt.Globs()) > 0 { - tmpDirSlash := strings.TrimSuffix(filepath.ToSlash(tmpDir), "/") + "/" - if err := filepath.WalkDir(tmpDirSlash, func(path string, d os.DirEntry, walkErr error) error { - if walkErr != nil { - return walkErr - } - - if d.IsDir() { - return nil - } - - base := strings.TrimPrefix(filepath.ToSlash(path), tmpDirSlash) - for _, g := range gt.Globs() { - if g.Match(base) { - content, err := os.ReadFile(path) - if err != nil { - return err - } - - if err := os.WriteFile(path, - []byte(generateExpansion(string(content), templateRepo, generateRepo, false)), - 0o644); err != nil { - return err - } - - substPath := filepath.FromSlash(filepath.Join(tmpDirSlash, - generateExpansion(base, templateRepo, generateRepo, true))) - - // Create parent subdirectories if needed or continue silently if it exists - if err := os.MkdirAll(filepath.Dir(substPath), 0o755); err != nil { - return err - } - - // Substitute filename variables - if err := os.Rename(path, substPath); err != nil { - return err - } - - break - } - } - return nil - }); err != nil { - return err - } + if giteaTemplateFile != nil { + err = processGiteaTemplateFile(ctx, tmpDir, templateRepo, generateRepo, giteaTemplateFile) + if err != nil { + return err } } - if err := git.InitRepository(ctx, tmpDir, false, templateRepo.ObjectFormatName); err != nil { + if err = git.InitRepository(ctx, tmpDir, false, templateRepo.ObjectFormatName); err != nil { return err } - repoPath := repo.RepoPath() - if stdout, _, err := git.NewCommand(ctx, "remote", "add", "origin").AddDynamicArguments(repoPath). + if stdout, _, err := git.NewCommand(ctx, "remote", "add", "origin").AddDynamicArguments(repo.RepoPath()). RunStdString(&git.RunOpts{Dir: tmpDir, Env: env}); err != nil { log.Error("Unable to add %v as remote origin to temporary repo to %s: stdout %s\nError: %v", repo, tmpDir, stdout, err) return fmt.Errorf("git remote add: %w", err) } + if err = git.AddTemplateSubmoduleIndexes(ctx, tmpDir, submodules); err != nil { + return fmt.Errorf("failed to add submodules: %v", err) + } + // set default branch based on whether it's specified in the newly generated repo or not defaultBranch := repo.DefaultBranch if strings.TrimSpace(defaultBranch) == "" { diff --git a/services/repository/migrate.go b/services/repository/migrate.go index 7fe63eb5ca7..6f3a87afa3e 100644 --- a/services/repository/migrate.go +++ b/services/repository/migrate.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" "net/http" - "strings" "time" "code.gitea.io/gitea/models/db" @@ -254,10 +253,10 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User, func cleanUpMigrateGitConfig(ctx context.Context, repoPath string) error { cmd := git.NewCommand(ctx, "remote", "rm", "origin") // if the origin does not exist - _, stderr, err := cmd.RunStdString(&git.RunOpts{ + _, _, err := cmd.RunStdString(&git.RunOpts{ Dir: repoPath, }) - if err != nil && !strings.HasPrefix(stderr, "fatal: No such remote") { + if err != nil && !git.IsRemoteNotExistError(err) { return err } return nil @@ -276,7 +275,7 @@ func CleanUpMigrateInfo(ctx context.Context, repo *repo_model.Repository) (*repo } _, _, err := git.NewCommand(ctx, "remote", "rm", "origin").RunStdString(&git.RunOpts{Dir: repoPath}) - if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") { + if err != nil && !git.IsRemoteNotExistError(err) { return repo, fmt.Errorf("CleanUpMigrateInfo: %w", err) } diff --git a/services/webhook/webhook_test.go b/services/webhook/webhook_test.go index 63cbce17712..6bac02712bb 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 41b00defb45..d591a645d89 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 6801c8240fd..6d4c1fb2db2 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 ed7a8d6f243..baf37494b98 100644 --- a/templates/base/head_navbar.tmpl +++ b/templates/base/head_navbar.tmpl @@ -70,7 +70,7 @@ {{svg "octicon-triangle-down"}}