diff options
557 files changed, 9984 insertions, 7290 deletions
diff --git a/.eslintrc.cjs b/.eslintrc.cjs index af744db3e1..f52da3fa5d 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -674,7 +674,7 @@ module.exports = { 'no-this-before-super': [2], 'no-throw-literal': [2], 'no-undef-init': [2], - 'no-undef': [0], + 'no-undef': [2], // it is still needed by eslint & IDE to prompt undefined names in real time 'no-undefined': [0], 'no-underscore-dangle': [0], 'no-unexpected-multiline': [2], diff --git a/.gitignore b/.gitignore index 86e6e4fefd..619cb1cabb 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,10 @@ _test # IntelliJ .idea + +# IntelliJ Gateway +.uuid + # Goland's output filename can not be set manually /go_build_* /gitea_* diff --git a/Dockerfile b/Dockerfile index 2a6b1dd6b7..b95ba83289 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build stage -FROM docker.io/library/golang:1.23-alpine3.20 AS build-env +FROM docker.io/library/golang:1.23-alpine3.21 AS build-env ARG GOPROXY ENV GOPROXY=${GOPROXY:-direct} @@ -41,7 +41,7 @@ RUN chmod 755 /tmp/local/usr/bin/entrypoint \ /go/src/code.gitea.io/gitea/environment-to-ini RUN chmod 644 /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete -FROM docker.io/library/alpine:3.20 +FROM docker.io/library/alpine:3.21 LABEL maintainer="maintainers@gitea.io" EXPOSE 22 3000 @@ -78,7 +78,7 @@ ENV GITEA_CUSTOM=/data/gitea VOLUME ["/data"] ENTRYPOINT ["/usr/bin/entrypoint"] -CMD ["/bin/s6-svscan", "/etc/s6"] +CMD ["/usr/bin/s6-svscan", "/etc/s6"] COPY --from=build-env /tmp/local / COPY --from=build-env /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea diff --git a/Dockerfile.rootless b/Dockerfile.rootless index 26f02205a7..be6f125104 100644 --- a/Dockerfile.rootless +++ b/Dockerfile.rootless @@ -1,5 +1,5 @@ # Build stage -FROM docker.io/library/golang:1.23-alpine3.20 AS build-env +FROM docker.io/library/golang:1.23-alpine3.21 AS build-env ARG GOPROXY ENV GOPROXY=${GOPROXY:-direct} @@ -39,7 +39,7 @@ RUN chmod 755 /tmp/local/usr/local/bin/docker-entrypoint.sh \ /go/src/code.gitea.io/gitea/environment-to-ini RUN chmod 644 /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete -FROM docker.io/library/alpine:3.20 +FROM docker.io/library/alpine:3.21 LABEL maintainer="maintainers@gitea.io" EXPOSE 2222 3000 diff --git a/MAINTAINERS b/MAINTAINERS index ad02ecc755..f0caae4d22 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -31,7 +31,6 @@ Gary Kim <gary@garykim.dev> (@gary-kim) Guillermo Prandi <gitea.maint@mailfilter.com.ar> (@guillep2k) Mura Li <typeless@ctli.io> (@typeless) 6543 <6543@obermui.de> (@6543) -jaqra <jaqra@hotmail.com> (@jaqra) David Svantesson <davidsvantesson@gmail.com> (@davidsvantesson) a1012112796 <1012112796@qq.com> (@a1012112796) Karl Heinz Marbaise <kama@soebes.de> (@khmarbaise) @@ -63,3 +62,4 @@ Yu Liu <1240335630@qq.com> (@HEREYUA) Kemal Zebari <kemalzebra@gmail.com> (@kemzeb) Rowan Bohde <rowan.bohde@gmail.com> (@bohde) hiifong <i@hiif.ong> (@hiifong) +metiftikci <metiftikci@hotmail.com> (@metiftikci) @@ -806,22 +806,22 @@ $(DIST_DIRS): .PHONY: release-windows release-windows: | $(DIST_DIRS) - CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION) . + CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo $(TAGS)' -ldflags '-s -w -linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION) . ifeq (,$(findstring gogit,$(TAGS))) - CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo gogit $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION)-gogit . + CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo gogit $(TAGS)' -ldflags '-s -w -linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION)-gogit . endif .PHONY: release-linux release-linux: | $(DIST_DIRS) - CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets '$(LINUX_ARCHS)' -out gitea-$(VERSION) . + CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w -linkmode external -extldflags "-static" $(LDFLAGS)' -targets '$(LINUX_ARCHS)' -out gitea-$(VERSION) . .PHONY: release-darwin release-darwin: | $(DIST_DIRS) - CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '$(LDFLAGS)' -targets 'darwin-10.12/amd64,darwin-10.12/arm64' -out gitea-$(VERSION) . + CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w $(LDFLAGS)' -targets 'darwin-10.12/amd64,darwin-10.12/arm64' -out gitea-$(VERSION) . .PHONY: release-freebsd release-freebsd: | $(DIST_DIRS) - CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '$(LDFLAGS)' -targets 'freebsd/amd64' -out gitea-$(VERSION) . + CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w $(LDFLAGS)' -targets 'freebsd/amd64' -out gitea-$(VERSION) . .PHONY: release-copy release-copy: | $(DIST_DIRS) diff --git a/assets/go-licenses.json b/assets/go-licenses.json index 64c3b8b51c..a20494184b 100644 --- a/assets/go-licenses.json +++ b/assets/go-licenses.json @@ -105,16 +105,6 @@ "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", "licenseText": "Simplified BSD License\n\nCopyright (c) 2016, Datadog \u003cinfo@datadoghq.com\u003e\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,\n this list of conditions and the following disclaimer.\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 * Neither the name of the copyright holder nor the names of its contributors\n may be used to endorse or promote products derived from this software\n 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 OWNER 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" @@ -180,16 +170,6 @@ "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", "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" @@ -520,16 +500,6 @@ "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", "licenseText": "BSD 3-Clause License\n\nCopyright (c) 2018, go-fed\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" @@ -570,11 +540,6 @@ "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", "licenseText": "Copyright (c) 2017 Duo Security, Inc. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions\nare met:\n\n1. Redistributions of source code must retain the above copyright\n notice, this list of conditions and the following disclaimer.\n2. Redistributions in binary form must reproduce the above copyright\n notice, this list of conditions and the following disclaimer in the\n documentation and/or other materials provided with the distribution.\n3. 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\nIS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR\nCONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\nEXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\nPROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\nPROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\nLIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\nNEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\nCopyright (c) 2021-2022 github.com/go-webauthn/webauthn authors.\n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the\nfollowing conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following\n disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following\n disclaimer in the documentation and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products\n derived from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES,\nINCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,\nWHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF\nTHIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." @@ -910,6 +875,11 @@ "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", "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" @@ -965,11 +935,6 @@ "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", "licenseText": "Copyright (c) 2015, Pierre Curto\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 xxHash 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\n" @@ -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", @@ -1060,21 +1025,11 @@ "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" }, { - "name": "github.com/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", "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2014 Simon Eskildsen\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" @@ -1140,11 +1095,6 @@ "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", "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"{}\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright {yyyy} {name of copyright owner}\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n\n" @@ -1180,21 +1130,16 @@ "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", "licenseText": "Copyright (c) 2016 Uber Technologies, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n" @@ -1255,6 +1200,11 @@ "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", "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" diff --git a/cmd/admin_user_create.go b/cmd/admin_user_create.go index 106d14b25a..bf8cbc7c4c 100644 --- a/cmd/admin_user_create.go +++ b/cmd/admin_user_create.go @@ -69,6 +69,10 @@ var microcmdUserCreate = &cli.Command{ } func runCreateUser(c *cli.Context) error { + // this command highly depends on the many setting options (create org, visibility, etc.), so it must have a full setting load first + // duplicate setting loading should be safe at the moment, but it should be refactored & improved in the future. + setting.LoadSettings() + if err := argsSet(c, "email"); err != nil { return err } diff --git a/cmd/main.go b/cmd/main.go index fd648946ef..7251bd09a3 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -165,6 +165,7 @@ func NewMainApp(appVer AppVersion) *cli.App { app.Commands = append(app.Commands, subCmdWithConfig...) app.Commands = append(app.Commands, subCmdStandalone...) + setting.InitGiteaEnvVars() return app } diff --git a/cmd/main_test.go b/cmd/main_test.go index c182b44019..3ec584d323 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -6,7 +6,6 @@ package cmd import ( "fmt" "io" - "os" "path/filepath" "strings" "testing" @@ -113,37 +112,17 @@ func TestCliCmd(t *testing.T) { _, _ = fmt.Fprint(ctx.App.Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf)) return nil }) - var envBackup []string - for _, s := range os.Environ() { - if strings.HasPrefix(s, "GITEA_") && strings.Contains(s, "=") { - envBackup = append(envBackup, s) - } - } - clearGiteaEnv := func() { - for _, s := range os.Environ() { - if strings.HasPrefix(s, "GITEA_") { - _ = os.Unsetenv(s) - } - } - } - defer func() { - clearGiteaEnv() - for _, s := range envBackup { - k, v, _ := strings.Cut(s, "=") - _ = os.Setenv(k, v) - } - }() - for _, c := range cases { - clearGiteaEnv() - for k, v := range c.env { - _ = os.Setenv(k, v) - } - args := strings.Split(c.cmd, " ") // for test only, "split" is good enough - r, err := runTestApp(app, args...) - assert.NoError(t, err, c.cmd) - assert.NotEmpty(t, c.exp, c.cmd) - assert.Contains(t, r.Stdout, c.exp, c.cmd) + t.Run(c.cmd, func(t *testing.T) { + for k, v := range c.env { + t.Setenv(k, v) + } + args := strings.Split(c.cmd, " ") // for test only, "split" is good enough + r, err := runTestApp(app, args...) + assert.NoError(t, err, c.cmd) + assert.NotEmpty(t, c.exp, c.cmd) + assert.Contains(t, r.Stdout, c.exp, c.cmd) + }) } } diff --git a/cmd/migrate.go b/cmd/migrate.go index 4e4dd45af3..459805a76d 100644 --- a/cmd/migrate.go +++ b/cmd/migrate.go @@ -18,7 +18,7 @@ import ( var CmdMigrate = &cli.Command{ Name: "migrate", Usage: "Migrate the database", - Description: "This is a command for migrating the database, so that you can run gitea admin create-user before starting the server.", + Description: `This is a command for migrating the database, so that you can run "gitea admin create user" before starting the server.`, Action: runMigrate, } diff --git a/cmd/web.go b/cmd/web.go index ef8a7426c1..f8217758e5 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -12,6 +12,7 @@ import ( "path/filepath" "strconv" "strings" + "time" _ "net/http/pprof" // Used for debugging if enabled and a web server is running @@ -115,6 +116,16 @@ func showWebStartupMessage(msg string) { log.Info("* CustomPath: %s", setting.CustomPath) log.Info("* ConfigFile: %s", setting.CustomConf) log.Info("%s", msg) // show startup message + + if setting.CORSConfig.Enabled { + log.Info("CORS Service Enabled") + } + if setting.DefaultUILocation != time.Local { + log.Info("Default UI Location is %v", setting.DefaultUILocation.String()) + } + if setting.MailService != nil { + log.Info("Mail Service Enabled: RegisterEmailConfirm=%v, Service.EnableNotifyMail=%v", setting.Service.RegisterEmailConfirm, setting.Service.EnableNotifyMail) + } } func serveInstall(ctx *cli.Context) error { diff --git a/cmd/web_acme.go b/cmd/web_acme.go index 90e4a02764..2fe14c1f54 100644 --- a/cmd/web_acme.go +++ b/cmd/web_acme.go @@ -54,7 +54,7 @@ func runACME(listenAddr string, m http.Handler) error { altTLSALPNPort = p } - magic := certmagic.NewDefault() + magic := &certmagic.Default magic.Storage = &certmagic.FileStorage{Path: setting.AcmeLiveDirectory} // Try to use private CA root if provided, otherwise defaults to system's trust var certPool *x509.CertPool diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 6377ebf9d2..8e64c834d7 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 @@ -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 @@ -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 f40bc1eb3d..147a8d8b3e 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 b35a76680c..0d5464a5be 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 fd6ba7ecad..bbd2af73b6 100644 --- a/models/actions/runner_token.go +++ b/models/actions/runner_token.go @@ -51,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 %q: %w", token, util.ErrNotExist) + return nil, fmt.Errorf(`runner token "%s...": %w`, util.TruncateRunes(token, 3), util.ErrNotExist) } return &runnerToken, nil } @@ -68,19 +68,15 @@ func UpdateRunnerToken(ctx context.Context, r *ActionRunnerToken, cols ...string return err } -// NewRunnerToken creates a new active runner token and invalidate all old tokens +// NewRunnerTokenWithValue creates a new active runner token and invalidate all old tokens // ownerID will be ignored and treated as 0 if repoID is non-zero. -func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) { +func NewRunnerTokenWithValue(ctx context.Context, ownerID, repoID int64, token string) (*ActionRunnerToken, error) { if ownerID != 0 && repoID != 0 { // It's trying to create a runner token that belongs to a repository, but OwnerID has been set accidentally. // Remove OwnerID to avoid confusion; it's not worth returning an error here. ownerID = 0 } - token, err := util.CryptoRandomString(40) - if err != nil { - return nil, err - } runnerToken := &ActionRunnerToken{ OwnerID: ownerID, RepoID: repoID, @@ -95,11 +91,19 @@ func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerTo return err } - _, err = db.GetEngine(ctx).Insert(runnerToken) + _, err := db.GetEngine(ctx).Insert(runnerToken) return err }) } +func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) { + token, err := util.CryptoRandomString(40) + if err != nil { + return nil, err + } + return NewRunnerTokenWithValue(ctx, ownerID, repoID, token) +} + // GetLatestRunnerToken returns the latest runner token func GetLatestRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) { if ownerID != 0 && repoID != 0 { diff --git a/models/actions/schedule.go b/models/actions/schedule.go index 961ffd0851..e2cc32eedc 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 af74faf937..9f13ff94c9 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 ff7fdb2f10..8304210188 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 3ffad035b7..3ccdbd47d3 100644 --- a/models/activities/repo_activity.go +++ b/models/activities/repo_activity.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" + "xorm.io/builder" "xorm.io/xorm" ) @@ -337,8 +338,10 @@ func newlyCreatedIssues(ctx context.Context, repoID int64, fromTime time.Time) * func activeIssues(ctx context.Context, repoID int64, fromTime time.Time) *xorm.Session { sess := db.GetEngine(ctx).Where("issue.repo_id = ?", repoID). And("issue.is_pull = ?", false). - And("issue.created_unix >= ?", fromTime.Unix()). - Or("issue.closed_unix >= ?", fromTime.Unix()) + And(builder.Or( + builder.Gte{"issue.created_unix": fromTime.Unix()}, + builder.Gte{"issue.closed_unix": fromTime.Unix()}, + )) return sess } diff --git a/models/activities/user_heatmap_test.go b/models/activities/user_heatmap_test.go index a039fd3613..380045d3c5 100644 --- a/models/activities/user_heatmap_test.go +++ b/models/activities/user_heatmap_test.go @@ -64,11 +64,9 @@ func TestGetUserHeatmapDataByUser(t *testing.T) { for _, tc := range testCases { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: tc.userID}) - doer := &user_model.User{ID: tc.doerID} - _, err := unittest.LoadBeanIfExists(doer) - assert.NoError(t, err) - if tc.doerID == 0 { - doer = nil + var doer *user_model.User + if tc.doerID != 0 { + doer = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: tc.doerID}) } // get the action for comparison diff --git a/models/auth/webauthn_test.go b/models/auth/webauthn_test.go index f1cf398adf..654427e974 100644 --- a/models/auth/webauthn_test.go +++ b/models/auth/webauthn_test.go @@ -44,7 +44,7 @@ func TestWebAuthnCredential_UpdateSignCount(t *testing.T) { cred := unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1}) cred.SignCount = 1 assert.NoError(t, cred.UpdateSignCount(db.DefaultContext)) - unittest.AssertExistsIf(t, true, &auth_model.WebAuthnCredential{ID: 1, SignCount: 1}) + unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1, SignCount: 1}) } func TestWebAuthnCredential_UpdateLargeCounter(t *testing.T) { @@ -52,7 +52,7 @@ func TestWebAuthnCredential_UpdateLargeCounter(t *testing.T) { cred := unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1}) cred.SignCount = 0xffffffff assert.NoError(t, cred.UpdateSignCount(db.DefaultContext)) - unittest.AssertExistsIf(t, true, &auth_model.WebAuthnCredential{ID: 1, SignCount: 0xffffffff}) + unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1, SignCount: 0xffffffff}) } func TestCreateCredential(t *testing.T) { @@ -63,5 +63,5 @@ func TestCreateCredential(t *testing.T) { assert.Equal(t, "WebAuthn Created Credential", res.Name) assert.Equal(t, []byte("Test"), res.CredentialID) - unittest.AssertExistsIf(t, true, &auth_model.WebAuthnCredential{Name: "WebAuthn Created Credential", UserID: 1}) + unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{Name: "WebAuthn Created Credential", UserID: 1}) } diff --git a/models/db/name.go b/models/db/name.go index 55c9dffb6a..5f98edbb28 100644 --- a/models/db/name.go +++ b/models/db/name.go @@ -5,20 +5,13 @@ package db import ( "fmt" - "regexp" "strings" "unicode/utf8" "code.gitea.io/gitea/modules/util" ) -var ( - // ErrNameEmpty name is empty error - ErrNameEmpty = util.SilentWrap{Message: "name is empty", Err: util.ErrInvalidArgument} - - // AlphaDashDotPattern characters prohibited in a username (anything except A-Za-z0-9_.-) - AlphaDashDotPattern = regexp.MustCompile(`[^\w-\.]`) -) +var ErrNameEmpty = util.SilentWrap{Message: "name is empty", Err: util.ErrInvalidArgument} // ErrNameReserved represents a "reserved name" error. type ErrNameReserved struct { @@ -82,20 +75,20 @@ func (err ErrNameCharsNotAllowed) Unwrap() error { // IsUsableName checks if name is reserved or pattern of name is not allowed // based on given reserved names and patterns. -// Names are exact match, patterns can be prefix or suffix match with placeholder '*'. -func IsUsableName(names, patterns []string, name string) error { +// Names are exact match, patterns can be a prefix or suffix match with placeholder '*'. +func IsUsableName(reservedNames, reservedPatterns []string, name string) error { name = strings.TrimSpace(strings.ToLower(name)) if utf8.RuneCountInString(name) == 0 { return ErrNameEmpty } - for i := range names { - if name == names[i] { + for i := range reservedNames { + if name == reservedNames[i] { return ErrNameReserved{name} } } - for _, pat := range patterns { + for _, pat := range reservedPatterns { if pat[0] == '*' && strings.HasSuffix(name, pat[1:]) || (pat[len(pat)-1] == '*' && strings.HasPrefix(name, pat[:len(pat)-1])) { return ErrNamePatternNotAllowed{pat} diff --git a/models/fixtures/action_run_job.yml b/models/fixtures/action_run_job.yml index 9b6f5b9a88..8837e6ec2d 100644 --- a/models/fixtures/action_run_job.yml +++ b/models/fixtures/action_run_job.yml @@ -64,7 +64,7 @@ name: job2 attempt: 1 job_id: job2 - needs: [job1] + needs: '["job1"]' task_id: 51 status: 5 started: 1683636528 diff --git a/models/fixtures/label.yml b/models/fixtures/label.yml index 2242b90dcd..acfac74968 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 dbec52c0c2..1944e7bd84 100644 --- a/models/fixtures/protected_tag.yml +++ b/models/fixtures/protected_tag.yml @@ -2,23 +2,23 @@ id: 1 repo_id: 4 name_pattern: /v.+/ - allowlist_user_i_ds: [] - allowlist_team_i_ds: [] + allowlist_user_i_ds: "[]" + allowlist_team_i_ds: "[]" created_unix: 1715596037 updated_unix: 1715596037 - id: 2 repo_id: 1 name_pattern: v-* - allowlist_user_i_ds: [] - allowlist_team_i_ds: [] + allowlist_user_i_ds: "[]" + allowlist_team_i_ds: "[]" created_unix: 1715596037 updated_unix: 1715596037 - id: 3 repo_id: 1 name_pattern: v-1.1 - allowlist_user_i_ds: [2] - allowlist_team_i_ds: [] + allowlist_user_i_ds: "[2]" + allowlist_team_i_ds: "[]" created_unix: 1715596037 updated_unix: 1715596037 diff --git a/models/issues/comment.go b/models/issues/comment.go index e4537aa872..a7ec8f57fc 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -197,6 +197,20 @@ func (t CommentType) HasMailReplySupport() bool { return false } +func (t CommentType) CountedAsConversation() bool { + for _, ct := range ConversationCountedCommentType() { + if t == ct { + return true + } + } + return false +} + +// ConversationCountedCommentType returns the comment types that are counted as a conversation +func ConversationCountedCommentType() []CommentType { + return []CommentType{CommentTypeComment, CommentTypeReview} +} + // RoleInRepo presents the user's participation in the repo type RoleInRepo string @@ -592,26 +606,26 @@ func (c *Comment) LoadAttachments(ctx context.Context) error { return nil } -// UpdateAttachments update attachments by UUIDs for the comment -func (c *Comment) UpdateAttachments(ctx context.Context, uuids []string) error { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - 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 d81f33f953..ae0bc3ce17 100644 --- a/models/issues/comment_test.go +++ b/models/issues/comment_test.go @@ -45,6 +45,24 @@ func TestCreateComment(t *testing.T) { unittest.AssertInt64InRange(t, now, then, int64(updatedIssue.UpdatedUnix)) } +func Test_UpdateCommentAttachment(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1}) + attachment := repo_model.Attachment{ + Name: "test.txt", + } + assert.NoError(t, db.Insert(db.DefaultContext, &attachment)) + + err := issues_model.UpdateCommentAttachments(db.DefaultContext, comment, []string{attachment.UUID}) + assert.NoError(t, err) + + attachment2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: attachment.ID}) + assert.EqualValues(t, attachment.Name, attachment2.Name) + assert.EqualValues(t, comment.ID, attachment2.CommentID) + assert.EqualValues(t, comment.IssueID, attachment2.IssueID) +} + func TestFetchCodeComments(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) @@ -97,3 +115,12 @@ func TestMigrate_InsertIssueComments(t *testing.T) { unittest.CheckConsistencyFor(t, &issues_model.Issue{}) } + +func Test_UpdateIssueNumComments(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) + + assert.NoError(t, issues_model.UpdateIssueNumComments(db.DefaultContext, issue2.ID)) + issue2 = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) + assert.EqualValues(t, 1, issue2.NumComments) +} diff --git a/models/issues/dependency_test.go b/models/issues/dependency_test.go index 6eed483cc9..67418039de 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 fe347c2715..1777fbb6a6 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 5b929c9115..7b3fe04aa5 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 ce47adb53a..7c21aa15ee 100644 --- a/models/issues/issue_user_test.go +++ b/models/issues/issue_user_test.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/models/unittest" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_NewIssueUsers(t *testing.T) { @@ -27,9 +28,8 @@ func Test_NewIssueUsers(t *testing.T) { } // artificially insert new issue - unittest.AssertSuccessfulInsert(t, newIssue) - - assert.NoError(t, issues_model.NewIssueUsers(db.DefaultContext, repo, newIssue)) + require.NoError(t, db.Insert(db.DefaultContext, newIssue)) + require.NoError(t, issues_model.NewIssueUsers(db.DefaultContext, repo, newIssue)) // issue_user table should now have entries for new issue unittest.AssertExistsAndLoadBean(t, &issues_model.IssueUser{IssueID: newIssue.ID, UID: newIssue.PosterID}) diff --git a/models/issues/issue_xref_test.go b/models/issues/issue_xref_test.go index f1b1bb2a6b..7f257330b7 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 d80578193e..b9d24bbe99 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 1d4b6f4684..185fa11bbc 100644 --- a/models/issues/label_test.go +++ b/models/issues/label_test.go @@ -387,7 +387,7 @@ func TestDeleteIssueLabel(t *testing.T) { expectedNumIssues := label.NumIssues expectedNumClosedIssues := label.NumClosedIssues - if unittest.BeanExists(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: labelID}) { + if unittest.GetBean(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: labelID}) != nil { expectedNumIssues-- if issue.IsClosed { expectedNumClosedIssues-- diff --git a/models/issues/pull.go b/models/issues/pull.go index 853e2a69e6..e3af00224d 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 8b345e5fd8..3e787273be 100644 --- a/models/issues/review.go +++ b/models/issues/review.go @@ -639,6 +639,10 @@ func InsertReviews(ctx context.Context, reviews []*Review) error { return err } } + + if err := UpdateIssueNumComments(ctx, review.IssueID); err != nil { + return err + } } return committer.Commit() diff --git a/models/issues/review_list.go b/models/issues/review_list.go index bc7d7ec0f0..928f24fb2d 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 50330e3ff2..2588b8ba41 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 2eb85cd8a7..fe6de9c517 100644 --- a/models/migrations/base/tests.go +++ b/models/migrations/base/tests.go @@ -76,7 +76,7 @@ func PrepareTestEnv(t *testing.T, skip int, syncModels ...any) (*xorm.Engine, fu t.Errorf("error whilst initializing fixtures from %s: %v", fixturesDir, err) return x, deferFn } - if err := unittest.LoadFixtures(x); err != nil { + if err := unittest.LoadFixtures(); err != nil { t.Errorf("error whilst loading fixtures from %s: %v", fixturesDir, err) return x, deferFn } diff --git a/models/migrations/v1_21/v276.go b/models/migrations/v1_21/v276.go index 15177bf040..9d22c9052e 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 55abb63203..c5110b2a34 100644 --- a/models/organization/org_user_test.go +++ b/models/organization/org_user_test.go @@ -131,7 +131,7 @@ func TestAddOrgUser(t *testing.T) { testSuccess := func(orgID, userID int64, isPublic bool) { org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: orgID}) expectedNumMembers := org.NumMembers - if !unittest.BeanExists(t, &organization.OrgUser{OrgID: orgID, UID: userID}) { + if unittest.GetBean(t, &organization.OrgUser{OrgID: orgID, UID: userID}) == nil { expectedNumMembers++ } assert.NoError(t, organization.AddOrgUser(db.DefaultContext, orgID, userID)) diff --git a/models/packages/package.go b/models/packages/package.go index c12f345f0e..31e1277a6e 100644 --- a/models/packages/package.go +++ b/models/packages/package.go @@ -248,6 +248,18 @@ func GetPackageByID(ctx context.Context, packageID int64) (*Package, error) { return p, nil } +// UpdatePackageNameByID updates the package's name, it is only for internal usage, for example: rename some legacy packages +func UpdatePackageNameByID(ctx context.Context, ownerID int64, packageType Type, packageID int64, name string) error { + var cond builder.Cond = builder.Eq{ + "package.id": packageID, + "package.owner_id": ownerID, + "package.type": packageType, + "package.is_internal": false, + } + _, err := db.GetEngine(ctx).Where(cond).Update(&Package{Name: name, LowerName: strings.ToLower(name)}) + return err +} + // GetPackageByName gets a package by name func GetPackageByName(ctx context.Context, ownerID int64, packageType Type, name string) (*Package, error) { var cond builder.Cond = builder.Eq{ diff --git a/models/project/project.go b/models/project/project.go index 9779908b9d..edeb0b4742 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -126,6 +126,14 @@ func (p *Project) LoadRepo(ctx context.Context) (err error) { return err } +func ProjectLinkForOrg(org *user_model.User, projectID int64) string { //nolint + return fmt.Sprintf("%s/-/projects/%d", org.HomeLink(), projectID) +} + +func ProjectLinkForRepo(repo *repo_model.Repository, projectID int64) string { //nolint + return fmt.Sprintf("%s/projects/%d", repo.Link(), projectID) +} + // Link returns the project's relative URL. func (p *Project) Link(ctx context.Context) string { if p.OwnerID > 0 { @@ -134,7 +142,7 @@ func (p *Project) Link(ctx context.Context) string { log.Error("LoadOwner: %v", err) return "" } - return fmt.Sprintf("%s/-/projects/%d", p.Owner.HomeLink(), p.ID) + return ProjectLinkForOrg(p.Owner, p.ID) } if p.RepoID > 0 { err := p.LoadRepo(ctx) @@ -142,7 +150,7 @@ func (p *Project) Link(ctx context.Context) string { log.Error("LoadRepo: %v", err) return "" } - return fmt.Sprintf("%s/projects/%d", p.Repo.Link(), p.ID) + return ProjectLinkForRepo(p.Repo, p.ID) } return "" } @@ -256,7 +264,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 { @@ -311,7 +319,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 3e9c52fdd9..9bc67079a9 100644 --- a/models/repo.go +++ b/models/repo.go @@ -16,6 +16,8 @@ import ( "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" + + "xorm.io/builder" ) // Init initialize model @@ -24,7 +26,7 @@ func Init(ctx context.Context) error { } type repoChecker struct { - querySQL func(ctx context.Context) ([]map[string][]byte, error) + querySQL func(ctx context.Context) ([]int64, error) correctSQL func(ctx context.Context, id int64) error desc string } @@ -35,8 +37,7 @@ func repoStatsCheck(ctx context.Context, checker *repoChecker) { log.Error("Select %s: %v", checker.desc, err) return } - for _, result := range results { - id, _ := strconv.ParseInt(string(result["id"]), 10, 64) + for _, id := range results { select { case <-ctx.Done(): log.Warn("CheckRepoStats: Cancelled before checking %s for with id=%d", checker.desc, id) @@ -51,21 +52,23 @@ func repoStatsCheck(ctx context.Context, checker *repoChecker) { } } -func StatsCorrectSQL(ctx context.Context, sql string, id int64) error { - _, err := db.GetEngine(ctx).Exec(sql, id, id) +func StatsCorrectSQL(ctx context.Context, sql any, ids ...any) error { + args := []any{sql} + args = append(args, ids...) + _, err := db.GetEngine(ctx).Exec(args...) return err } func repoStatsCorrectNumWatches(ctx context.Context, id int64) error { - return StatsCorrectSQL(ctx, "UPDATE `repository` SET num_watches=(SELECT COUNT(*) FROM `watch` WHERE repo_id=? AND mode<>2) WHERE id=?", id) + return StatsCorrectSQL(ctx, "UPDATE `repository` SET num_watches=(SELECT COUNT(*) FROM `watch` WHERE repo_id=? AND mode<>2) WHERE id=?", id, id) } func repoStatsCorrectNumStars(ctx context.Context, id int64) error { - return StatsCorrectSQL(ctx, "UPDATE `repository` SET num_stars=(SELECT COUNT(*) FROM `star` WHERE repo_id=?) WHERE id=?", id) + return StatsCorrectSQL(ctx, "UPDATE `repository` SET num_stars=(SELECT COUNT(*) FROM `star` WHERE repo_id=?) WHERE id=?", id, id) } func labelStatsCorrectNumIssues(ctx context.Context, id int64) error { - return StatsCorrectSQL(ctx, "UPDATE `label` SET num_issues=(SELECT COUNT(*) FROM `issue_label` WHERE label_id=?) WHERE id=?", id) + return StatsCorrectSQL(ctx, "UPDATE `label` SET num_issues=(SELECT COUNT(*) FROM `issue_label` WHERE label_id=?) WHERE id=?", id, id) } func labelStatsCorrectNumIssuesRepo(ctx context.Context, id int64) error { @@ -102,11 +105,11 @@ func milestoneStatsCorrectNumIssuesRepo(ctx context.Context, id int64) error { } func userStatsCorrectNumRepos(ctx context.Context, id int64) error { - return StatsCorrectSQL(ctx, "UPDATE `user` SET num_repos=(SELECT COUNT(*) FROM `repository` WHERE owner_id=?) WHERE id=?", id) + return StatsCorrectSQL(ctx, "UPDATE `user` SET num_repos=(SELECT COUNT(*) FROM `repository` WHERE owner_id=?) WHERE id=?", id, id) } func repoStatsCorrectIssueNumComments(ctx context.Context, id int64) error { - return StatsCorrectSQL(ctx, "UPDATE `issue` SET num_comments=(SELECT COUNT(*) FROM `comment` WHERE issue_id=? AND type=0) WHERE id=?", id) + return StatsCorrectSQL(ctx, issues_model.UpdateIssueNumCommentsBuilder(id)) } func repoStatsCorrectNumIssues(ctx context.Context, id int64) error { @@ -125,9 +128,12 @@ func repoStatsCorrectNumClosedPulls(ctx context.Context, id int64) error { return repo_model.UpdateRepoIssueNumbers(ctx, id, true, true) } -func statsQuery(args ...any) func(context.Context) ([]map[string][]byte, error) { - return func(ctx context.Context) ([]map[string][]byte, error) { - return db.GetEngine(ctx).Query(args...) +// statsQuery returns a function that queries the database for a list of IDs +// sql could be a string or a *builder.Builder +func statsQuery(sql any, args ...any) func(context.Context) ([]int64, error) { + return func(ctx context.Context) ([]int64, error) { + var ids []int64 + return ids, db.GetEngine(ctx).SQL(sql, args...).Find(&ids) } } @@ -198,7 +204,16 @@ func CheckRepoStats(ctx context.Context) error { }, // Issue.NumComments { - statsQuery("SELECT `issue`.id FROM `issue` WHERE `issue`.num_comments!=(SELECT COUNT(*) FROM `comment` WHERE issue_id=`issue`.id AND type=0)"), + statsQuery(builder.Select("`issue`.id").From("`issue`").Where( + builder.Neq{ + "`issue`.num_comments": builder.Select("COUNT(*)").From("`comment`").Where( + builder.Expr("issue_id = `issue`.id").And( + builder.In("type", issues_model.ConversationCountedCommentType()), + ), + ), + }, + ), + ), repoStatsCorrectIssueNumComments, "issue count 'num_comments'", }, diff --git a/models/repo/release.go b/models/repo/release.go index ba7a3b3159..1c2e4a48e3 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 2d9b9de88d..fb8a6642f5 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/<owner>/<repo>.git - // https://my.domain/sub-path/<owner>/<repo> - // git+ssh://user@my.domain/<owner>/<repo>.git - // git+ssh://user@my.domain/<owner>/<repo> - // user@my.domain:<owner>/<repo>.git - // user@my.domain:<owner>/<repo> - - 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 6d88d170da..a9e2cdfb75 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 e7ca224028..fce357a1ac 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 b378666a20..4239a815b2 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 629986f741..0157b7735d 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 2a8a4a743e..bcf62237f0 100644 --- a/models/repo_test.go +++ b/models/repo_test.go @@ -7,6 +7,7 @@ import ( "testing" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unittest" "github.com/stretchr/testify/assert" @@ -22,3 +23,16 @@ func TestDoctorUserStarNum(t *testing.T) { assert.NoError(t, DoctorUserStarNum(db.DefaultContext)) } + +func Test_repoStatsCorrectIssueNumComments(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) + assert.NotNil(t, issue2) + assert.EqualValues(t, 0, issue2.NumComments) // the fixture data is wrong, but we don't fix it here + + assert.NoError(t, repoStatsCorrectIssueNumComments(db.DefaultContext, 2)) + // reload the issue + issue2 = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) + assert.EqualValues(t, 1, issue2.NumComments) +} diff --git a/models/unittest/fixtures.go b/models/unittest/fixtures.go index 4dde5410d6..fb2d2d0085 100644 --- a/models/unittest/fixtures.go +++ b/models/unittest/fixtures.go @@ -1,97 +1,33 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -//nolint:forbidigo package unittest import ( "fmt" - "os" - "time" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/auth/password/hash" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" - "github.com/go-testfixtures/testfixtures/v3" "xorm.io/xorm" "xorm.io/xorm/schemas" ) -var fixturesLoader *testfixtures.Loader - -// 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 0000000000..14686caf63 --- /dev/null +++ b/models/unittest/fixtures_loader.go @@ -0,0 +1,201 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package unittest + +import ( + "database/sql" + "encoding/hex" + "fmt" + "os" + "path/filepath" + "slices" + "strings" + + "gopkg.in/yaml.v3" + "xorm.io/xorm" + "xorm.io/xorm/schemas" +) + +type fixtureItem struct { + tableName string + tableNameQuoted string + sqlInserts []string + sqlInsertArgs [][]any + + mssqlHasIdentityColumn bool +} + +type fixturesLoaderInternal struct { + db *sql.DB + dbType schemas.DBType + files []string + fixtures map[string]*fixtureItem + quoteObject func(string) string + paramPlaceholder func(idx int) string +} + +func (f *fixturesLoaderInternal) mssqlTableHasIdentityColumn(db *sql.DB, tableName string) (bool, error) { + row := db.QueryRow(`SELECT COUNT(*) FROM sys.identity_columns WHERE OBJECT_ID = OBJECT_ID(?)`, tableName) + var count int + if err := row.Scan(&count); err != nil { + return false, err + } + return count > 0, nil +} + +func (f *fixturesLoaderInternal) preprocessFixtureRow(row []map[string]any) (err error) { + for _, m := range row { + for k, v := range m { + if s, ok := v.(string); ok { + if strings.HasPrefix(s, "0x") { + if m[k], err = hex.DecodeString(s[2:]); err != nil { + return err + } + } + } + } + } + return nil +} + +func (f *fixturesLoaderInternal) prepareFixtureItem(file string) (_ *fixtureItem, err error) { + fixture := &fixtureItem{} + fixture.tableName, _, _ = strings.Cut(filepath.Base(file), ".") + fixture.tableNameQuoted = f.quoteObject(fixture.tableName) + + if f.dbType == schemas.MSSQL { + fixture.mssqlHasIdentityColumn, err = f.mssqlTableHasIdentityColumn(f.db, fixture.tableName) + if err != nil { + return nil, err + } + } + + data, err := os.ReadFile(file) + if err != nil { + return nil, fmt.Errorf("failed to read file %q: %w", file, err) + } + + var rows []map[string]any + if err = yaml.Unmarshal(data, &rows); err != nil { + return nil, fmt.Errorf("failed to unmarshal yaml data from %q: %w", file, err) + } + if err = f.preprocessFixtureRow(rows); err != nil { + return nil, fmt.Errorf("failed to preprocess fixture rows from %q: %w", file, err) + } + + var sqlBuf []byte + var sqlArguments []any + for _, row := range rows { + sqlBuf = append(sqlBuf, fmt.Sprintf("INSERT INTO %s (", fixture.tableNameQuoted)...) + for k, v := range row { + sqlBuf = append(sqlBuf, f.quoteObject(k)...) + sqlBuf = append(sqlBuf, ","...) + sqlArguments = append(sqlArguments, v) + } + sqlBuf = sqlBuf[:len(sqlBuf)-1] + sqlBuf = append(sqlBuf, ") VALUES ("...) + paramIdx := 1 + for range row { + sqlBuf = append(sqlBuf, f.paramPlaceholder(paramIdx)...) + sqlBuf = append(sqlBuf, ',') + paramIdx++ + } + sqlBuf[len(sqlBuf)-1] = ')' + fixture.sqlInserts = append(fixture.sqlInserts, string(sqlBuf)) + fixture.sqlInsertArgs = append(fixture.sqlInsertArgs, slices.Clone(sqlArguments)) + sqlBuf = sqlBuf[:0] + sqlArguments = sqlArguments[:0] + } + return fixture, nil +} + +func (f *fixturesLoaderInternal) loadFixtures(tx *sql.Tx, file string) (err error) { + fixture := f.fixtures[file] + if fixture == nil { + if fixture, err = f.prepareFixtureItem(file); err != nil { + return err + } + f.fixtures[file] = fixture + } + + _, err = tx.Exec(fmt.Sprintf("DELETE FROM %s", fixture.tableNameQuoted)) // sqlite3 doesn't support truncate + if err != nil { + return err + } + + if fixture.mssqlHasIdentityColumn { + _, err = tx.Exec(fmt.Sprintf("SET IDENTITY_INSERT %s ON", fixture.tableNameQuoted)) + if err != nil { + return err + } + defer func() { _, err = tx.Exec(fmt.Sprintf("SET IDENTITY_INSERT %s OFF", fixture.tableNameQuoted)) }() + } + for i := range fixture.sqlInserts { + _, err = tx.Exec(fixture.sqlInserts[i], fixture.sqlInsertArgs[i]...) + } + if err != nil { + return err + } + return nil +} + +func (f *fixturesLoaderInternal) Load() error { + tx, err := f.db.Begin() + if err != nil { + return err + } + defer func() { _ = tx.Rollback() }() + + for _, file := range f.files { + if err := f.loadFixtures(tx, file); err != nil { + return fmt.Errorf("failed to load fixtures from %s: %w", file, err) + } + } + return tx.Commit() +} + +func FixturesFileFullPaths(dir string, files []string) ([]string, error) { + if files != nil && len(files) == 0 { + return nil, nil // load nothing + } + files = slices.Clone(files) + if len(files) == 0 { + entries, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + for _, e := range entries { + files = append(files, e.Name()) + } + } + for i, file := range files { + if !filepath.IsAbs(file) { + files[i] = filepath.Join(dir, file) + } + } + return files, nil +} + +func NewFixturesLoader(x *xorm.Engine, opts FixturesOptions) (FixturesLoader, error) { + files, err := FixturesFileFullPaths(opts.Dir, opts.Files) + if err != nil { + return nil, fmt.Errorf("failed to get fixtures files: %w", err) + } + f := &fixturesLoaderInternal{db: x.DB().DB, dbType: x.Dialect().URI().DBType, files: files, fixtures: map[string]*fixtureItem{}} + switch f.dbType { + case schemas.SQLITE: + f.quoteObject = func(s string) string { return fmt.Sprintf(`"%s"`, s) } + f.paramPlaceholder = func(idx int) string { return "?" } + case schemas.POSTGRES: + f.quoteObject = func(s string) string { return fmt.Sprintf(`"%s"`, s) } + f.paramPlaceholder = func(idx int) string { return fmt.Sprintf(`$%d`, idx) } + case schemas.MYSQL: + f.quoteObject = func(s string) string { return fmt.Sprintf("`%s`", s) } + f.paramPlaceholder = func(idx int) string { return "?" } + case schemas.MSSQL: + f.quoteObject = func(s string) string { return fmt.Sprintf("[%s]", s) } + f.paramPlaceholder = func(idx int) string { return "?" } + } + return f, nil +} diff --git a/models/unittest/fixtures_test.go b/models/unittest/fixtures_test.go new file mode 100644 index 0000000000..a4c55f4e55 --- /dev/null +++ b/models/unittest/fixtures_test.go @@ -0,0 +1,114 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package unittest_test + +import ( + "path/filepath" + "testing" + + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/require" + "xorm.io/xorm" +) + +var NewFixturesLoaderVendor = func(e *xorm.Engine, opts unittest.FixturesOptions) (unittest.FixturesLoader, error) { + return nil, nil +} + +/* +// the old code is kept here in case we are still interested in benchmarking the two implementations +func init() { + NewFixturesLoaderVendor = func(e *xorm.Engine, opts unittest.FixturesOptions) (unittest.FixturesLoader, error) { + return NewFixturesLoaderVendorGoTestfixtures(e, opts) + } +} + +func NewFixturesLoaderVendorGoTestfixtures(e *xorm.Engine, opts unittest.FixturesOptions) (*testfixtures.Loader, error) { + files, err := unittest.FixturesFileFullPaths(opts.Dir, opts.Files) + if err != nil { + return nil, fmt.Errorf("failed to get fixtures files: %w", err) + } + var dialect string + switch e.Dialect().URI().DBType { + case schemas.POSTGRES: + dialect = "postgres" + case schemas.MYSQL: + dialect = "mysql" + case schemas.MSSQL: + dialect = "mssql" + case schemas.SQLITE: + dialect = "sqlite3" + default: + return nil, fmt.Errorf("unsupported RDBMS for integration tests: %q", e.Dialect().URI().DBType) + } + loaderOptions := []func(loader *testfixtures.Loader) error{ + testfixtures.Database(e.DB().DB), + testfixtures.Dialect(dialect), + testfixtures.DangerousSkipTestDatabaseCheck(), + testfixtures.Files(files...), + } + if e.Dialect().URI().DBType == schemas.POSTGRES { + loaderOptions = append(loaderOptions, testfixtures.SkipResetSequences()) + } + return testfixtures.New(loaderOptions...) +} +*/ + +func prepareTestFixturesLoaders(t testing.TB) unittest.FixturesOptions { + _ = user_model.User{} + opts := unittest.FixturesOptions{Dir: filepath.Join(test.SetupGiteaRoot(), "models", "fixtures"), Files: []string{ + "user.yml", + }} + require.NoError(t, unittest.CreateTestEngine(opts)) + return opts +} + +func TestFixturesLoader(t *testing.T) { + opts := prepareTestFixturesLoaders(t) + loaderInternal, err := unittest.NewFixturesLoader(unittest.GetXORMEngine(), opts) + require.NoError(t, err) + loaderVendor, err := NewFixturesLoaderVendor(unittest.GetXORMEngine(), opts) + require.NoError(t, err) + t.Run("Internal", func(t *testing.T) { + require.NoError(t, loaderInternal.Load()) + require.NoError(t, loaderInternal.Load()) + }) + t.Run("Vendor", func(t *testing.T) { + if loaderVendor == nil { + t.Skip() + } + require.NoError(t, loaderVendor.Load()) + require.NoError(t, loaderVendor.Load()) + }) +} + +func BenchmarkFixturesLoader(b *testing.B) { + opts := prepareTestFixturesLoaders(b) + require.NoError(b, unittest.CreateTestEngine(opts)) + loaderInternal, err := unittest.NewFixturesLoader(unittest.GetXORMEngine(), opts) + require.NoError(b, err) + loaderVendor, err := NewFixturesLoaderVendor(unittest.GetXORMEngine(), opts) + require.NoError(b, err) + + // BenchmarkFixturesLoader/Vendor + // BenchmarkFixturesLoader/Vendor-12 1696 719416 ns/op + // BenchmarkFixturesLoader/Internal + // BenchmarkFixturesLoader/Internal-12 1746 670457 ns/op + b.Run("Internal", func(b *testing.B) { + for i := 0; i < b.N; i++ { + require.NoError(b, loaderInternal.Load()) + } + }) + b.Run("Vendor", func(b *testing.B) { + if loaderVendor == nil { + b.Skip() + } + for i := 0; i < b.N; i++ { + require.NoError(b, loaderVendor.Load()) + } + }) +} diff --git a/models/unittest/fscopy.go b/models/unittest/fscopy.go index 4d7ee2151d..690089bbc5 100644 --- a/models/unittest/fscopy.go +++ b/models/unittest/fscopy.go @@ -67,7 +67,7 @@ func SyncDirs(srcPath, destPath string) error { } // find and delete all untracked files - destFiles, err := util.StatDir(destPath, true) + destFiles, err := util.ListDirRecursively(destPath, &util.ListDirOptions{IncludeDir: true}) if err != nil { return err } @@ -86,13 +86,13 @@ func SyncDirs(srcPath, destPath string) error { } // sync src files to dest - srcFiles, err := util.StatDir(srcPath, true) + srcFiles, err := util.ListDirRecursively(srcPath, &util.ListDirOptions{IncludeDir: true}) if err != nil { return err } for _, srcFile := range srcFiles { destFilePath := filepath.Join(destPath, srcFile) - // util.StatDir appends a slash to the directory name + // util.ListDirRecursively appends a slash to the directory name if strings.HasSuffix(srcFile, "/") { err = os.MkdirAll(destFilePath, os.ModePerm) } else { diff --git a/models/unittest/reflection.go b/models/unittest/reflection.go index 141fc66b99..bc96a05973 100644 --- a/models/unittest/reflection.go +++ b/models/unittest/reflection.go @@ -4,7 +4,7 @@ package unittest import ( - "log" + "fmt" "reflect" ) @@ -14,7 +14,7 @@ func fieldByName(v reflect.Value, field string) reflect.Value { } f := v.FieldByName(field) if !f.IsValid() { - log.Panicf("can not read %s for %v", field, v) + panic(fmt.Errorf("can not read %s for %v", field, v)) } return f } diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go index 5794d5109e..7a9ca9698d 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 4ac858e04e..1c5595aef8 100644 --- a/models/unittest/unit_tests.go +++ b/models/unittest/unit_tests.go @@ -4,13 +4,17 @@ package unittest import ( + "fmt" "math" + "os" + "strings" "code.gitea.io/gitea/models/db" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "xorm.io/builder" + "xorm.io/xorm" ) // Code in this file is mainly used by unittest.CheckConsistencyFor, which is not in the unit test for various reasons. @@ -51,22 +55,23 @@ func whereOrderConditions(e db.Engine, conditions []any) db.Engine { return e.OrderBy(orderBy) } -// LoadBeanIfExists loads beans from fixture database if exist -func LoadBeanIfExists(bean any, conditions ...any) (bool, error) { +func getBeanIfExists(bean any, conditions ...any) (bool, error) { e := db.GetEngine(db.DefaultContext) return whereOrderConditions(e, conditions).Get(bean) } -// BeanExists for testing, check if a bean exists -func BeanExists(t assert.TestingT, bean any, conditions ...any) bool { - exists, err := LoadBeanIfExists(bean, conditions...) - assert.NoError(t, err) - return exists +func GetBean[T any](t require.TestingT, bean T, conditions ...any) (ret T) { + exists, err := getBeanIfExists(bean, conditions...) + require.NoError(t, err) + if exists { + return bean + } + return ret } // AssertExistsAndLoadBean assert that a bean exists and load it from the test database func AssertExistsAndLoadBean[T any](t require.TestingT, bean T, conditions ...any) T { - exists, err := LoadBeanIfExists(bean, conditions...) + exists, err := getBeanIfExists(bean, conditions...) require.NoError(t, err) require.True(t, exists, "Expected to find %+v (of type %T, with conditions %+v), but did not", @@ -112,25 +117,11 @@ func GetCount(t assert.TestingT, bean any, conditions ...any) int { // AssertNotExistsBean assert that a bean does not exist in the test database func AssertNotExistsBean(t assert.TestingT, bean any, conditions ...any) { - exists, err := LoadBeanIfExists(bean, conditions...) + exists, err := getBeanIfExists(bean, conditions...) assert.NoError(t, err) assert.False(t, exists) } -// AssertExistsIf asserts that a bean exists or does not exist, depending on -// what is expected. -func AssertExistsIf(t assert.TestingT, expected bool, bean any, conditions ...any) { - exists, err := LoadBeanIfExists(bean, conditions...) - assert.NoError(t, err) - assert.Equal(t, expected, exists) -} - -// AssertSuccessfulInsert assert that beans is successfully inserted -func AssertSuccessfulInsert(t assert.TestingT, beans ...any) { - err := db.Insert(db.DefaultContext, beans...) - assert.NoError(t, err) -} - // AssertCount assert the count of a bean func AssertCount(t assert.TestingT, bean, expected any) bool { return assert.EqualValues(t, expected, GetCount(t, bean)) @@ -155,3 +146,39 @@ func AssertCountByCond(t assert.TestingT, tableName string, cond builder.Cond, e return assert.EqualValues(t, expected, GetCountByCond(t, tableName, cond), "Failed consistency test, the counted bean (of table %s) was %+v", tableName, cond) } + +// DumpQueryResult dumps the result of a query for debugging purpose +func DumpQueryResult(t require.TestingT, sqlOrBean any, sqlArgs ...any) { + x := db.GetEngine(db.DefaultContext).(*xorm.Engine) + goDB := x.DB().DB + sql, ok := sqlOrBean.(string) + if !ok { + sql = fmt.Sprintf("SELECT * FROM %s", db.TableName(sqlOrBean)) + } else if !strings.Contains(sql, " ") { + sql = fmt.Sprintf("SELECT * FROM %s", sql) + } + rows, err := goDB.Query(sql, sqlArgs...) + require.NoError(t, err) + defer rows.Close() + columns, err := rows.Columns() + require.NoError(t, err) + + _, _ = fmt.Fprintf(os.Stdout, "====== DumpQueryResult: %s ======\n", sql) + idx := 0 + for rows.Next() { + row := make([]any, len(columns)) + rowPointers := make([]any, len(columns)) + for i := range row { + rowPointers[i] = &row[i] + } + require.NoError(t, rows.Scan(rowPointers...)) + _, _ = fmt.Fprintf(os.Stdout, "- # row[%d]\n", idx) + for i, col := range columns { + _, _ = fmt.Fprintf(os.Stdout, " %s: %v\n", col, row[i]) + } + idx++ + } + if idx == 0 { + _, _ = fmt.Fprintf(os.Stdout, "(no result, columns: %s)\n", strings.Join(columns, ", ")) + } +} diff --git a/models/user/email_address.go b/models/user/email_address.go index 5c04909ed7..74ba5f617a 100644 --- a/models/user/email_address.go +++ b/models/user/email_address.go @@ -357,8 +357,8 @@ func VerifyActiveEmailCode(ctx context.Context, code, email string) *EmailAddres if user := GetVerifyUser(ctx, code); user != nil { // time limit code prefix := code[:base.TimeLimitCodeLength] - data := fmt.Sprintf("%d%s%s%s%s", user.ID, email, user.LowerName, user.Passwd, user.Rands) - + opts := &TimeLimitCodeOptions{Purpose: TimeLimitCodeActivateEmail, NewEmail: email} + data := makeTimeLimitCodeHashData(opts, user) if base.VerifyTimeLimitCode(time.Now(), data, setting.Service.ActiveCodeLives, prefix) { emailAddress := &EmailAddress{UID: user.ID, Email: email} if has, _ := db.GetEngine(ctx).Get(emailAddress); has { @@ -486,10 +486,10 @@ func ActivateUserEmail(ctx context.Context, userID int64, email string, activate // Activate/deactivate a user's primary email address and account if addr.IsPrimary { - user, exist, err := db.Get[User](ctx, builder.Eq{"id": userID, "email": email}) + user, exist, err := db.Get[User](ctx, builder.Eq{"id": userID}) if err != nil { return err - } else if !exist { + } else if !exist || !strings.EqualFold(user.Email, email) { return fmt.Errorf("no user with ID: %d and Email: %s", userID, email) } diff --git a/models/user/search.go b/models/user/search.go index 6af3389237..85915f4020 100644 --- a/models/user/search.go +++ b/models/user/search.go @@ -39,8 +39,6 @@ type SearchUserOptions struct { IsTwoFactorEnabled optional.Option[bool] IsProhibitLogin optional.Option[bool] IncludeReserved bool - - ExtraParamStrings map[string]string } func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Session { diff --git a/models/user/user.go b/models/user/user.go index 72caafc3ba..19879fbcc7 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -181,7 +181,8 @@ func (u *User) BeforeUpdate() { u.MaxRepoCreation = -1 } - // Organization does not need email + // FIXME: this email doesn't need to be in lowercase, because the emails are mainly managed by the email table with lower_email field + // This trick could be removed in new releases to display the user inputed email as-is. u.Email = strings.ToLower(u.Email) if !u.IsOrganization() { if len(u.AvatarEmail) == 0 { @@ -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 9678d23ad6..4f3811ba2b 100644 --- a/modules/assetfs/layered.go +++ b/modules/assetfs/layered.go @@ -103,7 +103,7 @@ func (l *LayeredFS) ReadLayeredFile(elems ...string) ([]byte, string, error) { } func shouldInclude(info fs.FileInfo, fileMode ...bool) bool { - if util.CommonSkip(info.Name()) { + if util.IsCommonHiddenFileName(info.Name()) { return false } if len(fileMode) == 0 { diff --git a/modules/auth/password/pwn/pwn_test.go b/modules/auth/password/pwn/pwn_test.go index b3e7734c3f..ae03fabc57 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 2303e64a68..1d16186bc5 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 de6c311856..c821a55c19 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 532dbad989..33e54fe75c 100644 --- a/modules/git/batch_reader.go +++ b/modules/git/batch_reader.go @@ -242,7 +242,7 @@ func BinToHex(objectFormat ObjectFormat, sha, out []byte) []byte { return out } -// ParseTreeLine reads an entry from a tree in a cat-file --batch stream +// ParseCatFileTreeLine reads an entry from a tree in a cat-file --batch stream // This carefully avoids allocations - except where fnameBuf is too small. // It is recommended therefore to pass in an fnameBuf large enough to avoid almost all allocations // @@ -250,7 +250,7 @@ func BinToHex(objectFormat ObjectFormat, sha, out []byte) []byte { // <mode-in-ascii-dropping-initial-zeros> SP <fname> NUL <binary HASH> // // We don't attempt to convert the raw HASH to save a lot of time -func ParseTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBuf, shaBuf []byte) (mode, fname, sha []byte, n int, err error) { +func ParseCatFileTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBuf, shaBuf []byte) (mode, fname, sha []byte, n int, err error) { var readBytes []byte // Read the Mode & fname @@ -260,7 +260,7 @@ func ParseTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBu } idx := bytes.IndexByte(readBytes, ' ') if idx < 0 { - log.Debug("missing space in readBytes ParseTreeLine: %s", readBytes) + log.Debug("missing space in readBytes ParseCatFileTreeLine: %s", readBytes) return mode, fname, sha, n, &ErrNotExist{} } diff --git a/modules/git/command.go b/modules/git/command.go index b231c3beea..2584e3cc57 100644 --- a/modules/git/command.go +++ b/modules/git/command.go @@ -236,10 +236,16 @@ type RunOpts struct { } func commonBaseEnvs() []string { - // at the moment, do not set "GIT_CONFIG_NOSYSTEM", users may have put some configs like "receive.certNonceSeed" in it envs := []string{ - "HOME=" + HomeDir(), // make Gitea use internal git config only, to prevent conflicts with user's git config - "GIT_NO_REPLACE_OBJECTS=1", // ignore replace references (https://git-scm.com/docs/git-replace) + // Make Gitea use internal git config only, to prevent conflicts with user's git config + // It's better to use GIT_CONFIG_GLOBAL, but it requires git >= 2.32, so we still use HOME at the moment. + "HOME=" + HomeDir(), + // Avoid using system git config, it would cause problems (eg: use macOS osxkeychain to show a modal dialog, auto installing lfs hooks) + // This might be a breaking change in 1.24, because some users said that they have put some configs like "receive.certNonceSeed" in "/etc/gitconfig" + // For these users, they need to migrate the necessary configs to Gitea's git config file manually. + "GIT_CONFIG_NOSYSTEM=1", + // Ignore replace references (https://git-scm.com/docs/git-replace) + "GIT_NO_REPLACE_OBJECTS=1", } // some environment variables should be passed to git command diff --git a/modules/git/commit_info.go b/modules/git/commit_info.go index 545081275b..c046acbb50 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 11b44f7c35..314c2df728 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 20d586f0ff..ef2df0b133 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 6603061da2..031fd4e5d0 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 bdec35f682..2ac744fbf6 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 473b996b82..4b5b767612 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 0000000000..eb26632cc0 --- /dev/null +++ b/modules/git/parse.go @@ -0,0 +1,78 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "bytes" + "fmt" + "strconv" + "strings" + + "code.gitea.io/gitea/modules/optional" +) + +var sepSpace = []byte{' '} + +type LsTreeEntry struct { + ID ObjectID + EntryMode EntryMode + Name string + Size optional.Option[int64] +} + +func parseLsTreeLine(line []byte) (*LsTreeEntry, error) { + // expect line to be of the form: + // <mode> <type> <sha> <space-padded-size>\t<filename> + // <mode> <type> <sha>\t<filename> + + var err error + posTab := bytes.IndexByte(line, '\t') + if posTab == -1 { + return nil, fmt.Errorf("invalid ls-tree output (no tab): %q", line) + } + + entry := new(LsTreeEntry) + + entryAttrs := line[:posTab] + entryName := line[posTab+1:] + + entryMode, entryAttrs, _ := bytes.Cut(entryAttrs, sepSpace) + _ /* entryType */, entryAttrs, _ = bytes.Cut(entryAttrs, sepSpace) // the type is not used, the mode is enough to determine the type + entryObjectID, entryAttrs, _ := bytes.Cut(entryAttrs, sepSpace) + if len(entryAttrs) > 0 { + entrySize := entryAttrs // the last field is the space-padded-size + size, _ := strconv.ParseInt(strings.TrimSpace(string(entrySize)), 10, 64) + entry.Size = optional.Some(size) + } + + switch string(entryMode) { + case "100644": + entry.EntryMode = EntryModeBlob + case "100755": + entry.EntryMode = EntryModeExec + case "120000": + entry.EntryMode = EntryModeSymlink + case "160000": + entry.EntryMode = EntryModeCommit + case "040000", "040755": // git uses 040000 for tree object, but some users may get 040755 for unknown reasons + entry.EntryMode = EntryModeTree + default: + return nil, fmt.Errorf("unknown type: %v", string(entryMode)) + } + + entry.ID, err = NewIDFromString(string(entryObjectID)) + if err != nil { + return nil, fmt.Errorf("invalid ls-tree output (invalid object id): %q, err: %w", line, err) + } + + if len(entryName) > 0 && entryName[0] == '"' { + entry.Name, err = strconv.Unquote(string(entryName)) + if err != nil { + return nil, fmt.Errorf("invalid ls-tree output (invalid name): %q, err: %w", line, err) + } + } else { + entry.Name = string(entryName) + } + return entry, nil +} diff --git a/modules/git/parse_nogogit.go b/modules/git/parse_nogogit.go index 546b38be37..676bb3c76c 100644 --- a/modules/git/parse_nogogit.go +++ b/modules/git/parse_nogogit.go @@ -10,8 +10,6 @@ import ( "bytes" "fmt" "io" - "strconv" - "strings" "code.gitea.io/gitea/modules/log" ) @@ -21,71 +19,30 @@ func ParseTreeEntries(data []byte) ([]*TreeEntry, error) { return parseTreeEntries(data, nil) } -var sepSpace = []byte{' '} - +// parseTreeEntries FIXME this function's design is not right, it should make the caller read all data into memory func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) { - var err error entries := make([]*TreeEntry, 0, bytes.Count(data, []byte{'\n'})+1) for pos := 0; pos < len(data); { - // expect line to be of the form: - // <mode> <type> <sha> <space-padded-size>\t<filename> - // <mode> <type> <sha>\t<filename> 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 b22805c132..92e35c5a10 100644 --- a/modules/git/pipeline/lfs_nogogit.go +++ b/modules/git/pipeline/lfs_nogogit.go @@ -114,7 +114,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err case "tree": var n int64 for n < size { - mode, fname, binObjectID, count, err := git.ParseTreeLine(objectID.Type(), batchReader, modeBuf, fnameBuf, workingShaBuf) + mode, fname, binObjectID, count, err := git.ParseCatFileTreeLine(objectID.Type(), batchReader, modeBuf, fnameBuf, workingShaBuf) if err != nil { return nil, err } diff --git a/modules/git/remote.go b/modules/git/remote.go index de8d74eded..ff8c040eb1 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 1bf1aa41b9..2b45a50f19 100644 --- a/modules/git/repo_archive.go +++ b/modules/git/repo_archive.go @@ -8,7 +8,6 @@ import ( "context" "fmt" "io" - "os" "path/filepath" "strings" ) @@ -63,15 +62,11 @@ func (repo *Repository) CreateArchive(ctx context.Context, format ArchiveType, t cmd.AddOptionFormat("--format=%s", format.String()) cmd.AddDynamicArguments(commitID) - // Avoid LFS hooks getting installed because of /etc/gitconfig, which can break pull requests. - env := append(os.Environ(), "GIT_CONFIG_NOSYSTEM=1") - var stderr strings.Builder err := cmd.Run(&RunOpts{ Dir: repo.Path, Stdout: target, Stderr: &stderr, - Env: env, }) if err != nil { return ConcatenateError(err, stderr.String()) diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go index 9ffadb833d..647894bb21 100644 --- a/modules/git/repo_commit.go +++ b/modules/git/repo_commit.go @@ -216,8 +216,6 @@ type CommitsByFileAndRangeOptions struct { // CommitsByFileAndRange return the commits according revision file and the page func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) ([]*Commit, error) { - skip := (opts.Page - 1) * setting.Git.CommitsRangeSize - stdoutReader, stdoutWriter := io.Pipe() defer func() { _ = stdoutReader.Close() @@ -226,8 +224,8 @@ func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) go func() { stderr := strings.Builder{} gitCmd := NewCommand(repo.Ctx, "rev-list"). - AddOptionFormat("--max-count=%d", setting.Git.CommitsRangeSize*opts.Page). - AddOptionFormat("--skip=%d", skip) + AddOptionFormat("--max-count=%d", setting.Git.CommitsRangeSize). + AddOptionFormat("--skip=%d", (opts.Page-1)*setting.Git.CommitsRangeSize) gitCmd.AddDynamicArguments(opts.Revision) if opts.Not != "" { diff --git a/modules/git/repo_commit_test.go b/modules/git/repo_commit_test.go index 4c26fa2a48..e9f469accd 100644 --- a/modules/git/repo_commit_test.go +++ b/modules/git/repo_commit_test.go @@ -8,7 +8,11 @@ import ( "path/filepath" "testing" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestRepository_GetCommitBranches(t *testing.T) { @@ -126,3 +130,21 @@ func TestGetRefCommitID(t *testing.T) { } } } + +func TestCommitsByFileAndRange(t *testing.T) { + defer test.MockVariableValue(&setting.Git.CommitsRangeSize, 2)() + + bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") + bareRepo1, err := openRepositoryWithDefaultContext(bareRepo1Path) + require.NoError(t, err) + defer bareRepo1.Close() + + // "foo" has 3 commits in "master" branch + commits, err := bareRepo1.CommitsByFileAndRange(CommitsByFileAndRangeOptions{Revision: "master", File: "foo", Page: 1}) + require.NoError(t, err) + assert.Len(t, commits, 2) + + commits, err = bareRepo1.CommitsByFileAndRange(CommitsByFileAndRangeOptions{Revision: "master", File: "foo", Page: 2}) + require.NoError(t, err) + assert.Len(t, commits, 1) +} diff --git a/modules/git/repo_compare.go b/modules/git/repo_compare.go index 16fcdcf4c8..877a7ff3b8 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 454ed6b9f8..25ee4c5198 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 0000000000..017b644052 --- /dev/null +++ b/modules/git/submodule.go @@ -0,0 +1,66 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "bufio" + "context" + "fmt" + "os" + + "code.gitea.io/gitea/modules/log" +) + +type TemplateSubmoduleCommit struct { + Path string + Commit string +} + +// GetTemplateSubmoduleCommits returns a list of submodules paths and their commits from a repository +// This function is only for generating new repos based on existing template, the template couldn't be too large. +func GetTemplateSubmoduleCommits(ctx context.Context, repoPath string) (submoduleCommits []TemplateSubmoduleCommit, _ error) { + stdoutReader, stdoutWriter, err := os.Pipe() + if err != nil { + return nil, err + } + opts := &RunOpts{ + Dir: repoPath, + Stdout: stdoutWriter, + PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error { + _ = stdoutWriter.Close() + defer stdoutReader.Close() + + scanner := bufio.NewScanner(stdoutReader) + for scanner.Scan() { + entry, err := parseLsTreeLine(scanner.Bytes()) + if err != nil { + cancel() + return err + } + if entry.EntryMode == EntryModeCommit { + submoduleCommits = append(submoduleCommits, TemplateSubmoduleCommit{Path: entry.Name, Commit: entry.ID.String()}) + } + } + return scanner.Err() + }, + } + err = NewCommand(ctx, "ls-tree", "-r", "--", "HEAD").Run(opts) + if err != nil { + return nil, fmt.Errorf("GetTemplateSubmoduleCommits: error running git ls-tree: %v", err) + } + return submoduleCommits, nil +} + +// AddTemplateSubmoduleIndexes Adds the given submodules to the git index. +// It is only for generating new repos based on existing template, requires the .gitmodules file to be already present in the work dir. +func AddTemplateSubmoduleIndexes(ctx context.Context, repoPath string, submodules []TemplateSubmoduleCommit) error { + for _, submodule := range submodules { + cmd := NewCommand(ctx, "update-index", "--add", "--cacheinfo", "160000").AddDynamicArguments(submodule.Commit, submodule.Path) + if stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath}); err != nil { + log.Error("Unable to add %s as submodule to repo %s: stdout %s\nError: %v", submodule.Path, repoPath, stdout, err) + return err + } + } + return nil +} diff --git a/modules/git/submodule_test.go b/modules/git/submodule_test.go new file mode 100644 index 0000000000..d53946a27d --- /dev/null +++ b/modules/git/submodule_test.go @@ -0,0 +1,48 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetTemplateSubmoduleCommits(t *testing.T) { + testRepoPath := filepath.Join(testReposDir, "repo4_submodules") + submodules, err := GetTemplateSubmoduleCommits(DefaultContext, testRepoPath) + require.NoError(t, err) + + assert.Len(t, submodules, 2) + + assert.EqualValues(t, "<°)))><", submodules[0].Path) + assert.EqualValues(t, "d2932de67963f23d43e1c7ecf20173e92ee6c43c", submodules[0].Commit) + + assert.EqualValues(t, "libtest", submodules[1].Path) + assert.EqualValues(t, "1234567890123456789012345678901234567890", submodules[1].Commit) +} + +func TestAddTemplateSubmoduleIndexes(t *testing.T) { + ctx := context.Background() + tmpDir := t.TempDir() + var err error + _, _, err = NewCommand(ctx, "init").RunStdString(&RunOpts{Dir: tmpDir}) + require.NoError(t, err) + _ = os.Mkdir(filepath.Join(tmpDir, "new-dir"), 0o755) + err = AddTemplateSubmoduleIndexes(ctx, tmpDir, []TemplateSubmoduleCommit{{Path: "new-dir", Commit: "1234567890123456789012345678901234567890"}}) + require.NoError(t, err) + _, _, err = NewCommand(ctx, "add", "--all").RunStdString(&RunOpts{Dir: tmpDir}) + require.NoError(t, err) + _, _, err = NewCommand(ctx, "-c", "user.name=a", "-c", "user.email=b", "commit", "-m=test").RunStdString(&RunOpts{Dir: tmpDir}) + require.NoError(t, err) + submodules, err := GetTemplateSubmoduleCommits(DefaultContext, tmpDir) + require.NoError(t, err) + assert.Len(t, submodules, 1) + assert.EqualValues(t, "new-dir", submodules[0].Path) + assert.EqualValues(t, "1234567890123456789012345678901234567890", submodules[0].Commit) +} diff --git a/modules/git/tests/repos/repo4_submodules/HEAD b/modules/git/tests/repos/repo4_submodules/HEAD new file mode 100644 index 0000000000..cb089cd89a --- /dev/null +++ b/modules/git/tests/repos/repo4_submodules/HEAD @@ -0,0 +1 @@ +ref: refs/heads/master diff --git a/modules/git/tests/repos/repo4_submodules/config b/modules/git/tests/repos/repo4_submodules/config new file mode 100644 index 0000000000..07d359d07c --- /dev/null +++ b/modules/git/tests/repos/repo4_submodules/config @@ -0,0 +1,4 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = true diff --git a/modules/git/tests/repos/repo4_submodules/objects/97/c3d30df0e6492348292600920a6482feaebb74 b/modules/git/tests/repos/repo4_submodules/objects/97/c3d30df0e6492348292600920a6482feaebb74 Binary files differnew file mode 100644 index 0000000000..7596090b49 --- /dev/null +++ b/modules/git/tests/repos/repo4_submodules/objects/97/c3d30df0e6492348292600920a6482feaebb74 diff --git a/modules/git/tests/repos/repo4_submodules/objects/c7/e064ed49b44523cba8a5dfbc37d2ce1bb41d34 b/modules/git/tests/repos/repo4_submodules/objects/c7/e064ed49b44523cba8a5dfbc37d2ce1bb41d34 Binary files differnew file mode 100644 index 0000000000..e3a13c156d --- /dev/null +++ b/modules/git/tests/repos/repo4_submodules/objects/c7/e064ed49b44523cba8a5dfbc37d2ce1bb41d34 diff --git a/modules/git/tests/repos/repo4_submodules/objects/e1/e59caba97193d48862d6809912043871f37437 b/modules/git/tests/repos/repo4_submodules/objects/e1/e59caba97193d48862d6809912043871f37437 new file mode 100644 index 0000000000..a8d6e5c17c --- /dev/null +++ b/modules/git/tests/repos/repo4_submodules/objects/e1/e59caba97193d48862d6809912043871f37437 @@ -0,0 +1,2 @@ +x[ +Â0EýÎ*æ_é$MÑ5tifBk IÅ•¹7æk~ÞÃ9ܘ—åÜ ü¦ð.jÖÈ ÅOÚäÉ"zÂ`ß#IirF…µÍ¹ÀØ$%¹Âçò|4)°¯?t¼É=”Ë:K¦ï#[$D¿¯û¿^˜…¡®Ó’y½HU/f?G
\ No newline at end of file diff --git a/modules/git/tests/repos/repo4_submodules/refs/heads/master b/modules/git/tests/repos/repo4_submodules/refs/heads/master new file mode 100644 index 0000000000..102bc34da8 --- /dev/null +++ b/modules/git/tests/repos/repo4_submodules/refs/heads/master @@ -0,0 +1 @@ +e1e59caba97193d48862d6809912043871f37437 diff --git a/modules/git/tree.go b/modules/git/tree.go index 1da4a9fa5d..5a644f6c87 100644 --- a/modules/git/tree.go +++ b/modules/git/tree.go @@ -17,7 +17,7 @@ func NewTree(repo *Repository, id ObjectID) *Tree { } } -// SubTree get a sub tree by the sub dir path +// SubTree get a subtree by the sub dir path func (t *Tree) SubTree(rpath string) (*Tree, error) { if len(rpath) == 0 { return t, nil @@ -62,3 +62,14 @@ func (repo *Repository) LsTree(ref string, filenames ...string) ([]string, error return filelist, err } + +// GetTreePathLatestCommit returns the latest commit of a tree path +func (repo *Repository) GetTreePathLatestCommit(refName, treePath string) (*Commit, error) { + stdout, _, err := NewCommand(repo.Ctx, "rev-list", "-1"). + AddDynamicArguments(refName).AddDashesAndList(treePath). + RunStdString(&RunOpts{Dir: repo.Path}) + if err != nil { + return nil, err + } + return repo.GetCommit(strings.TrimSpace(stdout)) +} diff --git a/modules/git/tree_blob_nogogit.go b/modules/git/tree_blob_nogogit.go index 92d3d107a7..b7bcf40edd 100644 --- a/modules/git/tree_blob_nogogit.go +++ b/modules/git/tree_blob_nogogit.go @@ -17,7 +17,6 @@ func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) { ptree: t, ID: t.ID, name: "", - fullName: "", entryMode: EntryModeTree, }, nil } diff --git a/modules/git/tree_entry_nogogit.go b/modules/git/tree_entry_nogogit.go index 1c3bcd197a..81fb638d56 100644 --- a/modules/git/tree_entry_nogogit.go +++ b/modules/git/tree_entry_nogogit.go @@ -9,23 +9,17 @@ import "code.gitea.io/gitea/modules/log" // TreeEntry the leaf in the git tree type TreeEntry struct { - ID ObjectID - + ID ObjectID ptree *Tree entryMode EntryMode name string - - size int64 - sized bool - fullName string + size int64 + sized bool } // Name returns the name of the entry func (te *TreeEntry) Name() string { - if te.fullName != "" { - return te.fullName - } return te.name } diff --git a/modules/git/tree_test.go b/modules/git/tree_test.go index 6d2b5c84d5..5fee64b038 100644 --- a/modules/git/tree_test.go +++ b/modules/git/tree_test.go @@ -25,3 +25,18 @@ func TestSubTree_Issue29101(t *testing.T) { assert.True(t, IsErrNotExist(err)) } } + +func Test_GetTreePathLatestCommit(t *testing.T) { + repo, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "repo6_blame")) + assert.NoError(t, err) + defer repo.Close() + + commitID, err := repo.GetBranchCommitID("master") + assert.NoError(t, err) + assert.EqualValues(t, "544d8f7a3b15927cddf2299b4b562d6ebd71b6a7", commitID) + + commit, err := repo.GetTreePathLatestCommit("master", "blame.txt") + assert.NoError(t, err) + assert.NotNil(t, commit) + assert.EqualValues(t, "45fb6cbc12f970b04eacd5cd4165edd11c8d7376", commit.ID.String()) +} diff --git a/modules/git/url/url.go b/modules/git/url/url.go index 637685183e..1c5e8377a6 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/<owner>/<repo>[.git] + // git+ssh://user@my.domain/<owner>/<repo>[.git] + // ssh://user@my.domain/<owner>/<repo>[.git] + // user@my.domain:<owner>/<repo>[.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 da820ed889..9c020adb4d 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 14d809aedb..540b724489 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 6370faf08e..709897ba0c 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 9b5f5a92d8..0ab0e71689 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 e3bad1e5fb..f543c09190 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 c1ab26569c..728b37fab6 100644 --- a/modules/indexer/code/indexer.go +++ b/modules/indexer/code/indexer.go @@ -123,13 +123,12 @@ func Init() { for _, indexerData := range items { log.Trace("IndexerData Process Repo: %d", indexerData.RepoID) if err := index(ctx, indexer, indexerData.RepoID); err != nil { - unhandled = append(unhandled, indexerData) if !setting.IsInTesting { log.Error("Codes indexer handler: index error for repo %v: %v", indexerData.RepoID, err) } } } - return unhandled + return nil // do not re-queue the failed items, otherwise some broken repo will block the queue } indexerQueue = queue.CreateUniqueQueue(ctx, "code_indexer", handler) diff --git a/modules/indexer/code/indexer_test.go b/modules/indexer/code/indexer_test.go index d04088531a..f358bbe785 100644 --- a/modules/indexer/code/indexer_test.go +++ b/modules/indexer/code/indexer_test.go @@ -15,6 +15,8 @@ import ( "code.gitea.io/gitea/modules/indexer/code/bleve" "code.gitea.io/gitea/modules/indexer/code/elasticsearch" "code.gitea.io/gitea/modules/indexer/code/internal" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" _ "code.gitea.io/gitea/models" _ "code.gitea.io/gitea/models/actions" @@ -279,7 +281,7 @@ func testIndexer(name string, t *testing.T, indexer internal.Indexer) { func TestBleveIndexAndSearch(t *testing.T) { unittest.PrepareTestEnv(t) - + defer test.MockVariableValue(&setting.Indexer.TypeBleveMaxFuzzniess, 2)() dir := t.TempDir() idx := bleve.NewIndexer(dir) diff --git a/modules/indexer/internal/bleve/util.go b/modules/indexer/internal/bleve/util.go index a0c3dc4ad4..b6daa9e14b 100644 --- a/modules/indexer/internal/bleve/util.go +++ b/modules/indexer/internal/bleve/util.go @@ -9,6 +9,7 @@ import ( "unicode" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "github.com/blevesearch/bleve/v2" @@ -54,9 +55,9 @@ func openIndexer(path string, latestVersion int) (bleve.Index, int, error) { return index, 0, nil } -// This method test the GuessFuzzinessByKeyword method. The fuzziness is based on the levenshtein distance and determines how many chars -// may be different on two string and they still be considered equivalent. -// Given a phrasse, its shortest word determines its fuzziness. If a phrase uses CJK (eg: `갃갃갃` `å•Šå•Šå•Š`), the fuzziness is zero. +// GuessFuzzinessByKeyword guesses fuzziness based on the levenshtein distance and determines how many chars +// may be different on two string, and they still be considered equivalent. +// Given a phrase, its shortest word determines its fuzziness. If a phrase uses CJK (eg: `갃갃갃` `å•Šå•Šå•Š`), the fuzziness is zero. func GuessFuzzinessByKeyword(s string) int { tokenizer := unicode_tokenizer.NewUnicodeTokenizer() tokens := tokenizer.Tokenize([]byte(s)) @@ -85,5 +86,5 @@ func guessFuzzinessByKeyword(s string) int { return 0 } } - return min(maxFuzziness, len(s)/4) + return min(min(setting.Indexer.TypeBleveMaxFuzzniess, maxFuzziness), len(s)/4) } diff --git a/modules/indexer/internal/bleve/util_test.go b/modules/indexer/internal/bleve/util_test.go index 8f7844464e..1a7e4db0f4 100644 --- a/modules/indexer/internal/bleve/util_test.go +++ b/modules/indexer/internal/bleve/util_test.go @@ -7,10 +7,15 @@ import ( "fmt" "testing" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + "github.com/stretchr/testify/assert" ) func TestBleveGuessFuzzinessByKeyword(t *testing.T) { + defer test.MockVariableValue(&setting.Indexer.TypeBleveMaxFuzzniess, 2)() + scenarios := []struct { Input string Fuzziness int // See util.go for the definition of fuzziness in this particular context @@ -46,7 +51,7 @@ func TestBleveGuessFuzzinessByKeyword(t *testing.T) { } for _, scenario := range scenarios { - t.Run(fmt.Sprintf("ensure fuzziness of '%s' is '%d'", scenario.Input, scenario.Fuzziness), func(t *testing.T) { + t.Run(fmt.Sprintf("Fuziniess:%s=%d", scenario.Input, scenario.Fuzziness), func(t *testing.T) { assert.Equal(t, scenario.Fuzziness, GuessFuzzinessByKeyword(scenario.Input)) }) } diff --git a/modules/issue/template/unmarshal.go b/modules/issue/template/unmarshal.go index 0fc13d7ddf..1d8e9dd02d 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 358e7b06ba..aa1b7d034a 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 e64ec76c3d..7a6f33011a 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 0000000000..8d189fbdf6 --- /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("<div>issue #%d</div>", 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", + `<p><a href="http://localhost:3000/test-user/test-repo/issues/12345" class="ref-issue" rel="nofollow">#12345</a></p>`, + ) + }) + + t.Run("ListIssueRef", func(t *testing.T) { + test( + "* #12345", + `<ul> +<li><div>issue #12345</div></li> +</ul>`, + ) + }) + + t.Run("ListIssueRefNormal", func(t *testing.T) { + test( + "* foo #12345 bar", + `<ul> +<li>foo <a href="http://localhost:3000/test-user/test-repo/issues/12345" class="ref-issue" rel="nofollow">#12345</a> bar</li> +</ul>`, + ) + }) + + t.Run("ListTodoIssueRef", func(t *testing.T) { + test( + "* [ ] #12345", + `<ul> +<li class="task-list-item"><input type="checkbox" disabled="" data-source-position="2"/><div>issue #12345</div></li> +</ul>`, + ) + }) +} diff --git a/modules/markup/html_link.go b/modules/markup/html_link.go index 5fd38b63cd..0e7a988d36 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 54bd91f3b3..6d8f24184b 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", `<p>ftps://gitea.com</p>`) + + t.Run("LinkEllipsis", func(t *testing.T) { + input := util.EllipsisDisplayString("http://10.1.2.3", 12) + assert.Equal(t, "http://10…", input) + test(input, "<p>http://10…</p>") + + input = util.EllipsisDisplayString("http://10.1.2.3", 13) + assert.Equal(t, "http://10.…", input) + test(input, "<p>http://10.…</p>") + }) } func TestRender_email(t *testing.T) { diff --git a/modules/markup/internal/renderinternal.go b/modules/markup/internal/renderinternal.go index 4d58f160a9..7a3e37b120 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 c29f061882..412e4d0dee 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, "", `<pre class="code-block is-loading">`) + `<code class="language-math display">` - _ = r.renderInternal.FormatWithSafeAttrs(w, code) + _ = r.renderInternal.FormatWithSafeAttrs(w, template.HTML(code)) r.writeLines(w, source, n) } else { _, _ = w.WriteString(`</code>` + giteaUtil.Iif(n.Inline, "", `</pre>`) + "\n") diff --git a/modules/markup/orgmode/orgmode.go b/modules/markup/orgmode/orgmode.go index c6cc334000..70d02c1321 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 e3cc05b4f0..de39bafebe 100644 --- a/modules/markup/orgmode/orgmode_test.go +++ b/modules/markup/orgmode/orgmode_test.go @@ -103,8 +103,8 @@ func HelloWorld() { } #+end_src `, `<div class="src src-go"> -<pre><code class="chroma language-go"><span class="c1">// HelloWorld prints "Hello World" -</span><span class="c1"></span><span class="kd">func</span> <span class="nf">HelloWorld</span><span class="p">()</span> <span class="p">{</span> +<pre><code class="chroma language-go"><span class="c1">// HelloWorld prints "Hello World"</span> +<span class="kd">func</span> <span class="nf">HelloWorld</span><span class="p">()</span> <span class="p">{</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">"Hello World"</span><span class="p">)</span> <span class="p">}</span></code></pre> </div>`) diff --git a/modules/markup/render_helper.go b/modules/markup/render_helper.go index 82796ef274..8ff0e7d6fb 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 5eeafe940a..14161eb533 100644 --- a/modules/markup/sanitizer_default.go +++ b/modules/markup/sanitizer_default.go @@ -48,7 +48,7 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy { policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(unchecked|checked|indeterminate)$`)).OnElements("li") // Allow 'color' and 'background-color' properties for the style attribute on text elements. - policy.AllowStyles("color", "background-color").OnElements("span", "p") + policy.AllowStyles("color", "background-color").OnElements("div", "span", "p", "tr", "th", "td") policy.AllowAttrs("src", "autoplay", "controls").OnElements("video") diff --git a/modules/packages/maven/metadata.go b/modules/packages/maven/metadata.go index 42aa250718..a61a62c086 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 e675467730..2cff290808 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 := `<?xml version="1.0"?> +<project> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>com.mycompany.app</groupId> + <artifactId>my-app</artifactId> + <version>1.0-SNAPSHOT</version> + </parent> + <artifactId>submodule1</artifactId> +</project> +` + 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 := `<?xml version="1.0"?> +<project> + <modelVersion>4.0.0</modelVersion> + <artifactId></artifactId> +</project> +` + _, 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 7d3d7cd6b5..8ba4dbfba7 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 6bb77f302b..d1d0263387 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 6e549cb875..a5b102b7f2 100644 --- a/modules/references/references.go +++ b/modules/references/references.go @@ -32,7 +32,7 @@ var ( // issueNumericPattern matches string that references to a numeric issue, e.g. #1287 issueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[|\'|\")([#!][0-9]+)(?:\s|$|\)|\]|\'|\"|[:;,.?!]\s|[:;,.?!]$)`) // issueAlphanumericPattern matches string that references to an alphanumeric issue, e.g. ABC-1234 - issueAlphanumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[|\"|\')([A-Z]{1,10}-[1-9][0-9]*)(?:\s|$|\)|\]|:|\.(\s|$)|\"|\')`) + issueAlphanumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[|\"|\')([A-Z]{1,10}-[1-9][0-9]*)(?:\s|$|\)|\]|:|\.(\s|$)|\"|\'|,)`) // crossReferenceIssueNumericPattern matches string that references a numeric issue in a different repository // e.g. org/repo#12345 crossReferenceIssueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-zA-Z-_\.]+/[0-9a-zA-Z-_\.]+[#!][0-9]+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`) @@ -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 e224c919e9..1b6a968d6a 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) @@ -463,6 +462,7 @@ func TestRegExp_issueAlphanumericPattern(t *testing.T) { "ABC-123:", "\"ABC-123\"", "'ABC-123'", + "ABC-123, unknown PR", } falseTestCases := []string{ "RC-08", diff --git a/modules/repository/branch.go b/modules/repository/branch.go index 2bf9930f19..4630e70aa8 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 5f500c5233..24602ae090 100644 --- a/modules/repository/init.go +++ b/modules/repository/init.go @@ -81,7 +81,7 @@ func LoadRepoConfig() error { if isDir, err := util.IsDir(customPath); err != nil { return fmt.Errorf("failed to check custom %s dir: %w", t, err) } else if isDir { - if typeFiles[i].custom, err = util.StatDir(customPath); err != nil { + if typeFiles[i].custom, err = util.ListDirRecursively(customPath, &util.ListDirOptions{SkipCommonHiddenNames: true}); err != nil { return fmt.Errorf("failed to list custom %s files: %w", t, err) } } diff --git a/modules/repository/license_test.go b/modules/repository/license_test.go index 3b0cfa1eed..d00156a496 100644 --- a/modules/repository/license_test.go +++ b/modules/repository/license_test.go @@ -31,12 +31,7 @@ func Test_getLicense(t *testing.T) { Copyright (c) 2023 Gitea -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -`, +Permission is hereby granted`, wantErr: assert.NoError, }, { @@ -53,7 +48,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI if !tt.wantErr(t, err, fmt.Sprintf("GetLicense(%v, %v)", tt.args.name, tt.args.values)) { return } - assert.Equalf(t, tt.want, string(got), "GetLicense(%v, %v)", tt.args.name, tt.args.values) + assert.Contains(t, string(got), tt.want, "GetLicense(%v, %v)", tt.args.name, tt.args.values) }) } } diff --git a/modules/reqctx/datastore.go b/modules/reqctx/datastore.go new file mode 100644 index 0000000000..94232450f3 --- /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 dfcb7db3c8..5d94a9641f 100644 --- a/modules/setting/config_env.go +++ b/modules/setting/config_env.go @@ -166,3 +166,25 @@ func EnvironmentToConfig(cfg ConfigProvider, envs []string) (changed bool) { } return changed } + +// InitGiteaEnvVars initializes the environment variables for gitea +func InitGiteaEnvVars() { + // Ideally Gitea should only accept the environment variables which it clearly knows instead of unsetting the ones it doesn't want, + // but the ideal behavior would be a breaking change, and it seems not bringing enough benefits to end users, + // so at the moment we could still keep "unsetting the unnecessary environments" + + // HOME is managed by Gitea, Gitea's git should use "HOME/.gitconfig". + // But git would try "XDG_CONFIG_HOME/git/config" first if "HOME/.gitconfig" does not exist, + // then our git.InitFull would still write to "XDG_CONFIG_HOME/git/config" if XDG_CONFIG_HOME is set. + _ = os.Unsetenv("XDG_CONFIG_HOME") +} + +func InitGiteaEnvVarsForTesting() { + InitGiteaEnvVars() + _ = os.Unsetenv("GIT_AUTHOR_NAME") + _ = os.Unsetenv("GIT_AUTHOR_EMAIL") + _ = os.Unsetenv("GIT_AUTHOR_DATE") + _ = os.Unsetenv("GIT_COMMITTER_NAME") + _ = os.Unsetenv("GIT_COMMITTER_EMAIL") + _ = os.Unsetenv("GIT_COMMITTER_DATE") +} diff --git a/modules/setting/cors.go b/modules/setting/cors.go index 63daaad60b..5260887d9d 100644 --- a/modules/setting/cors.go +++ b/modules/setting/cors.go @@ -5,8 +5,6 @@ package setting import ( "time" - - "code.gitea.io/gitea/modules/log" ) // CORSConfig defines CORS settings @@ -28,7 +26,4 @@ var CORSConfig = struct { func loadCorsFrom(rootCfg ConfigProvider) { mustMapSetting(rootCfg, "cors", &CORSConfig) - if CORSConfig.Enabled { - log.Info("CORS Service Enabled") - } } diff --git a/modules/setting/indexer.go b/modules/setting/indexer.go index 18585602c3..e34baae012 100644 --- a/modules/setting/indexer.go +++ b/modules/setting/indexer.go @@ -31,6 +31,8 @@ var Indexer = struct { IncludePatterns []*GlobMatcher ExcludePatterns []*GlobMatcher ExcludeVendored bool + + TypeBleveMaxFuzzniess int }{ IssueType: "bleve", IssuePath: "indexers/issues.bleve", @@ -88,6 +90,7 @@ func loadIndexerFrom(rootCfg ConfigProvider) { Indexer.ExcludeVendored = sec.Key("REPO_INDEXER_EXCLUDE_VENDORED").MustBool(true) Indexer.MaxIndexerFileSize = sec.Key("MAX_FILE_SIZE").MustInt64(1024 * 1024) Indexer.StartupTimeout = sec.Key("STARTUP_TIMEOUT").MustDuration(30 * time.Second) + Indexer.TypeBleveMaxFuzzniess = sec.Key("TYPE_BLEVE_MAX_FUZZINESS").MustInt(0) } // IndexerGlobFromString parses a comma separated list of patterns and returns a glob.Glob slice suited for repo indexing @@ -97,7 +100,7 @@ func IndexerGlobFromString(globstr string) []*GlobMatcher { expr = strings.TrimSpace(expr) if expr != "" { if g, err := GlobMatcherCompile(expr, '.', '/'); err != nil { - log.Info("Invalid glob expression '%s' (skipped): %v", expr, err) + log.Warn("Invalid glob expression '%s' (skipped): %v", expr, err) } else { extarr = append(extarr, g) } diff --git a/modules/setting/mailer.go b/modules/setting/mailer.go index d4db55dc7b..4c3dff6850 100644 --- a/modules/setting/mailer.go +++ b/modules/setting/mailer.go @@ -255,8 +255,6 @@ func loadMailerFrom(rootCfg ConfigProvider) { MailService.OverrideEnvelopeFrom = true MailService.EnvelopeFrom = parsed.Address } - - log.Info("Mail Service Enabled") } func loadRegisterMailFrom(rootCfg ConfigProvider) { @@ -267,7 +265,6 @@ func loadRegisterMailFrom(rootCfg ConfigProvider) { return } Service.RegisterEmailConfirm = true - log.Info("Register Mail Service Enabled") } func loadNotifyMailFrom(rootCfg ConfigProvider) { @@ -278,7 +275,6 @@ func loadNotifyMailFrom(rootCfg ConfigProvider) { return } Service.EnableNotifyMail = true - log.Info("Notify Mail Service Enabled") } func tryResolveAddr(addr string) []net.IPAddr { diff --git a/modules/setting/security.go b/modules/setting/security.go index 3d12fcf8d9..2f798b75c7 100644 --- a/modules/setting/security.go +++ b/modules/setting/security.go @@ -13,8 +13,9 @@ import ( "code.gitea.io/gitea/modules/log" ) +// Security settings + var ( - // Security settings InstallLock bool SecretKey string InternalToken string // internal access token @@ -27,7 +28,7 @@ var ( ReverseProxyTrustedProxies []string MinPasswordLength int ImportLocalPaths bool - DisableGitHooks bool + DisableGitHooks = true DisableWebhooks bool OnlyAllowPushIfGiteaEnvironmentSet bool PasswordComplexity []string diff --git a/modules/setting/session.go b/modules/setting/session.go index afe63bfdb7..19a05ce2c2 100644 --- a/modules/setting/session.go +++ b/modules/setting/session.go @@ -73,6 +73,4 @@ func loadSessionFrom(rootCfg ConfigProvider) { SessionConfig.ProviderConfig = string(shadowConfig) SessionConfig.OriginalProvider = SessionConfig.Provider SessionConfig.Provider = "VirtualSession" - - log.Info("Session Service Enabled") } diff --git a/modules/setting/setting.go b/modules/setting/setting.go index c93d199b1b..20da796b58 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 39acba12ef..97988211a9 100644 --- a/modules/setting/time.go +++ b/modules/setting/time.go @@ -20,7 +20,6 @@ func loadTimeFrom(rootCfg ConfigProvider) { if err != nil { log.Fatal("Load time zone failed: %v", err) } - log.Info("Default UI Location is %v", zone) } if DefaultUILocation == nil { DefaultUILocation = time.Local diff --git a/modules/setting/ui.go b/modules/setting/ui.go index db0fe9ef79..20fc612b43 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 96c2525b29..837afd0ba6 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 604870cb98..6905db5008 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 82248fb0c1..4fcb11a57d 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 d4b9e167c2..911319d00a 100644 --- a/modules/system/appstate_test.go +++ b/modules/system/appstate_test.go @@ -13,9 +13,7 @@ import ( ) func TestMain(m *testing.M) { - unittest.MainTest(m, &unittest.TestOptions{ - FixtureFiles: []string{""}, // load nothing - }) + unittest.MainTest(m, &unittest.TestOptions{FixtureFiles: []string{ /* load nothing */ }}) } type testItem1 struct { @@ -36,8 +34,6 @@ func (*testItem2) Name() string { } func TestAppStateDB(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - as := &DBStore{} item1 := new(testItem1) diff --git a/modules/templates/helper.go b/modules/templates/helper.go index 880769dc65..609407d36b 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 a530d484bc..5d7bc93622 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, `<a k="""><></a>`, 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 ace81bf4a5..310d645328 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 d645fa013e..2d42bc76b5 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 382e2de13f..683c77a870 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 1272f5af2e..d9f17bd124 100644 --- a/modules/util/path.go +++ b/modules/util/path.go @@ -140,81 +140,51 @@ func IsExist(path string) (bool, error) { return false, err } -func statDir(dirPath, recPath string, includeDir, isDirOnly, followSymlinks bool) ([]string, error) { - dir, err := os.Open(dirPath) +func listDirRecursively(result *[]string, fsDir, recordParentPath string, opts *ListDirOptions) error { + dir, err := os.Open(fsDir) if err != nil { - return nil, err + return err } defer dir.Close() fis, err := dir.Readdir(0) if err != nil { - return nil, err + return err } - statList := make([]string, 0) for _, fi := range fis { - if CommonSkip(fi.Name()) { + if opts.SkipCommonHiddenNames && IsCommonHiddenFileName(fi.Name()) { continue } - - relPath := path.Join(recPath, fi.Name()) - curPath := path.Join(dirPath, fi.Name()) + relPath := path.Join(recordParentPath, fi.Name()) + curPath := filepath.Join(fsDir, fi.Name()) if fi.IsDir() { - if includeDir { - statList = append(statList, relPath+"/") - } - 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 6a38bf4ace..79c37e55f7 100644 --- a/modules/util/path_test.go +++ b/modules/util/path_test.go @@ -5,10 +5,12 @@ package util import ( "net/url" + "os" "runtime" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestFileURLToPath(t *testing.T) { @@ -210,3 +212,21 @@ func TestCleanPath(t *testing.T) { assert.Equal(t, c.expected, FilePathJoinAbs(c.elems[0], c.elems[1:]...), "case: %v", c.elems) } } + +func TestListDirRecursively(t *testing.T) { + tmpDir := t.TempDir() + _ = os.WriteFile(tmpDir+"/.config", nil, 0o644) + _ = os.Mkdir(tmpDir+"/d1", 0o755) + _ = os.WriteFile(tmpDir+"/d1/f-d1", nil, 0o644) + _ = os.Mkdir(tmpDir+"/d1/s1", 0o755) + _ = os.WriteFile(tmpDir+"/d1/s1/f-d1s1", nil, 0o644) + _ = os.Mkdir(tmpDir+"/d2", 0o755) + + res, err := ListDirRecursively(tmpDir, &ListDirOptions{IncludeDir: true}) + require.NoError(t, err) + assert.ElementsMatch(t, []string{".config", "d1/", "d1/f-d1", "d1/s1/", "d1/s1/f-d1s1", "d2/"}, res) + + res, err = ListDirRecursively(tmpDir, &ListDirOptions{SkipCommonHiddenNames: true}) + require.NoError(t, err) + assert.ElementsMatch(t, []string{"d1/f-d1", "d1/s1/f-d1s1"}, res) +} diff --git a/modules/util/string.go b/modules/util/string.go index cf50f591c6..19cf75b8b3 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 0a4a8bbcfb..ff67b5c7d4 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 f2edbdc673..2bce248281 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 dfe1230fd4..8789c824f5 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 1812c664b3..9a3e4a7f17 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 08d83f94be..a47da0f836 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 88da2049a4..0caaa8c036 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 6e4c309293..0000000000 --- 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 index 787521dfb0..da06b955b1 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 0000000000..4478689027 --- /dev/null +++ b/modules/web/router_combo.go @@ -0,0 +1,41 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package web + +// Combo represents a tiny group routes with same pattern +type Combo struct { + r *Router + pattern string + h []any +} + +// Get delegates Get method +func (c *Combo) Get(h ...any) *Combo { + c.r.Get(c.pattern, append(c.h, h...)...) + return c +} + +// Post delegates Post method +func (c *Combo) Post(h ...any) *Combo { + c.r.Post(c.pattern, append(c.h, h...)...) + return c +} + +// Delete delegates Delete method +func (c *Combo) Delete(h ...any) *Combo { + c.r.Delete(c.pattern, append(c.h, h...)...) + return c +} + +// Put delegates Put method +func (c *Combo) Put(h ...any) *Combo { + c.r.Put(c.pattern, append(c.h, h...)...) + return c +} + +// Patch delegates Patch method +func (c *Combo) Patch(h ...any) *Combo { + c.r.Patch(c.pattern, append(c.h, h...)...) + return c +} diff --git a/modules/web/router_path.go b/modules/web/router_path.go new file mode 100644 index 0000000000..b59948581a --- /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: "/<name>" (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: "/<name[2]:(\d)-(\d)>" + // 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 0000000000..582980a27a --- /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 "<name:\w{3,4}>" easily and parse it easily + testProcess("/<p1>/<p2>", "/a/b", map[string]string{"p1": "a", "p2": "b"}) + testProcess("/<p1:*>", "", map[string]string{"p1": ""}) // this is a special case, because chi router could use empty path + testProcess("/<p1:*>", "/", map[string]string{"p1": ""}) + testProcess("/<p1:*>/<p2>", "/a", map[string]string{"p1": "", "p2": "a"}) + testProcess("/<p1:*>/<p2>", "/a/b", map[string]string{"p1": "a", "p2": "b"}) + testProcess("/<p1:*>/<p2>", "/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", `/<dir:*>/<file:[a-z]{1,2}>`, 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 fbec889272..aa4de45eb4 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/gitignore/AutomationStudio b/options/gitignore/AutomationStudio new file mode 100644 index 0000000000..b5552b17a0 --- /dev/null +++ b/options/gitignore/AutomationStudio @@ -0,0 +1,31 @@ +# gitignore template for B&R Automation Studio (AS) 4 +# website: https://www.br-automation.com/en-us/products/software/automation-software/automation-studio/ + +# AS temporary directories +Binaries/ +Diagnosis/ +Temp/ +TempObjects/ + +# AS transfer files +*artransfer.br +*arTrsfmode.nv + +# 'ignored' directory +ignored/ + +# ARNC0ext +*arnc0ext.br + +# AS File types +*.bak +*.isopen +*.orig +*.log +*.asar +*.csvlog* +*.set +!**/Physical/**/*.set + +# RevInfo variables +*RevInfo.var diff --git a/options/gitignore/Firebase b/options/gitignore/Firebase new file mode 100644 index 0000000000..55b8b0ea7f --- /dev/null +++ b/options/gitignore/Firebase @@ -0,0 +1,28 @@ +# Firebase build and deployment files +/firebase-debug.log +/firebase-debug.*.log +.firebaserc + +# Firebase Hosting +/firebase.json +*.cache +hosting/.cache + +# Firebase Functions +/functions/node_modules/ +/functions/.env +/functions/package-lock.json + +# Firebase Emulators +/firebase-*.zip +/.firebase/ +/emulator-ui/ + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Environment files (local configs) +/.env.* diff --git a/options/gitignore/Modelica b/options/gitignore/Modelica new file mode 100644 index 0000000000..aa2cc996da --- /dev/null +++ b/options/gitignore/Modelica @@ -0,0 +1,42 @@ +# Modelica - an object-oriented language for modeling of cyber-physical systems +# https://modelica.org/ +# Ignore temporary files, build results, simulation files + +## Modelica-specific files +*~ +*.bak +*.bak-mo +*.mof +\#*\# +*.moe +*.mol + +## Build artefacts +*.exe +*.exp +*.o +*.pyc + +## Simulation files +*.mat + +## Package files +*.gz +*.rar +*.tar +*.zip + +## Dymola-specific files +buildlog.txt +dsfinal.txt +dsin.txt +dslog.txt +dsmodel* +dsres.txt +dymosim* +request +stat +status +stop +success +*. diff --git a/options/gitignore/Python b/options/gitignore/Python index c2fb773388..15201acc11 100644 --- a/options/gitignore/Python +++ b/options/gitignore/Python @@ -166,3 +166,6 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + +# PyPI configuration file +.pypirc diff --git a/options/gitignore/TeX b/options/gitignore/TeX index a1f5212090..45e1706c28 100644 --- a/options/gitignore/TeX +++ b/options/gitignore/TeX @@ -26,6 +26,7 @@ ## Bibliography auxiliary files (bibtex/biblatex/biber): *.bbl +*.bbl-SAVE-ERROR *.bcf *.blg *-blx.aux diff --git a/options/license/Elastic-2.0 b/options/license/Elastic-2.0 index 809108b857..9496955678 100644 --- a/options/license/Elastic-2.0 +++ b/options/license/Elastic-2.0 @@ -2,18 +2,18 @@ Elastic License 2.0 URL: https://www.elastic.co/licensing/elastic-license -## Acceptance +Acceptance By using the software, you agree to all of the terms and conditions below. -## Copyright License +Copyright License The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject to the limitations and conditions below. -## Limitations +Limitations You may not provide the software to third parties as a hosted or managed service, where the service provides users with access to any substantial set of @@ -27,7 +27,7 @@ You may not alter, remove, or obscure any licensing, copyright, or other notices of the licensor in the software. Any use of the licensor’s trademarks is subject to applicable law. -## Patents +Patents The licensor grants you a license, under any patent claims the licensor can license, or becomes able to license, to make, have made, use, sell, offer for @@ -40,7 +40,7 @@ the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company. -## Notices +Notices You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms. @@ -53,7 +53,7 @@ software prominent notices stating that you have modified the software. These terms do not imply any licenses other than those expressly granted in these terms. -## Termination +Termination If you use the software in violation of these terms, such use is not licensed, and your licenses will automatically terminate. If the licensor provides you @@ -63,31 +63,31 @@ reinstated retroactively. However, if you violate these terms after such reinstatement, any additional violation of these terms will cause your licenses to terminate automatically and permanently. -## No Liability +No Liability -*As far as the law allows, the software comes as is, without any warranty or +As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of -legal claim.* +legal claim. -## Definitions +Definitions -The **licensor** is the entity offering these terms, and the **software** is the +The licensor is the entity offering these terms, and the software is the software the licensor makes available under these terms, including any portion of it. -**you** refers to the individual or entity agreeing to these terms. +you refers to the individual or entity agreeing to these terms. -**your company** is any legal entity, sole proprietorship, or other kind of +your company is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that -organization. **control** means ownership of substantially all the assets of an +organization. control means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect. -**your licenses** are all the licenses granted to you for the software under +your licenses are all the licenses granted to you for the software under these terms. -**use** means anything you do with the software requiring one of your licenses. +use means anything you do with the software requiring one of your licenses. -**trademark** means trademarks, service marks, and similar rights. +trademark means trademarks, service marks, and similar rights. diff --git a/options/license/MIT b/options/license/MIT index 2071b23b0e..fc2cf8e6b6 100644 --- a/options/license/MIT +++ b/options/license/MIT @@ -2,8 +2,17 @@ MIT License Copyright (c) <year> <copyright holders> -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index 2abe3672cd..beeb1dc3b8 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -647,6 +647,7 @@ joined_on=PÅ™idal/a se %s repositories=Repozitáře activity=VeÅ™ejná aktivita followers=SledujÃcà +show_more=Zobrazit vÃce starred=OblÃbené repozitáře watched=Sledované repozitáře code=Kód @@ -1011,7 +1012,6 @@ new_repo_helper=Repozitář obsahuje vÅ¡echny projektové soubory, vÄetnÄ› hist owner=VlastnÃk owner_helper=NÄ›které organizace se nemusejà v seznamu zobrazit kvůli maximálnÃmu dosaženému poÄtu repozitářů. repo_name=Název repozitáře -repo_name_helper=Dobrý název repozitáře vÄ›tÅ¡inou použÃvá krátká, zapamatovatelná a unikátnà klÃÄová slova. repo_size=Velikost repozitáře template=Å ablona template_select=Vyberte Å¡ablonu. @@ -2832,6 +2832,7 @@ teams.invite.title=Byli jste pozváni do týmu <strong>%s</strong> 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 c8dd3f71a2..ce2c43cb56 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -93,6 +93,7 @@ remove_all=Alle entfernen remove_label_str=Element "%s " entfernen edit=Bearbeiten view=Anzeigen +test=Test enabled=Aktiviert disabled=Deaktiviert @@ -103,6 +104,7 @@ copy_url=URL kopieren copy_hash=Hash kopieren copy_content=Inhalt kopieren copy_branch=Branchnamen kopieren +copy_path=Pfad kopieren copy_success=Kopiert! copy_error=Kopieren fehlgeschlagen copy_type_unsupported=Dieser Dateityp kann nicht kopiert werden @@ -143,6 +145,7 @@ confirm_delete_selected=Alle ausgewählten Elemente löschen? name=Name value=Wert +readme=Readme filter=Filter filter.clear=Filter leeren @@ -158,12 +161,15 @@ filter.public=Öffentlich filter.private=Privat no_results_found=Es wurden keine Ergebnisse gefunden. +internal_error_skipped=Ein interner Fehler ist aufgetreten, wurde aber übersprungen: %s [search] search=Suche ... type_tooltip=Suchmodus fuzzy=Ähnlich fuzzy_tooltip=Ergebnisse einbeziehen, die dem Suchbegriff ähnlich sind +exact=Exakt +exact_tooltip=Nur Suchbegriffe einbeziehen, die dem exakten Suchbegriff entsprechen repo_kind=Repositories durchsuchen ... user_kind=Benutzer durchsuchen ... org_kind=Organisationen durchsuchen ... @@ -174,9 +180,13 @@ code_search_by_git_grep=Aktuelle Code-Suchergebnisse werden von "git grep" berei package_kind=Pakete durchsuchen ... project_kind=Projekte durchsuchen ... branch_kind=Branches durchsuchen ... +tag_kind=Tags durchsuchen... +tag_tooltip=Suche nach passenden Tags. Benutze '%', um jede Sequenz von Zahlen zu treffen. commit_kind=Commits durchsuchen ... runner_kind=Runner durchsuchen ... no_results=Es wurden keine passenden Ergebnisse gefunden. +issue_kind=Issues durchsuchen ... +pull_kind=Pull-Requests durchsuchen... keyword_search_unavailable=Zurzeit ist die Stichwort-Suche nicht verfügbar. Bitte wende dich an den Website-Administrator. [aria] @@ -201,7 +211,10 @@ buttons.link.tooltip=Link hinzufügen buttons.list.unordered.tooltip=Liste hinzufügen buttons.list.ordered.tooltip=Nummerierte Liste hinzufügen buttons.list.task.tooltip=Aufgabenliste hinzufügen +buttons.table.add.tooltip=Tabelle hinzufügen buttons.table.add.insert=Hinzufügen +buttons.table.rows=Zeilen +buttons.table.cols=Spalten buttons.mention.tooltip=Benutzer oder Team erwähnen buttons.ref.tooltip=Issue oder Pull-Request referenzieren buttons.switch_to_legacy.tooltip=Legacy-Editor verwenden @@ -214,16 +227,20 @@ string.desc=Z–A [error] occurred=Ein Fehler ist aufgetreten +report_message=Wenn du glaubst, dass dies ein Fehler von Gitea ist, suche bitte auf <a href="%s" target="_blank">GitHub</a> 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 <a target="_blank" rel="noopener noreferrer" href="%[1]s">die Anwendung</a> für deine Plattform oder nutze <a target="_blank" rel="noopener noreferrer" href="%[2]s">Docker</a>. Es existieren auch <a target="_blank" rel="noopener noreferrer" href="%[3]s">paketierte Versionen</a>. platform=Plattformübergreifend +platform_desc=Gitea läuft überall, wo <a target="_blank" rel="noopener noreferrer" href="%s">Go</a> 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 <a target="_blank" rel="noopener noreferrer" href="%[1]s">%[2]s</a>! Leiste deinen <a target="_blank" rel="noopener noreferrer" href="%[3]s">Beitrag</a> 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, <a href="% [auth] create_new_account=Konto anlegen +already_have_account=Du hast bereits ein Konto? +sign_in_now=Jetzt anmelden! disable_register_prompt=Die Registrierung ist deaktiviert. Bitte wende dich an den Administrator. disable_register_mail=E-Mail-Bestätigung bei der Registrierung ist deaktiviert. manual_activation_only=Kontaktiere den Website-Administrator, um die Aktivierung abzuschließen. @@ -384,6 +404,8 @@ remember_me=Dieses Gerät speichern remember_me.compromised=Das Login-Token ist nicht mehr gültig, was auf ein kompromittiertes Konto hindeuten kann. Bitte überprüfe dein Konto auf ungewöhnliche Aktivitäten. forgot_password_title=Passwort vergessen forgot_password=Passwort vergessen? +need_account=Noch kein Konto? +sign_up_now=Jetzt registrieren. sign_up_successful=Konto wurde erfolgreich erstellt. Willkommen! confirmation_mail_sent_prompt_ex=Eine neue Bestätigungs-E-Mail wurde an <b>%s</b>gesendet. Bitte überprüfe deinen Posteingang innerhalb der nächsten %s, um den Registrierungsprozess abzuschließen. Wenn deine Registrierungs-E-Mail-Adresse falsch ist, kannst du dich erneut anmelden und diese ändern. must_change_password=Aktualisiere dein Passwort @@ -424,6 +446,7 @@ oauth_signin_submit=Konto verbinden oauth.signin.error=Beim Verarbeiten der Autorisierungsanfrage ist ein Fehler aufgetreten. Wenn dieser Fehler weiterhin besteht, wende dich bitte an deinen Administrator. oauth.signin.error.access_denied=Die Autorisierungsanfrage wurde abgelehnt. oauth.signin.error.temporarily_unavailable=Autorisierung fehlgeschlagen, da der Authentifizierungsserver vorübergehend nicht verfügbar ist. Bitte versuch es später erneut. +oauth_callback_unable_auto_reg=Automatische Registrierung ist aktiviert, aber der OAuth2-Provider %[1]s hat fehlende Felder zurückgegeben: %[2]s, kann den Account nicht automatisch erstellen. Bitte erstelle oder verbinde einen Account oder kontaktieren den Administrator. openid_connect_submit=Verbinden openid_connect_title=Mit bestehendem Konto verbinden openid_connect_desc=Die gewählte OpenID-URI ist unbekannt. Ordne sie hier einem neuen Account zu. @@ -437,12 +460,16 @@ authorize_application=Anwendung autorisieren authorize_redirect_notice=Du wirst zu %s weitergeleitet, wenn du diese Anwendung autorisierst. authorize_application_created_by=Diese Anwendung wurde von %s erstellt. authorize_application_description=Wenn du diese Anwendung autorisierst, wird sie die Berechtigung erhalten, alle Informationen zu deinem Account zu bearbeiten oder zu lesen. Dies beinhaltet auch private Repositories und Organisationen. +authorize_application_with_scopes=Mit Bereichen: %s authorize_title=`"%s" den Zugriff auf deinen Account gestatten?` authorization_failed=Autorisierung fehlgeschlagen authorization_failed_desc=Die Autorisierung ist fehlgeschlagen, da wir eine ungültige Anfrage erkannt haben. Bitte kontaktiere den Betreuer der App, die du zu autorisieren versucht hast. sspi_auth_failed=SSPI-Authentifizierung fehlgeschlagen +password_pwned=Das von dir gewählte Passwort befindet sich auf einer <a target="_blank" rel="noopener noreferrer" href="%s">Liste gestohlener Passwörter</a>, 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 <b>%s</b> 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 <a %s>API</a>-Routen. Lies die <a %s>Dokumentation</a> 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. <strong>Dies kann ein Sicherheitsrisiko darstellen.</strong> 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 <strong>eingeschaltet</strong>. 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 "<a rel="noreferrer" target="_blank" href="%s">WebAuthn</a>" 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 <a target="_blank" rel="noopener noreferrer" href="%s">eine Lizenz wählen</a>. +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 <a href="%s">.git-blame-ignore-revs</a> werden ignoriert. Klicke <a href="%s">hier, um das zu umgehen</a> und die normale Blame-Ansicht zu sehen. blame.ignore_revs.failed=Fehler beim Ignorieren der Revisionen in <a href="%s">.git-blame-ignore-revs</a>. +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 <b>%[1]s</b> Meilenstein hinzugefügt` issues.add_project_at=`hat dieses zum <b>%s</b> 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 <b>%[1]s</b> zu <b>%[2]s</b> geändert` issues.change_project_at=`hat das Projekt %[3]s von <b>%[1]s</b> zu <b>%[2]s</b> geändert` issues.remove_milestone_at=`hat dieses Issue %[2]s vom <b>%[1]s</b> 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 <a id="%[1]s" href="#%[1]s">%[2]s</a> 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 <b>%s</b> %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 <a href="%s">diesem Issue</a> gestartet!` +issues.stop_tracking_history=hat für <b>%s</b> gearbeitet %s issues.cancel_tracking_history=`hat die Zeiterfassung %s abgebrochen` issues.del_time=Diese Zeiterfassung löschen +issues.add_time_history=hat <b>%s</b> 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 <strong>%[1]s</strong> %[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=<strong>%d%%</strong> 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 <code>*</code> ist, oder falls es leer ist. Siehe die <a href="%[1]s">%[2]s</a> Dokumentation für die Syntax (Englisch). Beispiele: <code>master</code>, <code>{master,release*}</code>. 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 '<b>%s</b>' 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 <a href="%s">die Dokumentation</a> 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 <a href='%[1]s'>%[2]s</a> Dokumentation zur Pattern-Syntax. Beispiele: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>. 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 <a href='%[1]s'>%[2]s</a> Dokumentation zur Mustersyntax. Beispiele: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>. +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 <a target="_blank" rel="noopener" href="%s">Guide für geschützte Tags (Englisch)</a>. 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=<strong>Das Ändern der Sichtbarkeit auf privat wird:</strong> +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 <strong>Forks</strong>, <strong>Beobachtern</strong>und <strong>Sternen</strong> 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=<strong>Das Ändern der Sichtbarkeit auf öffentlich wird:</strong> +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=<strong>%d</strong> 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 <strong>%s</strong> 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 <a target="_blank" rel="noreferrer" href="%s">Blog</a>. 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 <a target="_blank" rel="noopener" href="%s">Webhooks-Anleitung</a> (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 <a target="_blank" rel="noopener" href="%s">Webhooks-Anleitung</a> (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: <strong>%s</strong>? 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 <a href="%s">%s</a> erstellt @@ -3233,6 +3436,7 @@ mirror_sync_create=neue Referenz <a href="%[2]s">%[3]s</a> bei <a href="%[1]s">% mirror_sync_delete=hat die Referenz des Mirrors <code>%[2]s</code> in <a href="%[1]s">%[3]s</a> synchronisiert und gelöscht approve_pull_request=`hat <a href="%[1]s">%[3]s#%[2]s</a> approved` reject_pull_request=`schlug Änderungen für <a href="%[1]s">%[3]s#%[2]s</a> vor` +publish_release=`veröffentlichte Release <a href="%[2]s"> "%[4]s" </a> in <a href="%[1]s">%[3]s</a>` review_dismissed=`verwarf das Review von <b>%[4]s</b> in <a href="%[1]s">%[3]s#%[2]s</a>` review_dismissed_reason=Grund: create_branch=legte den Branch <a href="%[2]s">%[3]s</a> in <a href="%[1]s">%[4]s</a> 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 <a target="_blank" rel="noopener noreferrer" href="%s">Dokumentation</a>. empty.repo=Hast du ein Paket hochgeladen, das hier nicht angezeigt wird? Gehe zu den <a href="%[1]s">Paketeinstellungen</a> und verlinke es mit diesem Repo. registry.documentation=Für weitere Informationen zur %s-Registry, schaue in der <a target="_blank" rel="noopener noreferrer" href="%s">Dokumentation</a> 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 <code>/etc/pacman.conf</code> 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 <a target="_blank" rel="noopener noreferrer" href="%s"> Dokumentation</a>. 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 193441828a..31e57bbf97 100644 --- a/options/locale/locale_el-GR.ini +++ b/options/locale/locale_el-GR.ini @@ -580,6 +580,7 @@ joined_on=ΕγγÏάφηκε την %s repositories=ΑποθετήÏια activity=Δημόσια ΔÏαστηÏιότητα followers=Ακόλουθοι +show_more=Εμφάνιση ΠεÏισσότεÏων starred=ΑγαπημÎνα ΑποθετήÏια watched=ΑκολουθοÏμενα ΑποθετήÏια code=Κώδικας @@ -907,7 +908,6 @@ new_repo_helper=Ένα αποθετήÏιο πεÏιÎχει όλα τα αÏχΠowner=Ιδιοκτήτης owner_helper=ΟÏισμÎνοι οÏγανισμοί ενδÎχεται να μην εμφανίζονται στο αναπτυσσόμενο Î¼ÎµÎ½Î¿Ï Î»ÏŒÎ³Ï‰ του μÎγιστου αÏÎ¹Î¸Î¼Î¿Ï Î±Ï€Î¿Î¸ÎµÏ„Î·Ïίων. repo_name=Όνομα αποθετηÏίου -repo_name_helper=Τα καλά ονόματα αποθετηÏίων χÏησιμοποιοÏν σÏντομες, αξÎχαστες και μοναδικÎÏ‚ λÎξεις-κλειδιά. repo_size=ÎœÎγεθος ΑποθετηÏίου template=Î Ïότυπο template_select=ΕπιλÎξτε Ï€Ïότυπο. @@ -2592,6 +2592,7 @@ teams.invite.title=Έχετε Ï€Ïοσκληθεί να συμμετάσχετε teams.invite.by=Î Ïοσκλήθηκε από %s teams.invite.description=ΠαÏακαλώ κάντε κλικ στον παÏακάτω σÏνδεσμο για συμμετοχή στην ομάδα. + [admin] dashboard=Πίνακας ΕλÎγχου identity_access=Ταυτότητα & Î Ïόσβαση @@ -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 73041ceee5..140e2efe57 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -244,6 +244,7 @@ license_desc = Go get <a target="_blank" rel="noopener noreferrer" href="%[1]s"> [install] install = Installation +installing_desc = Installing now, please wait... title = Initial Configuration docker_helper = If you run Gitea inside Docker, please read the <a target="_blank" rel="noopener noreferrer" href="%s">documentation</a> 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 <strong>%s</strong> 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 <code>/etc/apk/repositories</code> file: alpine.registry.key = Download the registry public RSA key into the <code>/etc/apk/keys/</code> folder to verify the index signature: alpine.registry.info = Choose $branch and $repository from the list below. @@ -3722,6 +3735,7 @@ runners.status.active = Active runners.status.offline = Offline runners.version = Version runners.reset_registration_token = Reset registration token +runners.reset_registration_token_confirm = Would you like to invalidate the current token and generate a new one? runners.reset_registration_token_success = Runner registration token reset successfully runs.all_workflows = All Workflows @@ -3754,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. @@ -3773,6 +3788,9 @@ variables.creation.success = The variable "%s" has been added. variables.update.failed = Failed to edit variable. variables.update.success = The variable has been edited. +logs.always_auto_scroll = Always auto scroll logs +logs.always_expand_running = Always expand running logs + [projects] deleted.display_name = Deleted Project type-1.display_name = Individual Project diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index e95513766b..cdfe1fb2e5 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -577,6 +577,7 @@ joined_on=Se unió el %s repositories=Repositorios activity=Actividad pública followers=Seguidores +show_more=Ver más starred=Repositorios Favoritos watched=Repositorios seguidos code=Código @@ -897,7 +898,6 @@ visibility.private_tooltip=Visible sólo para los miembros de organizaciones a l owner=Propietario owner_helper=Algunas organizaciones pueden no aparecer en el menú desplegable debido a un lÃmite máximo de recuento de repositorios. repo_name=Nombre del repositorio -repo_name_helper=Un buen nombre de repositorio está compuesto por palabras clave cortas, memorables y únicas. repo_size=Tamaño del repositorio template=Plantilla template_select=Seleccionar una plantilla. @@ -2573,6 +2573,7 @@ teams.invite.title=Has sido invitado a unirte al equipo <strong>%s</strong> 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 640592f2bf..4d90cf9876 100644 --- a/options/locale/locale_fa-IR.ini +++ b/options/locale/locale_fa-IR.ini @@ -463,6 +463,7 @@ change_avatar=تغییر آواتار… repositories=مخازن activity=Ùعالیت های عمومی followers=دنبال کنندگان +show_more=نمایش بیشتر starred=مخان ستاره دار watched=مخازنی Ú©Ù‡ دنبال می‌شوند projects=پروژه‌ها @@ -703,7 +704,6 @@ visibility.private=خصوصی owner=مالک owner_helper=بخاطر بیشینه تعداد مخزن، ممکن است برخی از سازمان‌ها در لیست کشویی دیده نشود. repo_name=نام مخزن -repo_name_helper=نام خوب مخزن معمولا از کلمات کلیدی کوتاه Ùˆ به یاد ماندنی Ùˆ منØصر به Ùرد تشکیل شده است. repo_size=اندازه مخزن template=قالب / الگو template_select=انتخاب یک قالب/ الگو. @@ -1992,6 +1992,7 @@ teams.all_repositories_read_permission_desc=این تیم دسترسی<strong> Ø teams.all_repositories_write_permission_desc=این تیم دسترسی<strong> نوشتن </strong> <strong> مخازن همه</strong> را Ù…ÛŒ بخشد: اعضا Ù…ÛŒ توانند مخازن را مشاهده Ùˆ درج کنند. teams.all_repositories_admin_permission_desc=این تیم دسترسی<strong> مدیر </strong> به <strong> مخازن همه</strong> را Ù…ÛŒ بخشد: اعضا Ù…ÛŒ توانند مخازن را بخواند، همکار Ùˆ مخزن اضاÙÙ‡ کنند. + [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 375c7b11bf..b5fa5c8afc 100644 --- a/options/locale/locale_fi-FI.ini +++ b/options/locale/locale_fi-FI.ini @@ -635,7 +635,6 @@ visibility.private=Yksityinen owner=Omistaja owner_helper=Jotkin organisaatiot eivät välttämättä näy pudotusvalikossa, koska repojen maksimimäärää on rajoitettu. repo_name=Repon nimi -repo_name_helper=Hyvä repon nimi on lyhyt, mieleenpainuva ja yksilöllinen. repo_size=Repon koko template=Malli template_select=Valitse malli. @@ -1361,6 +1360,7 @@ teams.repositories=Tiimin repot teams.members.none=Ei jäseniä tässä tiimissä. teams.all_repositories=Kaikki repot + [admin] dashboard=Kojelauta users=Käyttäjätilit @@ -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 d522abbd11..743b766256 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -649,6 +649,7 @@ joined_on=Inscrit le %s repositories=Dépôts activity=Activité publique followers=abonnés +show_more=Voir plus starred=Dépôts favoris watched=Dépôts surveillés code=Code @@ -1014,7 +1015,6 @@ new_repo_helper=Un dépôt contient tous les fichiers d’un projet, ainsi que l owner=Propriétaire owner_helper=Certaines organisations peuvent ne pas apparaître dans la liste déroulante en raison d'une limite maximale du nombre de dépôts. repo_name=Nom du dépôt -repo_name_helper=Idéalement, le nom d'un dépôt devrait être court, mémorisable et unique. repo_size=Taille du dépôt template=Modèle template_select=Répliquer un modèle @@ -1945,6 +1945,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 <strong>%[1]s</strong> %[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 +2860,7 @@ teams.invite.title=Vous avez été invité à rejoindre l'équipe <strong>%s</st teams.invite.by=Invité par %s teams.invite.description=Veuillez cliquer sur le bouton ci-dessous pour rejoindre l’équipe. + [admin] maintenance=Maintenance dashboard=Tableau de bord @@ -3770,6 +3773,7 @@ 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. + [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 6d989d3347..cf6c76a9db 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 <strong>%[1]s</strong> %[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 <strong>%s</str teams.invite.by=Ar cuireadh ó %s teams.invite.description=Cliceáil ar an gcnaipe thÃos le do thoil chun dul isteach san fhoireann. + [admin] maintenance=Cothabháil dashboard=Deais @@ -3719,6 +3722,7 @@ runners.status.active=GnÃomhach runners.status.offline=As lÃne runners.version=Leagan runners.reset_registration_token=Athshocraigh comhartha clár +runners.reset_registration_token_confirm=Ar mhaith leat an comhartha reatha a neamhbhailiú agus ceann nua a ghiniúint? runners.reset_registration_token_success=D'éirigh le hathshocrú comhartha clárúcháin an dara háit runs.all_workflows=Gach Sreafaà Oibre @@ -3770,6 +3774,9 @@ variables.creation.success=Tá an athróg "%s" curtha leis. variables.update.failed=Theip ar athróg a chur in eagar. variables.update.success=Tá an t-athróg curtha in eagar. +logs.always_auto_scroll=Logchomhaid scrollaithe uathoibrÃoch i gcónaà +logs.always_expand_running=Leathnaigh logs reatha i gcónaà + [projects] deleted.display_name=Tionscadal scriosta type-1.display_name=Tionscadal Aonair diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini index 88ccc9fac2..f0935a2916 100644 --- a/options/locale/locale_hu-HU.ini +++ b/options/locale/locale_hu-HU.ini @@ -563,7 +563,6 @@ visibility.private=Privát [repo] owner=Tulajdonos repo_name=Tároló neve -repo_name_helper=A jó tárolónév általában rövid, megjegyezhetÅ‘ és egyedi kulcsszavakból tevÅ‘dik össze. repo_size=Repozitórium mérete template=Sablon template_select=Válasszon sablont. @@ -1229,6 +1228,7 @@ teams.members.none=Ennek a csapatnak nincsenek tagjai. teams.specific_repositories=Meghatározott tárolók teams.all_repositories=Minden tároló + [admin] dashboard=Műszerfal users=Felhasználói fiókok @@ -1615,6 +1615,7 @@ runs.commit=Commit + [projects] [git.filemode] diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini index 00a406aae4..1fcf6d59b6 100644 --- a/options/locale/locale_id-ID.ini +++ b/options/locale/locale_id-ID.ini @@ -585,7 +585,6 @@ visibility.private=Pribadi [repo] owner=Pemilik repo_name=Nama Repositori -repo_name_helper=Nama repositori yang baik menggunakan kata kunci yang pendek, unik, dan bisa diingat. repo_size=Ukuran Repositori template=Templat template_select=Pilih template. @@ -1083,6 +1082,7 @@ teams.add_team_member=Tambahkan Anggota Tim teams.delete_team_success=Tim sudah di hapus. teams.repositories=Tim repositori + [admin] dashboard=Dasbor organizations=Organisasi @@ -1444,6 +1444,7 @@ variables.creation.success=Variabel "%s" telah ditambahkan. variables.update.failed=Gagal mengedit variabel. variables.update.success=Variabel telah diedit. + [projects] type-1.display_name=Proyek Individu type-2.display_name=Proyek Repositori diff --git a/options/locale/locale_is-IS.ini b/options/locale/locale_is-IS.ini index 0564d49b1c..1eab4d58be 100644 --- a/options/locale/locale_is-IS.ini +++ b/options/locale/locale_is-IS.ini @@ -1136,6 +1136,7 @@ teams.settings=Stillingar teams.update_settings=Uppfæra Stillingar teams.all_repositories=Öll hugbúnaðarsöfn + [admin] repositories=Hugbúnaðarsöfn config=Stilling @@ -1342,6 +1343,7 @@ runs.commit=Framlag + [projects] [git.filemode] diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini index 567e6acdce..17f0aa83d2 100644 --- a/options/locale/locale_it-IT.ini +++ b/options/locale/locale_it-IT.ini @@ -488,6 +488,7 @@ change_avatar=Modifica il tuo avatar… repositories=Repository activity=Attività pubblica followers=Seguaci +show_more=Mostra Altro starred=Repositories votate watched=Repository Osservate projects=Progetti @@ -754,7 +755,6 @@ visibility.private=Privato owner=Proprietario owner_helper=Alcune organizzazioni potrebbero non essere visualizzate nel menu a discesa a causa di un limite massimo al numero di repository. repo_name=Nome Repository -repo_name_helper=Un buon nome per un repository è costituito da parole chiave corte, facili da ricordare e uniche. repo_size=Dimensione repository template=Modello template_select=Seleziona un modello. @@ -2153,6 +2153,7 @@ teams.all_repositories_read_permission_desc=Questo team concede <strong>permessi teams.all_repositories_write_permission_desc=Questo team concede <strong>permessi di scrittura</strong> accesso a <strong>tutte le repository</strong>: i membri possono leggere e pushare le repository. teams.all_repositories_admin_permission_desc=Questo team concede a <strong>Amministratore</strong> l'accesso a <strong>tutte le repository</strong>: 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 bbd48989f7..8ee9112c34 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=ã‚ãªãŸã¯çµ„ç¹” <strong>%[2]s</strong> 内ã®ãƒãƒ¼ãƒ <st teams.invite.by=%s ã‹ã‚‰ã®æ‹›å¾… teams.invite.description=下ã®ãƒœã‚¿ãƒ³ã‚’クリックã—ã¦ãƒãƒ¼ãƒ ã«å‚åŠ ã—ã¦ãã ã•ã„。 + [admin] maintenance=メンテナンス dashboard=ダッシュボード @@ -3762,6 +3763,9 @@ variables.creation.success=変数 "%s" ã‚’è¿½åŠ ã—ã¾ã—ãŸã€‚ variables.update.failed=変数を更新ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ variables.update.success=変数を更新ã—ã¾ã—ãŸã€‚ +logs.always_auto_scroll=常ã«ãƒã‚°ã‚’自動スクãƒãƒ¼ãƒ« +logs.always_expand_running=常ã«å®Ÿè¡Œä¸ã®ãƒã‚°ã‚’展開 + [projects] deleted.display_name=削除ã•ã‚ŒãŸãƒ—ãƒã‚¸ã‚§ã‚¯ãƒˆ type-1.display_name=個人プãƒã‚¸ã‚§ã‚¯ãƒˆ diff --git a/options/locale/locale_ko-KR.ini b/options/locale/locale_ko-KR.ini index 48220d5c99..5485a53c81 100644 --- a/options/locale/locale_ko-KR.ini +++ b/options/locale/locale_ko-KR.ini @@ -111,7 +111,7 @@ license=오픈 소스 [install] install=설치 title=초기 ì„¤ì • -docker_helper="Gitea를 Dockerì—ì„œ ì‹¤í–‰í•˜ë ¤ë©´ ì„¤ì • ì „ì— ì´ <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">문서</a>를 ì½ì–´ë³´ì„¸ìš”." +docker_helper=Gitea를 Dockerì—ì„œ ì‹¤í–‰í•˜ë ¤ë©´ ì„¤ì • ì „ì— ì´ <a target="_blank" rel="noopener noreferrer" href="%s">문서</a>를 ì½ì–´ë³´ì„¸ìš”. db_title=ë°ì´í„°ë² ì´ìŠ¤ ì„¤ì • db_type=ë°ì´í„°ë² ì´ìŠ¤ ìœ í˜• host=호스트 @@ -436,8 +436,8 @@ manage_gpg_keys=GPG 키 관리 add_key=키 추가 ssh_desc=ì´ëŸ¬í•œ SSH 공용 키는 ê·€í•˜ì˜ ê³„ì •ê³¼ ì—°ê²°ë˜ì–´ 있습니다. 해당 ê°œì¸ í‚¤ëŠ” ë‹¹ì‹ ì˜ ì €ìž¥ì†Œì— ëŒ€í•œ ì „ì²´ 액세스를 가능하게 합니다. gpg_desc=ì´ëŸ¬í•œ GPG 공개키는 ë‹¹ì‹ ì˜ ê³„ì •ê³¼ ì—°ê²°ë˜ì–´ìžˆìŠµë‹ˆë‹¤. ì»¤ë°‹ì´ ê²€ì¦ë 수 있ë„ë¡ ë‹¹ì‹ ì˜ ê°œì¸ í‚¤ë¥¼ ì•ˆì „í•˜ê²Œ ìœ ì§€í•˜ì‹ì‹œì˜¤. -ssh_helper="<strong>ë„ì›€ì´ í•„ìš”í•˜ì„¸ìš”?</strong> GitHubì˜ ì„¤ëª…ì„œë¥¼ 참조하시기 ë°”ëžë‹ˆë‹¤: <a href=\"%s\">SSH 키 ìƒì„±í•˜ê¸°</a> ë˜ëŠ” SSH를 ì‚¬ìš©í• ë•Œ <a href=\"%s\">ì¼ë°˜ì ì¸ ë¬¸ì œ</a>" -gpg_helper="<strong>ë„ì›€ì´ í•„ìš”í•˜ì„¸ìš”?</strong> GitHubì˜ ì„¤ëª…ì„œë¥¼ 참조하시기 ë°”ëžë‹ˆë‹¤: <a href=\"%s\">GPGí‚¤ì— ëŒ€í•˜ì—¬</a>." +ssh_helper=<strong>ë„ì›€ì´ í•„ìš”í•˜ì„¸ìš”?</strong> GitHubì˜ ì„¤ëª…ì„œë¥¼ 참조하시기 ë°”ëžë‹ˆë‹¤: <a href="%s">SSH 키 ìƒì„±í•˜ê¸°</a> ë˜ëŠ” SSH를 ì‚¬ìš©í• ë•Œ <a href="%s">ì¼ë°˜ì ì¸ ë¬¸ì œ</a> +gpg_helper=<strong>ë„ì›€ì´ í•„ìš”í•˜ì„¸ìš”?</strong> GitHubì˜ ì„¤ëª…ì„œë¥¼ 참조하시기 ë°”ëžë‹ˆë‹¤: <a href="%s">GPGí‚¤ì— ëŒ€í•˜ì—¬</a>. 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="í´ë¡ 하는ë°ì— ë„ì›€ì´ í•„ìš”í•˜ë©´ <a target=\"_blank\" href=\"%s\">Help</a>ì— ë°©ë¬¸í•˜ì„¸ìš”." +clone_helper=í´ë¡ 하는ë°ì— ë„ì›€ì´ í•„ìš”í•˜ë©´ <a target="_blank" href="%s">Help</a>ì— ë°©ë¬¸í•˜ì„¸ìš”. 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="<strong class=\"branch-name\">%s</strong> 브랜치ì—ì„œ ì§ì ‘ 커밋해주세요." +editor.commit_directly_to_this_branch=<strong class="branch-name">%s</strong> 브랜치ì—ì„œ ì§ì ‘ 커밋해주세요. editor.create_new_branch=ì´ ì»¤ë°‹ì— ëŒ€í•œ <strong>새로운 브랜치</strong>를 ë§Œë“¤ê³ ëŒì–´ì˜¤ê¸° ìš”ì²ì„ 시작합니다. 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="<a href=\"%[2]s\"> %[3]s</a>ê°€ %[1]sì„ ì˜¤í”ˆ" +issues.opened_by=<a href="%[2]s"> %[3]s</a>ê°€ %[1]sì„ ì˜¤í”ˆ issues.previous=ì´ì „ issues.next=ë‹¤ìŒ issues.open_title=오픈 @@ -748,7 +747,7 @@ issues.create_comment=코멘트 issues.commit_ref_at=` 커밋 <a id="%[1]s" href="#%[1]s">%[2]s</a>ì—ì„œ ì´ ì´ìŠˆ 언급` issues.role.owner=ì†Œìœ ìž issues.role.member=멤버 -issues.sign_in_require_desc="<a href=\"%s\">로그ì¸</a>하여 ì´ ëŒ€í™”ì— ì°¸ì—¬" +issues.sign_in_require_desc=<a href="%s">로그ì¸</a>하여 ì´ ëŒ€í™”ì— ì°¸ì—¬ 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="<code>%[2]s</code> ì—ì„œ <code id=\"branch_target\">%[3]s</code> ë¡œ %[1]d commits 를 ë¨¸ì§€í•˜ë ¤ 합니다" +pulls.title_desc=<code>%[2]s</code> ì—ì„œ <code id="branch_target">%[3]s</code> ë¡œ %[1]d commits 를 ë¨¸ì§€í•˜ë ¤ 합니다 pulls.merged_title_desc=<code>%[2]s</code> ì—ì„œ <code>%[3]s</code> ë¡œ %[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 fd412b95b4..0dfc5683ec 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 <strong>%[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 a4da8177bc..8a6dabbceb 100644 --- a/options/locale/locale_nl-NL.ini +++ b/options/locale/locale_nl-NL.ini @@ -487,6 +487,7 @@ change_avatar=Wijzig je profielfoto… repositories=repositories activity=Openbare activiteit followers=Volgers +show_more=Meer weergeven starred=Repositories met ster watched=Gevolgde repositories projects=Projecten @@ -752,7 +753,6 @@ visibility.private=Privé owner=Eigenaar owner_helper=Sommige organisaties kunnen niet worden weergegeven in de dropdown vanwege een limiet op het maximale aantal repositories. repo_name=Naam van repository -repo_name_helper=Goede repository-namen zijn kort, makkelijk te onthouden en uniek. repo_size=Repositorygrootte template=Sjabloon template_select=Selecteer een sjabloon. @@ -2054,6 +2054,7 @@ teams.all_repositories=Alle repositories teams.all_repositories_helper=Team heeft toegang tot alle repositories. Door dit te selecteren worden <strong>alle bestaande</strong> repositories aan het team toegevoegd. teams.all_repositories_read_permission_desc=Dit team heeft <strong>Lees</strong> toegang tot <strong>alle repositories</strong>: 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 22f701219d..4d049c83d1 100644 --- a/options/locale/locale_pl-PL.ini +++ b/options/locale/locale_pl-PL.ini @@ -711,7 +711,6 @@ visibility.private=Prywatny owner=WÅ‚aÅ›ciciel owner_helper=Niektóre organizacje mogÄ… nie pojawiać siÄ™ w liÅ›cie ze wzglÄ™du na limit maksymalnej liczby repozytoriów. repo_name=Nazwa repozytorium -repo_name_helper=Dobra nazwa repozytorium jest utworzona z krótkich, Å‚atwych do zapamiÄ™tania i unikalnych słów kluczowych. repo_size=Rozmiar repozytorium template=Szablon template_select=Wybierz szablon. @@ -1934,6 +1933,7 @@ teams.all_repositories_read_permission_desc=Ten zespół nadaje uprawnienie <str teams.all_repositories_write_permission_desc=Ten zespół nadaje uprawnienie <strong>Zapisu</strong> do <strong>wszystkich repozytoriów</strong>: jego czÅ‚onkowie mogÄ… odczytywać i przesyÅ‚ać do repozytoriów. teams.all_repositories_admin_permission_desc=Ten zespół nadaje uprawnienia <strong>Administratora</strong> do <strong>wszystkich repozytoriów</strong>: 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 8fb869898b..f0c034a133 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -582,6 +582,7 @@ joined_on=Inscreveu-se em %s repositories=Repositórios activity=Atividade pública followers=Seguidores +show_more=Mostrar mais starred=Repositórios favoritos watched=Repositórios observados code=Código @@ -907,7 +908,6 @@ new_repo_helper=Um repositório contém todos os arquivos do projeto, inclusive owner=Proprietário owner_helper=Algumas organizações podem não aparecer no menu devido a um limite de contagem dos repositórios. repo_name=Nome do repositório -repo_name_helper=Um bom nome de repositório é composto por palavras curtas, memorizáveis e únicas. repo_size=Tamanho do repositório template=Modelo template_select=Selecione um modelo. @@ -2550,6 +2550,7 @@ teams.invite.title=Você foi convidado para fazer parte da equipe <strong>%s</st teams.invite.by=Convidado por %s teams.invite.description=Por favor, clique no botão abaixo para se juntar à equipe. + [admin] dashboard=Painel identity_access=Identidade e acesso @@ -3353,6 +3354,7 @@ runs.empty_commit_message=(mensagem de commit vazia) need_approval_desc=Precisa de aprovação para executar workflows para pull request do fork. + [projects] type-1.display_name=Projeto individual type-2.display_name=Projeto do repositório diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index 4db7b5a2a2..586b8d89b6 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -244,6 +244,7 @@ license_desc=Vá buscar <a target="_blank" rel="noopener noreferrer" href="%[1]s [install] install=Instalação +installing_desc=Instalando agora, por favor aguarde... title=Configuração inicial docker_helper=Se correr o Gitea dentro do Docker, leia a <a target="_blank" rel="noopener noreferrer" href="%s">documentação</a> 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, <a href="%s">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 <b>%s</b>: +reset_password.text=Para recuperar a sua conta, clique na ligação seguinte (válida por <b>%s</b>): 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 <a id="%[1]s" href="#%[1]s">%[2]s</a>` pulls.reopened_at=`reabriu este pedido de integração <a id="%[1]s" href="#%[1]s">%[2]s</a>` pulls.cmd_instruction_hint=`Ver <a class="show-instruction">instruções para a linha de comandos</a>.` -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 <strong>%[1]s</strong> %[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 <strong>%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 <code>/etc/apk/repositories</code>: alpine.registry.key=Descarregue a chave RSA pública do registo para dentro da pasta <code>/etc/apk/keys/</code> 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 735077bd41..027a2cb19d 100644 --- a/options/locale/locale_ru-RU.ini +++ b/options/locale/locale_ru-RU.ini @@ -578,6 +578,7 @@ joined_on=ПриÑоединил(ÑÑ/аÑÑŒ) %s repositories=Репозитории activity=ÐктивноÑÑ‚ÑŒ followers=ПодпиÑчики +show_more=Показать больше starred=Избранные репозитории watched=ОтÑлеживаемые репозитории code=Код @@ -898,7 +899,6 @@ visibility.private_tooltip=Виден только членам организа owner=Владелец owner_helper=Ðекоторые организации могут не отображатьÑÑ Ð² раÑкрывающемÑÑ ÑпиÑке из-за макÑимального Ð¾Ð³Ñ€Ð°Ð½Ð¸Ñ‡ÐµÐ½Ð¸Ñ ÐºÐ¾Ð»Ð¸Ñ‡ÐµÑтва репозиториев. repo_name=Ðазвание Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ð¸Ñ -repo_name_helper=Лучшие Ð½Ð°Ð·Ð²Ð°Ð½Ð¸Ñ Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ð¸ÐµÐ² ÑоÑтоÑÑ‚ из коротких, легко запоминаемых и уникальных ключевых Ñлов. repo_size=Размер Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ð¸Ñ template=Шаблон template_select=Выбрать шаблон. @@ -2541,6 +2541,7 @@ teams.invite.title=Ð’Ð°Ñ Ð¿Ñ€Ð¸Ð³Ð»Ð°Ñили приÑоединитьÑÑ Ðº Ð teams.invite.by=Приглашен(а) %s teams.invite.description=Ðажмите на кнопку ниже, чтобы приÑоединитьÑÑ Ðº команде. + [admin] dashboard=Панель identity_access=Ð˜Ð´ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ñ Ð¸ доÑтуп @@ -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 506fa5b492..167ecaf24a 100644 --- a/options/locale/locale_si-LK.ini +++ b/options/locale/locale_si-LK.ini @@ -452,6 +452,7 @@ change_avatar=ඔබගේ අවà¶à·à¶»à¶º වෙනස් කරන්න… repositories=කà·à·‚්ඨ activity=ප්â€à¶»à·ƒà·’ද්ධ ක්â€à¶»à·’යà·à¶šà·à¶»à¶šà¶¸ followers=අනුගà·à¶¸à·’කයන් +show_more=à¶à·€ පෙන්වන්න starred=à¶à¶»à·” ගබඩà·à·€ watched=නරඹන ලද ගබඩà·à·€à¶½à¶¯à·“ projects=ව්â€à¶ºà·à¶´à·˜à¶à·’ @@ -692,7 +693,6 @@ visibility.private=පෞද්ගලික owner=හිමිකරු owner_helper=උපරිම නිධි ගණන් සීමà·à·€à¶šà·Š à·„à·šà¶à·”වෙන් සමහර සංවිධà·à¶± පහචවà·à¶§à·“මේ දී පෙන්විය නොහà·à¶š. repo_name=කà·à·‚්ඨයේ නම -repo_name_helper=හොඳ ගබඩà·à·€à¶šà·Š නම් කෙටි, අමà¶à¶š නොවන සහ අද්විà¶à·“ය මූල පද භà·à·€à·’à¶à· කරයි. repo_size=කà·à·‚්ඨයේ ප්â€à¶»à¶¸à·à¶«à¶º template=à·ƒà·à¶šà·’ල්ල template_select=අච්චුවක් à¶à·à¶»à¶±à·Šà¶±. @@ -1954,6 +1954,7 @@ teams.all_repositories_read_permission_desc=මෙම කණ්ඩà·à¶ºà¶¸ පà teams.all_repositories_write_permission_desc=මෙම කණ්ඩà·à¶ºà¶¸ ප්රදà·à¶±à¶º කරයි <strong></strong> වෙචප්රවේà·à¶º ලියන්න <strong>සියලු ගබඩà·à·€à¶±à·Šà¶§</strong>: à·ƒà·à¶¸à·à¶¢à·’කයින්ට කියවීමට සහ ගබඩà·à·€à¶±à·Šà¶§ à¶à¶½à·Šà¶½à·” කළ à·„à·à¶šà·’ය. teams.all_repositories_admin_permission_desc=මෙම කණ්ඩà·à¶ºà¶¸ ප්රදà·à¶±à¶º කරයි <strong>පරිපà·à¶½à¶š</strong> වෙචප්රවේà·à¶º <strong>සියලු ගබඩà·à·€à¶±à·Šà¶§</strong>: à·ƒà·à¶¸à·à¶¢à·’කයින්ට කියවීමට, à¶à¶½à·Šà¶½à·” කිරීමට සහ ගබඩà·à·€à¶±à·Šà¶§ සහයà·à¶œà·“කයින් එකà¶à·” කිරීමට. + [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 b4bf6fb552..43b190098f 100644 --- a/options/locale/locale_sk-SK.ini +++ b/options/locale/locale_sk-SK.ini @@ -817,7 +817,6 @@ visibility.private=Súkromný owner=VlastnÃk owner_helper=Niektoré organizácie sa nemusia zobraziÅ¥ v rozbaľovacej ponuke z dôvodu maximálneho limitu poÄtu repozitárov. repo_name=Názov repozitára -repo_name_helper=Dobrý názov repozitára sa zvyÄajne skladá z krátkych, jedineÄných a ľahko zapamätateľných kľúÄových slov. repo_size=VeľkosÅ¥ repozitára template=Å ablóna template_select=Vyberte Å¡ablónu. @@ -1235,6 +1234,7 @@ teams.all_repositories_read_permission_desc=Tomuto tÃmu je pridelený prÃstup teams.all_repositories_write_permission_desc=Tomuto tÃmu je pridelený prÃstup na <strong>Zápis</strong> do <strong>vÅ¡etkých repozitárov</strong>: Älenovia môžu prezeraÅ¥ a nahrávaÅ¥ do repozitárov. teams.all_repositories_admin_permission_desc=Tomuto tÃmu je pridelený <strong>Admin</strong> prÃstup ku <strong>vÅ¡etkým repozitárom</strong>: Ä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 fc138381db..0315ebe9a1 100644 --- a/options/locale/locale_sv-SE.ini +++ b/options/locale/locale_sv-SE.ini @@ -598,7 +598,6 @@ visibility.private=Privat [repo] owner=Ägare repo_name=Utvecklingskatalogens namn -repo_name_helper=Bra namn pÃ¥ utvecklingskataloger bestÃ¥r utav korta, unika nyckelord som är enkla att komma ihÃ¥g. repo_size=Utvecklingskatalogens storlek template=Mall template_select=Välj mall. @@ -1592,6 +1591,7 @@ teams.all_repositories_read_permission_desc=Detta team beviljar <strong>Läs</st teams.all_repositories_write_permission_desc=Detta team beviljar <strong>Skriv</strong>-rättigheter till <strong>alla utvecklingskataloger</strong>: medlemmar kan läsa frÃ¥n och pusha till utvecklingskataloger. teams.all_repositories_admin_permission_desc=Detta team beviljar <strong>Admin</strong>-rättigheter till <strong>alla utvecklingskataloger</strong>: 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 9b7f2cb5c6..fb30ab3b67 100644 --- a/options/locale/locale_tr-TR.ini +++ b/options/locale/locale_tr-TR.ini @@ -104,6 +104,7 @@ copy_url=URL'yi kopyala copy_hash=Hash'i kopyala copy_content=İçeriÄŸi kopyala copy_branch=Dal adını kopyala +copy_path=Yolu kopyala copy_success=Kopyalandı! copy_error=Kopyalama baÅŸarısız oldu copy_type_unsupported=Bu dosya türü kopyalanamaz @@ -144,6 +145,7 @@ confirm_delete_selected=Tüm seçili öğeleri gerçekten silmek istiyor musunuz name=Ä°sim value=DeÄŸer +readme=Benioku filter=Filtre filter.clear=Filtreyi Temizle @@ -159,6 +161,7 @@ filter.public=Genel filter.private=Özel no_results_found=Sonuç bulunamadı. +internal_error_skipped=Dahili bir hata oluÅŸtu ama atlandı: %s [search] search=Ara... @@ -177,6 +180,8 @@ code_search_by_git_grep=Mevcut kod arama sonuçları "git grep" ile saÄŸlanıyor package_kind=Paketleri ara... project_kind=Projeleri ara... branch_kind=Dalları ara... +tag_kind=Etiketleri ara... +tag_tooltip=EÅŸleÅŸen etiketler için arama. Herhangi bir numara serisi bulmak için '%' kullanın. commit_kind=Ä°ÅŸlemeleri ara... runner_kind=Çalıştırıcıları ara... no_results=EÅŸleÅŸen sonuç bulunamadı. @@ -206,7 +211,10 @@ buttons.link.tooltip=BaÄŸlantı ekle buttons.list.unordered.tooltip=Maddeli liste ekle buttons.list.ordered.tooltip=Numaralandırılmış liste ekle buttons.list.task.tooltip=Görev listesi ekle +buttons.table.add.tooltip=Tablo ekle buttons.table.add.insert=Ekle +buttons.table.rows=Satırlar +buttons.table.cols=Sütunlar buttons.mention.tooltip=Bir kiÅŸiye veya takıma deÄŸin buttons.ref.tooltip=Bir konuya veya deÄŸiÅŸiklik isteÄŸine deÄŸin buttons.switch_to_legacy.tooltip=Eski düzenleyiciyi kullan @@ -219,6 +227,7 @@ string.desc=Z - A [error] occurred=Bir hata oluÅŸtu +report_message=Bunun bir Gitea hatası olduÄŸunu düşünüyorsanız, lütfen <a href="%s" target="_blank">GitHub</a> 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=<strong>%s</strong> takımına (Organizasyon: <strong>%s</str teams.invite.by=%s tarafından davet edildi teams.invite.description=Takıma katılmak için aÅŸağıdaki düğmeye tıklayın. + [admin] maintenance=Bakım dashboard=Pano @@ -3633,6 +3643,7 @@ variables.creation.success=`"%s" deÄŸiÅŸkeni eklendi.` variables.update.failed=DeÄŸiÅŸken düzenlenemedi. variables.update.success=DeÄŸiÅŸken düzenlendi. + [projects] deleted.display_name=SilinmiÅŸ Proje type-1.display_name=KiÅŸisel Proje diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini index efefeeb436..2b0e57c8e0 100644 --- a/options/locale/locale_uk-UA.ini +++ b/options/locale/locale_uk-UA.ini @@ -466,6 +466,7 @@ change_avatar=Змінити Ñвій аватар… repositories=Репозиторії activity=Публічна активніÑÑ‚ÑŒ followers=Читачі +show_more=Показати більше starred=Обрані Репозиторії watched=ВідÑтежувані репозиторії projects=Проєкт @@ -711,7 +712,6 @@ visibility.private=Приватний owner=ВлаÑник owner_helper=ДеÑкі організації можуть не відображатиÑÑ Ñƒ випадаючому ÑпиÑку через макÑимальну кількіÑÑ‚ÑŒ репозиторііїв. repo_name=Ðазва репозиторію -repo_name_helper=Хороші назви репозиторіїв викориÑтовують короткі, унікальні ключові Ñлова що легко запам'Ñтати. repo_size=Розмір репозиторію template=Шаблон template_select=Оберіть шаблон. @@ -2002,6 +2002,7 @@ teams.all_repositories_read_permission_desc=Ð¦Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð° надає доРteams.all_repositories_write_permission_desc=Ð¦Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð° надає дозвіл <strong>ЗапиÑ</strong> Ð´Ð»Ñ <strong>вÑÑ–Ñ… репозиторіїв</strong>: учаÑники можуть переглÑдати та виконувати push в репозиторіÑÑ…. teams.all_repositories_admin_permission_desc=Ð¦Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð° надає дозвіл <strong>ÐдмініÑтруваннÑ</strong> Ð´Ð»Ñ <strong>вÑÑ–Ñ… репозиторіїв</strong>: учаÑники можуть переглÑдати, виконувати 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 60fb75df4f..5e4723a4cd 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -649,6 +649,7 @@ joined_on=åŠ å…¥äºŽ %s repositories=仓库列表 activity=公开活动 followers=关注者 +show_more=显示更多 starred=已点赞 watched=已关注仓库 code=代ç @@ -1014,7 +1015,6 @@ new_repo_helper=代ç 仓库包å«äº†æ‰€æœ‰çš„项目文件,包括版本历å²è owner=拥有者 owner_helper=由于最大仓库数é‡é™åˆ¶ï¼Œä¸€äº›ç»„织å¯èƒ½ä¸ä¼šæ˜¾ç¤ºåœ¨ä¸‹æ‹‰åˆ—表ä¸ã€‚ repo_name=仓库å称 -repo_name_helper=好的仓库å称应当使用简çŸã€æœ‰æ„义和独特的关键å—。 repo_size=ä»“åº“å¤§å° template=æ¨¡æ¿ template_select=é€‰æ‹©æ¨¡æ¿ @@ -1945,6 +1945,8 @@ pulls.delete.title=åˆ é™¤æ¤åˆå¹¶è¯·æ±‚? pulls.delete.text=ä½ çœŸçš„è¦åˆ 除这个åˆå¹¶è¯·æ±‚å—? (è¿™å°†æ°¸ä¹…åˆ é™¤æ‰€æœ‰å†…å®¹ã€‚å¦‚æžœä½ æ‰“ç®—å°†å†…å®¹å˜æ¡£ï¼Œè¯·è€ƒè™‘å…³é—它) pulls.recently_pushed_new_branches=您已ç»äºŽ%[2]s推é€äº†åˆ†æ”¯ <strong>%[1]s</strong> +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=æ‚¨å·²è¢«é‚€è¯·åŠ å…¥ç»„ç»‡ <strong>%s</strong> ä¸çš„团队 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 6f37d30efc..77f8d8a25d 100644 --- a/options/locale/locale_zh-HK.ini +++ b/options/locale/locale_zh-HK.ini @@ -684,6 +684,7 @@ teams.add_team_member=新增團隊æˆå“¡ teams.delete_team_success=該團隊已被刪除。 teams.repositories=團隊儲å˜åº« + [admin] dashboard=控制é¢ç‰ˆ organizations=çµ„ç¹”ç®¡ç† @@ -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 0bfc6d6365..948b47bc9c 100644 --- a/options/locale/locale_zh-TW.ini +++ b/options/locale/locale_zh-TW.ini @@ -647,6 +647,7 @@ joined_on=åŠ å…¥æ–¼ %s repositories=儲å˜åº« activity=公開動態 followers=追蹤者 +show_more=顯示更多 starred=å·²åŠ æ˜Ÿè™Ÿ watched=關注的儲å˜åº« code=程å¼ç¢¼ @@ -1011,7 +1012,6 @@ new_repo_helper=儲å˜åº«åŒ…å«æ‰€æœ‰å°ˆæ¡ˆæª”案,包括修訂æ·å²ã€‚已經å owner=æ“有者 owner_helper=組織å¯èƒ½å› 為儲å˜åº«æ•¸é‡ä¸Šé™è€Œæœªåˆ—å…¥æ¤é¸å–®ã€‚ repo_name=儲å˜åº«å稱 -repo_name_helper=好的儲å˜åº«å稱通常是簡çŸçš„ã€å¥½è¨˜çš„ã€ä¸”ç¨ç‰¹çš„。 repo_size=儲å˜åº«å¤§å° template=範本 template_select=é¸æ“‡ç¯„本 @@ -2851,6 +2851,7 @@ teams.invite.title=æ‚¨å·²è¢«é‚€è«‹åŠ å…¥çµ„ç¹” <strong>%s</strong> ä¸çš„團隊 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 60edfa95d0..1e3c5ab155 100644 --- a/package-lock.json +++ b/package-lock.json @@ -87,7 +87,7 @@ "eslint": "8.57.0", "eslint-import-resolver-typescript": "3.7.0", "eslint-plugin-array-func": "4.0.0", - "eslint-plugin-github": "5.1.4", + "eslint-plugin-github": "5.0.2", "eslint-plugin-import-x": "4.6.1", "eslint-plugin-no-jquery": "3.1.0", "eslint-plugin-no-use-extend-native": "0.5.0", @@ -7953,166 +7953,35 @@ } }, "node_modules/eslint-plugin-github": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-github/-/eslint-plugin-github-5.1.4.tgz", - "integrity": "sha512-j5IgIxsDoch06zJzeqPvenfzRXDKI9Z8YwfUg1pm2ay1q44tMSFwvEu6l0uEIrTpA3v8QdPyLr98LqDl1TIhSA==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-github/-/eslint-plugin-github-5.0.2.tgz", + "integrity": "sha512-nMdzWJQ5CimjQDY6SFeJ0KIXuNFf0dgDWEd4eP3UWfuTuP/dXcZJDg7MQRvAFt743T1zUi4+/HdOihfu8xJkLA==", "dev": true, "license": "MIT", "dependencies": { - "@eslint/compat": "^1.2.3", - "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "^9.14.0", "@github/browserslist-config": "^1.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "aria-query": "^5.3.0", "eslint-config-prettier": ">=8.0.0", - "eslint-plugin-escompat": "^3.11.3", + "eslint-plugin-escompat": "^3.3.3", "eslint-plugin-eslint-comments": "^3.2.0", "eslint-plugin-filenames": "^1.3.2", "eslint-plugin-i18n-text": "^1.0.1", "eslint-plugin-import": "^2.25.2", "eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-no-only-tests": "^3.0.0", - "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-prettier": "^5.0.0", "eslint-rule-documentation": ">=1.0.0", - "globals": "^15.12.0", "jsx-ast-utils": "^3.3.2", "prettier": "^3.0.0", - "svg-element-attributes": "^1.3.1", - "typescript-eslint": "^8.14.0" + "svg-element-attributes": "^1.3.1" }, "bin": { "eslint-ignore-errors": "bin/eslint-ignore-errors.js" }, "peerDependencies": { - "eslint": "^8 || ^9" - } - }, - "node_modules/eslint-plugin-github/node_modules/@eslint/compat": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.2.4.tgz", - "integrity": "sha512-S8ZdQj/N69YAtuqFt7653jwcvuUj131+6qGLUyDqfDg1OIoBQ66OCuXC473YQfO2AaxITTutiRQiDwoo7ZLYyg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "peerDependencies": { - "eslint": "^9.10.0" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-github/node_modules/@eslint/eslintrc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", - "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-plugin-github/node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-plugin-github/node_modules/@eslint/js": { - "version": "9.17.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz", - "integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/eslint-plugin-github/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/eslint-plugin-github/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-github/node_modules/globals": { - "version": "15.14.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz", - "integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-plugin-github/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint-plugin-github/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" + "eslint": "^8.0.1" } }, "node_modules/eslint-plugin-i18n-text": { @@ -14063,29 +13932,6 @@ "node": ">=14.17" } }, - "node_modules/typescript-eslint": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.18.1.tgz", - "integrity": "sha512-Mlaw6yxuaDEPQvb/2Qwu3/TfgeBHy9iTJ3mTwe7OvpPmF6KPQjVOfGyEJpPv6Ez2C34OODChhXrzYw/9phI0MQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.18.1", - "@typescript-eslint/parser": "8.18.1", - "@typescript-eslint/utils": "8.18.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" - } - }, "node_modules/typo-js": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.2.5.tgz", diff --git a/package.json b/package.json index 9f5fbe3fdd..6881ddb306 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "eslint": "8.57.0", "eslint-import-resolver-typescript": "3.7.0", "eslint-plugin-array-func": "4.0.0", - "eslint-plugin-github": "5.1.4", + "eslint-plugin-github": "5.0.2", "eslint-plugin-import-x": "4.6.1", "eslint-plugin-no-jquery": "3.1.0", "eslint-plugin-no-use-extend-native": "0.5.0", diff --git a/routers/api/actions/artifacts.go b/routers/api/actions/artifacts.go index 0a7f92ac40..910edd6d58 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 6dd36888d2..8917a7a8a2 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 8f365cc926..c55b30f7eb 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 4b652c9ecc..f35cff3df2 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 47ea7137b8..41c3eb95e9 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", "/<repository:*>", reqPackageAccess(perm.AccessModeWrite), arch.UploadPackageFile) + g.MatchPath("HEAD,GET", "/<repository:*>/<architecture>/<filename>", arch.GetPackageOrRepositoryFile) + g.MatchPath("DELETE", "/<repository:*>/<name>/<version>/<architecture>", 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", "/<image:*>/blobs/uploads", reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName, container.InitiateUploadBlob) + g.MatchPath("GET", "/<image:*>/tags/list", container.VerifyImageName, container.GetTagList) + g.MatchPath("GET,PATCH,PUT,DELETE", `/<image:*>/blobs/uploads/<uuid:[-.=\w]+>`, reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName, func(ctx *context.Context) { + if ctx.Req.Method == http.MethodGet { 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", `/<image:*>/blobs/<digest>`, container.VerifyImageName, container.HeadBlob) + g.MatchPath("GET", `/<image:*>/blobs/<digest>`, container.VerifyImageName, container.GetBlob) + g.MatchPath("DELETE", `/<image:*>/blobs/<digest>`, container.VerifyImageName, reqPackageAccess(perm.AccessModeWrite), container.DeleteBlob) + + g.MatchPath("HEAD", `/<image:*>/manifests/<reference>`, container.VerifyImageName, container.HeadManifest) + g.MatchPath("GET", `/<image:*>/manifests/<reference>`, container.VerifyImageName, container.GetManifest) + g.MatchPath("PUT", `/<image:*>/manifests/<reference>`, container.VerifyImageName, reqPackageAccess(perm.AccessModeWrite), container.UploadManifest) + g.MatchPath("DELETE", `/<image:*>/manifests/<reference>`, 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 573e93cfb0..f5dc6c1d01 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 3d8407e6b6..42ef13476c 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 b3cdf12697..a0c8c5696c 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 40f72f6484..c6c14e5cf4 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 4a9f0a3ffc..8019eee9f7 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 7370c702cd..0dbbd500d2 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 4595b9a33d..671803788a 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 3a470ad685..bb14db9db7 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 4a79a58f51..ad035cf473 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 162122ccbd..fec34c91a6 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 868caf9cf0..0b5daa7334 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 cb30a20074..fb12daaa46 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 9474b17bc7..4d04d4d1e9 100644 --- a/routers/api/packages/maven/maven.go +++ b/routers/api/packages/maven/maven.go @@ -13,7 +13,7 @@ import ( "errors" "io" "net/http" - "path/filepath" + "path" "regexp" "sort" "strconv" @@ -25,6 +25,7 @@ import ( "code.gitea.io/gitea/modules/log" packages_module "code.gitea.io/gitea/modules/packages" maven_module "code.gitea.io/gitea/modules/packages/maven" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/api/packages/helper" "code.gitea.io/gitea/services/context" packages_service "code.gitea.io/gitea/services/packages" @@ -44,7 +45,7 @@ const ( var ( errInvalidParameters = errors.New("request parameters are invalid") - illegalCharacters = regexp.MustCompile(`[\\/:"<>|?\*]`) + illegalCharacters = regexp.MustCompile(`[\\/:"<>|?*]`) ) func apiError(ctx *context.Context, status int, obj any) { @@ -85,8 +86,10 @@ func handlePackageFile(ctx *context.Context, serveContent bool) { func serveMavenMetadata(ctx *context.Context, params parameters) { // /com/foo/project/maven-metadata.xml[.md5/.sha1/.sha256/.sha512] - packageName := params.GroupID + "-" + params.ArtifactID - pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, packageName) + pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageName()) + if errors.Is(err, util.ErrNotExist) { + pvs, err = packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageNameLegacy()) + } if err != nil { apiError(ctx, http.StatusInternalServerError, err) return @@ -116,10 +119,10 @@ func serveMavenMetadata(ctx *context.Context, params parameters) { latest := pds[len(pds)-1] // http.TimeFormat required a UTC time, refer to https://pkg.go.dev/net/http#TimeFormat - lastModifed := latest.Version.CreatedUnix.AsTime().UTC().Format(http.TimeFormat) - ctx.Resp.Header().Set("Last-Modified", lastModifed) + lastModified := latest.Version.CreatedUnix.AsTime().UTC().Format(http.TimeFormat) + ctx.Resp.Header().Set("Last-Modified", lastModified) - ext := strings.ToLower(filepath.Ext(params.Filename)) + ext := strings.ToLower(path.Ext(params.Filename)) if isChecksumExtension(ext) { var hash []byte switch ext { @@ -147,11 +150,12 @@ func serveMavenMetadata(ctx *context.Context, params parameters) { } func servePackageFile(ctx *context.Context, params parameters, serveContent bool) { - packageName := params.GroupID + "-" + params.ArtifactID - - pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, packageName, params.Version) + pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageName(), params.Version) + if errors.Is(err, util.ErrNotExist) { + pv, err = packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageNameLegacy(), params.Version) + } if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -161,14 +165,14 @@ func servePackageFile(ctx *context.Context, params parameters, serveContent bool filename := params.Filename - ext := strings.ToLower(filepath.Ext(filename)) + ext := strings.ToLower(path.Ext(filename)) if isChecksumExtension(ext) { filename = filename[:len(filename)-len(ext)] } pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, filename, packages_model.EmptyFileKey) if err != nil { - if err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -238,15 +242,17 @@ func UploadPackageFile(ctx *context.Context) { return } - log.Trace("Parameters: %+v", params) - // Ignore the package index /<name>/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 b4379f3f49..636680242a 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 284723e0d7..6ec46bcb36 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 70b95e6a77..07a8de0a68 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 2be27323fd..e7b07aefd0 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 5ea86071a9..199f4e7478 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 3023692177..786105693f 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 958063e70a..de8c7ef3ed 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 1daf2a0527..3afaa5de1f 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 613d123494..55ea8c6758 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 fba9d33f25..962e007776 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 6fe418249b..3de94d6868 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 db481fbf59..6b4689047b 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 3f4a73dcad..21cb2f9ccd 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 96365e7c14..2f943d306c 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 0761e684a3..58a38cfd18 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 24ee4ed642..2a9bd92e87 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 294d33014d..23c7da3d96 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 3fb653bcb6..d65f922434 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 8164d2cfe9..7f44f6ed95 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 9a31aec314..2fcdd02058 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 0bbf5a1ea4..da3ee54e69 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 788c75fab2..3b144d0c43 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 1678bc033c..a1813a8a76 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 3620c1465f..a8a23c4a8d 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 83848b7add..7c7f53a565 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 0887a90096..868acf3d85 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 9ef57da1b9..03143c8f99 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 c2f3a972ef..c659a16f54 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 cbe709c030..86dbcee5f7 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 f9b5aa816b..96a61a527e 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 ae7502c661..19dcf999b8 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 cc517619e9..ee1a842bc6 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 0ef9033291..388d4a3e99 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 8d43cd518b..ead86a717f 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 4605ae2110..e7fba6d0ed 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 e51baad0b6..4fb80b1ec4 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 8d5e9fdad4..57961b0660 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 060694d085..23cc922628 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 c2c43db6a4..1ece2521e0 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 78907c85a5..8d7516491e 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 047203501e..c911f6830c 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 8689d25e15..8fec844cc4 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 71c4c81b67..d0c3459b63 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 def860eee8..6d7a326370 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 141f812172..076f00f1d1 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 ed6cc8e1ea..54ca1fc843 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 99f7a8cbf2..7380c5231c 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 40990a28cb..ce09e7fc0f 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 fe0910c735..8447a8f1f2 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 42fb0a1d75..e5a2d5c320 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 6b9eedf6e0..a1a15e7f46 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 787ec34404..b2090cac41 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 efb247c19e..768e5d41c1 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 f9906ed250..352d8f48fc 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 9583bb548c..bfbc2ba622 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 6abb70de19..8f46808f9e 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 ba5c0fdc45..ef667a1883 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 23a526cd67..9a6f305700 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 e4278c2ec0..5a9125b4f3 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 e668326861..43dabe1b60 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 0000000000..a14af126e5 --- /dev/null +++ b/routers/common/codesearch.go @@ -0,0 +1,39 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package common + +import ( + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/services/context" +) + +func PrepareCodeSearch(ctx *context.Context) (ret struct { + Keyword string + Language string + IsFuzzy bool +}, +) { + ret.Language = ctx.FormTrim("l") + ret.Keyword = ctx.FormTrim("q") + + fuzzyDefault := setting.Indexer.RepoIndexerEnabled + fuzzyAllow := true + if setting.Indexer.RepoType == "bleve" && setting.Indexer.TypeBleveMaxFuzzniess == 0 { + fuzzyDefault = false + fuzzyAllow = false + } + isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(fuzzyDefault) + if isFuzzy && !fuzzyAllow { + ctx.Flash.Info("Fuzzy search is disabled by default due to performance reasons") + isFuzzy = false + } + + ctx.Data["IsBleveFuzzyDisabled"] = true + ctx.Data["Keyword"] = ret.Keyword + ctx.Data["Language"] = ret.Language + ctx.Data["IsFuzzy"] = isFuzzy + + ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled + return ret +} diff --git a/routers/common/errpage_test.go b/routers/common/errpage_test.go index 4fd63ba49e..dfea55f510 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 51e42d87a0..12b0c67b01 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 2091f5967a..e7aa765bf0 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") @@ -171,7 +171,7 @@ func InitWebInstalled(ctx context.Context) { auth.Init() mustInit(svg.Init) - actions_service.Init() + mustInitCtx(ctx, actions_service.Init) mustInit(repo_service.InitLicenseClassifier) diff --git a/routers/install/install.go b/routers/install/install.go index 1819bafc62..8a1d57aa0b 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 8f6e9084df..c375d70dc6 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 8d12b7a953..dba6aef9a3 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 1fb72f13d9..a78c76f897 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 aad0a3fb1a..e111d6689e 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 063db76520..9fd0a16c07 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 4dd7d06fb3..ecff3b7a53 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 6a65cfa697..249347e835 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 e925de8937..23ddfa583a 100644 --- a/routers/web/admin/emails.go +++ b/routers/web/admin/emails.go @@ -94,7 +94,7 @@ func Emails(ctx *context.Context) { ctx.Data["Emails"] = emails pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5) - pager.SetDefaultParams(ctx) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplEmails) diff --git a/routers/web/admin/packages.go b/routers/web/admin/packages.go index da345f2f89..5122342259 100644 --- a/routers/web/admin/packages.go +++ b/routers/web/admin/packages.go @@ -77,9 +77,7 @@ func Packages(ctx *context.Context) { ctx.Data["TotalUnreferencedBlobSize"] = totalUnreferencedBlobSize pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5) - pager.AddParamString("q", query) - pager.AddParamString("type", packageType) - pager.AddParamString("sort", sort) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplPackagesList) diff --git a/routers/web/admin/repos.go b/routers/web/admin/repos.go index 27d39dcf4b..1bc8abb88c 100644 --- a/routers/web/admin/repos.go +++ b/routers/web/admin/repos.go @@ -4,7 +4,6 @@ package admin import ( - "fmt" "net/http" "net/url" "strings" @@ -84,8 +83,7 @@ func UnadoptedRepos(ctx *context.Context) { if !doSearch { pager := context.NewPagination(0, opts.PageSize, opts.Page, 5) - pager.SetDefaultParams(ctx) - pager.AddParamString("search", fmt.Sprint(doSearch)) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplUnadoptedRepos) return @@ -99,8 +97,7 @@ func UnadoptedRepos(ctx *context.Context) { } ctx.Data["Dirs"] = repoNames pager := context.NewPagination(count, opts.PageSize, opts.Page, 5) - pager.SetDefaultParams(ctx) - pager.AddParamString("search", fmt.Sprint(doSearch)) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplUnadoptedRepos) } diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go index cf158f7aa9..f6a3af1c86 100644 --- a/routers/web/admin/users.go +++ b/routers/web/admin/users.go @@ -47,16 +47,12 @@ func Users(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("admin.users") ctx.Data["PageIsAdminUsers"] = true - extraParamStrings := map[string]string{} statusFilterKeys := []string{"is_active", "is_admin", "is_restricted", "is_2fa_enabled", "is_prohibit_login"} statusFilterMap := map[string]string{} for _, filterKey := range statusFilterKeys { paramKey := "status_filter[" + filterKey + "]" paramVal := ctx.FormString(paramKey) statusFilterMap[filterKey] = paramVal - if paramVal != "" { - extraParamStrings[paramKey] = paramVal - } } sortType := ctx.FormString("sort") @@ -82,7 +78,6 @@ func Users(ctx *context.Context) { IsTwoFactorEnabled: util.OptionalBoolParse(statusFilterMap["is_2fa_enabled"]), IsProhibitLogin: util.OptionalBoolParse(statusFilterMap["is_prohibit_login"]), IncludeReserved: true, // administrator needs to list all accounts include reserved, bot, remote ones - ExtraParamStrings: extraParamStrings, }, tplUsers) } @@ -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 42736a423f..3fe1d5970e 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -689,7 +689,7 @@ func Activate(ctx *context.Context) { } // TODO: ctx.Doer/ctx.Data["SignedUser"] could be nil or not the same user as the one being activated - user := user_model.VerifyUserActiveCode(ctx, code) + user := user_model.VerifyUserTimeLimitCode(ctx, &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateAccount}, code) if user == nil { // if code is wrong renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.invalid_code")) return @@ -734,7 +734,7 @@ func ActivatePost(ctx *context.Context) { } // TODO: ctx.Doer/ctx.Data["SignedUser"] could be nil or not the same user as the one being activated - user := user_model.VerifyUserActiveCode(ctx, code) + user := user_model.VerifyUserTimeLimitCode(ctx, &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateAccount}, code) if user == nil { // if code is wrong renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.invalid_code")) return diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index 32c30c71e8..7a9721cf56 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 3812d582e5..614e086f77 100644 --- a/routers/web/auth/password.go +++ b/routers/web/auth/password.go @@ -113,7 +113,7 @@ func commonResetPassword(ctx *context.Context) (*user_model.User, *auth.TwoFacto } // Fail early, don't frustrate the user - u := user_model.VerifyUserActiveCode(ctx, code) + u := user_model.VerifyUserTimeLimitCode(ctx, &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeResetPassword}, code) if u == nil { ctx.Flash.Error(ctx.Tr("auth.invalid_code_forgot_password", fmt.Sprintf("%s/user/forgot_password", setting.AppSubURL)), true) return nil, nil diff --git a/routers/web/devtest/devtest.go b/routers/web/devtest/devtest.go index 0bc84d2d1e..1ea1398173 100644 --- a/routers/web/devtest/devtest.go +++ b/routers/web/devtest/devtest.go @@ -9,6 +9,10 @@ import ( "strings" "time" + "code.gitea.io/gitea/models/asymkey" + "code.gitea.io/gitea/models/db" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/services/context" ) @@ -41,16 +45,85 @@ func FetchActionTest(ctx *context.Context) { ctx.JSONRedirect("") } -func Tmpl(ctx *context.Context) { - now := time.Now() - ctx.Data["TimeNow"] = now - ctx.Data["TimePast5s"] = now.Add(-5 * time.Second) - ctx.Data["TimeFuture5s"] = now.Add(5 * time.Second) - ctx.Data["TimePast2m"] = now.Add(-2 * time.Minute) - ctx.Data["TimeFuture2m"] = now.Add(2 * time.Minute) - ctx.Data["TimePast1y"] = now.Add(-1 * 366 * 86400 * time.Second) - ctx.Data["TimeFuture1y"] = now.Add(1 * 366 * 86400 * time.Second) +func prepareMockData(ctx *context.Context) { + if ctx.Req.URL.Path == "/devtest/gitea-ui" { + now := time.Now() + ctx.Data["TimeNow"] = now + ctx.Data["TimePast5s"] = now.Add(-5 * time.Second) + ctx.Data["TimeFuture5s"] = now.Add(5 * time.Second) + ctx.Data["TimePast2m"] = now.Add(-2 * time.Minute) + ctx.Data["TimeFuture2m"] = now.Add(2 * time.Minute) + ctx.Data["TimePast1y"] = now.Add(-1 * 366 * 86400 * time.Second) + ctx.Data["TimeFuture1y"] = now.Add(1 * 366 * 86400 * time.Second) + } + + if ctx.Req.URL.Path == "/devtest/commit-sign-badge" { + var commits []*asymkey.SignCommit + mockUsers, _ := db.Find[user_model.User](ctx, user_model.SearchUserOptions{ListOptions: db.ListOptions{PageSize: 1}}) + mockUser := mockUsers[0] + commits = append(commits, &asymkey.SignCommit{ + Verification: &asymkey.CommitVerification{}, + UserCommit: &user_model.UserCommit{ + Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, + }, + }) + commits = append(commits, &asymkey.SignCommit{ + Verification: &asymkey.CommitVerification{ + Verified: true, + Reason: "name / key-id", + SigningUser: mockUser, + SigningKey: &asymkey.GPGKey{KeyID: "12345678"}, + TrustStatus: "trusted", + }, + UserCommit: &user_model.UserCommit{ + User: mockUser, + Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, + }, + }) + commits = append(commits, &asymkey.SignCommit{ + Verification: &asymkey.CommitVerification{ + Verified: true, + Reason: "name / key-id", + SigningUser: mockUser, + SigningSSHKey: &asymkey.PublicKey{Fingerprint: "aa:bb:cc:dd:ee"}, + TrustStatus: "untrusted", + }, + UserCommit: &user_model.UserCommit{ + User: mockUser, + Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, + }, + }) + commits = append(commits, &asymkey.SignCommit{ + Verification: &asymkey.CommitVerification{ + Verified: true, + Reason: "name / key-id", + SigningUser: mockUser, + SigningSSHKey: &asymkey.PublicKey{Fingerprint: "aa:bb:cc:dd:ee"}, + TrustStatus: "other(unmatch)", + }, + UserCommit: &user_model.UserCommit{ + User: mockUser, + Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, + }, + }) + commits = append(commits, &asymkey.SignCommit{ + Verification: &asymkey.CommitVerification{ + Warning: true, + Reason: "gpg.error", + SigningEmail: "test@example.com", + }, + UserCommit: &user_model.UserCommit{ + User: mockUser, + Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, + }, + }) + + ctx.Data["MockCommits"] = commits + } +} +func Tmpl(ctx *context.Context) { + prepareMockData(ctx) if ctx.Req.Method == "POST" { _ = ctx.Req.ParseForm() ctx.Flash.Info("form: "+ctx.Req.Method+" "+ctx.Req.RequestURI+"<br>"+ @@ -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/devtest/mock_actions.go b/routers/web/devtest/mock_actions.go index 46e302d634..f29b8e4046 100644 --- a/routers/web/devtest/mock_actions.go +++ b/routers/web/devtest/mock_actions.go @@ -31,7 +31,11 @@ func generateMockStepsLog(logCur actions.LogCursor) (stepsLog []*actions.ViewSte "##[endgroup]", } cur := logCur.Cursor // usually the cursor is the "file offset", but here we abuse it as "line number" to make the mock easier, intentionally - for i := 0; i < util.Iif(logCur.Step == 0, 3, 1); i++ { + mockCount := util.Iif(logCur.Step == 0, 3, 1) + if logCur.Step == 1 && logCur.Cursor == 0 { + mockCount = 30 // for the first batch, return as many as possible to test the auto-expand and auto-scroll + } + for i := 0; i < mockCount; i++ { logStr := mockedLogs[int(cur)%len(mockedLogs)] cur++ logStr = strings.ReplaceAll(logStr, "{step}", fmt.Sprintf("%d", logCur.Step)) @@ -56,6 +60,21 @@ func MockActionsRunsJobs(ctx *context.Context) { resp.State.Run.Status = actions_model.StatusRunning.String() resp.State.Run.CanCancel = true resp.State.Run.CanDeleteArtifact = true + resp.State.Run.WorkflowID = "workflow-id" + resp.State.Run.WorkflowLink = "./workflow-link" + resp.State.Run.Commit = actions.ViewCommit{ + ShortSha: "ccccdddd", + Link: "./commit-link", + Pusher: actions.ViewUser{ + DisplayName: "pusher user", + Link: "./pusher-link", + }, + Branch: actions.ViewBranch{ + Name: "commit-branch", + Link: "./branch-link", + IsDeleted: false, + }, + } resp.Artifacts = append(resp.Artifacts, &actions.ArtifactsViewItem{ Name: "artifact-a", Size: 100 * 1024, diff --git a/routers/web/explore/code.go b/routers/web/explore/code.go index 4df89253b4..ae5ff3db76 100644 --- a/routers/web/explore/code.go +++ b/routers/web/explore/code.go @@ -11,6 +11,7 @@ import ( code_indexer "code.gitea.io/gitea/modules/indexer/code" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/services/context" ) @@ -32,18 +33,10 @@ func Code(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("explore") ctx.Data["PageIsExplore"] = true ctx.Data["PageIsExploreCode"] = true - - language := ctx.FormTrim("l") - keyword := ctx.FormTrim("q") - - isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true) - - ctx.Data["Keyword"] = keyword - ctx.Data["Language"] = language - ctx.Data["IsFuzzy"] = isFuzzy ctx.Data["PageIsViewCode"] = true - if keyword == "" { + prepareSearch := common.PrepareCodeSearch(ctx) + if prepareSearch.Keyword == "" { ctx.HTML(http.StatusOK, tplExploreCode) return } @@ -80,9 +73,9 @@ func Code(ctx *context.Context) { if (len(repoIDs) > 0) || isAdmin { total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{ RepoIDs: repoIDs, - Keyword: keyword, - IsKeywordFuzzy: isFuzzy, - Language: language, + Keyword: prepareSearch.Keyword, + IsKeywordFuzzy: prepareSearch.IsFuzzy, + Language: prepareSearch.Language, Paginator: &db.ListOptions{ Page: page, PageSize: setting.UI.RepoSearchPagingNum, @@ -137,8 +130,7 @@ func Code(ctx *context.Context) { ctx.Data["SearchResultLanguages"] = searchResultLanguages pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5) - pager.SetDefaultParams(ctx) - pager.AddParamString("l", language) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplExploreCode) diff --git a/routers/web/explore/repo.go b/routers/web/explore/repo.go index c421aea715..cf3128314b 100644 --- a/routers/web/explore/repo.go +++ b/routers/web/explore/repo.go @@ -4,7 +4,6 @@ package explore import ( - "fmt" "net/http" "code.gitea.io/gitea/models/db" @@ -139,25 +138,7 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) { ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled pager := context.NewPagination(int(count), opts.PageSize, page, 5) - pager.SetDefaultParams(ctx) - pager.AddParamString("topic", fmt.Sprint(topicOnly)) - pager.AddParamString("language", language) - pager.AddParamString(relevantReposOnlyParam, fmt.Sprint(opts.OnlyShowRelevant)) - if archived.Has() { - pager.AddParamString("archived", fmt.Sprint(archived.Value())) - } - if fork.Has() { - pager.AddParamString("fork", fmt.Sprint(fork.Value())) - } - if mirror.Has() { - pager.AddParamString("mirror", fmt.Sprint(mirror.Value())) - } - if template.Has() { - pager.AddParamString("template", fmt.Sprint(template.Value())) - } - if private.Has() { - pager.AddParamString("private", fmt.Sprint(private.Value())) - } + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, opts.TplName) diff --git a/routers/web/explore/user.go b/routers/web/explore/user.go index ef103af8cf..b14272c76a 100644 --- a/routers/web/explore/user.go +++ b/routers/web/explore/user.go @@ -120,10 +120,7 @@ func RenderUserSearch(ctx *context.Context, opts *user_model.SearchUserOptions, ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5) - pager.SetDefaultParams(ctx) - for paramKey, paramVal := range opts.ExtraParamStrings { - pager.AddParamString(paramKey, paramVal) - } + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplName) diff --git a/routers/web/feed/render.go b/routers/web/feed/render.go index f975fc7cb2..462ebb97b5 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 3714dd8eb0..79d5c2b207 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 7122aff6bd..277adb60ca 100644 --- a/routers/web/org/home.go +++ b/routers/web/org/home.go @@ -4,7 +4,6 @@ package org import ( - "fmt" "net/http" "path" "strings" @@ -13,6 +12,7 @@ import ( "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/renderhelper" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" @@ -22,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 f91062957e..1665a12302 100644 --- a/routers/web/org/members.go +++ b/routers/web/org/members.go @@ -54,9 +54,9 @@ func Members(ctx *context.Context) { return } - err = shared_user.RenderOrgHeader(ctx) + _, err = shared_user.PrepareOrgHeader(ctx) if err != nil { - ctx.ServerError("RenderOrgHeader", err) + ctx.ServerError("PrepareOrgHeader", err) return } @@ -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 08201e5eaa..32da1b41d1 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -120,7 +120,7 @@ func Projects(ctx *context.Context) { } pager := context.NewPagination(int(total), setting.UI.IssuePagingNum, page, numPages) - pager.AddParamString("state", fmt.Sprint(ctx.Data["State"])) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.Data["CanWriteProjects"] = canWriteProjects(ctx) @@ -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 c52cb7ed4c..c3a769e621 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 7414a11308..26031029d6 100644 --- a/routers/web/org/teams.go +++ b/routers/web/org/teams.go @@ -58,9 +58,9 @@ func Teams(ctx *context.Context) { } ctx.Data["Teams"] = ctx.Org.Teams - err := shared_user.RenderOrgHeader(ctx) + _, err := shared_user.PrepareOrgHeader(ctx) if err != nil { - ctx.ServerError("RenderOrgHeader", err) + ctx.ServerError("PrepareOrgHeader", err) return } @@ -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 099593bff0..539c4b6ed0 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="/<repoLink>/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 ba17fa427d..9a18ca5305 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 04f480f611..f8e51521be 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 72fd958e28..2bcd7821b4 100644 --- a/routers/web/repo/branch.go +++ b/routers/web/repo/branch.go @@ -87,7 +87,7 @@ func Branches(ctx *context.Context) { ctx.Data["CommitStatuses"] = commitStatuses ctx.Data["DefaultBranchBranch"] = defaultBranch pager := context.NewPagination(int(branchesCount), pageSize, page, 5) - pager.SetDefaultParams(ctx) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplBranch) } diff --git a/routers/web/repo/cherry_pick.go b/routers/web/repo/cherry_pick.go index 30f4c8a90e..35f158df52 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 1447b17a36..3655233312 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -101,7 +101,7 @@ func Commits(ctx *context.Context) { ctx.Data["CommitCount"] = commitsCount pager := context.NewPagination(int(commitsCount), pageSize, page, 5) - pager.SetDefaultParams(ctx) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplCommits) } @@ -139,7 +139,6 @@ func Graph(ctx *context.Context) { if err != nil { log.Warn("GetCommitGraphsCount error for generate graph exclude prs: %t branches: %s in %-v, Will Ignore branches and try again. Underlying Error: %v", hidePRRefs, branches, ctx.Repo.Repository, err) realBranches = []string{} - branches = []string{} graphCommitsCount, err = ctx.Repo.GetCommitGraphsCount(ctx, hidePRRefs, realBranches, files) if err != nil { ctx.ServerError("GetCommitGraphsCount", err) @@ -175,14 +174,7 @@ func Graph(ctx *context.Context) { ctx.Data["CommitCount"] = commitsCount paginator := context.NewPagination(int(graphCommitsCount), setting.UI.GraphMaxCommitNum, page, 5) - paginator.AddParamString("mode", mode) - paginator.AddParamString("hide-pr-refs", fmt.Sprint(hidePRRefs)) - for _, branch := range branches { - paginator.AddParamString("branch", branch) - } - for _, file := range files { - paginator.AddParamString("file", file) - } + paginator.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = paginator if ctx.FormBool("div-only") { ctx.HTML(http.StatusOK, tplGraphDiv) @@ -262,7 +254,7 @@ func FileHistory(ctx *context.Context) { ctx.Data["CommitCount"] = commitsCount pager := context.NewPagination(int(commitsCount), setting.Git.CommitsRangeSize, page, 5) - pager.SetDefaultParams(ctx) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplCommits) } @@ -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 6c59421bda..b3c1eb7cb0 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 1ed907b2f9..cb1163c70b 100644 --- a/routers/web/repo/download.go +++ b/routers/web/repo/download.go @@ -5,7 +5,6 @@ package repo import ( - "path" "time" git_model "code.gitea.io/gitea/models/git" @@ -82,7 +81,7 @@ func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob, lastModified *time.Tim return common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, lastModified) } -func getBlobForEntry(ctx *context.Context) (blob *git.Blob, lastModified *time.Time) { +func getBlobForEntry(ctx *context.Context) (*git.Blob, *time.Time) { entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath) if err != nil { if git.IsErrNotExist(err) { @@ -98,19 +97,14 @@ func getBlobForEntry(ctx *context.Context) (blob *git.Blob, lastModified *time.T return nil, nil } - info, _, err := git.Entries([]*git.TreeEntry{entry}).GetCommitsInfo(ctx, ctx.Repo.Commit, path.Dir("/" + ctx.Repo.TreePath)[1:]) + latestCommit, err := ctx.Repo.GitRepo.GetTreePathLatestCommit(ctx.Repo.Commit.ID.String(), ctx.Repo.TreePath) if err != nil { - ctx.ServerError("GetCommitsInfo", err) + ctx.ServerError("GetTreePathLatestCommit", err) return nil, nil } + lastModified := &latestCommit.Committer.When - if len(info) == 1 { - // Not Modified - lastModified = &info[0].Commit.Committer.When - } - blob = entry.Blob() - - return blob, lastModified + return entry.Blob(), lastModified } // SingleDownload download a file by repos path diff --git a/routers/web/repo/editor_test.go b/routers/web/repo/editor_test.go index 68d69408ac..566db31693 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 a8a7a4bd79..6b2a7fd076 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 23012dda3d..f150897a2d 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 6b7b29d9d7..8564c613de 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 c86a03da51..8a613e2c7e 100644 --- a/routers/web/repo/issue_label_test.go +++ b/routers/web/repo/issue_label_test.go @@ -162,10 +162,11 @@ func TestUpdateIssueLabel_Toggle(t *testing.T) { UpdateIssueLabel(ctx) assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) for _, issueID := range testCase.IssueIDs { - unittest.AssertExistsIf(t, testCase.ExpectedAdd, &issues_model.IssueLabel{ - IssueID: issueID, - LabelID: testCase.LabelID, - }) + if testCase.ExpectedAdd { + unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: testCase.LabelID}) + } else { + unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: testCase.LabelID}) + } } unittest.CheckConsistencyFor(t, &issues_model.Label{}) } diff --git a/routers/web/repo/issue_list.go b/routers/web/repo/issue_list.go index ff98bf8ec8..2f615a100e 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 9a941ce857..32daa3e48f 100644 --- a/routers/web/repo/issue_new.go +++ b/routers/web/repo/issue_new.go @@ -396,8 +396,15 @@ func NewIssuePost(ctx *context.Context) { log.Trace("Issue created: %d/%d", repo.ID, issue.ID) if ctx.FormString("redirect_after_creation") == "project" && projectID > 0 { - ctx.JSONRedirect(ctx.Repo.RepoLink + "/projects/" + strconv.FormatInt(projectID, 10)) - } else { - ctx.JSONRedirect(issue.Link()) + project, err := project_model.GetProjectByID(ctx, projectID) + if err == nil { + if project.Type == project_model.TypeOrganization { + ctx.JSONRedirect(project_model.ProjectLinkForOrg(ctx.Repo.Owner, project.ID)) + } else { + ctx.JSONRedirect(project_model.ProjectLinkForRepo(repo, project.ID)) + } + return + } } + ctx.JSONRedirect(issue.Link()) } diff --git a/routers/web/repo/issue_page_meta.go b/routers/web/repo/issue_page_meta.go index b536b04d7c..272343f460 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 0074e31f03..d7d3205c37 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 88c539488e..36e931a48f 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 09b57f4e78..61e75e211b 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 d6e41a89b2..6a0e6b25a9 100644 --- a/routers/web/repo/milestone.go +++ b/routers/web/repo/milestone.go @@ -4,7 +4,6 @@ package repo import ( - "fmt" "net/http" "net/url" @@ -93,8 +92,7 @@ func Milestones(ctx *context.Context) { ctx.Data["IsShowClosed"] = isShowClosed pager := context.NewPagination(int(total), setting.UI.IssuePagingNum, page, 5) - pager.AddParamString("state", fmt.Sprint(ctx.Data["State"])) - pager.AddParamString("q", keyword) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplMilestone) @@ -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 c8d3719bc0..5dcfd0454c 100644 --- a/routers/web/repo/packages.go +++ b/routers/web/repo/packages.go @@ -70,8 +70,7 @@ func Packages(ctx *context.Context) { ctx.Data["RepositoryAccessMap"] = map[int64]bool{ctx.Repo.Repository.ID: true} // There is only the current repository pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5) - pager.AddParamString("q", query) - pager.AddParamString("type", packageType) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplPackagesList) diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 92227e3f3e..346132102f 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -115,7 +115,7 @@ func Projects(ctx *context.Context) { } pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, numPages) - pager.AddParamString("state", fmt.Sprint(ctx.Data["State"])) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) @@ -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 1a42c615ab..d0690d9a4d 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 0948282ca2..9f3d1c1b7c 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 b8176cb70b..5b099fe8d4 100644 --- a/routers/web/repo/release.go +++ b/routers/web/repo/release.go @@ -186,7 +186,7 @@ func Releases(ctx *context.Context) { numReleases := ctx.Data["NumReleases"].(int64) pager := context.NewPagination(int(numReleases), listOptions.PageSize, listOptions.Page, 5) - pager.SetDefaultParams(ctx) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplReleasesList) } @@ -240,7 +240,7 @@ func TagsList(ctx *context.Context) { ctx.Data["TagCount"] = count pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5) - pager.SetDefaultParams(ctx) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.Data["PageIsViewCode"] = !ctx.Repo.Repository.UnitEnabled(ctx, unit.TypeReleases) ctx.HTML(http.StatusOK, tplTagsList) diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index 6df7d78f7a..0f408b22e0 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 a037a34833..c60301475f 100644 --- a/routers/web/repo/search.go +++ b/routers/web/repo/search.go @@ -12,6 +12,7 @@ import ( code_indexer "code.gitea.io/gitea/modules/indexer/code" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/services/context" ) @@ -29,18 +30,9 @@ func indexSettingToGitGrepPathspecList() (list []string) { // Search render repository search page func Search(ctx *context.Context) { - language := ctx.FormTrim("l") - keyword := ctx.FormTrim("q") - - isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true) - - ctx.Data["Keyword"] = keyword - ctx.Data["Language"] = language - ctx.Data["IsFuzzy"] = isFuzzy ctx.Data["PageIsViewCode"] = true - ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled - - if keyword == "" { + prepareSearch := common.PrepareCodeSearch(ctx) + if prepareSearch.Keyword == "" { ctx.HTML(http.StatusOK, tplSearch) return } @@ -57,9 +49,9 @@ func Search(ctx *context.Context) { var err error total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{ RepoIDs: []int64{ctx.Repo.Repository.ID}, - Keyword: keyword, - IsKeywordFuzzy: isFuzzy, - Language: language, + Keyword: prepareSearch.Keyword, + IsKeywordFuzzy: prepareSearch.IsFuzzy, + Language: prepareSearch.Language, Paginator: &db.ListOptions{ Page: page, PageSize: setting.UI.RepoSearchPagingNum, @@ -75,9 +67,9 @@ func Search(ctx *context.Context) { ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx) } } else { - res, err := git.GrepSearch(ctx, ctx.Repo.GitRepo, keyword, git.GrepOptions{ + res, err := git.GrepSearch(ctx, ctx.Repo.GitRepo, prepareSearch.Keyword, git.GrepOptions{ ContextLineNumber: 1, - IsFuzzy: isFuzzy, + IsFuzzy: prepareSearch.IsFuzzy, RefName: git.RefNameFromBranch(ctx.Repo.BranchName).String(), // BranchName should be default branch or the first existing branch PathspecList: indexSettingToGitGrepPathspecList(), }) @@ -108,8 +100,7 @@ func Search(ctx *context.Context) { ctx.Data["SearchResultLanguages"] = searchResultLanguages pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5) - pager.SetDefaultParams(ctx) - pager.AddParamString("l", language) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplSearch) diff --git a/routers/web/repo/setting/git_hooks.go b/routers/web/repo/setting/git_hooks.go index 2e9caa4c86..1d92211303 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 1c24c8a7ba..1730ad4a8b 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 ec037baec1..94f2ae7a0c 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 bcf8a7eac0..1b0ba83af4 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 14fc9038f3..9fe2b58ebc 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 b318c4a621..8c9f54656b 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 3fca7cebea..19366c0104 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -440,8 +440,7 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) ctx.Data["Commits"] = git_model.ConvertFromGitCommit(ctx, commitsHistory, ctx.Repo.Repository) pager := context.NewPagination(int(commitsCount), setting.Git.CommitsRangeSize, page, 5) - pager.SetDefaultParams(ctx) - pager.AddParamString("action", "_revision") + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager return wikiRepo, entry diff --git a/routers/web/shared/actions/runners.go b/routers/web/shared/actions/runners.go index f38933226b..6d77bdd2fa 100644 --- a/routers/web/shared/actions/runners.go +++ b/routers/web/shared/actions/runners.go @@ -136,9 +136,8 @@ func RunnerResetRegistrationToken(ctx *context.Context, ownerID, repoID int64, r ctx.ServerError("ResetRunnerRegistrationToken", err) return } - ctx.Flash.Success(ctx.Tr("actions.runners.reset_registration_token_success")) - ctx.Redirect(redirectTo) + ctx.JSONRedirect(redirectTo) } // RunnerDeletePost response for deleting a runner diff --git a/routers/web/shared/actions/variables.go b/routers/web/shared/actions/variables.go index 5c5768243a..f895475748 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 ba1f527bea..141c80716f 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 4cb0592b4b..62b146c7f3 100644 --- a/routers/web/shared/user/header.go +++ b/routers/web/shared/user/header.go @@ -20,6 +20,7 @@ import ( "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" ) @@ -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 7000e25778..f77bd602b3 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 f805f2b028..665ce1a6a6 100644 --- a/routers/web/user/code.go +++ b/routers/web/user/code.go @@ -11,6 +11,7 @@ import ( code_indexer "code.gitea.io/gitea/modules/indexer/code" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/routers/common" shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/context" ) @@ -34,20 +35,11 @@ func CodeSearch(ctx *context.Context) { } ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled - ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled ctx.Data["Title"] = ctx.Tr("explore.code") - - language := ctx.FormTrim("l") - keyword := ctx.FormTrim("q") - - isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true) - - ctx.Data["Keyword"] = keyword - ctx.Data["Language"] = language - ctx.Data["IsFuzzy"] = isFuzzy ctx.Data["IsCodePage"] = true - if keyword == "" { + prepareSearch := common.PrepareCodeSearch(ctx) + if prepareSearch.Keyword == "" { ctx.HTML(http.StatusOK, tplUserCode) return } @@ -77,9 +69,9 @@ func CodeSearch(ctx *context.Context) { if len(repoIDs) > 0 { total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{ RepoIDs: repoIDs, - Keyword: keyword, - IsKeywordFuzzy: isFuzzy, - Language: language, + Keyword: prepareSearch.Keyword, + IsKeywordFuzzy: prepareSearch.IsFuzzy, + Language: prepareSearch.Language, Paginator: &db.ListOptions{ Page: page, PageSize: setting.UI.RepoSearchPagingNum, @@ -121,8 +113,7 @@ func CodeSearch(ctx *context.Context) { ctx.Data["SearchResultLanguages"] = searchResultLanguages pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5) - pager.SetDefaultParams(ctx) - pager.AddParamString("l", language) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplUserCode) diff --git a/routers/web/user/home.go b/routers/web/user/home.go index e118aa051e..c79648a455 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 732fac2595..1c91ff6364 100644 --- a/routers/web/user/notification.go +++ b/routers/web/user/notification.go @@ -173,7 +173,7 @@ func getNotifications(ctx *context.Context) { ctx.Data["Status"] = status ctx.Data["Notifications"] = notifications - pager.SetDefaultParams(ctx) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager } @@ -357,8 +357,7 @@ func NotificationSubscriptions(ctx *context.Context) { ctx.Redirect(fmt.Sprintf("/notifications/subscriptions?page=%d", pager.Paginater.Current())) return } - pager.AddParamString("sort", sortType) - pager.AddParamString("state", state) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplNotificationSubscriptions) @@ -446,22 +445,7 @@ func NotificationWatching(ctx *context.Context) { // redirect to last page if request page is more than total pages pager := context.NewPagination(total, setting.UI.User.RepoPagingNum, page, 5) - pager.SetDefaultParams(ctx) - if archived.Has() { - pager.AddParamString("archived", fmt.Sprint(archived.Value())) - } - if fork.Has() { - pager.AddParamString("fork", fmt.Sprint(fork.Value())) - } - if mirror.Has() { - pager.AddParamString("mirror", fmt.Sprint(mirror.Value())) - } - if template.Has() { - pager.AddParamString("template", fmt.Sprint(template.Value())) - } - if private.Has() { - pager.AddParamString("private", fmt.Sprint(private.Value())) - } + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.Data["Status"] = 2 diff --git a/routers/web/user/package.go b/routers/web/user/package.go index d5aac513b6..1f75faf1c6 100644 --- a/routers/web/user/package.go +++ b/routers/web/user/package.go @@ -128,8 +128,7 @@ func ListPackages(ctx *context.Context) { } pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5) - pager.AddParamString("q", query) - pager.AddParamString("type", packageType) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplPackagesList) @@ -348,11 +347,6 @@ func ListPackageVersions(ctx *context.Context) { ctx.Data["Query"] = query ctx.Data["Sort"] = sort - pagerParams := map[string]string{ - "q": query, - "sort": sort, - } - var ( total int64 pvs []*packages_model.PackageVersion @@ -361,7 +355,6 @@ func ListPackageVersions(ctx *context.Context) { case packages_model.TypeContainer: tagged := ctx.FormTrim("tagged") - pagerParams["tagged"] = tagged ctx.Data["Tagged"] = tagged pvs, total, err = container_model.SearchImageTags(ctx, &container_model.ImageTagsSearchOptions{ @@ -407,9 +400,7 @@ func ListPackageVersions(ctx *context.Context) { } pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5) - for k, v := range pagerParams { - pager.AddParamString(k, v) - } + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplPackageVersionList) @@ -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 44824de752..006ffdcf7e 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 4b3c214096..ebf682bf58 100644 --- a/routers/web/user/setting/profile.go +++ b/routers/web/user/setting/profile.go @@ -222,7 +222,7 @@ func Organization(ctx *context.Context) { ctx.Data["Orgs"] = orgs pager := context.NewPagination(int(total), opts.PageSize, opts.Page, 5) - pager.SetDefaultParams(ctx) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplSettingsOrganization) } @@ -329,7 +329,7 @@ func Repos(ctx *context.Context) { } ctx.Data["ContextUser"] = ctxUser pager := context.NewPagination(count, opts.PageSize, opts.Page, 5) - pager.SetDefaultParams(ctx) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplSettingsRepositories) } diff --git a/routers/web/web.go b/routers/web/web.go index aa37d4dc10..32d65865ac 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -4,7 +4,6 @@ package web import ( - gocontext "context" "net/http" "strings" @@ -463,7 +462,7 @@ func registerRoutes(m *web.Router) { m.Combo("/{runnerid}").Get(repo_setting.RunnersEdit). Post(web.Bind(forms.EditRunnerForm{}), repo_setting.RunnersEditPost) m.Post("/{runnerid}/delete", repo_setting.RunnerDeletePost) - m.Get("/reset_registration_token", repo_setting.ResetRunnerRegistrationToken) + m.Post("/reset_registration_token", repo_setting.ResetRunnerRegistrationToken) }) } @@ -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 7f52c9d31b..94ab89a3b7 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/init.go b/services/actions/init.go index 0f49cb6297..7136da05ed 100644 --- a/services/actions/init.go +++ b/services/actions/init.go @@ -4,23 +4,68 @@ package actions import ( + "context" + "errors" + "fmt" + "os" + "strings" + + actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/queue" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" notify_service "code.gitea.io/gitea/services/notify" ) -func Init() { +func initGlobalRunnerToken(ctx context.Context) error { + // use the same env name as the runner, for consistency + token := os.Getenv("GITEA_RUNNER_REGISTRATION_TOKEN") + tokenFile := os.Getenv("GITEA_RUNNER_REGISTRATION_TOKEN_FILE") + if token != "" && tokenFile != "" { + return errors.New("both GITEA_RUNNER_REGISTRATION_TOKEN and GITEA_RUNNER_REGISTRATION_TOKEN_FILE are set, only one can be used") + } + if tokenFile != "" { + file, err := os.ReadFile(tokenFile) + if err != nil { + return fmt.Errorf("unable to read GITEA_RUNNER_REGISTRATION_TOKEN_FILE: %w", err) + } + token = strings.TrimSpace(string(file)) + } + if token == "" { + return nil + } + + if len(token) < 32 { + return errors.New("GITEA_RUNNER_REGISTRATION_TOKEN must be at least 32 random characters") + } + + existing, err := actions_model.GetRunnerToken(ctx, token) + if err != nil && !errors.Is(err, util.ErrNotExist) { + return fmt.Errorf("unable to check existing token: %w", err) + } + if existing != nil { + if !existing.IsActive { + log.Warn("The token defined by GITEA_RUNNER_REGISTRATION_TOKEN is already invalidated, please use the latest one from web UI") + } + return nil + } + _, err = actions_model.NewRunnerTokenWithValue(ctx, 0, 0, token) + return err +} + +func Init(ctx context.Context) error { if !setting.Actions.Enabled { - return + return nil } jobEmitterQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "actions_ready_job", jobEmitterQueueHandler) if jobEmitterQueue == nil { - log.Fatal("Unable to create actions_ready_job queue") + return errors.New("unable to create actions_ready_job queue") } go graceful.GetManager().RunWithCancel(jobEmitterQueue) notify_service.RegisterNotifier(NewNotifier()) + return initGlobalRunnerToken(ctx) } diff --git a/services/actions/init_test.go b/services/actions/init_test.go new file mode 100644 index 0000000000..59c321ccd7 --- /dev/null +++ b/services/actions/init_test.go @@ -0,0 +1,80 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "os" + "testing" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/util" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMain(m *testing.M) { + unittest.MainTest(m, &unittest.TestOptions{ + FixtureFiles: []string{"action_runner_token.yml"}, + }) + os.Exit(m.Run()) +} + +func TestInitToken(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + t.Run("NoToken", func(t *testing.T) { + _, _ = db.Exec(db.DefaultContext, "DELETE FROM action_runner_token") + t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN", "") + t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN_FILE", "") + err := initGlobalRunnerToken(db.DefaultContext) + require.NoError(t, err) + notEmpty, err := db.IsTableNotEmpty(&actions_model.ActionRunnerToken{}) + require.NoError(t, err) + assert.False(t, notEmpty) + }) + + t.Run("EnvToken", func(t *testing.T) { + tokenValue, _ := util.CryptoRandomString(32) + t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN", tokenValue) + t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN_FILE", "") + err := initGlobalRunnerToken(db.DefaultContext) + require.NoError(t, err) + token := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunnerToken{Token: tokenValue}) + assert.True(t, token.IsActive) + + // init with the same token again, should not create a new token + err = initGlobalRunnerToken(db.DefaultContext) + require.NoError(t, err) + token2 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunnerToken{Token: tokenValue}) + assert.Equal(t, token.ID, token2.ID) + assert.True(t, token.IsActive) + }) + + t.Run("EnvFileToken", func(t *testing.T) { + tokenValue, _ := util.CryptoRandomString(32) + f := t.TempDir() + "/token" + _ = os.WriteFile(f, []byte(tokenValue), 0o644) + t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN", "") + t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN_FILE", f) + err := initGlobalRunnerToken(db.DefaultContext) + require.NoError(t, err) + token := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunnerToken{Token: tokenValue}) + assert.True(t, token.IsActive) + + // if the env token is invalidated by another new token, then it shouldn't be active anymore + _, err = actions_model.NewRunnerToken(db.DefaultContext, 0, 0) + require.NoError(t, err) + token = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunnerToken{Token: tokenValue}) + assert.False(t, token.IsActive) + }) + + t.Run("InvalidToken", func(t *testing.T) { + t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN", "abc") + err := initGlobalRunnerToken(db.DefaultContext) + assert.ErrorContains(t, err, "must be at least") + }) +} diff --git a/services/actions/notifier.go b/services/actions/notifier.go index a4ebdf9e88..67e33e7cce 100644 --- a/services/actions/notifier.go +++ b/services/actions/notifier.go @@ -58,7 +58,15 @@ func (n *actionsNotifier) NewIssue(ctx context.Context, issue *issues_model.Issu // IssueChangeContent notifies change content of issue func (n *actionsNotifier) IssueChangeContent(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldContent string) { ctx = withMethod(ctx, "IssueChangeContent") + n.notifyIssueChangeWithTitleOrContent(ctx, doer, issue) +} + +func (n *actionsNotifier) IssueChangeTitle(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldTitle string) { + ctx = withMethod(ctx, "IssueChangeTitle") + n.notifyIssueChangeWithTitleOrContent(ctx, doer, issue) +} +func (n *actionsNotifier) notifyIssueChangeWithTitleOrContent(ctx context.Context, doer *user_model.User, issue *issues_model.Issue) { var err error if err = issue.LoadRepo(ctx); err != nil { log.Error("LoadRepo: %v", err) diff --git a/services/auth/interface.go b/services/auth/interface.go index ece28af12d..275b4dd56c 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 b706847e8e..0d9e793cf3 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 b45e80a329..bda705cb48 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 d627095584..7a39353e09 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 0000000000..5b8cae9e99 --- /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 0000000000..3678deaff9 --- /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 823f20e00b..b936b76f58 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 0d5429e366..6715c5663d 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 4c086ea9f5..c7044791eb 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 984593398d..54044644f0 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 bf482fa754..be87cef7a3 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 271b61e99c..e98e01acbb 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 42117cf96d..d33dd217d0 100644 --- a/services/context/pagination.go +++ b/services/context/pagination.go @@ -27,19 +27,13 @@ func NewPagination(total, pagingNum, current, numPages int) *Pagination { return p } -// AddParamString adds a string parameter directly -func (p *Pagination) AddParamString(key, value string) { - urlParam := fmt.Sprintf("%s=%v", url.QueryEscape(key), url.QueryEscape(value)) - p.urlParams = append(p.urlParams, urlParam) -} - func (p *Pagination) AddParamFromRequest(req *http.Request) { for key, values := range req.URL.Query() { - if key == "page" || len(values) == 0 { + if key == "page" || len(values) == 0 || (len(values) == 1 && values[0] == "") { continue } for _, value := range values { - urlParam := fmt.Sprintf("%s=%v", key, url.QueryEscape(value)) + urlParam := fmt.Sprintf("%s=%v", url.QueryEscape(key), url.QueryEscape(value)) p.urlParams = append(p.urlParams, urlParam) } } @@ -49,17 +43,3 @@ func (p *Pagination) AddParamFromRequest(req *http.Request) { func (p *Pagination) GetParams() template.URL { return template.URL(strings.Join(p.urlParams, "&")) } - -// SetDefaultParams sets common pagination params that are often used -func (p *Pagination) SetDefaultParams(ctx *Context) { - if v, ok := ctx.Data["SortType"].(string); ok { - p.AddParamString("sort", v) - } - if v, ok := ctx.Data["Keyword"].(string); ok { - p.AddParamString("q", v) - } - if v, ok := ctx.Data["IsFuzzy"].(bool); ok { - p.AddParamString("fuzzy", fmt.Sprint(v)) - } - // do not add any more uncommon params here! -} diff --git a/services/context/private.go b/services/context/private.go index 8b41949f60..51857da8fe 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 e96916ca42..4de905ef2c 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(`<meta name="go-import" content="%s">`, 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 cefd13ebb6..da4370a433 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 b0e855e923..dbc35e198d 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 39ad5a362f..b0f71cad20 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 88ccd88fcf..632b6392d5 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 5e9d32cc8e..b59884ec50 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 1ac03a3097..a8363ec6bd 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 d941027c35..702eb5ad53 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 0b5a855d42..f42686bb71 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 0000000000..02ca666544 --- /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(`<a href="%s">%s</a>`, 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(`<a href="%s">%s...%s</a>`, 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(`<a href="%s">%s</a>`, webLink.RepoWebLink, si.SubmoduleName) +} diff --git a/services/gitdiff/submodule_test.go b/services/gitdiff/submodule_test.go new file mode 100644 index 0000000000..89f32c0e0c --- /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, `<a href="https://github.com/owner/repo/commit/1111">1111</a>`, sdi.CommitRefIDLinkHTML(ctx, "1111")) + assert.EqualValues(t, `<a href="https://github.com/owner/repo/compare/aaaa...bbbb">aaaa...bbbb</a>`, sdi.CompareRefIDLinkHTML(ctx)) + assert.EqualValues(t, `<a href="https://github.com/owner/repo">name</a>`, sdi.SubmoduleRepoLinkHTML(ctx)) +} diff --git a/services/issue/commit.go b/services/issue/commit.go index 0579e0f5c5..963d0359fd 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 ff645744a7..beb6f131a9 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 42b910166f..bf5abc85b7 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 967c29bd22..e18b891175 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 a6763e4f03..52e19bde6f 100644 --- a/services/mailer/mail.go +++ b/services/mailer/mail.go @@ -93,7 +93,8 @@ func SendActivateAccountMail(locale translation.Locale, u *user_model.User) { // No mail service configured return } - sendUserMail(locale.Language(), u, mailAuthActivate, u.GenerateEmailActivateCode(u.Email), locale.TrString("mail.activate_account"), "activate account") + opts := &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateAccount} + sendUserMail(locale.Language(), u, mailAuthActivate, user_model.GenerateUserTimeLimitCode(opts, u), locale.TrString("mail.activate_account"), "activate account") } // SendResetPasswordMail sends a password reset mail to the user @@ -103,7 +104,8 @@ func SendResetPasswordMail(u *user_model.User) { return } locale := translation.NewLocale(u.Language) - sendUserMail(u.Language, u, mailAuthResetPassword, u.GenerateEmailActivateCode(u.Email), locale.TrString("mail.reset_password"), "recover account") + opts := &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeResetPassword} + sendUserMail(u.Language, u, mailAuthResetPassword, user_model.GenerateUserTimeLimitCode(opts, u), locale.TrString("mail.reset_password"), "recover account") } // SendActivateEmailMail sends confirmation email to confirm new email address @@ -113,11 +115,12 @@ func SendActivateEmailMail(u *user_model.User, email string) { return } locale := translation.NewLocale(u.Language) + opts := &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateEmail, NewEmail: email} data := map[string]any{ "locale": locale, "DisplayName": u.DisplayName(), "ActiveCodeLives": timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, locale), - "Code": u.GenerateEmailActivateCode(email), + "Code": user_model.GenerateUserTimeLimitCode(opts, u), "Email": email, "Language": locale.Language(), } diff --git a/services/mailer/sender/message.go b/services/mailer/sender/message.go index db20675572..55f03e4f7e 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), "<html>") { + if strings.Contains(util.TruncateRunes(m.Body, 100), "<html>") { 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 5553ebc058..d04a18bfa1 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 index 1f1abf496a..4b9852b48b 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 index 0500e57e46..170c70c409 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 index 154e4e8e44..ea945584b4 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 0000000000..53a508e908 --- /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(`<a href="%s">%s %s %s</a>`, 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 0000000000..adce8401e0 --- /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, `<a href="/link"><span>octicon-issue-opened(16/text green)</span> issue1 (#1)</a>`, 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, `<a href="/link"><span>octicon-issue-opened(16/text green)</span> issue1 (user2/repo1#1)</a>`, 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 index 170edae0e0..c244fa3d21 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 ccda62fc3d..fead527f5b 100644 --- a/services/migrations/codecommit.go +++ b/services/migrations/codecommit.go @@ -14,11 +14,11 @@ import ( "code.gitea.io/gitea/modules/log" base "code.gitea.io/gitea/modules/migration" "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/codecommit" "github.com/aws/aws-sdk-go-v2/service/codecommit/types" - "github.com/aws/aws-sdk-go/aws" ) var ( @@ -94,7 +94,7 @@ func (c *CodeCommitDownloader) SetContext(ctx context.Context) { // GetRepoInfo returns a repository information func (c *CodeCommitDownloader) GetRepoInfo() (*base.Repository, error) { output, err := c.codeCommitClient.GetRepository(c.ctx, &codecommit.GetRepositoryInput{ - RepositoryName: aws.String(c.repoName), + RepositoryName: util.ToPointer(c.repoName), }) if err != nil { return nil, err @@ -126,7 +126,7 @@ func (c *CodeCommitDownloader) GetComments(commentable base.Commentable) ([]*bas for { resp, err := c.codeCommitClient.GetCommentsForPullRequest(c.ctx, &codecommit.GetCommentsForPullRequestInput{ NextToken: nextToken, - PullRequestId: aws.String(strconv.FormatInt(commentable.GetForeignIndex(), 10)), + PullRequestId: util.ToPointer(strconv.FormatInt(commentable.GetForeignIndex(), 10)), }) if err != nil { return nil, false, err @@ -171,7 +171,7 @@ func (c *CodeCommitDownloader) GetPullRequests(page, perPage int) ([]*base.PullR prs := make([]*base.PullRequest, 0, len(batch)) for _, id := range batch { output, err := c.codeCommitClient.GetPullRequest(c.ctx, &codecommit.GetPullRequestInput{ - PullRequestId: aws.String(id), + PullRequestId: util.ToPointer(id), }) if err != nil { return nil, false, err @@ -243,7 +243,7 @@ func (c *CodeCommitDownloader) getAllPullRequestIDs() ([]string, error) { for { output, err := c.codeCommitClient.ListPullRequests(c.ctx, &codecommit.ListPullRequestsInput{ - RepositoryName: aws.String(c.repoName), + RepositoryName: util.ToPointer(c.repoName), NextToken: nextToken, }) if err != nil { diff --git a/services/migrations/gitea_downloader_test.go b/services/migrations/gitea_downloader_test.go index c37c70947e..d04e12abdb 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 eb21b6534b..9e06b77b66 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 295bc7c29f..07d5040b5b 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 eccfc4def1..556fe771c5 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 22d380e8e6..948222a436 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 56d01a3b63..96d1a1c8ca 100644 --- a/services/org/user_test.go +++ b/services/org/user_test.go @@ -47,7 +47,7 @@ func TestRemoveOrgUser(t *testing.T) { testSuccess := func(org *organization.Organization, user *user_model.User) { expectedNumMembers := org.NumMembers - if unittest.BeanExists(t, &organization.OrgUser{OrgID: org.ID, UID: user.ID}) { + if unittest.GetBean(t, &organization.OrgUser{OrgID: org.ID, UID: user.ID}) != nil { expectedNumMembers-- } assert.NoError(t, RemoveOrgUser(db.DefaultContext, org, user)) diff --git a/services/packages/packages.go b/services/packages/packages.go index 55351afce2..bd1d460fd3 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 bffca394a8..e1adc3ca3b 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 fba85f1e51..9c909ef795 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 197d8102dd..7258671888 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 36ca9dbdb6..13623d73c6 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 0256c2c3f6..52abf35cec 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 84c60a105a..835a5943b1 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/avatar.go b/services/repository/avatar.go index 38c2621bc4..15e51d4a25 100644 --- a/services/repository/avatar.go +++ b/services/repository/avatar.go @@ -8,7 +8,6 @@ import ( "fmt" "io" "strconv" - "strings" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" @@ -107,7 +106,8 @@ func RemoveRandomAvatars(ctx context.Context) error { // generateAvatar generates the avatar from a template repository func generateAvatar(ctx context.Context, templateRepo, generateRepo *repo_model.Repository) error { - generateRepo.Avatar = strings.Replace(templateRepo.Avatar, strconv.FormatInt(templateRepo.ID, 10), strconv.FormatInt(generateRepo.ID, 10), 1) + // generate a new different hash, whatever the "hash data" is, it doesn't matter + generateRepo.Avatar = avatar.HashAvatar(generateRepo.ID, []byte("new-avatar")) if _, err := storage.Copy(storage.RepoAvatars, generateRepo.CustomAvatarRelativePath(), storage.RepoAvatars, templateRepo.CustomAvatarRelativePath()); err != nil { return err } diff --git a/services/repository/avatar_test.go b/services/repository/avatar_test.go index 4a0ba61853..bea820e85f 100644 --- a/services/repository/avatar_test.go +++ b/services/repository/avatar_test.go @@ -61,3 +61,11 @@ func TestDeleteAvatar(t *testing.T) { assert.Equal(t, "", repo.Avatar) } + +func TestGenerateAvatar(t *testing.T) { + templateRepo := &repo_model.Repository{ID: 10, Avatar: "a"} + generateRepo := &repo_model.Repository{ID: 11} + _ = generateAvatar(db.DefaultContext, templateRepo, generateRepo) + assert.NotEmpty(t, generateRepo.Avatar) + assert.NotEqual(t, templateRepo.Avatar, generateRepo.Avatar) +} diff --git a/services/repository/create.go b/services/repository/create.go index a3199f2a40..23aacd6f95 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/delete.go b/services/repository/delete.go index 61e39fe105..2166b4dd5c 100644 --- a/services/repository/delete.go +++ b/services/repository/delete.go @@ -317,7 +317,8 @@ func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID if len(repo.Avatar) > 0 { if err := storage.RepoAvatars.Delete(repo.CustomAvatarRelativePath()); err != nil { - return fmt.Errorf("Failed to remove %s: %w", repo.Avatar, err) + log.Error("remove avatar file %q: %v", repo.CustomAvatarRelativePath(), err) + // go on } } diff --git a/services/repository/files/content_test.go b/services/repository/files/content_test.go index a899be70e3..7cb46c0bb6 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 ea6ffe60c3..b7bdcd8ecf 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 b2f51e3c82..52c0574883 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 786bc15857..0c60fddf7b 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 24cf9d1b9b..d5c07e9800 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 7fe63eb5ca..6f3a87afa3 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 63cbce1771..6bac02712b 100644 --- a/services/webhook/webhook_test.go +++ b/services/webhook/webhook_test.go @@ -9,11 +9,15 @@ import ( "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" + "code.gitea.io/gitea/services/convert" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestWebhook_GetSlackHook(t *testing.T) { @@ -77,3 +81,11 @@ func TestPrepareWebhooksBranchFilterNoMatch(t *testing.T) { unittest.AssertNotExistsBean(t, hookTask) } } + +func TestWebhookUserMail(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + setting.Service.NoReplyAddress = "no-reply.com" + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + assert.Equal(t, user.GetPlaceholderEmail(), convert.ToUser(db.DefaultContext, user, nil).Email) + assert.Equal(t, user.Email, convert.ToUser(db.DefaultContext, user, user).Email) +} diff --git a/templates/admin/user/edit.tmpl b/templates/admin/user/edit.tmpl index 41b00defb4..d591a645d8 100644 --- a/templates/admin/user/edit.tmpl +++ b/templates/admin/user/edit.tmpl @@ -128,16 +128,16 @@ <input name="restricted" type="checkbox" {{if .User.IsRestricted}}checked{{end}}> </div> </div> - <div class="inline field {{if DisableGitHooks}}tw-hidden{{end}}"> + <div class="inline field {{if .DisableGitHooks}}tw-hidden{{end}}"> <div class="ui checkbox" data-tooltip-content="{{ctx.Locale.Tr "admin.users.allow_git_hook_tooltip"}}"> <label><strong>{{ctx.Locale.Tr "admin.users.allow_git_hook"}}</strong></label> - <input name="allow_git_hook" type="checkbox" {{if .User.CanEditGitHook}}checked{{end}} {{if DisableGitHooks}}disabled{{end}}> + <input name="allow_git_hook" type="checkbox" {{if .User.CanEditGitHook}}checked{{end}} {{if .DisableGitHooks}}disabled{{end}}> </div> </div> - <div class="inline field {{if or (DisableImportLocal) (.DisableMigrations)}}tw-hidden{{end}}"> + <div class="inline field {{if or (.DisableImportLocal) (.DisableMigrations)}}tw-hidden{{end}}"> <div class="ui checkbox"> <label><strong>{{ctx.Locale.Tr "admin.users.allow_import_local"}}</strong></label> - <input name="allow_import_local" type="checkbox" {{if .User.CanImportLocal}}checked{{end}} {{if DisableImportLocal}}disabled{{end}}> + <input name="allow_import_local" type="checkbox" {{if .User.CanImportLocal}}checked{{end}} {{if .DisableImportLocal}}disabled{{end}}> </div> </div> {{if not .DisableRegularOrgCreation}} diff --git a/templates/base/alert_details.tmpl b/templates/base/alert_details.tmpl index 6801c8240f..6d4c1fb2db 100644 --- a/templates/base/alert_details.tmpl +++ b/templates/base/alert_details.tmpl @@ -1,7 +1,11 @@ {{.Message}} +{{if .Details}} <details> <summary>{{.Summary}}</summary> - <code> - {{.Details | SanitizeHTML}} - </code> + <code>{{.Details | SanitizeHTML}}</code> </details> +{{else}} +<div> + {{.Summary}} +</div> +{{end}} diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl index ed7a8d6f24..baf37494b9 100644 --- a/templates/base/head_navbar.tmpl +++ b/templates/base/head_navbar.tmpl @@ -70,7 +70,7 @@ <span class="not-mobile">{{svg "octicon-triangle-down"}}</span> </span> <div class="menu user-menu"> - <div class="ui header"> + <div class="header"> {{ctx.Locale.Tr "signed_in_as"}} <strong>{{.SignedUser.Name}}</strong> </div> @@ -130,7 +130,7 @@ {{/* do not localize it, here it needs the fixed length (width) to make UI comfortable */}} {{if .IsAdmin}}<span class="navbar-profile-admin">admin</span>{{end}} <div class="menu user-menu"> - <div class="ui header"> + <div class="header"> {{ctx.Locale.Tr "signed_in_as"}} <strong>{{.SignedUser.Name}}</strong> </div> @@ -192,7 +192,7 @@ <span class="stopwatch-issue">{{.ActiveStopwatch.RepoSlug}}#{{.ActiveStopwatch.IssueIndex}}</span> </a> <div class="tw-flex tw-gap-1"> - <form class="stopwatch-commit" method="post" action="{{.ActiveStopwatch.IssueLink}}/times/stopwatch/toggle"> + <form class="stopwatch-commit form-fetch-action" method="post" action="{{.ActiveStopwatch.IssueLink}}/times/stopwatch/toggle"> {{.CsrfTokenHtml}} <button type="submit" @@ -200,7 +200,7 @@ data-tooltip-content="{{ctx.Locale.Tr "repo.issues.stop_tracking"}}" >{{svg "octicon-square-fill"}}</button> </form> - <form class="stopwatch-cancel" method="post" action="{{.ActiveStopwatch.IssueLink}}/times/stopwatch/cancel"> + <form class="stopwatch-cancel form-fetch-action" method="post" action="{{.ActiveStopwatch.IssueLink}}/times/stopwatch/cancel"> {{.CsrfTokenHtml}} <button type="submit" diff --git a/templates/base/head_script.tmpl b/templates/base/head_script.tmpl index c0c7235e3b..7c931e7404 100644 --- a/templates/base/head_script.tmpl +++ b/templates/base/head_script.tmpl @@ -1,5 +1,4 @@ -{{/* -==== DO NOT EDIT ==== +{{/* ==== DO NOT EDIT ==== If you are customizing Gitea, please do not change this file. If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly. */}} diff --git a/templates/base/modal_actions_confirm.tmpl b/templates/base/modal_actions_confirm.tmpl index 9f7eb4adf2..ec6942b9e7 100644 --- a/templates/base/modal_actions_confirm.tmpl +++ b/templates/base/modal_actions_confirm.tmpl @@ -1,5 +1,4 @@ -{{/* -Two buttons (negative, positive): +{{/* Two buttons (negative, positive): * ModalButtonTypes: "yes" (default) or "confirm" * ModalButtonCancelText * ModalButtonOkText diff --git a/templates/devtest/commit-sign-badge.tmpl b/templates/devtest/commit-sign-badge.tmpl new file mode 100644 index 0000000000..a6677c4495 --- /dev/null +++ b/templates/devtest/commit-sign-badge.tmpl @@ -0,0 +1,13 @@ +{{template "devtest/devtest-header"}} +<div class="page-content devtest ui container"> + <div> + <h1>Commit Sign Badges</h1> + {{range $commit := .MockCommits}} + <div class="flex-text-block tw-my-2"> + {{template "repo/commit_sign_badge" dict "Commit" $commit "CommitBaseLink" "/devtest/commit" "CommitSignVerification" $commit.Verification}} + {{template "repo/commit_sign_badge" dict "CommitSignVerification" $commit.Verification}} + </div> + {{end}} + </div> +</div> +{{template "devtest/devtest-footer"}} diff --git a/templates/devtest/repo-action-view.tmpl b/templates/devtest/repo-action-view.tmpl index 1fa71c0e5f..9c6bdf13da 100644 --- a/templates/devtest/repo-action-view.tmpl +++ b/templates/devtest/repo-action-view.tmpl @@ -1,30 +1,9 @@ {{template "base/head" .}} <div class="page-content"> - <div id="repo-action-view" - data-run-index="1" - data-job-index="2" - data-actions-url="{{AppSubUrl}}/devtest/actions-mock" - data-locale-approve="approve" - data-locale-cancel="cancel" - data-locale-rerun="re-run" - data-locale-rerun-all="re-run all" - data-locale-runs-scheduled="scheduled" - data-locale-runs-commit="commit" - data-locale-runs-pushed-by="pushed by" - data-locale-status-unknown="unknown" - data-locale-status-waiting="waiting" - data-locale-status-running="running" - data-locale-status-success="success" - data-locale-status-failure="failure" - data-locale-status-cancelled="cancelled" - data-locale-status-skipped="skipped" - data-locale-status-blocked="blocked" - data-locale-artifacts-title="artifacts" - data-locale-confirm-delete-artifact="confirm delete artifact" - data-locale-show-timestamps="show timestamps" - data-locale-show-log-seconds="show log seconds" - data-locale-show-full-screen="show full screen" - data-locale-download-logs="download logs" - ></div> + {{template "repo/actions/view_component" (dict + "RunIndex" 1 + "JobIndex" 2 + "ActionsURL" (print AppSubUrl "/devtest/actions-mock") + )}} </div> {{template "base/footer" .}} diff --git a/templates/org/create.tmpl b/templates/org/create.tmpl index 004cd9be80..7934d5b722 100644 --- a/templates/org/create.tmpl +++ b/templates/org/create.tmpl @@ -1,55 +1,51 @@ {{template "base/head" .}} <div role="main" aria-label="{{.Title}}" class="page-content organization new org"> - <div class="ui middle very relaxed page grid"> - <div class="column"> - <form class="ui form" action="{{.Link}}" method="post"> + <div class="ui container medium-width"> + {{template "base/alert" .}} + <h3 class="ui top attached header"> + {{ctx.Locale.Tr "new_org"}} + </h3> + <div class="ui attached segment"> + <form class="ui form left-right-form" action="{{.Link}}" method="post"> {{.CsrfTokenHtml}} - <h3 class="ui top attached header"> - {{ctx.Locale.Tr "new_org"}} - </h3> - <div class="ui attached segment"> - {{template "base/alert" .}} - <div class="inline required field {{if .Err_OrgName}}error{{end}}"> - <label for="org_name">{{ctx.Locale.Tr "org.org_name_holder"}}</label> - <input id="org_name" name="org_name" value="{{.org_name}}" autofocus required maxlength="40"> - <span class="help">{{ctx.Locale.Tr "org.org_name_helper"}}</span> - </div> + <div class="inline required field {{if .Err_OrgName}}error{{end}}"> + <label for="org_name">{{ctx.Locale.Tr "org.org_name_holder"}}</label> + <input id="org_name" name="org_name" value="{{.org_name}}" autofocus required maxlength="40"> + <span class="help">{{ctx.Locale.Tr "org.org_name_helper"}}</span> + </div> - <div class="inline field {{if .Err_OrgVisibility}}error{{end}}"> - <span class="inline required field"><label for="visibility">{{ctx.Locale.Tr "org.settings.visibility"}}</label></span> - <div class="inline-grouped-list"> - <div class="ui radio checkbox"> - <input class="enable-system-radio" name="visibility" type="radio" value="0" {{if .DefaultOrgVisibilityMode.IsPublic}}checked{{end}}> - <label>{{ctx.Locale.Tr "org.settings.visibility.public"}}</label> - </div> - <div class="ui radio checkbox"> - <input class="enable-system-radio" name="visibility" type="radio" value="1" {{if .DefaultOrgVisibilityMode.IsLimited}}checked{{end}}> - <label>{{ctx.Locale.Tr "org.settings.visibility.limited"}}</label> - </div> - <div class="ui radio checkbox"> - <input class="enable-system-radio" name="visibility" type="radio" value="2" {{if .DefaultOrgVisibilityMode.IsPrivate}}checked{{end}}> - <label>{{ctx.Locale.Tr "org.settings.visibility.private"}}</label> - </div> + <div class="inline field required {{if .Err_OrgVisibility}}error{{end}}"> + <label for="visibility">{{ctx.Locale.Tr "org.settings.visibility"}}</label> + <div class="inline-right"> + <div class="ui radio checkbox"> + <input class="enable-system-radio" name="visibility" type="radio" value="0" {{if .DefaultOrgVisibilityMode.IsPublic}}checked{{end}}> + <label>{{ctx.Locale.Tr "org.settings.visibility.public"}}</label> </div> - </div> - - <div class="inline field" id="permission_box"> - <label>{{ctx.Locale.Tr "org.settings.permission"}}</label> - <div class="inline-grouped-list"> - <div class="ui checkbox"> - <input type="checkbox" name="repo_admin_change_team_access" checked> - <label>{{ctx.Locale.Tr "org.settings.repoadminchangeteam"}}</label> - </div> + <div class="ui radio checkbox"> + <input class="enable-system-radio" name="visibility" type="radio" value="1" {{if .DefaultOrgVisibilityMode.IsLimited}}checked{{end}}> + <label>{{ctx.Locale.Tr "org.settings.visibility.limited"}}</label> + </div> + <div class="ui radio checkbox"> + <input class="enable-system-radio" name="visibility" type="radio" value="2" {{if .DefaultOrgVisibilityMode.IsPrivate}}checked{{end}}> + <label>{{ctx.Locale.Tr "org.settings.visibility.private"}}</label> </div> </div> + </div> - <div class="inline field"> - <label></label> - <button class="ui primary button"> - {{ctx.Locale.Tr "org.create_org"}} - </button> + <div class="inline field" id="permission_box"> + <label>{{ctx.Locale.Tr "org.settings.permission"}}</label> + <div class="ui checkbox"> + <input type="checkbox" name="repo_admin_change_team_access" checked> + <label>{{ctx.Locale.Tr "org.settings.repoadminchangeteam"}}</label> </div> </div> + + <div class="inline field"> + <label></label> + <button class="ui primary button"> + {{ctx.Locale.Tr "org.create_org"}} + </button> + </div> </form> </div> </div> diff --git a/templates/org/home.tmpl b/templates/org/home.tmpl index 4851b69979..db750692bf 100644 --- a/templates/org/home.tmpl +++ b/templates/org/home.tmpl @@ -5,8 +5,8 @@ <div class="ui container"> <div class="ui mobile reversed stackable grid"> <div class="ui {{if .ShowMemberAndTeamTab}}eleven wide{{end}} column"> - {{if .ProfileReadme}} - <div id="readme_profile" class="markup">{{.ProfileReadme}}</div> + {{if .ProfileReadmeContent}} + <div id="readme_profile" class="markup" data-profile-view-as-member="{{.IsViewingOrgAsMember}}">{{.ProfileReadmeContent}}</div> {{end}} {{template "shared/repo_search" .}} {{template "explore/repo_list" .}} @@ -24,6 +24,29 @@ </div> <div class="divider"></div> {{end}} + + {{if and .ShowMemberAndTeamTab .ShowOrgProfileReadmeSelector}} + <div class="tw-my-4"> + <div id="org-home-view-as-dropdown" class="ui dropdown jump"> + {{- $viewAsRole := Iif (.IsViewingOrgAsMember) (ctx.Locale.Tr "org.members.member") (ctx.Locale.Tr "settings.visibility.public") -}} + <span class="text">{{svg "octicon-eye"}} {{ctx.Locale.Tr "org.view_as_role" $viewAsRole}}</span> + {{svg "octicon-triangle-down" 14 "dropdown icon"}} + <div class="menu"> + {{/* TODO: does it really need to use CurrentURL with query parameters? Why not construct a new link with clear parameters */}} + <a href="?view_as=public" class="item {{if not .IsViewingOrgAsMember}}selected{{end}}"> + {{svg "octicon-check" 14 (Iif (not .IsViewingOrgAsMember) "" "tw-invisible")}} {{ctx.Locale.Tr "settings.visibility.public"}} + </a> + <a href="?view_as=member" class="item {{if .IsViewingOrgAsMember}}selected{{end}}"> + {{svg "octicon-check" 14 (Iif .IsViewingOrgAsMember "" "tw-invisible")}} {{ctx.Locale.Tr "org.members.member"}} + </a> + </div> + </div> + <div class="tw-my-2"> + {{if .IsViewingOrgAsMember}}{{ctx.Locale.Tr "org.view_as_member_hint"}}{{else}}{{ctx.Locale.Tr "org.view_as_public_hint"}}{{end}} + </div> + </div> + {{end}} + {{if .NumMembers}} <h4 class="ui top attached header tw-flex"> <strong class="tw-flex-1">{{ctx.Locale.Tr "org.members"}}</strong> diff --git a/templates/org/menu.tmpl b/templates/org/menu.tmpl index 29238f8d6b..4a8aee68a7 100644 --- a/templates/org/menu.tmpl +++ b/templates/org/menu.tmpl @@ -1,12 +1,12 @@ <div class="ui container"> <overflow-menu class="ui secondary pointing tabular borderless menu tw-mb-4"> <div class="overflow-menu-items"> - {{if .HasProfileReadme}} + {{if .HasOrgProfileReadme}} <a class="{{if .PageIsViewOverview}}active {{end}}item" href="{{$.Org.HomeLink}}"> {{svg "octicon-info"}} {{ctx.Locale.Tr "user.overview"}} </a> {{end}} - <a class="{{if .PageIsViewRepositories}}active {{end}}item" href="{{$.Org.HomeLink}}{{if .HasProfileReadme}}/-/repositories{{end}}"> + <a class="{{if .PageIsViewRepositories}}active {{end}}item" href="{{$.Org.HomeLink}}{{if .HasOrgProfileReadme}}/-/repositories{{end}}"> {{svg "octicon-repo"}} {{ctx.Locale.Tr "user.repositories"}} {{if .RepoCount}} <div class="ui small label">{{.RepoCount}}</div> diff --git a/templates/org/team/repositories.tmpl b/templates/org/team/repositories.tmpl index 92c3d724ba..4f4667ca8b 100644 --- a/templates/org/team/repositories.tmpl +++ b/templates/org/team/repositories.tmpl @@ -9,7 +9,7 @@ {{template "org/team/navbar" .}} {{$canAddRemove := and $.IsOrganizationOwner (not $.Team.IncludesAllRepositories)}} {{if $canAddRemove}} - <div class="ui attached segment tw-flex tw-flex-wrap tw-gap-2"> + <div class="ui top attached segment tw-flex tw-flex-wrap tw-gap-2"> <form class="ui form ignore-dirty tw-flex-1 tw-flex" action="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/action/repo/add" method="post"> {{.CsrfTokenHtml}} <div id="search-repo-box" data-uid="{{.Org.ID}}" class="ui search"> diff --git a/templates/package/content/nuget.tmpl b/templates/package/content/nuget.tmpl index 5bb98a86dd..4a4ea8ca43 100644 --- a/templates/package/content/nuget.tmpl +++ b/templates/package/content/nuget.tmpl @@ -35,11 +35,12 @@ </tr> </thead> <tbody> + {{$tooltipSearchInNuget := ctx.Locale.Tr "packages.search_in_external_registry" "nuget.org"}} {{range $framework, $dependencies := .PackageDescriptor.Metadata.Dependencies}} {{range $dependencies}} <tr> - <td>{{.ID}}</td> - <td>{{.Version}}</td> + <td>{{.ID}} <a target="_blank" rel="noreferrer" href="https://www.nuget.org/packages/{{.ID}}" data-tooltip-content="{{$tooltipSearchInNuget}}">{{svg "octicon-link-external"}}</a></td> + <td>{{.Version}} <a target="_blank" rel="noreferrer" href="https://www.nuget.org/packages/{{.ID}}/{{.Version}}" data-tooltip-content="{{$tooltipSearchInNuget}}">{{svg "octicon-link-external"}}</a></td> <td>{{$framework}}</td> </tr> {{end}} diff --git a/templates/package/metadata/arch.tmpl b/templates/package/metadata/arch.tmpl index 2aea036ec2..62308e4aa0 100644 --- a/templates/package/metadata/arch.tmpl +++ b/templates/package/metadata/arch.tmpl @@ -1,4 +1,4 @@ {{if eq .PackageDescriptor.Package.Type "arch"}} - {{range .PackageDescriptor.Metadata.Licenses}}<div class="item" title="{{$.locale.Tr "packages.details.license"}}">{{svg "octicon-law"}} {{.}}</div>{{end}} + {{range .PackageDescriptor.Metadata.Licenses}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law"}} {{.}}</div>{{end}} {{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} {{end}} diff --git a/templates/post-install.tmpl b/templates/post-install.tmpl index fb234008fb..fa10827295 100644 --- a/templates/post-install.tmpl +++ b/templates/post-install.tmpl @@ -1,23 +1,10 @@ {{template "base/head" .}} -<div role="main" aria-label="{{.Title}}" class="page-content install post-install"> - <div class="ui container"> - <div class="ui grid"> - <div class="sixteen wide column content"> - <div class="home"> - <div class="ui stackable middle very relaxed page grid"> - <div class="sixteen wide center aligned centered column"> - <div> - <img src="{{AssetUrlPrefix}}/img/loading.png" alt="{{ctx.Locale.Tr "loading"}}"> - </div> - </div> - </div> - <div class="ui stackable middle very relaxed page grid"> - <div class="sixteen wide center aligned centered column"> - <p><a id="goto-user-login" href="{{AppSubUrl}}/user/login">{{ctx.Locale.Tr "loading"}}</a></p> - </div> - </div> - </div> - </div> +<div role="main" aria-label="{{.Title}}" class="page-content install post-install tw-h-full"> + <div class="home tw-text-center tw-h-full tw-flex tw-flex-col tw-justify-center"><!-- the "home" class makes the links green --> + <!-- the "cup" has a handler, so move it a little leftward to make it visually in the center --> + <div class="tw-ml-[-30px]"><img width="160" src="{{AssetUrlPrefix}}/img/loading.png" alt="" aria-hidden="true"></div> + <div class="tw-my-[2em] tw-text-[18px]"> + <a id="goto-user-login" href="{{AppSubUrl}}/user/login">{{ctx.Locale.Tr "install.installing_desc"}}</a> </div> </div> </div> diff --git a/templates/repo/actions/view.tmpl b/templates/repo/actions/view.tmpl index f7b03608ee..bde579f882 100644 --- a/templates/repo/actions/view.tmpl +++ b/templates/repo/actions/view.tmpl @@ -2,33 +2,11 @@ <div class="page-content repository"> {{template "repo/header" .}} - <div id="repo-action-view" - data-run-index="{{.RunIndex}}" - data-job-index="{{.JobIndex}}" - data-actions-url="{{.ActionsURL}}" - data-locale-approve="{{ctx.Locale.Tr "repo.diff.review.approve"}}" - data-locale-cancel="{{ctx.Locale.Tr "cancel"}}" - data-locale-rerun="{{ctx.Locale.Tr "rerun"}}" - data-locale-rerun-all="{{ctx.Locale.Tr "rerun_all"}}" - data-locale-runs-scheduled="{{ctx.Locale.Tr "actions.runs.scheduled"}}" - data-locale-runs-commit="{{ctx.Locale.Tr "actions.runs.commit"}}" - data-locale-runs-pushed-by="{{ctx.Locale.Tr "actions.runs.pushed_by"}}" - data-locale-status-unknown="{{ctx.Locale.Tr "actions.status.unknown"}}" - data-locale-status-waiting="{{ctx.Locale.Tr "actions.status.waiting"}}" - data-locale-status-running="{{ctx.Locale.Tr "actions.status.running"}}" - data-locale-status-success="{{ctx.Locale.Tr "actions.status.success"}}" - data-locale-status-failure="{{ctx.Locale.Tr "actions.status.failure"}}" - data-locale-status-cancelled="{{ctx.Locale.Tr "actions.status.cancelled"}}" - data-locale-status-skipped="{{ctx.Locale.Tr "actions.status.skipped"}}" - data-locale-status-blocked="{{ctx.Locale.Tr "actions.status.blocked"}}" - data-locale-artifacts-title="{{ctx.Locale.Tr "artifacts"}}" - data-locale-confirm-delete-artifact="{{ctx.Locale.Tr "confirm_delete_artifact"}}" - data-locale-show-timestamps="{{ctx.Locale.Tr "show_timestamps"}}" - data-locale-show-log-seconds="{{ctx.Locale.Tr "show_log_seconds"}}" - data-locale-show-full-screen="{{ctx.Locale.Tr "show_full_screen"}}" - data-locale-download-logs="{{ctx.Locale.Tr "download_logs"}}" - > - </div> + {{template "repo/actions/view_component" (dict + "RunIndex" .RunIndex + "JobIndex" .JobIndex + "ActionsURL" .ActionsURL + )}} </div> {{template "base/footer" .}} diff --git a/templates/repo/actions/view_component.tmpl b/templates/repo/actions/view_component.tmpl new file mode 100644 index 0000000000..8d1de41f70 --- /dev/null +++ b/templates/repo/actions/view_component.tmpl @@ -0,0 +1,30 @@ +<div id="repo-action-view" + data-run-index="{{.RunIndex}}" + data-job-index="{{.JobIndex}}" + data-actions-url="{{.ActionsURL}}" + + data-locale-approve="{{ctx.Locale.Tr "repo.diff.review.approve"}}" + data-locale-cancel="{{ctx.Locale.Tr "cancel"}}" + data-locale-rerun="{{ctx.Locale.Tr "rerun"}}" + data-locale-rerun-all="{{ctx.Locale.Tr "rerun_all"}}" + data-locale-runs-scheduled="{{ctx.Locale.Tr "actions.runs.scheduled"}}" + data-locale-runs-commit="{{ctx.Locale.Tr "actions.runs.commit"}}" + data-locale-runs-pushed-by="{{ctx.Locale.Tr "actions.runs.pushed_by"}}" + data-locale-status-unknown="{{ctx.Locale.Tr "actions.status.unknown"}}" + data-locale-status-waiting="{{ctx.Locale.Tr "actions.status.waiting"}}" + data-locale-status-running="{{ctx.Locale.Tr "actions.status.running"}}" + data-locale-status-success="{{ctx.Locale.Tr "actions.status.success"}}" + data-locale-status-failure="{{ctx.Locale.Tr "actions.status.failure"}}" + data-locale-status-cancelled="{{ctx.Locale.Tr "actions.status.cancelled"}}" + data-locale-status-skipped="{{ctx.Locale.Tr "actions.status.skipped"}}" + data-locale-status-blocked="{{ctx.Locale.Tr "actions.status.blocked"}}" + data-locale-artifacts-title="{{ctx.Locale.Tr "artifacts"}}" + data-locale-confirm-delete-artifact="{{ctx.Locale.Tr "confirm_delete_artifact"}}" + data-locale-show-timestamps="{{ctx.Locale.Tr "show_timestamps"}}" + data-locale-show-log-seconds="{{ctx.Locale.Tr "show_log_seconds"}}" + data-locale-show-full-screen="{{ctx.Locale.Tr "show_full_screen"}}" + data-locale-download-logs="{{ctx.Locale.Tr "download_logs"}}" + data-locale-logs-always-auto-scroll="{{ctx.Locale.Tr "actions.logs.always_auto_scroll"}}" + data-locale-logs-always-expand-running="{{ctx.Locale.Tr "actions.logs.always_expand_running"}}" +> +</div> diff --git a/templates/repo/actions/workflow_dispatch.tmpl b/templates/repo/actions/workflow_dispatch.tmpl index 21f3ef2077..55fe122419 100644 --- a/templates/repo/actions/workflow_dispatch.tmpl +++ b/templates/repo/actions/workflow_dispatch.tmpl @@ -11,7 +11,7 @@ <label>{{ctx.Locale.Tr "actions.workflow.from_ref"}}:</label> </span> <div class="ui inline field dropdown button select-branch branch-selector-dropdown ellipsis-items-nowrap"> - <input type="hidden" name="ref" value="refs/heads/{{index .Branches 0}}"> + <input type="hidden" name="ref" hx-sync="this:replace" hx-target="#runWorkflowDispatchModalInputs" hx-swap="innerHTML" hx-get="{{$.Link}}/workflow-dispatch-inputs?workflow={{$.CurWorkflow}}" hx-trigger="change" value="refs/heads/{{index .Branches 0}}"> {{svg "octicon-git-branch" 14}} <div class="default text">{{index .Branches 0}}</div> {{svg "octicon-triangle-down" 14 "dropdown icon"}} @@ -49,30 +49,9 @@ <div class="divider"></div> - {{range $item := .WorkflowDispatchConfig.Inputs}} - <div class="ui field {{if .Required}}required{{end}}"> - {{if eq .Type "choice"}} - <label>{{.Description}}:</label> - <select class="ui selection type dropdown" name="{{.Name}}"> - {{range .Options}} - <option value="{{.}}" {{if eq $item.Default .}}selected{{end}} >{{.}}</option> - {{end}} - </select> - {{else if eq .Type "boolean"}} - <div class="ui inline checkbox"> - <label>{{.Description}}</label> - <input type="checkbox" name="{{.Name}}" {{if eq .Default "true"}}checked{{end}}> - </div> - {{else if eq .Type "number"}} - <label>{{.Description}}:</label> - <input name="{{.Name}}" value="{{.Default}}" {{if .Required}}required{{end}}> - {{else}} - <label>{{.Description}}:</label> - <input name="{{.Name}}" value="{{.Default}}" {{if .Required}}required{{end}}> - {{end}} + <div id="runWorkflowDispatchModalInputs"> + {{template "repo/actions/workflow_dispatch_inputs" .}} </div> - {{end}} - <button class="ui tiny primary button" type="submit">Submit</button> </form> </div> </div> diff --git a/templates/repo/actions/workflow_dispatch_inputs.tmpl b/templates/repo/actions/workflow_dispatch_inputs.tmpl new file mode 100644 index 0000000000..8b8292af1d --- /dev/null +++ b/templates/repo/actions/workflow_dispatch_inputs.tmpl @@ -0,0 +1,45 @@ +{{if not .WorkflowDispatchConfig}} + <div class="ui error message tw-block">{{/* using "ui message" in "ui form" needs to force to display */}} + {{if not .CurWorkflowExists}} + {{ctx.Locale.Tr "actions.workflow.not_found" $.CurWorkflow}} + {{else}} + {{ctx.Locale.Tr "actions.workflow.has_no_workflow_dispatch" $.CurWorkflow}} + {{end}} + </div> +{{else}} + {{range $item := .WorkflowDispatchConfig.Inputs}} + <div class="ui field {{if .Required}}required{{end}}"> + {{if eq .Type "choice"}} + <label>{{or .Description .Name}}:</label> + {{/* htmx won't initialize the fomantic dropdown, so it is a standard "select" input */}} + <select class="ui selection dropdown" name="{{.Name}}"> + {{range .Options}} + <option value="{{.}}" {{if eq $item.Default .}}selected{{end}}>{{.}}</option> + {{end}} + </select> + {{else if eq .Type "boolean"}} + {{/* htmx doesn't trigger our JS code to attach fomantic label to checkbox, so here we use standard checkbox */}} + <label class="tw-flex flex-text-inline"> + <input type="checkbox" name="{{.Name}}" {{if eq .Default "true"}}checked{{end}}> + {{or .Description .Name}} + </label> + {{else if eq .Type "number"}} + <label>{{or .Description .Name}}:</label> + <input name="{{.Name}}" value="{{.Default}}" {{if .Required}}required{{end}}> + {{else}} + <label>{{or .Description .Name}}:</label> + <input name="{{.Name}}" value="{{.Default}}" {{if .Required}}required{{end}}> + {{end}} + </div> + {{end}} + <div class="ui field"> + <button class="ui tiny primary button" type="submit">{{ctx.Locale.Tr "actions.workflow.run"}}</button> + </div> +{{end}} +{{range .workflows}} + {{if and .ErrMsg (eq .Entry.Name $.CurWorkflow)}} + <div class="ui field"> + <div>{{svg "octicon-alert" 16 "text red"}} {{.ErrMsg}}</div> + </div> + {{end}} +{{end}} diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl index 5484024ff8..cb504e2b75 100644 --- a/templates/repo/branch/list.tmpl +++ b/templates/repo/branch/list.tmpl @@ -29,7 +29,8 @@ </div> <p class="info tw-flex tw-items-center tw-my-1">{{svg "octicon-git-commit" 16 "tw-mr-1"}}<a href="{{.RepoLink}}/commit/{{PathEscape .DefaultBranchBranch.DBBranch.CommitID}}">{{ShortSha .DefaultBranchBranch.DBBranch.CommitID}}</a> · <span class="commit-message">{{ctx.RenderUtils.RenderCommitMessage .DefaultBranchBranch.DBBranch.CommitMessage (.Repository.ComposeMetas ctx)}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{DateUtils.TimeSince .DefaultBranchBranch.DBBranch.CommitTime}}{{if .DefaultBranchBranch.DBBranch.Pusher}} {{template "shared/user/avatarlink" dict "user" .DefaultBranchBranch.DBBranch.Pusher}}{{template "shared/user/namelink" .DefaultBranchBranch.DBBranch.Pusher}}{{end}}</p> </td> - <td class="right aligned middle aligned overflow-visible"> + {{/* FIXME: here and below, the tw-overflow-visible is not quite right but it is still needed the moment: to show the important buttons when the width is narrow */}} + <td class="right aligned middle aligned tw-overflow-visible"> {{if and $.IsWriter (not $.Repository.IsArchived) (not .IsDeleted)}} <button class="btn interact-bg show-create-branch-modal tw-p-2" data-modal="#create-branch-modal" @@ -148,7 +149,8 @@ {{end}} {{end}} </td> - <td class="three wide right aligned overflow-visible"> + {{/* FIXME: here and above, the tw-overflow-visible is not quite right */}} + <td class="three wide right aligned tw-overflow-visible"> {{if and $.IsWriter (not $.Repository.IsArchived) (not .DBBranch.IsDeleted)}} <button class="btn interact-bg tw-p-2 show-modal show-create-branch-modal" data-branch-from="{{.DBBranch.Name}}" diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl index 71f77154fb..bc63db9b62 100644 --- a/templates/repo/commit_page.tmpl +++ b/templates/repo/commit_page.tmpl @@ -1,23 +1,9 @@ {{template "base/head" .}} +{{$commitLinkBase := print $.RepoLink (Iif $.PageIsWiki "/wiki" "") "/commit"}} <div role="main" aria-label="{{.Title}}" class="page-content repository diff"> {{template "repo/header" .}} <div class="ui container fluid padded"> - {{$class := ""}} - {{if .Commit.Signature}} - {{$class = (print $class " isSigned")}} - {{if .Verification.Verified}} - {{if eq .Verification.TrustStatus "trusted"}} - {{$class = (print $class " isVerified")}} - {{else if eq .Verification.TrustStatus "untrusted"}} - {{$class = (print $class " isVerifiedUntrusted")}} - {{else}} - {{$class = (print $class " isVerifiedUnmatched")}} - {{end}} - {{else if .Verification.Warning}} - {{$class = (print $class " isWarning")}} - {{end}} - {{end}} - <div class="ui top attached header clearing segment tw-relative commit-header {{$class}}"> + <div class="ui top attached header clearing segment tw-relative commit-header"> <div class="tw-flex tw-mb-4 tw-gap-1"> <h3 class="tw-mb-0 tw-flex-1"><span class="commit-summary" title="{{.Commit.Summary}}">{{ctx.RenderUtils.RenderCommitMessage .Commit.Message ($.Repository.ComposeMetas ctx)}}</span>{{template "repo/commit_statuses" dict "Status" .CommitStatus "Statuses" .CommitStatuses}}</h3> {{if not $.PageIsWiki}} @@ -30,7 +16,7 @@ {{ctx.Locale.Tr "repo.commit.operations"}} {{svg "octicon-triangle-down" 14 "dropdown icon"}} <div class="menu"> - <div class="ui header">{{ctx.Locale.Tr "repo.commit.operations"}}</div> + <div class="header">{{ctx.Locale.Tr "repo.commit.operations"}}</div> <div class="divider"></div> <div class="item show-create-branch-modal" data-content="{{ctx.Locale.Tr "repo.branch.new_branch_from" (.CommitID)}}" {{/* used by the form */}} @@ -142,125 +128,59 @@ {{end}} {{template "repo/commit_load_branches_and_tags" .}} </div> - <div class="ui{{if not .Commit.Signature}} bottom{{end}} attached segment tw-flex tw-items-center tw-justify-between tw-py-1 commit-header-row tw-flex-wrap {{$class}}"> - <div class="tw-flex tw-items-center author"> - {{if .Author}} - {{ctx.AvatarUtils.Avatar .Author 28 "tw-mr-2"}} - {{if .Author.FullName}} - <a href="{{.Author.HomeLink}}"><strong>{{.Author.FullName}}</strong></a> - {{else}} - <a href="{{.Author.HomeLink}}"><strong>{{.Commit.Author.Name}}</strong></a> - {{end}} + + <div class="ui bottom attached segment flex-text-block tw-flex-wrap"> + <div class="flex-text-inline"> + {{if .Author}} + {{ctx.AvatarUtils.Avatar .Author 20}} + {{if .Author.FullName}} + <a href="{{.Author.HomeLink}}"><strong>{{.Author.FullName}}</strong></a> {{else}} - {{ctx.AvatarUtils.AvatarByEmail .Commit.Author.Email .Commit.Author.Email 28 "tw-mr-2"}} - <strong>{{.Commit.Author.Name}}</strong> + <a href="{{.Author.HomeLink}}"><strong>{{.Commit.Author.Name}}</strong></a> {{end}} - <span class="text grey tw-ml-2" id="authored-time">{{DateUtils.TimeSince .Commit.Author.When}}</span> - {{if or (ne .Commit.Committer.Name .Commit.Author.Name) (ne .Commit.Committer.Email .Commit.Author.Email)}} - <span class="text grey tw-mx-2">{{ctx.Locale.Tr "repo.diff.committed_by"}}</span> - {{if ne .Verification.CommittingUser.ID 0}} - {{ctx.AvatarUtils.Avatar .Verification.CommittingUser 28 "tw-mx-2"}} - <a href="{{.Verification.CommittingUser.HomeLink}}"><strong>{{.Commit.Committer.Name}}</strong></a> - {{else}} - {{ctx.AvatarUtils.AvatarByEmail .Commit.Committer.Email .Commit.Committer.Name 28 "tw-mr-2"}} - <strong>{{.Commit.Committer.Name}}</strong> - {{end}} - {{end}} - </div> - <div class="tw-flex tw-items-center"> - {{if .Parents}} - <div> - <span>{{ctx.Locale.Tr "repo.diff.parent"}}</span> - {{range .Parents}} - {{if $.PageIsWiki}} - <a class="ui primary sha label" href="{{$.RepoLink}}/wiki/commit/{{PathEscape .}}">{{ShortSha .}}</a> - {{else}} - <a class="ui primary sha label" href="{{$.RepoLink}}/commit/{{PathEscape .}}">{{ShortSha .}}</a> - {{end}} - {{end}} - </div> - {{end}} - <div class="item"> - <span>{{ctx.Locale.Tr "repo.diff.commit"}}</span> - <span class="ui primary sha label">{{ShortSha .CommitID}}</span> - </div> - </div> - </div> - {{if .Commit.Signature}} - <div class="ui bottom attached message tw-text-left tw-flex tw-items-center tw-justify-between commit-header-row tw-flex-wrap tw-mb-0 {{$class}}"> - <div class="tw-flex tw-items-center"> - {{if .Verification.Verified}} - {{if ne .Verification.SigningUser.ID 0}} - {{svg "gitea-lock" 16 "tw-mr-2"}} - {{if eq .Verification.TrustStatus "trusted"}} - <span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.signed_by"}}:</span> - {{else if eq .Verification.TrustStatus "untrusted"}} - <span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.signed_by_untrusted_user"}}:</span> - {{else}} - <span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.signed_by_untrusted_user_unmatched"}}:</span> - {{end}} - {{ctx.AvatarUtils.Avatar .Verification.SigningUser 28 "tw-mr-2"}} - <a href="{{.Verification.SigningUser.HomeLink}}"><strong>{{.Verification.SigningUser.GetDisplayName}}</strong></a> - {{else}} - <span title="{{ctx.Locale.Tr "gpg.default_key"}}">{{svg "gitea-lock-cog" 16 "tw-mr-2"}}</span> - <span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.signed_by"}}:</span> - {{ctx.AvatarUtils.AvatarByEmail .Verification.SigningEmail "" 28 "tw-mr-2"}} - <strong>{{.Verification.SigningUser.GetDisplayName}}</strong> - {{end}} + {{else}} + {{ctx.AvatarUtils.AvatarByEmail .Commit.Author.Email .Commit.Author.Email 20}} + <strong>{{.Commit.Author.Name}}</strong> + {{end}} + </div> + + <span class="text grey">{{DateUtils.TimeSince .Commit.Author.When}}</span> + + <div class="flex-text-inline"> + {{if or (ne .Commit.Committer.Name .Commit.Author.Name) (ne .Commit.Committer.Email .Commit.Author.Email)}} + <span class="text grey">{{ctx.Locale.Tr "repo.diff.committed_by"}}</span> + {{if ne .Verification.CommittingUser.ID 0}} + {{ctx.AvatarUtils.Avatar .Verification.CommittingUser 20}} + <a href="{{.Verification.CommittingUser.HomeLink}}"><strong>{{.Commit.Committer.Name}}</strong></a> {{else}} - {{svg "gitea-unlock" 16 "tw-mr-2"}} - <span class="ui text">{{ctx.Locale.Tr .Verification.Reason}}</span> + {{ctx.AvatarUtils.AvatarByEmail .Commit.Committer.Email .Commit.Committer.Name 20}} + <strong>{{.Commit.Committer.Name}}</strong> {{end}} - </div> - <div class="tw-flex tw-items-center"> - {{if .Verification.Verified}} - {{if ne .Verification.SigningUser.ID 0}} - {{svg "octicon-verified" 16 "tw-mr-2"}} - {{if .Verification.SigningSSHKey}} - <span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span> - {{.Verification.SigningSSHKey.Fingerprint}} - {{else}} - <span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span> - {{.Verification.SigningKey.PaddedKeyID}} - {{end}} - {{else}} - {{svg "octicon-unverified" 16 "tw-mr-2"}} - {{if .Verification.SigningSSHKey}} - <span class="ui text tw-mr-2" data-tooltip-content="{{ctx.Locale.Tr "gpg.default_key"}}">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span> - {{.Verification.SigningSSHKey.Fingerprint}} - {{else}} - <span class="ui text tw-mr-2" data-tooltip-content="{{ctx.Locale.Tr "gpg.default_key"}}">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span> - {{.Verification.SigningKey.PaddedKeyID}} - {{end}} - {{end}} - {{else if .Verification.Warning}} - {{svg "octicon-unverified" 16 "tw-mr-2"}} - {{if .Verification.SigningSSHKey}} - <span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span> - {{.Verification.SigningSSHKey.Fingerprint}} - {{else}} - <span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span> - {{.Verification.SigningKey.PaddedKeyID}} - {{end}} - {{else}} - {{if .Verification.SigningKey}} - {{if ne .Verification.SigningKey.KeyID ""}} - {{svg "octicon-verified" 16 "tw-mr-2"}} - <span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span> - {{.Verification.SigningKey.PaddedKeyID}} - {{end}} - {{end}} - {{if .Verification.SigningSSHKey}} - {{if ne .Verification.SigningSSHKey.Fingerprint ""}} - {{svg "octicon-verified" 16 "tw-mr-2"}} - <span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span> - {{.Verification.SigningSSHKey.Fingerprint}} - {{end}} + {{end}} + </div> + + {{if .Verification}} + {{template "repo/commit_sign_badge" dict "CommitSignVerification" .Verification}} + {{end}} + + <div class="tw-flex-1"></div> + + <div class="flex-text-inline tw-gap-5"> + {{if .Parents}} + <div class="flex-text-inline"> + <span>{{ctx.Locale.Tr "repo.diff.parent"}}</span> + {{range .Parents}} + <a class="ui label commit-id-short" href="{{$commitLinkBase}}/{{PathEscape .}}">{{ShortSha .}}</a> {{end}} - {{end}} + </div> + {{end}} + <div class="flex-text-inline"> + <span>{{ctx.Locale.Tr "repo.diff.commit"}}</span> + <a class="ui label commit-id-short" href="{{$commitLinkBase}}/{{PathEscape .CommitID}}">{{ShortSha .CommitID}}</a> </div> </div> - {{end}} + </div> + {{if .NoteRendered}} <div class="ui top attached header segment git-notes"> {{svg "octicon-note" 16 "tw-mr-2"}} @@ -276,12 +196,13 @@ {{else}} <strong>{{.NoteCommit.Author.Name}}</strong> {{end}} - <span class="text grey" id="note-authored-time">{{DateUtils.TimeSince .NoteCommit.Author.When}}</span> + <span class="text grey">{{DateUtils.TimeSince .NoteCommit.Author.When}}</span> </div> <div class="ui bottom attached info segment git-notes"> <pre class="commit-body">{{.NoteRendered | SanitizeHTML}}</pre> </div> {{end}} + {{template "repo/diff/box" .}} </div> </div> diff --git a/templates/repo/commit_sign_badge.tmpl b/templates/repo/commit_sign_badge.tmpl new file mode 100644 index 0000000000..aa68e9dd23 --- /dev/null +++ b/templates/repo/commit_sign_badge.tmpl @@ -0,0 +1,78 @@ +{{/* Template attributes: +* Commit +* CommitBaseLink +* CommitSignVerification +If you'd like to modify this template, you could test it on the devtest page. +ATTENTION: this template could be re-rendered many times (on the graph and commit list page), +so this template should be kept as small as possbile, DO NOT put large components like modal/dialog into it. +*/}} +{{- $commit := $.Commit -}} +{{- $commitBaseLink := $.CommitBaseLink -}} +{{- $verification := $.CommitSignVerification -}} + +{{- $extraClass := "" -}} +{{- $verified := false -}} +{{- $signingUser := NIL -}} +{{- $signingEmail := "" -}} +{{- $msgReasonPrefix := "" -}} +{{- $msgReason := "" -}} +{{- $msgSigningKey := "" -}} + +{{- if $verification -}} + {{- $signingUser = $verification.SigningUser -}} + {{- $signingEmail = $verification.SigningEmail -}} + {{- $extraClass = print $extraClass " commit-is-signed" -}} + {{- if $verification.Verified -}} + {{- /* reason is "{name} / {key-id}" */ -}} + {{- $msgReason = $verification.Reason -}} + {{- $verified = true -}} + {{- if eq $verification.TrustStatus "trusted" -}} + {{- $extraClass = print $extraClass " sign-trusted" -}} + {{- else if eq $verification.TrustStatus "untrusted" -}} + {{- $extraClass = print $extraClass " sign-untrusted" -}} + {{- $msgReasonPrefix = ctx.Locale.Tr "repo.commits.signed_by_untrusted_user" -}} + {{- else -}} + {{- $extraClass = print $extraClass " sign-unmatched" -}} + {{- $msgReasonPrefix = ctx.Locale.Tr "repo.commits.signed_by_untrusted_user_unmatched" -}} + {{- end -}} + {{- else -}} + {{- if $verification.Warning -}} + {{- $extraClass = print $extraClass " sign-warning" -}} + {{- end -}} + {{- $msgReason = ctx.Locale.Tr $verification.Reason -}}{{- /* dirty part: it is the translation key ..... */ -}} + {{- end -}} + + {{- if $msgReasonPrefix -}} + {{- $msgReason = print $msgReasonPrefix ": " $msgReason -}} + {{- end -}} + + {{- if $verification.SigningSSHKey -}} + {{- $msgSigningKey = print (ctx.Locale.Tr "repo.commits.ssh_key_fingerprint") ": " $verification.SigningSSHKey.Fingerprint -}} + {{- else if $verification.SigningKey -}} + {{- $msgSigningKey = print (ctx.Locale.Tr "repo.commits.gpg_key_id") ": " $verification.SigningKey.PaddedKeyID -}} + {{- end -}} +{{- end -}} + +{{- if $commit -}} +<a {{if $commitBaseLink}}href="{{$commitBaseLink}}/{{$commit.ID}}"{{end}} class="ui label commit-id-short {{$extraClass}}" rel="nofollow"> + {{- ShortSha $commit.ID.String -}} +{{- end -}} + <span class="ui label commit-sign-badge {{$extraClass}}"> + {{- if $verified -}} + {{- if and $signingUser $signingUser.ID -}} + <span data-tooltip-content="{{$msgReason}}">{{svg "gitea-lock"}}</span> + <span data-tooltip-content="{{$msgSigningKey}}">{{ctx.AvatarUtils.Avatar $signingUser 16}}</span> + {{- else -}} + <span data-tooltip-content="{{$msgReason}}">{{svg "gitea-lock-cog"}}</span> + <span data-tooltip-content="{{$msgSigningKey}}">{{ctx.AvatarUtils.AvatarByEmail $signingEmail "" 16}}</span> + {{- end -}} + {{- else -}} + <span data-tooltip-content="{{$msgReason}}">{{svg "gitea-unlock"}}</span> + {{- end -}} + </span> + +{{- if $commit -}} +</a> +{{- end -}} + +{{- /* This template should be kept as small as possbile, DO NOT put large components like modal/dialog into it. */ -}} diff --git a/templates/repo/commit_statuses.tmpl b/templates/repo/commit_statuses.tmpl index f451ac06a1..a6f75584a3 100644 --- a/templates/repo/commit_statuses.tmpl +++ b/templates/repo/commit_statuses.tmpl @@ -1,10 +1,10 @@ {{if .Statuses}} {{if and (eq (len .Statuses) 1) .Status.TargetURL}} - <a class="tw-align-middle {{.AdditionalClasses}} tw-no-underline" data-tippy="commit-statuses" href="{{.Status.TargetURL}}"> + <a class="flex-text-inline tw-no-underline {{.AdditionalClasses}}" data-tippy="commit-statuses" href="{{.Status.TargetURL}}"> {{template "repo/commit_status" .Status}} </a> {{else}} - <span class="tw-align-middle {{.AdditionalClasses}}" data-tippy="commit-statuses" tabindex="0"> + <span class="flex-text-inline {{.AdditionalClasses}}" data-tippy="commit-statuses" tabindex="0"> {{template "repo/commit_status" .Status}} </span> {{end}} diff --git a/templates/repo/commits_list.tmpl b/templates/repo/commits_list.tmpl index 50b754cc23..329dc45149 100644 --- a/templates/repo/commits_list.tmpl +++ b/templates/repo/commits_list.tmpl @@ -28,33 +28,15 @@ </div> </td> <td class="sha"> - {{$class := "ui sha label"}} - {{if .Signature}} - {{$class = (print $class " isSigned")}} - {{if .Verification.Verified}} - {{if eq .Verification.TrustStatus "trusted"}} - {{$class = (print $class " isVerified")}} - {{else if eq .Verification.TrustStatus "untrusted"}} - {{$class = (print $class " isVerifiedUntrusted")}} - {{else}} - {{$class = (print $class " isVerifiedUnmatched")}} - {{end}} - {{else if .Verification.Warning}} - {{$class = (print $class " isWarning")}} - {{end}} - {{end}} - {{$commitShaLink := ""}} + {{$commitBaseLink := ""}} {{if $.PageIsWiki}} - {{$commitShaLink = (printf "%s/wiki/commit/%s" $commitRepoLink (PathEscape .ID.String))}} + {{$commitBaseLink = printf "%s/wiki/commit" $commitRepoLink}} {{else if $.PageIsPullCommits}} - {{$commitShaLink = (printf "%s/pulls/%d/commits/%s" $commitRepoLink $.Issue.Index (PathEscape .ID.String))}} + {{$commitBaseLink = printf "%s/pulls/%d/commits" $commitRepoLink $.Issue.Index}} {{else if $.Reponame}} - {{$commitShaLink = (printf "%s/commit/%s" $commitRepoLink (PathEscape .ID.String))}} + {{$commitBaseLink = printf "%s/commit" $commitRepoLink}} {{end}} - <a {{if $commitShaLink}}href="{{$commitShaLink}}"{{end}} class="{{$class}}"> - <span class="shortsha">{{ShortSha .ID.String}}</span> - {{if .Signature}}{{template "repo/shabox_badge" dict "root" $ "verification" .Verification}}{{end}} - </a> + {{template "repo/commit_sign_badge" dict "Commit" . "CommitBaseLink" $commitBaseLink "CommitSignVerification" .Verification}} </td> <td class="message"> <span class="message-wrapper"> diff --git a/templates/repo/commits_list_small.tmpl b/templates/repo/commits_list_small.tmpl index 0657eaba1d..2acf7c58b8 100644 --- a/templates/repo/commits_list_small.tmpl +++ b/templates/repo/commits_list_small.tmpl @@ -3,7 +3,7 @@ {{range .comment.Commits}} {{$tag := printf "%s-%d" $.comment.HashTag $index}} {{$index = Eval $index "+" 1}} - <div class="singular-commit" id="{{$tag}}"> + <div class="flex-text-block" id="{{$tag}}">{{/*singular-commit*/}} <span class="badge badge-commit">{{svg "octicon-git-commit"}}</span> {{if .User}} <a class="avatar" href="{{.User.HomeLink}}">{{ctx.AvatarUtils.Avatar .User 20}}</a> @@ -11,7 +11,8 @@ {{ctx.AvatarUtils.AvatarByEmail .Author.Email .Author.Name 20}} {{end}} - {{$commitLink:= printf "%s/commit/%s" $.comment.Issue.PullRequest.BaseRepo.Link (PathEscape .ID.String)}} + {{$commitBaseLink := printf "%s/commit" $.comment.Issue.PullRequest.BaseRepo.Link}} + {{$commitLink:= printf "%s/%s" $commitBaseLink (PathEscape .ID.String)}} <span class="tw-flex-1 tw-font-mono gt-ellipsis" title="{{.Summary}}"> {{- ctx.RenderUtils.RenderCommitMessageLinkSubject .Message $commitLink ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx) -}} @@ -21,29 +22,9 @@ <button class="ui button ellipsis-button show-panel toggle" data-panel="[data-singular-commit-body-for='{{$tag}}']">...</button> {{end}} - <span class="shabox tw-flex tw-items-center"> + <span class="tw-flex tw-items-center tw-gap-2"> {{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}} - {{$class := "ui sha label"}} - {{if .Signature}} - {{$class = (print $class " isSigned")}} - {{if .Verification.Verified}} - {{if eq .Verification.TrustStatus "trusted"}} - {{$class = (print $class " isVerified")}} - {{else if eq .Verification.TrustStatus "untrusted"}} - {{$class = (print $class " isVerifiedUntrusted")}} - {{else}} - {{$class = (print $class " isVerifiedUnmatched")}} - {{end}} - {{else if .Verification.Warning}} - {{$class = (print $class " isWarning")}} - {{end}} - {{end}} - <a href="{{$commitLink}}" rel="nofollow" class="tw-ml-2 {{$class}}"> - <span class="shortsha">{{ShortSha .ID.String}}</span> - {{if .Signature}} - {{template "repo/shabox_badge" dict "root" $.root "verification" .Verification}} - {{end}} - </a> + {{template "repo/commit_sign_badge" dict "Commit" . "CommitBaseLink" $commitBaseLink "CommitSignVerification" .Verification}} </span> </div> {{if IsMultilineCommitMessage .Message}} diff --git a/templates/repo/create.tmpl b/templates/repo/create.tmpl index 2e1de244ea..ad308c857c 100644 --- a/templates/repo/create.tmpl +++ b/templates/repo/create.tmpl @@ -1,219 +1,221 @@ {{template "base/head" .}} -<div role="main" aria-label="{{.Title}}" class="page-content repository new repo"> - <div class="ui middle very relaxed page one column grid"> - <div class="column"> - <form class="ui form" action="{{.Link}}" method="post"> - {{.CsrfTokenHtml}} - <h3 class="ui top attached header"> - {{ctx.Locale.Tr "new_repo"}} - </h3> - <div class="ui attached segment"> - {{template "base/alert" .}} - {{template "repo/create_helper" .}} +<div role="main" aria-label="{{.Title}}" class="page-content repository new-repo"> + <div class="ui container medium-width"> + <h3 class="ui top attached header"> + {{ctx.Locale.Tr "new_repo"}} + </h3> + <div class="ui attached segment"> + {{template "base/alert" .}} + {{template "repo/create_helper" .}} - {{if not .CanCreateRepo}} - <div class="ui negative message"> - <p>{{ctx.Locale.TrN .MaxCreationLimit "repo.form.reach_limit_of_creation_1" "repo.form.reach_limit_of_creation_n" .MaxCreationLimit}}</p> - </div> - {{end}} - <div class="inline required field {{if .Err_Owner}}error{{end}}"> - <label>{{ctx.Locale.Tr "repo.owner"}}</label> - <div class="ui selection owner dropdown"> - <input type="hidden" id="uid" name="uid" value="{{.ContextUser.ID}}" required> - <span class="text truncated-item-container" title="{{.ContextUser.Name}}"> - {{ctx.AvatarUtils.Avatar .ContextUser 28 "mini"}} - <span class="truncated-item-name">{{.ContextUser.ShortName 40}}</span> - </span> - {{svg "octicon-triangle-down" 14 "dropdown icon"}} - <div class="menu"> - <div class="item truncated-item-container" data-value="{{.SignedUser.ID}}" title="{{.SignedUser.Name}}"> - {{ctx.AvatarUtils.Avatar .SignedUser 28 "mini"}} - <span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span> + {{if not .CanCreateRepo}} + <div class="ui negative message"> + <p>{{ctx.Locale.TrN .MaxCreationLimit "repo.form.reach_limit_of_creation_1" "repo.form.reach_limit_of_creation_n" .MaxCreationLimit}}</p> + </div> + {{end}} + <form class="ui form left-right-form new-repo-form" action="{{.Link}}" method="post"> + {{.CsrfTokenHtml}} + <div class="inline required field {{if .Err_Owner}}error{{end}}"> + <label>{{ctx.Locale.Tr "repo.owner"}}</label> + <div class="ui selection owner dropdown"> + <input type="hidden" id="uid" name="uid" value="{{.ContextUser.ID}}" required> + <span class="text truncated-item-container" title="{{.ContextUser.Name}}"> + {{ctx.AvatarUtils.Avatar .ContextUser 28 "mini"}} + <span class="truncated-item-name">{{.ContextUser.ShortName 40}}</span> + </span> + {{svg "octicon-triangle-down" 14 "dropdown icon"}} + <div class="menu"> + <div class="item truncated-item-container" data-value="{{.SignedUser.ID}}" title="{{.SignedUser.Name}}"> + {{ctx.AvatarUtils.Avatar .SignedUser 28 "mini"}} + <span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span> + </div> + {{range .Orgs}} + <div class="item truncated-item-container" data-value="{{.ID}}" title="{{.Name}}"> + {{ctx.AvatarUtils.Avatar . 28 "mini"}} + <span class="truncated-item-name">{{.ShortName 40}}</span> </div> - {{range .Orgs}} - <div class="item truncated-item-container" data-value="{{.ID}}" title="{{.Name}}"> - {{ctx.AvatarUtils.Avatar . 28 "mini"}} - <span class="truncated-item-name">{{.ShortName 40}}</span> - </div> - {{end}} - </div> + {{end}} </div> - <span class="help">{{ctx.Locale.Tr "repo.owner_helper"}}</span> </div> + <span class="help">{{ctx.Locale.Tr "repo.owner_helper"}}</span> + </div> + + <div class="inline required field {{if .Err_RepoName}}error{{end}}"> + <label for="repo_name">{{ctx.Locale.Tr "repo.repo_name"}}</label> + <input id="repo_name" name="repo_name" value="{{.repo_name}}" autofocus required maxlength="100"> + <span class="help" data-help-for-repo-name>{{ctx.Locale.Tr "repo.repo_name_helper"}}</span> + <span class="help tw-hidden" data-help-for-repo-name=".profile">{{ctx.Locale.Tr "repo.repo_name_profile_public_hint"}}</span> + <span class="help tw-hidden" data-help-for-repo-name=".profile-private">{{ctx.Locale.Tr "repo.repo_name_profile_private_hint"}}</span> + </div> - <div class="inline required field {{if .Err_RepoName}}error{{end}}"> - <label for="repo_name">{{ctx.Locale.Tr "repo.repo_name"}}</label> - <input id="repo_name" name="repo_name" value="{{.repo_name}}" autofocus required maxlength="100"> - <span class="help">{{ctx.Locale.Tr "repo.repo_name_helper"}}</span> + <div class="inline field"> + <label>{{ctx.Locale.Tr "repo.visibility"}}</label> + <div class="ui checkbox"> + {{if .IsForcedPrivate}} + <input name="private" type="checkbox" checked disabled> + <label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label> + {{else}} + <input name="private" type="checkbox" {{if .private}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label> + {{end}} </div> + <span class="help">{{ctx.Locale.Tr "repo.visibility_description"}}</span> + </div> + <div class="inline field {{if .Err_Description}}error{{end}}"> + <label for="description">{{ctx.Locale.Tr "repo.repo_desc"}}</label> + <textarea id="description" rows="2" name="description" placeholder="{{ctx.Locale.Tr "repo.repo_desc_helper"}}" maxlength="2048">{{.description}}</textarea> + </div> + <div class="inline field"> + <label>{{ctx.Locale.Tr "repo.template"}}</label> + <div id="repo_template_search" class="ui search selection dropdown"> + <input type="hidden" id="repo_template" name="repo_template" value="{{or .repo_template ""}}"> + <div class="default text">{{.repo_template_name}}</div> + <div class="menu"> + </div> + </div> + </div> + + <div id="template_units" class="tw-hidden"> <div class="inline field"> - <label>{{ctx.Locale.Tr "repo.visibility"}}</label> + <label>{{ctx.Locale.Tr "repo.template.items"}}</label> <div class="ui checkbox"> - {{if .IsForcedPrivate}} - <input name="private" type="checkbox" checked disabled> - <label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label> - {{else}} - <input name="private" type="checkbox" {{if .private}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label> - {{end}} + <input name="git_content" type="checkbox" {{if .git_content}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.template.git_content"}}</label> + </div> + <div class="ui checkbox" {{if not .SignedUser.CanEditGitHook}}data-tooltip-content="{{ctx.Locale.Tr "repo.template.git_hooks_tooltip"}}"{{end}}> + <input name="git_hooks" type="checkbox" {{if .git_hooks}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.template.git_hooks"}}</label> </div> - <span class="help">{{ctx.Locale.Tr "repo.visibility_description"}}</span> - </div> - <div class="inline field {{if .Err_Description}}error{{end}}"> - <label for="description">{{ctx.Locale.Tr "repo.repo_desc"}}</label> - <textarea id="description" rows="2" name="description" placeholder="{{ctx.Locale.Tr "repo.repo_desc_helper"}}" maxlength="2048">{{.description}}</textarea> </div> <div class="inline field"> - <label>{{ctx.Locale.Tr "repo.template"}}</label> - <div id="repo_template_search" class="ui search selection dropdown"> - <input type="hidden" id="repo_template" name="repo_template" value="{{.repo_template}}"> - <div class="default text">{{.repo_template_name}}</div> - <div class="menu"> - </div> + <label></label> + <div class="ui checkbox"> + <input name="webhooks" type="checkbox" {{if .webhooks}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.template.webhooks"}}</label> </div> - </div> - - <div id="template_units" class="tw-hidden"> - <div class="inline field"> - <label>{{ctx.Locale.Tr "repo.template.items"}}</label> - <div class="ui checkbox"> - <input name="git_content" type="checkbox" {{if .git_content}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.template.git_content"}}</label> - </div> - <div class="ui checkbox" {{if not .SignedUser.CanEditGitHook}}data-tooltip-content="{{ctx.Locale.Tr "repo.template.git_hooks_tooltip"}}"{{end}}> - <input name="git_hooks" type="checkbox" {{if .git_hooks}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.template.git_hooks"}}</label> - </div> + <div class="ui checkbox"> + <input name="topics" type="checkbox" {{if .topics}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.template.topics"}}</label> </div> - <div class="inline field"> - <label></label> - <div class="ui checkbox"> - <input name="webhooks" type="checkbox" {{if .webhooks}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.template.webhooks"}}</label> - </div> - <div class="ui checkbox"> - <input name="topics" type="checkbox" {{if .topics}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.template.topics"}}</label> - </div> + </div> + <div class="inline field"> + <label></label> + <div class="ui checkbox"> + <input name="avatar" type="checkbox" {{if .avatar}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.template.avatar"}}</label> </div> - <div class="inline field"> - <label></label> - <div class="ui checkbox"> - <input name="avatar" type="checkbox" {{if .avatar}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.template.avatar"}}</label> - </div> - <div class="ui checkbox"> - <input name="labels" type="checkbox" {{if .labels}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.template.issue_labels"}}</label> - </div> + <div class="ui checkbox"> + <input name="labels" type="checkbox" {{if .labels}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.template.issue_labels"}}</label> </div> - <div class="inline field"> - <label></label> - <div class="ui checkbox"> - <input name="protected_branch" type="checkbox" {{if .protected_branch}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.settings.protected_branch"}}</label> - </div> + </div> + <div class="inline field"> + <label></label> + <div class="ui checkbox"> + <input name="protected_branch" type="checkbox" {{if .protected_branch}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.settings.protected_branch"}}</label> </div> </div> + </div> - <div id="non_template"> - <div class="inline field"> - <label>{{ctx.Locale.Tr "repo.issue_labels"}}</label> - <div class="ui search selection dropdown"> - <input type="hidden" name="issue_labels" value="{{.issueLabels}}"> - <div class="default text">{{ctx.Locale.Tr "repo.issue_labels_helper"}}</div> - <div class="menu"> - <div class="item" data-value="">{{ctx.Locale.Tr "repo.issue_labels_helper"}}</div> - {{range .LabelTemplateFiles}} - <div class="item" data-value="{{.DisplayName}}">{{.DisplayName}}<br><i>({{.Description}})</i></div> - {{end}} - </div> + <div id="non_template"> + <div class="inline field"> + <label>{{ctx.Locale.Tr "repo.issue_labels"}}</label> + <div class="ui search selection dropdown"> + <input type="hidden" name="issue_labels" value="{{.issueLabels}}"> + <div class="default text">{{ctx.Locale.Tr "repo.issue_labels_helper"}}</div> + <div class="menu"> + <div class="item" data-value="">{{ctx.Locale.Tr "repo.issue_labels_helper"}}</div> + {{range .LabelTemplateFiles}} + <div class="item" data-value="{{.DisplayName}}">{{.DisplayName}}<br><i>({{.Description}})</i></div> + {{end}} </div> </div> + </div> - <div class="divider"></div> + <div class="divider"></div> - <div class="inline field"> - <label>.gitignore</label> - <div class="ui multiple search selection dropdown"> - <input type="hidden" name="gitignores" value="{{.gitignores}}"> - <div class="default text">{{ctx.Locale.Tr "repo.repo_gitignore_helper"}}</div> - <div class="menu"> - {{range .Gitignores}} - <div class="item" data-value="{{.}}">{{.}}</div> - {{end}} - </div> - </div> - <span class="help">{{ctx.Locale.Tr "repo.repo_gitignore_helper_desc"}}</span> - </div> - <div class="inline field"> - <label>{{ctx.Locale.Tr "repo.license"}}</label> - <div class="ui search selection dropdown"> - <input type="hidden" name="license" value="{{.license}}"> - <div class="default text">{{ctx.Locale.Tr "repo.license_helper"}}</div> - <div class="menu"> - <div class="item" data-value="">{{ctx.Locale.Tr "repo.license_helper"}}</div> - {{range .Licenses}} - <div class="item" data-value="{{.}}">{{.}}</div> - {{end}} - </div> + <div class="inline field"> + <label>.gitignore</label> + <div class="ui multiple search selection dropdown"> + <input type="hidden" name="gitignores" value="{{.gitignores}}"> + <div class="default text">{{ctx.Locale.Tr "repo.repo_gitignore_helper"}}</div> + <div class="menu"> + {{range .Gitignores}} + <div class="item" data-value="{{.}}">{{.}}</div> + {{end}} </div> - <span class="help">{{ctx.Locale.Tr "repo.license_helper_desc" "https://choosealicense.com/"}}</span> </div> - - <div class="inline field"> - <label>{{ctx.Locale.Tr "repo.readme"}}</label> - <div class="ui selection dropdown"> - <input type="hidden" name="readme" value="{{.readme}}"> - <div class="default text">{{ctx.Locale.Tr "repo.readme_helper"}}</div> - <div class="menu"> - {{range .Readmes}} - <div class="item" data-value="{{.}}">{{.}}</div> - {{end}} - </div> + <span class="help">{{ctx.Locale.Tr "repo.repo_gitignore_helper_desc"}}</span> + </div> + <div class="inline field"> + <label>{{ctx.Locale.Tr "repo.license"}}</label> + <div class="ui search selection dropdown"> + <input type="hidden" name="license" value="{{.license}}"> + <div class="default text">{{ctx.Locale.Tr "repo.license_helper"}}</div> + <div class="menu"> + <div class="item" data-value="">{{ctx.Locale.Tr "repo.license_helper"}}</div> + {{range .Licenses}} + <div class="item" data-value="{{.}}">{{.}}</div> + {{end}} </div> - <span class="help">{{ctx.Locale.Tr "repo.readme_helper_desc"}}</span> </div> - <div class="inline field"> - <div class="ui checkbox" id="auto-init"> - <input name="auto_init" type="checkbox" {{if .auto_init}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.auto_init"}}</label> + <span class="help">{{ctx.Locale.Tr "repo.license_helper_desc" "https://choosealicense.com/"}}</span> + </div> + + <div class="inline field"> + <label>{{ctx.Locale.Tr "repo.readme"}}</label> + <div class="ui selection dropdown"> + <input type="hidden" name="readme" value="{{.readme}}"> + <div class="default text">{{ctx.Locale.Tr "repo.readme_helper"}}</div> + <div class="menu"> + {{range .Readmes}} + <div class="item" data-value="{{.}}">{{.}}</div> + {{end}} </div> </div> - <div class="inline field"> - <label for="default_branch">{{ctx.Locale.Tr "repo.default_branch"}}</label> - <input id="default_branch" name="default_branch" value="{{.default_branch}}" placeholder="{{.default_branch}}"> - <span class="help">{{ctx.Locale.Tr "repo.default_branch_helper"}}</span> - </div> - <div class="inline field"> - <label>{{ctx.Locale.Tr "repo.object_format"}}</label> - <div class="ui selection owner dropdown"> - <input type="hidden" id="object_format_name" name="object_format_name" value="{{.DefaultObjectFormat.Name}}" required> - <div class="default text">{{.DefaultObjectFormat.Name}}</div> - <div class="menu"> - {{range .SupportedObjectFormats}} - <div class="item" data-value="{{.Name}}">{{.Name}}</div> - {{end}} - </div> - </div> - <span class="help">{{ctx.Locale.Tr "repo.object_format_helper"}}</span> + <span class="help">{{ctx.Locale.Tr "repo.readme_helper_desc"}}</span> + </div> + <div class="inline field"> + <label></label> + <div class="ui checkbox" id="auto-init"> + <input name="auto_init" type="checkbox" {{if .auto_init}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.auto_init"}}</label> </div> - <div class="inline field"> - <label>{{ctx.Locale.Tr "repo.template"}}</label> - <div class="ui checkbox"> - <input name="template" type="checkbox"> - <label>{{ctx.Locale.Tr "repo.template_helper"}}</label> + </div> + <div class="inline field"> + <label for="default_branch">{{ctx.Locale.Tr "repo.default_branch"}}</label> + <input id="default_branch" name="default_branch" value="{{.default_branch}}" placeholder="{{.default_branch}}"> + <span class="help">{{ctx.Locale.Tr "repo.default_branch_helper"}}</span> + </div> + <div class="inline field"> + <label>{{ctx.Locale.Tr "repo.object_format"}}</label> + <div class="ui selection owner dropdown"> + <input type="hidden" id="object_format_name" name="object_format_name" value="{{or .object_format_name .DefaultObjectFormat.Name}}" required> + <div class="default text">{{.DefaultObjectFormat.Name}}</div> + <div class="menu"> + {{range .SupportedObjectFormats}} + <div class="item" data-value="{{.Name}}">{{.Name}}</div> + {{end}} </div> </div> + <span class="help">{{ctx.Locale.Tr "repo.object_format_helper"}}</span> </div> - <br> <div class="inline field"> - <label></label> - <button class="ui primary button{{if not .CanCreateRepo}} disabled{{end}}"> - {{ctx.Locale.Tr "repo.create_repo"}} - </button> + <label>{{ctx.Locale.Tr "repo.template"}}</label> + <div class="ui checkbox"> + <input name="template" type="checkbox"> + <label>{{ctx.Locale.Tr "repo.template_helper"}}</label> + </div> </div> </div> + <br> + <div class="inline field"> + <label></label> + <button class="ui primary button{{if not .CanCreateRepo}} disabled{{end}}"> + {{ctx.Locale.Tr "repo.create_repo"}} + </button> + </div> </form> </div> </div> diff --git a/templates/repo/diff/blob_excerpt.tmpl b/templates/repo/diff/blob_excerpt.tmpl index cc2237029b..4089d8fb33 100644 --- a/templates/repo/diff/blob_excerpt.tmpl +++ b/templates/repo/diff/blob_excerpt.tmpl @@ -1,3 +1,4 @@ +{{$blobExcerptLink := print $.RepoLink (Iif $.PageIsWiki "/wiki" "") "/blob_excerpt/" (PathEscape $.AfterCommitID) (QueryBuild "?" "anchor" $.Anchor)}} {{if $.IsSplitStyle}} {{range $k, $line := $.section.Lines}} <tr class="{{.GetHTMLDiffLineType}}-code nl-{{$k}} ol-{{$k}} line-expanded"> @@ -6,42 +7,48 @@ <td class="lines-num lines-num-old" data-line-num="{{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}}"> <div class="code-expander-buttons" data-expand-direction="{{$expandDirection}}"> {{if or (eq $expandDirection 3) (eq $expandDirection 5)}} - <button class="code-expander-button" hx-target="closest tr" hx-get="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=split&direction=down&wiki={{$.PageIsWiki}}&anchor={{$.Anchor}}"> + <button class="code-expander-button" hx-target="closest tr" hx-get="{{$blobExcerptLink}}&{{$line.GetBlobExcerptQuery}}&style=split&direction=down"> {{svg "octicon-fold-down"}} </button> {{end}} {{if or (eq $expandDirection 3) (eq $expandDirection 4)}} - <button class="code-expander-button" hx-target="closest tr" hx-get="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=split&direction=up&wiki={{$.PageIsWiki}}&anchor={{$.Anchor}}"> + <button class="code-expander-button" hx-target="closest tr" hx-get="{{$blobExcerptLink}}&{{$line.GetBlobExcerptQuery}}&style=split&direction=up"> {{svg "octicon-fold-up"}} </button> {{end}} {{if eq $expandDirection 2}} - <button class="code-expander-button" hx-target="closest tr" hx-get="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=split&direction=&wiki={{$.PageIsWiki}}&anchor={{$.Anchor}}"> + <button class="code-expander-button" hx-target="closest tr" hx-get="{{$blobExcerptLink}}&{{$line.GetBlobExcerptQuery}}&style=split"> {{svg "octicon-fold"}} </button> {{end}} </div> </td> - <td colspan="7" class="lines-code lines-code-old ">{{$inlineDiff := $.section.GetComputedInlineDiffFor $line ctx.Locale}}{{/* - */}}{{template "repo/diff/section_code" dict "diff" $inlineDiff}}</td> + <td colspan="7" class="lines-code lines-code-old"> + {{- $inlineDiff := $.section.GetComputedInlineDiffFor $line ctx.Locale -}} + {{- template "repo/diff/section_code" dict "diff" $inlineDiff -}} + </td> {{else}} {{$inlineDiff := $.section.GetComputedInlineDiffFor $line ctx.Locale}} <td class="lines-num lines-num-old" data-line-num="{{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}}"><span rel="{{if $line.LeftIdx}}diff-{{$.FileNameHash}}L{{$line.LeftIdx}}{{end}}"></span></td> <td class="lines-escape lines-escape-old">{{if and $line.LeftIdx $inlineDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"></button>{{end}}</td> <td class="lines-type-marker lines-type-marker-old">{{if $line.LeftIdx}}<span class="tw-font-mono" data-type-marker=""></span>{{end}}</td> - <td class="lines-code lines-code-old">{{/* - */}}{{if $line.LeftIdx}}{{template "repo/diff/section_code" dict "diff" $inlineDiff}}{{else}}{{/* - */}}<code class="code-inner"></code>{{/* - */}}{{end}}{{/* - */}}</td> + <td class="lines-code lines-code-old"> + {{- if $line.LeftIdx -}} + {{- template "repo/diff/section_code" dict "diff" $inlineDiff -}} + {{- else -}} + <code class="code-inner"></code> + {{- end -}} + </td> <td class="lines-num lines-num-new" data-line-num="{{if $line.RightIdx}}{{$line.RightIdx}}{{end}}"><span rel="{{if $line.RightIdx}}diff-{{$.FileNameHash}}R{{$line.RightIdx}}{{end}}"></span></td> <td class="lines-escape lines-escape-new">{{if and $line.RightIdx $inlineDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"></button>{{end}}</td> <td class="lines-type-marker lines-type-marker-new">{{if $line.RightIdx}}<span class="tw-font-mono" data-type-marker=""></span>{{end}}</td> - <td class="lines-code lines-code-new">{{/* - */}}{{if $line.RightIdx}}{{template "repo/diff/section_code" dict "diff" $inlineDiff}}{{else}}{{/* - */}}<code class="code-inner"></code>{{/* - */}}{{end}}{{/* - */}}</td> + <td class="lines-code lines-code-new"> + {{- if $line.RightIdx -}} + {{- template "repo/diff/section_code" dict "diff" $inlineDiff -}} + {{- else -}} + <code class="code-inner"></code> + {{- end -}} + </td> {{end}} </tr> {{end}} @@ -53,17 +60,17 @@ <td colspan="2" class="lines-num"> <div class="code-expander-buttons" data-expand-direction="{{$expandDirection}}"> {{if or (eq $expandDirection 3) (eq $expandDirection 5)}} - <button class="code-expander-button" hx-target="closest tr" hx-get="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=unified&direction=down&wiki={{$.PageIsWiki}}&anchor={{$.Anchor}}"> + <button class="code-expander-button" hx-target="closest tr" hx-get="{{$blobExcerptLink}}&{{$line.GetBlobExcerptQuery}}&style=unified&direction=down"> {{svg "octicon-fold-down"}} </button> {{end}} {{if or (eq $expandDirection 3) (eq $expandDirection 4)}} - <button class="code-expander-button" hx-target="closest tr" hx-get="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=unified&direction=up&wiki={{$.PageIsWiki}}&anchor={{$.Anchor}}"> + <button class="code-expander-button" hx-target="closest tr" hx-get="{{$blobExcerptLink}}&{{$line.GetBlobExcerptQuery}}&style=unified&direction=up"> {{svg "octicon-fold-up"}} </button> {{end}} {{if eq $expandDirection 2}} - <button class="code-expander-button" hx-target="closest tr" hx-get="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=unified&direction=&wiki={{$.PageIsWiki}}&anchor={{$.Anchor}}"> + <button class="code-expander-button" hx-target="closest tr" hx-get="{{$blobExcerptLink}}&{{$line.GetBlobExcerptQuery}}&style=unified"> {{svg "octicon-fold"}} </button> {{end}} diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl index 53ea4fd2e3..ea01d96928 100644 --- a/templates/repo/diff/box.tmpl +++ b/templates/repo/diff/box.tmpl @@ -36,10 +36,7 @@ {{template "repo/diff/options_dropdown" .}} {{if .PageIsPullFiles}} <div id="diff-commit-select" data-issuelink="{{$.Issue.Link}}" data-queryparams="?style={{if $.IsSplitStyle}}split{{else}}unified{{end}}&whitespace={{$.WhitespaceBehavior}}&show-outdated={{$.ShowOutdatedComments}}" data-filter_changes_by_commit="{{ctx.Locale.Tr "repo.pulls.filter_changes_by_commit"}}"> - {{/* - the following will be replaced by vue component - but this avoids any loading artifacts till the vue component is initialized - */}} + {{/* the following will be replaced by vue component, but this avoids any loading artifacts till the vue component is initialized */}} <div class="ui jump dropdown basic button custom"> {{svg "octicon-git-commit"}} </div> @@ -61,7 +58,7 @@ </div> {{end}} <script id="diff-data-script" type="module"> - const diffDataFiles = [{{range $i, $file := .Diff.Files}}{Name:"{{$file.Name}}",NameHash:"{{$file.NameHash}}",Type:{{$file.Type}},IsBin:{{$file.IsBin}},Addition:{{$file.Addition}},Deletion:{{$file.Deletion}},IsViewed:{{$file.IsViewed}}},{{end}}]; + const diffDataFiles = [{{range $i, $file := .Diff.Files}}{Name:"{{$file.Name}}",NameHash:"{{$file.NameHash}}",Type:{{$file.Type}},IsBin:{{$file.IsBin}},IsSubmodule:{{$file.IsSubmodule}},Addition:{{$file.Addition}},Deletion:{{$file.Deletion}},IsViewed:{{$file.IsViewed}}},{{end}}]; const diffData = { isIncomplete: {{.Diff.IsIncomplete}}, tooManyFilesMessage: "{{ctx.Locale.Tr "repo.diff.too_many_files"}}", @@ -164,23 +161,25 @@ <input type="checkbox" name="{{$file.GetDiffFileName}}" autocomplete="off"{{if $file.IsViewed}} checked{{end}}> {{ctx.Locale.Tr "repo.pulls.has_viewed_file"}} </label> {{end}} - <button class="btn diff-header-popup-btn tw-p-1">{{svg "octicon-kebab-horizontal" 18}}</button> - <div class="tippy-target"> - {{if not (or $file.IsIncomplete $file.IsBin $file.IsSubmodule)}} - <button class="unescape-button item" data-unicode-content-selector="#diff-{{$file.NameHash}}">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</button> - <button class="escape-button tw-hidden item" data-unicode-content-selector="#diff-{{$file.NameHash}}">{{ctx.Locale.Tr "repo.escape_control_characters"}}</button> - {{end}} - {{if and (not $file.IsSubmodule) (not $.PageIsWiki)}} - {{if $file.IsDeleted}} - <a class="item" rel="nofollow" href="{{$.BeforeSourcePath}}/{{PathEscapeSegments .Name}}">{{ctx.Locale.Tr "repo.diff.view_file"}}</a> - {{else}} - <a class="item" rel="nofollow" href="{{$.SourcePath}}/{{PathEscapeSegments .Name}}">{{ctx.Locale.Tr "repo.diff.view_file"}}</a> - {{if and $.Repository.CanEnableEditor $.CanEditFile (not $file.IsLFSFile) (not $file.IsBin)}} - <a class="item" rel="nofollow" href="{{$.HeadRepoLink}}/_edit/{{PathEscapeSegments $.HeadBranchName}}/{{PathEscapeSegments $file.Name}}?return_uri={{print $.BackToLink "#diff-" $file.NameHash | QueryEscape}}">{{ctx.Locale.Tr "repo.editor.edit_this_file"}}</a> + {{if not $file.IsSubmodule}} + <button class="btn diff-header-popup-btn tw-p-1">{{svg "octicon-kebab-horizontal" 18}}</button> + <div class="tippy-target"> + {{if not (or $file.IsIncomplete $file.IsBin)}} + <button class="unescape-button item" data-unicode-content-selector="#diff-{{$file.NameHash}}">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</button> + <button class="escape-button tw-hidden item" data-unicode-content-selector="#diff-{{$file.NameHash}}">{{ctx.Locale.Tr "repo.escape_control_characters"}}</button> + {{end}} + {{if not $.PageIsWiki}} + {{if $file.IsDeleted}} + <a class="item" rel="nofollow" href="{{$.BeforeSourcePath}}/{{PathEscapeSegments .Name}}">{{ctx.Locale.Tr "repo.diff.view_file"}}</a> + {{else}} + <a class="item" rel="nofollow" href="{{$.SourcePath}}/{{PathEscapeSegments .Name}}">{{ctx.Locale.Tr "repo.diff.view_file"}}</a> + {{if and $.Repository.CanEnableEditor $.CanEditFile (not $file.IsLFSFile) (not $file.IsBin)}} + <a class="item" rel="nofollow" href="{{$.HeadRepoLink}}/_edit/{{PathEscapeSegments $.HeadBranchName}}/{{PathEscapeSegments $file.Name}}?return_uri={{print $.BackToLink "#diff-" $file.NameHash | QueryEscape}}">{{ctx.Locale.Tr "repo.editor.edit_this_file"}}</a> + {{end}} {{end}} {{end}} - {{end}} - </div> + </div> + {{end}} </div> </h4> <div class="diff-file-body ui attached unstackable table segment" {{if and $file.IsViewed $.IsShowingAllCommits}}data-folded="true"{{end}}> @@ -198,6 +197,17 @@ {{ctx.Locale.Tr "repo.diff.bin_not_shown"}} {{end}} </div> + {{else if $file.SubmoduleDiffInfo}} + <div class="tw-p-3">{{svg "octicon-file-submodule"}} {{$submoduleDiffInfo := $file.SubmoduleDiffInfo -}} + {{- $submoduleName := $submoduleDiffInfo.SubmoduleRepoLinkHTML ctx -}} + {{- if $file.IsDeleted -}} + {{- ctx.Locale.Tr "repo.diff.submodule_deleted" $submoduleName ($submoduleDiffInfo.CommitRefIDLinkHTML ctx $submoduleDiffInfo.PreviousRefID) -}} + {{- else if $file.IsCreated -}} + {{- ctx.Locale.Tr "repo.diff.submodule_added" $submoduleName ($submoduleDiffInfo.CommitRefIDLinkHTML ctx $submoduleDiffInfo.NewRefID) -}} + {{- else -}} + {{- ctx.Locale.Tr "repo.diff.submodule_updated" $submoduleName ($submoduleDiffInfo.CompareRefIDLinkHTML ctx) -}} + {{end}} + </div> {{else}} <table class="chroma" data-new-comment-url="{{$.Issue.Link}}/files/reviews/new_comment" data-path="{{$file.Name}}"> {{if $.IsSplitStyle}} @@ -238,7 +248,7 @@ {{if and (not $.Repository.IsArchived) (not .DiffNotAvailable)}} <template id="issue-comment-editor-template"> - <div class="ui form comment"> + <form class="ui form comment"> {{template "shared/combomarkdowneditor" (dict "CustomInit" true "MarkdownPreviewInRepo" $.Repository @@ -255,7 +265,7 @@ <button class="ui cancel button">{{ctx.Locale.Tr "repo.issues.cancel"}}</button> <button class="ui primary button">{{ctx.Locale.Tr "repo.issues.save"}}</button> </div> - </div> + </form> </template> {{end}} {{if (not .DiffNotAvailable)}} diff --git a/templates/repo/diff/compare.tmpl b/templates/repo/diff/compare.tmpl index 118d6478d1..9a7a04a328 100644 --- a/templates/repo/diff/compare.tmpl +++ b/templates/repo/diff/compare.tmpl @@ -1,230 +1,229 @@ {{template "base/head" .}} <div role="main" aria-label="{{.Title}}" class="page-content repository diff {{if .PageIsComparePull}}compare pull{{end}}"> {{template "repo/header" .}} - {{$showDiffBox := false}} + <div class="ui container fluid padded"> - <h2 class="ui header"> - {{if and $.PageIsComparePull $.IsSigned (not .Repository.IsArchived)}} - {{ctx.Locale.Tr "repo.pulls.compare_changes"}} - <div class="sub header">{{ctx.Locale.Tr "repo.pulls.compare_changes_desc"}}</div> - {{else}} - {{ctx.Locale.Tr "action.compare_commits_general"}} - {{end}} - </h2> - {{$BaseCompareName := $.BaseName -}} - {{- $HeadCompareName := $.HeadRepo.OwnerName -}} - {{- if and (eq $.BaseName $.HeadRepo.OwnerName) (ne $.Repository.Name $.HeadRepo.Name) -}} - {{- $HeadCompareName = printf "%s/%s" $.HeadRepo.OwnerName $.HeadRepo.Name -}} - {{- end -}} - {{- $OwnForkCompareName := "" -}} - {{- if .OwnForkRepo -}} - {{- $OwnForkCompareName = .OwnForkRepo.OwnerName -}} - {{- end -}} - {{- $RootRepoCompareName := "" -}} - {{- if .RootRepo -}} - {{- $RootRepoCompareName = .RootRepo.OwnerName -}} - {{- if eq $.HeadRepo.OwnerName .RootRepo.OwnerName -}} + <h2 class="ui header"> + {{if and $.PageIsComparePull $.IsSigned (not .Repository.IsArchived)}} + {{ctx.Locale.Tr "repo.pulls.compare_changes"}} + <div class="sub header">{{ctx.Locale.Tr "repo.pulls.compare_changes_desc"}}</div> + {{else}} + {{ctx.Locale.Tr "action.compare_commits_general"}} + {{end}} + </h2> + {{$BaseCompareName := $.BaseName -}} + {{- $HeadCompareName := $.HeadRepo.OwnerName -}} + {{- if and (eq $.BaseName $.HeadRepo.OwnerName) (ne $.Repository.Name $.HeadRepo.Name) -}} {{- $HeadCompareName = printf "%s/%s" $.HeadRepo.OwnerName $.HeadRepo.Name -}} {{- end -}} - {{- end -}} - <div class="ui segment choose branch"> - <a class="tw-mr-2" href="{{$.HeadRepo.Link}}/compare/{{PathEscapeSegments $.HeadBranch}}{{$.CompareSeparator}}{{if not $.PullRequestCtx.SameRepo}}{{PathEscape $.BaseName}}/{{PathEscape $.Repository.Name}}:{{end}}{{PathEscapeSegments $.BaseBranch}}" title="{{ctx.Locale.Tr "repo.pulls.switch_head_and_base"}}">{{svg "octicon-git-compare"}}</a> - <div class="ui floating filter dropdown" data-no-results="{{ctx.Locale.Tr "no_results_found"}}"> - <div class="ui basic small button"> - <span class="text">{{if $.PageIsComparePull}}{{ctx.Locale.Tr "repo.pulls.compare_base"}}{{else}}{{ctx.Locale.Tr "repo.compare.compare_base"}}{{end}}: {{$BaseCompareName}}:{{$.BaseBranch}}</span> - {{svg "octicon-triangle-down" 14 "dropdown icon"}} - </div> - <div class="menu"> - <div class="ui icon search input"> - <i class="icon">{{svg "octicon-filter" 16}}</i> - <input name="search" placeholder="{{ctx.Locale.Tr "repo.filter_branch_and_tag"}}..."> + {{- $OwnForkCompareName := "" -}} + {{- if .OwnForkRepo -}} + {{- $OwnForkCompareName = .OwnForkRepo.OwnerName -}} + {{- end -}} + {{- $RootRepoCompareName := "" -}} + {{- if .RootRepo -}} + {{- $RootRepoCompareName = .RootRepo.OwnerName -}} + {{- if eq $.HeadRepo.OwnerName .RootRepo.OwnerName -}} + {{- $HeadCompareName = printf "%s/%s" $.HeadRepo.OwnerName $.HeadRepo.Name -}} + {{- end -}} + {{- end -}} + + <div class="ui segment choose branch"> + <a class="tw-mr-2" href="{{$.HeadRepo.Link}}/compare/{{PathEscapeSegments $.HeadBranch}}{{$.CompareSeparator}}{{if not $.PullRequestCtx.SameRepo}}{{PathEscape $.BaseName}}/{{PathEscape $.Repository.Name}}:{{end}}{{PathEscapeSegments $.BaseBranch}}" title="{{ctx.Locale.Tr "repo.pulls.switch_head_and_base"}}">{{svg "octicon-git-compare"}}</a> + <div class="ui dropdown jump select-branch"> + <div class="ui basic small button"> + <span class="text">{{if $.PageIsComparePull}}{{ctx.Locale.Tr "repo.pulls.compare_base"}}{{else}}{{ctx.Locale.Tr "repo.compare.compare_base"}}{{end}}: {{$BaseCompareName}}:{{$.BaseBranch}}</span> + {{svg "octicon-triangle-down" 14 "dropdown icon"}} </div> - <div class="header"> - <div class="ui grid"> - <div class="two column row"> - <a class="reference column" href="#" data-target=".base-branch-list"> - <span class="text black"> - {{svg "octicon-git-branch" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.branches"}} - </span> - </a> - <a class="reference column" href="#" data-target=".base-tag-list"> - <span class="text black"> - {{svg "octicon-tag" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.tags"}} - </span> - </a> + <div class="menu"> + <div class="ui icon search input"> + <i class="icon">{{svg "octicon-filter" 16}}</i> + <input name="search" placeholder="{{ctx.Locale.Tr "repo.filter_branch_and_tag"}}..."> + </div> + <div class="header"> + <div class="ui grid"> + <div class="two column row"> + <a class="reference column" href="#" data-target=".base-branch-list"> + <span class="text black"> + {{svg "octicon-git-branch"}} {{ctx.Locale.Tr "repo.branches"}} + </span> + </a> + <a class="reference column" href="#" data-target=".base-tag-list"> + <span class="text black"> + {{svg "octicon-tag"}} {{ctx.Locale.Tr "repo.tags"}} + </span> + </a> + </div> </div> </div> - </div> - <div class="scrolling menu reference-list-menu base-branch-list"> - {{range .Branches}} - <div class="item {{if eq $.BaseBranch .}}selected{{end}}" data-url="{{$.RepoLink}}/compare/{{PathEscapeSegments .}}{{$.CompareSeparator}}{{if not $.PullRequestCtx.SameRepo}}{{PathEscape $.HeadUser.Name}}/{{PathEscape $.HeadRepo.Name}}:{{end}}{{PathEscapeSegments $.HeadBranch}}">{{$BaseCompareName}}:{{.}}</div> - {{end}} - {{if not .PullRequestCtx.SameRepo}} - {{range .HeadBranches}} - <div class="item" data-url="{{$.HeadRepo.Link}}/compare/{{PathEscapeSegments .}}{{$.CompareSeparator}}{{PathEscape $.HeadUser.Name}}/{{PathEscape $.HeadRepo.Name}}:{{PathEscapeSegments $.HeadBranch}}">{{$HeadCompareName}}:{{.}}</div> + <div class="scrolling menu reference-list-menu base-branch-list"> + {{range .Branches}} + <a class="item {{if eq $.BaseBranch .}}selected{{end}}" href="{{$.RepoLink}}/compare/{{PathEscapeSegments .}}{{$.CompareSeparator}}{{if not $.PullRequestCtx.SameRepo}}{{PathEscape $.HeadUser.Name}}/{{PathEscape $.HeadRepo.Name}}:{{end}}{{PathEscapeSegments $.HeadBranch}}">{{$BaseCompareName}}:{{.}}</a> {{end}} - {{end}} - {{if .OwnForkRepo}} - {{range .OwnForkRepoBranches}} - <div class="item" data-url="{{$.OwnForkRepo.Link}}/compare/{{PathEscapeSegments .}}{{$.CompareSeparator}}{{PathEscape $.HeadUser.Name}}/{{PathEscape $.HeadRepo.Name}}:{{PathEscapeSegments $.HeadBranch}}">{{$OwnForkCompareName}}:{{.}}</div> + {{if not .PullRequestCtx.SameRepo}} + {{range .HeadBranches}} + <a class="item" href="{{$.HeadRepo.Link}}/compare/{{PathEscapeSegments .}}{{$.CompareSeparator}}{{PathEscape $.HeadUser.Name}}/{{PathEscape $.HeadRepo.Name}}:{{PathEscapeSegments $.HeadBranch}}">{{$HeadCompareName}}:{{.}}</a> + {{end}} {{end}} - {{end}} - {{if and .RootRepo (.RootRepo.AllowsPulls ctx)}} - {{range .RootRepoBranches}} - <div class="item" data-url="{{$.RootRepo.Link}}/compare/{{PathEscapeSegments .}}{{$.CompareSeparator}}{{PathEscape $.HeadUser.Name}}/{{PathEscape $.HeadRepo.Name}}:{{PathEscapeSegments $.HeadBranch}}">{{$RootRepoCompareName}}:{{.}}</div> + {{if .OwnForkRepo}} + {{range .OwnForkRepoBranches}} + <a class="item" href="{{$.OwnForkRepo.Link}}/compare/{{PathEscapeSegments .}}{{$.CompareSeparator}}{{PathEscape $.HeadUser.Name}}/{{PathEscape $.HeadRepo.Name}}:{{PathEscapeSegments $.HeadBranch}}">{{$OwnForkCompareName}}:{{.}}</a> + {{end}} {{end}} - {{end}} - </div> - <div class="scrolling menu reference-list-menu base-tag-list tw-hidden"> - {{range .Tags}} - <div class="item {{if eq $.BaseBranch .}}selected{{end}}" data-url="{{$.RepoLink}}/compare/{{PathEscapeSegments .}}{{$.CompareSeparator}}{{if not $.PullRequestCtx.SameRepo}}{{PathEscape $.HeadUser.Name}}/{{PathEscape $.HeadRepo.Name}}:{{end}}{{PathEscapeSegments $.HeadBranch}}">{{$BaseCompareName}}:{{.}}</div> - {{end}} - {{if not .PullRequestCtx.SameRepo}} - {{range .HeadTags}} - <div class="item" data-url="{{$.HeadRepo.Link}}/compare/{{PathEscapeSegments .}}{{$.CompareSeparator}}{{PathEscape $.HeadUser.Name}}/{{PathEscape $.HeadRepo.Name}}:{{PathEscapeSegments $.HeadBranch}}">{{$HeadCompareName}}:{{.}}</div> + {{if and .RootRepo (.RootRepo.AllowsPulls ctx)}} + {{range .RootRepoBranches}} + <a class="item" href="{{$.RootRepo.Link}}/compare/{{PathEscapeSegments .}}{{$.CompareSeparator}}{{PathEscape $.HeadUser.Name}}/{{PathEscape $.HeadRepo.Name}}:{{PathEscapeSegments $.HeadBranch}}">{{$RootRepoCompareName}}:{{.}}</a> + {{end}} {{end}} - {{end}} - {{if .OwnForkRepo}} - {{range .OwnForkRepoTags}} - <div class="item" data-url="{{$.OwnForkRepo.Link}}/compare/{{PathEscapeSegments .}}{{$.CompareSeparator}}{{PathEscape $.HeadUser.Name}}/{{PathEscape $.HeadRepo.Name}}:{{PathEscapeSegments $.HeadBranch}}">{{$OwnForkCompareName}}:{{.}}</div> + </div> + <div class="scrolling menu reference-list-menu base-tag-list tw-hidden"> + {{range .Tags}} + <a class="item {{if eq $.BaseBranch .}}selected{{end}}" href="{{$.RepoLink}}/compare/{{PathEscapeSegments .}}{{$.CompareSeparator}}{{if not $.PullRequestCtx.SameRepo}}{{PathEscape $.HeadUser.Name}}/{{PathEscape $.HeadRepo.Name}}:{{end}}{{PathEscapeSegments $.HeadBranch}}">{{$BaseCompareName}}:{{.}}</a> {{end}} - {{end}} - {{if .RootRepo}} - {{range .RootRepoTags}} - <div class="item" data-url="{{$.RootRepo.Link}}/compare/{{PathEscapeSegments .}}{{$.CompareSeparator}}{{PathEscape $.HeadUser.Name}}/{{PathEscape $.HeadRepo.Name}}:{{PathEscapeSegments $.HeadBranch}}">{{$RootRepoCompareName}}:{{.}}</div> + {{if not .PullRequestCtx.SameRepo}} + {{range .HeadTags}} + <a class="item" href="{{$.HeadRepo.Link}}/compare/{{PathEscapeSegments .}}{{$.CompareSeparator}}{{PathEscape $.HeadUser.Name}}/{{PathEscape $.HeadRepo.Name}}:{{PathEscapeSegments $.HeadBranch}}">{{$HeadCompareName}}:{{.}}</a> + {{end}} {{end}} - {{end}} + {{if .OwnForkRepo}} + {{range .OwnForkRepoTags}} + <a class="item" href="{{$.OwnForkRepo.Link}}/compare/{{PathEscapeSegments .}}{{$.CompareSeparator}}{{PathEscape $.HeadUser.Name}}/{{PathEscape $.HeadRepo.Name}}:{{PathEscapeSegments $.HeadBranch}}">{{$OwnForkCompareName}}:{{.}}</a> + {{end}} + {{end}} + {{if .RootRepo}} + {{range .RootRepoTags}} + <a class="item" href="{{$.RootRepo.Link}}/compare/{{PathEscapeSegments .}}{{$.CompareSeparator}}{{PathEscape $.HeadUser.Name}}/{{PathEscape $.HeadRepo.Name}}:{{PathEscapeSegments $.HeadBranch}}">{{$RootRepoCompareName}}:{{.}}</a> + {{end}} + {{end}} + </div> </div> </div> - </div> - <a href="{{.RepoLink}}/compare/{{PathEscapeSegments .BaseBranch}}{{.OtherCompareSeparator}}{{if not $.PullRequestCtx.SameRepo}}{{PathEscape $.HeadUser.Name}}/{{PathEscape $.HeadRepo.Name}}:{{end}}{{PathEscapeSegments $.HeadBranch}}" title="{{ctx.Locale.Tr "repo.pulls.switch_comparison_type"}}">{{svg "octicon-arrow-left" 16}}<div class="compare-separator">{{.CompareSeparator}}</div></a> - <div class="ui floating filter dropdown"> - <div class="ui basic small button"> - <span class="text">{{if $.PageIsComparePull}}{{ctx.Locale.Tr "repo.pulls.compare_compare"}}{{else}}{{ctx.Locale.Tr "repo.compare.compare_head"}}{{end}}: {{$HeadCompareName}}:{{$.HeadBranch}}</span> - {{svg "octicon-triangle-down" 14 "dropdown icon"}} - </div> - <div class="menu"> - <div class="ui icon search input"> - <i class="icon">{{svg "octicon-filter" 16}}</i> - <input name="search" placeholder="{{ctx.Locale.Tr "repo.filter_branch_and_tag"}}..."> + + <a href="{{.RepoLink}}/compare/{{PathEscapeSegments .BaseBranch}}{{.OtherCompareSeparator}}{{if not $.PullRequestCtx.SameRepo}}{{PathEscape $.HeadUser.Name}}/{{PathEscape $.HeadRepo.Name}}:{{end}}{{PathEscapeSegments $.HeadBranch}}" title="{{ctx.Locale.Tr "repo.pulls.switch_comparison_type"}}">{{svg "octicon-arrow-left" 16}}<div class="compare-separator">{{.CompareSeparator}}</div></a> + + <div class="ui dropdown jump select-branch"> + <div class="ui basic small button"> + <span class="text">{{if $.PageIsComparePull}}{{ctx.Locale.Tr "repo.pulls.compare_compare"}}{{else}}{{ctx.Locale.Tr "repo.compare.compare_head"}}{{end}}: {{$HeadCompareName}}:{{$.HeadBranch}}</span> + {{svg "octicon-triangle-down" 14 "dropdown icon"}} </div> - <div class="header"> - <div class="ui grid"> - <div class="two column row"> - <a class="reference column" href="#" data-target=".head-branch-list"> - <span class="text black"> - {{svg "octicon-git-branch" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.branches"}} - </span> - </a> - <a class="reference column" href="#" data-target=".head-tag-list"> - <span class="text black"> - {{svg "octicon-tag" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.tags"}} - </span> - </a> + <div class="menu"> + <div class="ui icon search input"> + <i class="icon">{{svg "octicon-filter" 16}}</i> + <input name="search" placeholder="{{ctx.Locale.Tr "repo.filter_branch_and_tag"}}..."> + </div> + <div class="header"> + <div class="ui grid"> + <div class="two column row"> + <a class="reference column" href="#" data-target=".head-branch-list"> + <span class="text black"> + {{svg "octicon-git-branch"}} {{ctx.Locale.Tr "repo.branches"}} + </span> + </a> + <a class="reference column" href="#" data-target=".head-tag-list"> + <span class="text black"> + {{svg "octicon-tag"}} {{ctx.Locale.Tr "repo.tags"}} + </span> + </a> + </div> </div> </div> - </div> - <div class="scrolling menu reference-list-menu head-branch-list"> - {{range .HeadBranches}} - <div class="{{if eq $.HeadBranch .}}selected{{end}} item" data-url="{{$.RepoLink}}/compare/{{PathEscapeSegments $.BaseBranch}}{{$.CompareSeparator}}{{if not $.PullRequestCtx.SameRepo}}{{PathEscape $.HeadUser.Name}}/{{PathEscape $.HeadRepo.Name}}:{{end}}{{PathEscapeSegments .}}">{{$HeadCompareName}}:{{.}}</div> - {{end}} - {{if not .PullRequestCtx.SameRepo}} - {{range .Branches}} - <div class="item" data-url="{{$.RepoLink}}/compare/{{PathEscapeSegments $.BaseBranch}}{{$.CompareSeparator}}{{PathEscape $.BaseName}}/{{PathEscape $.Repository.Name}}:{{PathEscapeSegments .}}">{{$BaseCompareName}}:{{.}}</div> + <div class="scrolling menu reference-list-menu head-branch-list"> + {{range .HeadBranches}} + <a class="{{if eq $.HeadBranch .}}selected{{end}} item" href="{{$.RepoLink}}/compare/{{PathEscapeSegments $.BaseBranch}}{{$.CompareSeparator}}{{if not $.PullRequestCtx.SameRepo}}{{PathEscape $.HeadUser.Name}}/{{PathEscape $.HeadRepo.Name}}:{{end}}{{PathEscapeSegments .}}">{{$HeadCompareName}}:{{.}}</a> {{end}} - {{end}} - {{if .OwnForkRepo}} - {{range .OwnForkRepoBranches}} - <div class="item" data-url="{{$.RepoLink}}/compare/{{PathEscapeSegments $.BaseBranch}}{{$.CompareSeparator}}{{PathEscape $.OwnForkRepo.OwnerName}}/{{PathEscape $.OwnForkRepo.Name}}:{{PathEscapeSegments .}}">{{$OwnForkCompareName}}:{{.}}</div> + {{if not .PullRequestCtx.SameRepo}} + {{range .Branches}} + <a class="item" href="{{$.RepoLink}}/compare/{{PathEscapeSegments $.BaseBranch}}{{$.CompareSeparator}}{{PathEscape $.BaseName}}/{{PathEscape $.Repository.Name}}:{{PathEscapeSegments .}}">{{$BaseCompareName}}:{{.}}</a> + {{end}} {{end}} - {{end}} - {{if .RootRepo}} - {{range .RootRepoBranches}} - <div class="item" data-url="{{$.RepoLink}}/compare/{{PathEscapeSegments $.BaseBranch}}{{$.CompareSeparator}}{{PathEscape $.RootRepo.OwnerName}}/{{PathEscape $.RootRepo.Name}}:{{PathEscapeSegments .}}">{{$RootRepoCompareName}}:{{.}}</div> + {{if .OwnForkRepo}} + {{range .OwnForkRepoBranches}} + <a class="item" href="{{$.RepoLink}}/compare/{{PathEscapeSegments $.BaseBranch}}{{$.CompareSeparator}}{{PathEscape $.OwnForkRepo.OwnerName}}/{{PathEscape $.OwnForkRepo.Name}}:{{PathEscapeSegments .}}">{{$OwnForkCompareName}}:{{.}}</a> + {{end}} {{end}} - {{end}} - </div> - <div class="scrolling menu reference-list-menu head-tag-list tw-hidden"> - {{range .HeadTags}} - <div class="{{if eq $.HeadBranch .}}selected{{end}} item" data-url="{{$.RepoLink}}/compare/{{PathEscapeSegments $.BaseBranch}}{{$.CompareSeparator}}{{if not $.PullRequestCtx.SameRepo}}{{PathEscape $.HeadUser.Name}}/{{PathEscape $.HeadRepo.Name}}:{{end}}{{PathEscapeSegments .}}">{{$HeadCompareName}}:{{.}}</div> - {{end}} - {{if not .PullRequestCtx.SameRepo}} - {{range .Tags}} - <div class="item" data-url="{{$.RepoLink}}/compare/{{PathEscapeSegments $.BaseBranch}}{{$.CompareSeparator}}{{PathEscape $.BaseName}}/{{PathEscape $.Repository.Name}}:{{PathEscapeSegments .}}">{{$BaseCompareName}}:{{.}}</div> + {{if .RootRepo}} + {{range .RootRepoBranches}} + <a class="item" href="{{$.RepoLink}}/compare/{{PathEscapeSegments $.BaseBranch}}{{$.CompareSeparator}}{{PathEscape $.RootRepo.OwnerName}}/{{PathEscape $.RootRepo.Name}}:{{PathEscapeSegments .}}">{{$RootRepoCompareName}}:{{.}}</a> + {{end}} {{end}} - {{end}} - {{if .OwnForkRepo}} - {{range .OwnForkRepoTags}} - <div class="item" data-url="{{$.RepoLink}}/compare/{{PathEscapeSegments $.BaseBranch}}{{$.CompareSeparator}}{{PathEscape $.OwnForkRepo.OwnerName}}/{{PathEscape $.OwnForkRepo.Name}}:{{PathEscapeSegments .}}">{{$OwnForkCompareName}}:{{.}}</div> + </div> + <div class="scrolling menu reference-list-menu head-tag-list tw-hidden"> + {{range .HeadTags}} + <a class="{{if eq $.HeadBranch .}}selected{{end}} item" href="{{$.RepoLink}}/compare/{{PathEscapeSegments $.BaseBranch}}{{$.CompareSeparator}}{{if not $.PullRequestCtx.SameRepo}}{{PathEscape $.HeadUser.Name}}/{{PathEscape $.HeadRepo.Name}}:{{end}}{{PathEscapeSegments .}}">{{$HeadCompareName}}:{{.}}</a> {{end}} - {{end}} - {{if .RootRepo}} - {{range .RootRepoTags}} - <div class="item" data-url="{{$.RepoLink}}/compare/{{PathEscapeSegments $.BaseBranch}}{{$.CompareSeparator}}{{PathEscape $.RootRepo.OwnerName}}/{{PathEscape $.RootRepo.Name}}:{{PathEscapeSegments .}}">{{$RootRepoCompareName}}:{{.}}</div> + {{if not .PullRequestCtx.SameRepo}} + {{range .Tags}} + <a class="item" href="{{$.RepoLink}}/compare/{{PathEscapeSegments $.BaseBranch}}{{$.CompareSeparator}}{{PathEscape $.BaseName}}/{{PathEscape $.Repository.Name}}:{{PathEscapeSegments .}}">{{$BaseCompareName}}:{{.}}</a> + {{end}} {{end}} - {{end}} + {{if .OwnForkRepo}} + {{range .OwnForkRepoTags}} + <a class="item" href="{{$.RepoLink}}/compare/{{PathEscapeSegments $.BaseBranch}}{{$.CompareSeparator}}{{PathEscape $.OwnForkRepo.OwnerName}}/{{PathEscape $.OwnForkRepo.Name}}:{{PathEscapeSegments .}}">{{$OwnForkCompareName}}:{{.}}</a> + {{end}} + {{end}} + {{if .RootRepo}} + {{range .RootRepoTags}} + <a class="item" href="{{$.RepoLink}}/compare/{{PathEscapeSegments $.BaseBranch}}{{$.CompareSeparator}}{{PathEscape $.RootRepo.OwnerName}}/{{PathEscape $.RootRepo.Name}}:{{PathEscapeSegments .}}">{{$RootRepoCompareName}}:{{.}}</a> + {{end}} + {{end}} + </div> </div> </div> </div> - </div> - {{if .IsNothingToCompare}} - {{if and $.IsSigned $.AllowEmptyPr (not .Repository.IsArchived) .PageIsComparePull}} - <div class="ui segment">{{ctx.Locale.Tr "repo.pulls.nothing_to_compare_and_allow_empty_pr"}}</div> - <div class="ui info message show-form-container {{if .Flash}}tw-hidden{{end}}"> - <button class="ui button primary show-form">{{ctx.Locale.Tr "repo.pulls.new"}}</button> - </div> - <div class="pullrequest-form {{if not .Flash}}tw-hidden{{end}}"> - {{template "repo/issue/new_form" .}} - </div> - {{else if and .HeadIsBranch .BaseIsBranch}} - <div class="ui segment">{{ctx.Locale.Tr "repo.pulls.nothing_to_compare"}}</div> - {{else}} - <div class="ui segment">{{ctx.Locale.Tr "repo.pulls.nothing_to_compare_have_tag"}}</div> - {{end}} - {{else if and .PageIsComparePull (gt .CommitCount 0)}} - {{if .HasPullRequest}} - <div class="ui segment flex-text-block tw-gap-4"> - {{template "shared/issueicon" .}} - <div class="issue-title tw-break-anywhere"> - {{ctx.RenderUtils.RenderIssueTitle .PullRequest.Issue.Title ($.Repository.ComposeMetas ctx)}} - <span class="index">#{{.PullRequest.Issue.Index}}</span> + {{$showDiffBox := and .CommitCount (not .IsNothingToCompare)}} + {{if and .IsSigned .PageIsComparePull}} + {{$allowCreatePR := or $.AllowEmptyPr (not .IsNothingToCompare)}} + {{if .IsNothingToCompare}} + <div class="ui segment"> + {{if $allowCreatePR}} + {{ctx.Locale.Tr "repo.pulls.nothing_to_compare_and_allow_empty_pr"}} + {{else if and .HeadIsBranch .BaseIsBranch}} + {{ctx.Locale.Tr "repo.pulls.nothing_to_compare"}} + {{else}} + {{ctx.Locale.Tr "repo.pulls.nothing_to_compare_have_tag"}} + {{end}} </div> - <a href="{{$.RepoLink}}/pulls/{{.PullRequest.Issue.Index}}" class="ui compact button primary"> - {{ctx.Locale.Tr "repo.pulls.view"}} - </a> - </div> - {{else}} - {{if and $.IsSigned (not .Repository.IsArchived)}} - <div class="ui info message show-form-container {{if .Flash}}tw-hidden{{end}}"> - <button class="ui button primary show-form">{{ctx.Locale.Tr "repo.pulls.new"}}</button> + {{end}} + {{if .HasPullRequest}} + <div class="ui segment flex-text-block tw-gap-4"> + {{template "shared/issueicon" .}} + <div class="issue-title tw-break-anywhere"> + {{ctx.RenderUtils.RenderIssueTitle .PullRequest.Issue.Title ($.Repository.ComposeMetas ctx)}} + <span class="index">#{{.PullRequest.Issue.Index}}</span> + </div> + <a href="{{$.RepoLink}}/pulls/{{.PullRequest.Issue.Index}}" class="ui compact button primary"> + {{ctx.Locale.Tr "repo.pulls.view"}} + </a> </div> {{else if .Repository.IsArchived}} - <div class="ui warning message tw-text-center"> + <div class="ui warning message"> {{if .Repository.ArchivedUnix.IsZero}} {{ctx.Locale.Tr "repo.archive.title"}} {{else}} {{ctx.Locale.Tr "repo.archive.title_date" (DateUtils.AbsoluteLong .Repository.ArchivedUnix)}} {{end}} </div> - {{end}} - {{if $.IsSigned}} + {{else if $allowCreatePR}} + <div class="ui info message pullrequest-form-toggle {{if .Flash}}tw-hidden{{end}}"> + <button class="ui button primary show-panel toggle" data-panel=".pullrequest-form-toggle, .pullrequest-form">{{ctx.Locale.Tr "repo.pulls.new"}}</button> + </div> <div class="pullrequest-form {{if not .Flash}}tw-hidden{{end}}"> {{template "repo/issue/new_form" .}} </div> {{end}} - {{$showDiffBox = true}} + {{else}}{{/* not singed-in or not for pull-request */}} + {{if not .CommitCount}} + <div class="ui segment">{{ctx.Locale.Tr "repo.commits.nothing_to_compare"}}</div> + {{end}} {{end}} - {{else if not .IsNothingToCompare}} - {{$showDiffBox = true}} - {{end}} </div> {{if $showDiffBox}} - <div class="ui container fluid padded"> - {{template "repo/commits_table" .}} - {{template "repo/diff/box" .}} - </div> + <div class="ui container fluid padded tw-my-4"> + {{template "repo/commits_table" .}} + {{template "repo/diff/box" .}} + </div> {{end}} </div> {{template "base/footer" .}} diff --git a/templates/repo/diff/escape_title.tmpl b/templates/repo/diff/escape_title.tmpl index e70f4021c7..9787ae1d42 100644 --- a/templates/repo/diff/escape_title.tmpl +++ b/templates/repo/diff/escape_title.tmpl @@ -1,2 +1,2 @@ -{{if .diff.EscapeStatus.HasInvisible}}{{ctx.Locale.Tr "repo.invisible_runes_line"}} {{end}}{{/* -*/}}{{if .diff.EscapeStatus.HasAmbiguous}}{{ctx.Locale.Tr "repo.ambiguous_runes_line"}}{{end}} +{{if .diff.EscapeStatus.HasInvisible}}{{ctx.Locale.Tr "repo.invisible_runes_line"}} {{end -}} +{{- if .diff.EscapeStatus.HasAmbiguous}}{{ctx.Locale.Tr "repo.ambiguous_runes_line"}}{{end}} diff --git a/templates/repo/diff/section_split.tmpl b/templates/repo/diff/section_split.tmpl index 37b42bcb37..9953db5eb2 100644 --- a/templates/repo/diff/section_split.tmpl +++ b/templates/repo/diff/section_split.tmpl @@ -1,5 +1,5 @@ {{$file := .file}} -{{$blobExcerptRepoLink := or ctx.RootData.CommitRepoLink ctx.RootData.RepoLink}} +{{$blobExcerptLink := print (or ctx.RootData.CommitRepoLink ctx.RootData.RepoLink) (Iif $.root.PageIsWiki "/wiki" "") "/blob_excerpt/" (PathEscape $.root.AfterCommitID) "?"}} <colgroup> <col width="50"> <col width="10"> @@ -20,26 +20,24 @@ <td class="lines-num lines-num-old"> <div class="code-expander-buttons" data-expand-direction="{{$expandDirection}}"> {{if or (eq $expandDirection 3) (eq $expandDirection 5)}} - <button class="code-expander-button" hx-target="closest tr" hx-get="{{$blobExcerptRepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=split&direction=down&wiki={{$.root.PageIsWiki}}&anchor=diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}"> + <button class="code-expander-button" hx-target="closest tr" hx-get="{{$blobExcerptLink}}&{{$line.GetBlobExcerptQuery}}&style=split&direction=down&&anchor=diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}"> {{svg "octicon-fold-down"}} </button> {{end}} {{if or (eq $expandDirection 3) (eq $expandDirection 4)}} - <button class="code-expander-button" hx-target="closest tr" hx-get="{{$blobExcerptRepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=split&direction=up&wiki={{$.root.PageIsWiki}}&anchor=diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}"> + <button class="code-expander-button" hx-target="closest tr" hx-get="{{$blobExcerptLink}}&{{$line.GetBlobExcerptQuery}}&style=split&direction=up&anchor=diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}"> {{svg "octicon-fold-up"}} </button> {{end}} {{if eq $expandDirection 2}} - <button class="code-expander-button" hx-target="closest tr" hx-get="{{$blobExcerptRepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=split&direction=&wiki={{$.root.PageIsWiki}}&anchor=diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}"> + <button class="code-expander-button" hx-target="closest tr" hx-get="{{$blobExcerptLink}}&{{$line.GetBlobExcerptQuery}}&style=split&anchor=diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}"> {{svg "octicon-fold"}} </button> {{end}} </div> </td>{{$inlineDiff := $section.GetComputedInlineDiffFor $line ctx.Locale}} <td class="lines-escape lines-escape-old">{{if $inlineDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"></button>{{end}}</td> - <td colspan="6" class="lines-code lines-code-old ">{{/* - */}}{{template "repo/diff/section_code" dict "diff" $inlineDiff}}{{/* - */}}</td> + <td colspan="6" class="lines-code lines-code-old">{{template "repo/diff/section_code" dict "diff" $inlineDiff}}</td> {{else if and (eq .GetType 3) $hasmatch}}{{/* DEL */}} {{$match := index $section.Lines $line.Match}} {{- $leftDiff := ""}}{{if $line.LeftIdx}}{{$leftDiff = $section.GetComputedInlineDiffFor $line ctx.Locale}}{{end}} @@ -47,65 +45,65 @@ <td class="lines-num lines-num-old del-code" data-line-num="{{$line.LeftIdx}}"><span rel="diff-{{$file.NameHash}}L{{$line.LeftIdx}}"></span></td> <td class="lines-escape del-code lines-escape-old">{{if $line.LeftIdx}}{{if $leftDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $leftDiff}}"></button>{{end}}{{end}}</td> <td class="lines-type-marker lines-type-marker-old del-code"><span class="tw-font-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span></td> - <td class="lines-code lines-code-old del-code">{{/* - */}}{{if and $.root.SignedUserID $.root.PageIsPullFiles}}{{/* - */}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-left{{if (not $line.CanComment)}} tw-invisible{{end}}" data-side="left" data-idx="{{$line.LeftIdx}}">{{/* - */}}{{svg "octicon-plus"}}{{/* - */}}</button>{{/* - */}}{{end}}{{/* - */}}{{if $line.LeftIdx}}{{/* - */}}{{template "repo/diff/section_code" dict "diff" $leftDiff}}{{/* - */}}{{else}}{{/* - */}}<code class="code-inner"></code>{{/* - */}}{{end}}{{/* - */}}</td> + <td class="lines-code lines-code-old del-code"> + {{- if and $.root.SignedUserID $.root.PageIsPullFiles -}} + <button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-left{{if (not $line.CanComment)}} tw-invisible{{end}}" data-side="left" data-idx="{{$line.LeftIdx}}"> + {{- svg "octicon-plus" -}} + </button> + {{- end -}} + {{- if $line.LeftIdx -}} + {{- template "repo/diff/section_code" dict "diff" $leftDiff -}} + {{- else -}} + <code class="code-inner"></code> + {{- end -}} + </td> <td class="lines-num lines-num-new add-code" data-line-num="{{if $match.RightIdx}}{{$match.RightIdx}}{{end}}"><span rel="{{if $match.RightIdx}}diff-{{$file.NameHash}}R{{$match.RightIdx}}{{end}}"></span></td> <td class="lines-escape add-code lines-escape-new">{{if $match.RightIdx}}{{if $rightDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $rightDiff}}"></button>{{end}}{{end}}</td> <td class="lines-type-marker lines-type-marker-new add-code">{{if $match.RightIdx}}<span class="tw-font-mono" data-type-marker="{{$match.GetLineTypeMarker}}"></span>{{end}}</td> - <td class="lines-code lines-code-new add-code">{{/* - */}}{{if and $.root.SignedUserID $.root.PageIsPullFiles}}{{/* - */}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-right{{if (not $match.CanComment)}} tw-invisible{{end}}" data-side="right" data-idx="{{$match.RightIdx}}">{{/* - */}}{{svg "octicon-plus"}}{{/* - */}}</button>{{/* - */}}{{end}}{{/* - */}}{{if $match.RightIdx}}{{/* - */}}{{template "repo/diff/section_code" dict "diff" $rightDiff}}{{/* - */}}{{else}}{{/* - */}}<code class="code-inner"></code>{{/* - */}}{{end}}{{/* - */}}</td> + <td class="lines-code lines-code-new add-code"> + {{- if and $.root.SignedUserID $.root.PageIsPullFiles -}} + <button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-right{{if (not $match.CanComment)}} tw-invisible{{end}}" data-side="right" data-idx="{{$match.RightIdx}}"> + {{- svg "octicon-plus" -}} + </button> + {{- end -}} + {{- if $match.RightIdx -}} + {{- template "repo/diff/section_code" dict "diff" $rightDiff -}} + {{- else -}} + <code class="code-inner"></code> + {{- end -}} + </td> {{else}} {{$inlineDiff := $section.GetComputedInlineDiffFor $line ctx.Locale}} <td class="lines-num lines-num-old" data-line-num="{{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}}"><span rel="{{if $line.LeftIdx}}diff-{{$file.NameHash}}L{{$line.LeftIdx}}{{end}}"></span></td> <td class="lines-escape lines-escape-old">{{if $line.LeftIdx}}{{if $inlineDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"></button>{{end}}{{end}}</td> <td class="lines-type-marker lines-type-marker-old">{{if $line.LeftIdx}}<span class="tw-font-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span>{{end}}</td> - <td class="lines-code lines-code-old">{{/* - */}}{{if and $.root.SignedUserID $.root.PageIsPullFiles (not (eq .GetType 2))}}{{/* - */}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-left{{if (not $line.CanComment)}} tw-invisible{{end}}" data-side="left" data-idx="{{$line.LeftIdx}}">{{/* - */}}{{svg "octicon-plus"}}{{/* - */}}</button>{{/* - */}}{{end}}{{/* - */}}{{if $line.LeftIdx}}{{/* - */}}{{template "repo/diff/section_code" dict "diff" $inlineDiff}}{{/* - */}}{{else}}{{/* - */}}<code class="code-inner"></code>{{/* - */}}{{end}}{{/* - */}}</td> + <td class="lines-code lines-code-old"> + {{- if and $.root.SignedUserID $.root.PageIsPullFiles (not (eq .GetType 2)) -}} + <button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-left{{if (not $line.CanComment)}} tw-invisible{{end}}" data-side="left" data-idx="{{$line.LeftIdx}}"> + {{- svg "octicon-plus" -}} + </button> + {{- end -}} + {{- if $line.LeftIdx -}} + {{- template "repo/diff/section_code" dict "diff" $inlineDiff -}} + {{- else -}} + <code class="code-inner"></code> + {{- end -}} + </td> <td class="lines-num lines-num-new" data-line-num="{{if $line.RightIdx}}{{$line.RightIdx}}{{end}}"><span rel="{{if $line.RightIdx}}diff-{{$file.NameHash}}R{{$line.RightIdx}}{{end}}"></span></td> <td class="lines-escape lines-escape-new">{{if $line.RightIdx}}{{if $inlineDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"></button>{{end}}{{end}}</td> <td class="lines-type-marker lines-type-marker-new">{{if $line.RightIdx}}<span class="tw-font-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span>{{end}}</td> - <td class="lines-code lines-code-new">{{/* - */}}{{if and $.root.SignedUserID $.root.PageIsPullFiles (not (eq .GetType 3))}}{{/* - */}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-right{{if (not $line.CanComment)}} tw-invisible{{end}}" data-side="right" data-idx="{{$line.RightIdx}}">{{/* - */}}{{svg "octicon-plus"}}{{/* - */}}</button>{{/* - */}}{{end}}{{/* - */}}{{if $line.RightIdx}}{{/* - */}}{{template "repo/diff/section_code" dict "diff" $inlineDiff}}{{/* - */}}{{else}}{{/* - */}}<code class="code-inner"></code>{{/* - */}}{{end}}{{/* - */}}</td> + <td class="lines-code lines-code-new"> + {{- if and $.root.SignedUserID $.root.PageIsPullFiles (not (eq .GetType 3)) -}} + <button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-right{{if (not $line.CanComment)}} tw-invisible{{end}}" data-side="right" data-idx="{{$line.RightIdx}}"> + {{- svg "octicon-plus" -}} + </button> + {{- end -}} + {{- if $line.RightIdx -}} + {{- template "repo/diff/section_code" dict "diff" $inlineDiff -}} + {{- else -}} + <code class="code-inner"></code> + {{- end -}} + </td> {{end}} </tr> {{if and (eq .GetType 3) $hasmatch}} diff --git a/templates/repo/diff/section_unified.tmpl b/templates/repo/diff/section_unified.tmpl index 708b333291..cb612bc27c 100644 --- a/templates/repo/diff/section_unified.tmpl +++ b/templates/repo/diff/section_unified.tmpl @@ -1,5 +1,7 @@ {{$file := .file}} -{{$blobExcerptRepoLink := or ctx.RootData.CommitRepoLink ctx.RootData.RepoLink}} +{{$repoLink := or ctx.RootData.CommitRepoLink ctx.RootData.RepoLink}} +{{$afterCommitID := or $.root.AfterCommitID "no-after-commit-id"}}{{/* this tmpl is also used by the PR Conversation page, so the "AfterCommitID" may not exist */}} +{{$blobExcerptLink := print $repoLink (Iif $.root.PageIsWiki "/wiki" "") "/blob_excerpt/" (PathEscape $afterCommitID) "?"}} <colgroup> <col width="50"> <col width="50"> @@ -16,17 +18,17 @@ <td colspan="2" class="lines-num"> <div class="code-expander-buttons" data-expand-direction="{{$expandDirection}}"> {{if or (eq $expandDirection 3) (eq $expandDirection 5)}} - <button class="code-expander-button" hx-target="closest tr" hx-get="{{$blobExcerptRepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=unified&direction=down&wiki={{$.root.PageIsWiki}}&anchor=diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}"> + <button class="code-expander-button" hx-target="closest tr" hx-get="{{$blobExcerptLink}}&{{$line.GetBlobExcerptQuery}}&style=unified&direction=down&anchor=diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}"> {{svg "octicon-fold-down"}} </button> {{end}} {{if or (eq $expandDirection 3) (eq $expandDirection 4)}} - <button class="code-expander-button" hx-target="closest tr" hx-get="{{$blobExcerptRepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=unified&direction=up&wiki={{$.root.PageIsWiki}}&anchor=diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}"> + <button class="code-expander-button" hx-target="closest tr" hx-get="{{$blobExcerptLink}}&{{$line.GetBlobExcerptQuery}}&style=unified&direction=up&anchor=diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}"> {{svg "octicon-fold-up"}} </button> {{end}} {{if eq $expandDirection 2}} - <button class="code-expander-button" hx-target="closest tr" hx-get="{{$blobExcerptRepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=unified&direction=&wiki={{$.root.PageIsWiki}}&anchor=diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}"> + <button class="code-expander-button" hx-target="closest tr" hx-get="{{$blobExcerptLink}}&{{$line.GetBlobExcerptQuery}}&style=unified&anchor=diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}"> {{svg "octicon-fold"}} </button> {{end}} @@ -48,18 +50,16 @@ </td> <td class="lines-type-marker"><span class="tw-font-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span></td> {{if eq .GetType 4}} - <td class="chroma lines-code blob-hunk">{{/* - */}}{{template "repo/diff/section_code" dict "diff" $inlineDiff}}{{/* - */}}</td> + <td class="chroma lines-code blob-hunk">{{template "repo/diff/section_code" dict "diff" $inlineDiff}}</td> {{else}} - <td class="chroma lines-code{{if (not $line.RightIdx)}} lines-code-old{{end}}">{{/* - */}}{{if and $.root.SignedUserID $.root.PageIsPullFiles}}{{/* - */}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-{{if $line.RightIdx}}right{{else}}left{{end}}{{if (not $line.CanComment)}} tw-invisible{{end}}" data-side="{{if $line.RightIdx}}right{{else}}left{{end}}" data-idx="{{if $line.RightIdx}}{{$line.RightIdx}}{{else}}{{$line.LeftIdx}}{{end}}">{{/* - */}}{{svg "octicon-plus"}}{{/* - */}}</button>{{/* - */}}{{end}}{{/* - */}}{{template "repo/diff/section_code" dict "diff" $inlineDiff}}{{/* - */}}</td> + <td class="chroma lines-code{{if (not $line.RightIdx)}} lines-code-old{{end}}"> + {{- if and $.root.SignedUserID $.root.PageIsPullFiles -}} + <button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-{{if $line.RightIdx}}right{{else}}left{{end}}{{if (not $line.CanComment)}} tw-invisible{{end}}" data-side="{{if $line.RightIdx}}right{{else}}left{{end}}" data-idx="{{if $line.RightIdx}}{{$line.RightIdx}}{{else}}{{$line.LeftIdx}}{{end}}"> + {{- svg "octicon-plus" -}} + </button> + {{- end -}} + {{- template "repo/diff/section_code" dict "diff" $inlineDiff -}} + </td> {{end}} </tr> {{if $line.Comments}} diff --git a/templates/repo/empty.tmpl b/templates/repo/empty.tmpl index 7170fe3602..ae3f95045b 100644 --- a/templates/repo/empty.tmpl +++ b/templates/repo/empty.tmpl @@ -14,14 +14,13 @@ {{end}} </div> {{end}} + {{if .Repository.IsBroken}} - <div class="ui segment center"> - {{ctx.Locale.Tr "repo.broken_message"}} - </div> + <div class="ui segment center">{{ctx.Locale.Tr "repo.broken_message"}}</div> + {{else if .RepoHasContentsWithoutBranch}} + <div class="ui segment center">{{ctx.Locale.Tr "repo.no_branch"}}</div> {{else if .CanWriteCode}} - <h4 class="ui top attached header"> - {{ctx.Locale.Tr "repo.quick_guide"}} - </h4> + <h4 class="ui top attached header">{{ctx.Locale.Tr "repo.quick_guide"}}</h4> <div class="ui attached guide table segment empty-repo-guide"> <div class="item"> <h3>{{ctx.Locale.Tr "repo.clone_this_repo"}} <small>{{ctx.Locale.Tr "repo.clone_helper" "http://git-scm.com/book/en/v2/Git-Basics-Getting-a-Git-Repository"}}</small></h3> @@ -66,12 +65,10 @@ git push -u origin {{.Repository.DefaultBranch}}</code></pre> </div> </div> {{end}} - {{else}} - <div class="ui segment center"> - {{ctx.Locale.Tr "repo.empty_message"}} - </div> - {{end}} - </div> + </div> + {{else}} + <div class="ui segment center">{{ctx.Locale.Tr "repo.empty_message"}}</div> + {{end}} </div> </div> </div> diff --git a/templates/repo/graph/commits.tmpl b/templates/repo/graph/commits.tmpl index f1d0e62330..6af0ba1f0f 100644 --- a/templates/repo/graph/commits.tmpl +++ b/templates/repo/graph/commits.tmpl @@ -5,33 +5,13 @@ {{if $commit.OnlyRelation}} <span></span> {{else}} - <span class="sha" id="{{$commit.ShortRev}}"> - {{$class := "ui sha label"}} - {{if $commit.Commit.Signature}} - {{$class = (print $class " isSigned")}} - {{if $commit.Verification.Verified}} - {{if eq $commit.Verification.TrustStatus "trusted"}} - {{$class = (print $class " isVerified")}} - {{else if eq $commit.Verification.TrustStatus "untrusted"}} - {{$class = (print $class " isVerifiedUntrusted")}} - {{else}} - {{$class = (print $class " isVerifiedUnmatched")}} - {{end}} - {{else if $commit.Verification.Warning}} - {{$class = (print $class " isWarning")}} - {{end}} - {{end}} - <a href="{{$.RepoLink}}/commit/{{$commit.Rev|PathEscape}}" rel="nofollow" class="{{$class}}"> - <span class="shortsha">{{ShortSha $commit.Commit.ID.String}}</span> - {{- if $commit.Commit.Signature -}} - {{template "repo/shabox_badge" dict "root" $ "verification" $commit.Verification}} - {{- end -}} - </a> - </span> - <span class="message tw-inline-block gt-ellipsis tw-mr-2"> + {{template "repo/commit_sign_badge" dict "Commit" $commit.Commit "CommitBaseLink" (print $.RepoLink "/commit") "CommitSignVerification" $commit.Verification}} + + <span class="message tw-inline-block gt-ellipsis"> <span>{{ctx.RenderUtils.RenderCommitMessage $commit.Subject ($.Repository.ComposeMetas ctx)}}</span> </span> - <span class="commit-refs tw-flex tw-items-center tw-mr-1"> + + <span class="commit-refs flex-text-inline"> {{range $commit.Refs}} {{$refGroup := .RefGroup}} {{if eq $refGroup "pull"}} @@ -56,7 +36,8 @@ {{end}} {{end}} </span> - <span class="author tw-flex tw-items-center tw-mr-2 tw-gap-1"> + + <span class="author flex-text-inline"> {{$userName := $commit.Commit.Author.Name}} {{if $commit.User}} {{if and $commit.User.FullName DefaultShowFullName}} @@ -69,7 +50,8 @@ {{$userName}} {{end}} </span> - <span class="time tw-flex tw-items-center">{{DateUtils.FullTime $commit.Date}}</span> + + <span class="time flex-text-inline">{{DateUtils.FullTime $commit.Date}}</span> {{end}} </li> {{end}} diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl index c3ae697f31..e187ef1a87 100644 --- a/templates/repo/header.tmpl +++ b/templates/repo/header.tmpl @@ -162,7 +162,7 @@ </a> {{end}} - {{if and .EnableActions (.Permission.CanRead ctx.Consts.RepoUnitTypeActions)}} + {{if and .EnableActions (.Permission.CanRead ctx.Consts.RepoUnitTypeActions) (not .IsEmptyRepo)}} <a class="{{if .PageIsActions}}active {{end}}item" href="{{.RepoLink}}/actions"> {{svg "octicon-play"}} {{ctx.Locale.Tr "actions.actions"}} {{if .Repository.NumOpenActionRuns}} diff --git a/templates/repo/issue/filter_item_label.tmpl b/templates/repo/issue/filter_item_label.tmpl index 88e2e43120..0883d93804 100644 --- a/templates/repo/issue/filter_item_label.tmpl +++ b/templates/repo/issue/filter_item_label.tmpl @@ -1,4 +1,4 @@ -{{/* +{{/* Template Attributes: * "labels" from query string (needed by JS) * QueryLink * Labels diff --git a/templates/repo/issue/sidebar/milestone_list.tmpl b/templates/repo/issue/sidebar/milestone_list.tmpl index 0e926f7b03..8b3e5b0eee 100644 --- a/templates/repo/issue/sidebar/milestone_list.tmpl +++ b/templates/repo/issue/sidebar/milestone_list.tmpl @@ -6,7 +6,7 @@ {{if $pageMeta.Issue}}data-update-url="{{$pageMeta.RepoLink}}/issues/milestone?issue_ids={{$pageMeta.Issue.ID}}"{{end}} > <input class="combo-value" name="milestone_id" type="hidden" value="{{$data.SelectedMilestoneID}}"> - <div class="ui dropdown {{if not $pageMeta.CanModifyIssueOrPull}}disabled{{end}} "> + <div class="ui dropdown {{if not $pageMeta.CanModifyIssueOrPull}}disabled{{end}}"> <a class="text muted"> <strong>{{ctx.Locale.Tr "repo.issues.new.milestone"}}</strong> {{if $pageMeta.CanModifyIssueOrPull}}{{svg "octicon-gear"}}{{end}} </a> diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index 3f9af18027..2e1a67edcc 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -365,8 +365,9 @@ {{if .Review}}{{$reviewType = .Review.Type}}{{end}} {{if not .OriginalAuthor}} {{/* Some timeline avatars need a offset to correctly align with their speech bubble. - The condition depends on whether the comment has contents/attachments or reviews */}} - <a class="timeline-avatar{{if or .Content .Attachments (and .Review .Review.CodeComments)}} timeline-avatar-offset{{end}}"{{if gt .Poster.ID 0}} href="{{.Poster.HomeLink}}"{{end}}> + The condition depends on whether the comment has contents/attachments, + review's comment is also controlled/rendered by issue comment's Content field */}} + <a class="timeline-avatar{{if or .Content .Attachments}} timeline-avatar-offset{{end}}"{{if gt .Poster.ID 0}} href="{{.Poster.HomeLink}}"{{end}}> {{ctx.AvatarUtils.Avatar .Poster 40}} </a> {{end}} @@ -693,7 +694,7 @@ {{else if eq .Type 38}} <div class="timeline-item event" id="{{.HashTag}}"> <span class="badge">{{svg "octicon-clock"}}</span> - {{template "shared/user/avatarlink" dict "Context" $.Context "user" .Poster}} + {{template "shared/user/avatarlink" dict "user" .Poster}} <span class="text grey muted-links"> {{template "shared/user/authorlink" .Poster}} {{$timeStr := .Content|TimeEstimateString}} diff --git a/templates/repo/issue/view_content/pull.tmpl b/templates/repo/issue/view_content/pull.tmpl index d117bfbc51..7c53c5edb5 100644 --- a/templates/repo/issue/view_content/pull.tmpl +++ b/templates/repo/issue/view_content/pull.tmpl @@ -367,8 +367,7 @@ {{end}} {{end}}{{/* end if: pull request status */}} - {{/* - Manually Merged is not a well-known feature, it is used to mark a non-mergeable PR (already merged, conflicted) as merged + {{/* Manually Merged is not a well-known feature, it is used to mark a non-mergeable PR (already merged, conflicted) as merged To test it: * Enable "Manually Merged" feature in the Repository Settings * Create a pull request, either: diff --git a/templates/repo/latest_commit.tmpl b/templates/repo/latest_commit.tmpl index 34a5df8f77..c62efc8e88 100644 --- a/templates/repo/latest_commit.tmpl +++ b/templates/repo/latest_commit.tmpl @@ -1,8 +1,9 @@ +<div class="latest-commit"> {{if not .LatestCommit}} … {{else}} {{if .LatestCommitUser}} - {{ctx.AvatarUtils.Avatar .LatestCommitUser 24 "tw-mr-1"}} + {{ctx.AvatarUtils.Avatar .LatestCommitUser 24}} {{if and .LatestCommitUser.FullName DefaultShowFullName}} <a class="muted author-wrapper" title="{{.LatestCommitUser.FullName}}" href="{{.LatestCommitUser.HomeLink}}"><strong>{{.LatestCommitUser.FullName}}</strong></a> {{else}} @@ -10,17 +11,15 @@ {{end}} {{else}} {{if .LatestCommit.Author}} - {{ctx.AvatarUtils.AvatarByEmail .LatestCommit.Author.Email .LatestCommit.Author.Name 24 "tw-mr-1"}} + {{ctx.AvatarUtils.AvatarByEmail .LatestCommit.Author.Email .LatestCommit.Author.Name 24}} <span class="author-wrapper" title="{{.LatestCommit.Author.Name}}"><strong>{{.LatestCommit.Author.Name}}</strong></span> {{end}} {{end}} - <a rel="nofollow" class="ui sha label {{if .LatestCommit.Signature}} isSigned {{if .LatestCommitVerification.Verified}} isVerified{{if eq .LatestCommitVerification.TrustStatus "trusted"}}{{else if eq .LatestCommitVerification.TrustStatus "untrusted"}}Untrusted{{else}}Unmatched{{end}}{{else if .LatestCommitVerification.Warning}} isWarning{{end}}{{end}}" href="{{.RepoLink}}/commit/{{PathEscape .LatestCommit.ID.String}}"> - <span class="shortsha">{{ShortSha .LatestCommit.ID.String}}</span> - {{if .LatestCommit.Signature}} - {{template "repo/shabox_badge" dict "root" $ "verification" .LatestCommitVerification}} - {{end}} - </a> + + {{template "repo/commit_sign_badge" dict "Commit" .LatestCommit "CommitBaseLink" (print .RepoLink "/commit") "CommitSignVerification" .LatestCommitVerification}} + {{template "repo/commit_statuses" dict "Status" .LatestCommitStatus "Statuses" .LatestCommitStatuses}} + {{$commitLink:= printf "%s/commit/%s" .RepoLink (PathEscape .LatestCommit.ID.String)}} <span class="grey commit-summary" title="{{.LatestCommit.Summary}}"><span class="message-wrapper">{{ctx.RenderUtils.RenderCommitMessageLinkSubject .LatestCommit.Message $commitLink ($.Repository.ComposeMetas ctx)}}</span> {{if IsMultilineCommitMessage .LatestCommit.Message}} @@ -29,3 +28,4 @@ {{end}} </span> {{end}} +</div> diff --git a/templates/repo/migrate/codebase.tmpl b/templates/repo/migrate/codebase.tmpl index c8059b7c7b..35f3614ec5 100644 --- a/templates/repo/migrate/codebase.tmpl +++ b/templates/repo/migrate/codebase.tmpl @@ -1,114 +1,112 @@ {{template "base/head" .}} <div role="main" aria-label="{{.Title}}" class="page-content repository new migrate"> - <div class="ui middle very relaxed page grid"> - <div class="column"> - <form class="ui form" action="{{.Link}}" method="post"> + <div class="ui container medium-width"> + <h3 class="ui top attached header"> + {{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}} + <input id="service_type" type="hidden" name="service" value="{{.service}}"> + </h3> + <div class="ui attached segment"> + {{template "base/alert" .}} + <form class="ui form left-right-form" action="{{.Link}}" method="post"> {{template "base/disable_form_autofill"}} {{.CsrfTokenHtml}} - <h3 class="ui top attached header"> - {{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}} - <input id="service_type" type="hidden" name="service" value="{{.service}}"> - </h3> - <div class="ui attached segment"> - {{template "base/alert" .}} - <div class="inline required field {{if .Err_CloneAddr}}error{{end}}"> - <label for="clone_addr">{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label> - <input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required> - <span class="help"> - {{ctx.Locale.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{ctx.Locale.Tr "repo.migrate.clone_local_path"}}{{end}} - </span> - </div> + <div class="inline required field {{if .Err_CloneAddr}}error{{end}}"> + <label for="clone_addr">{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label> + <input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required> + <span class="help"> + {{ctx.Locale.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{ctx.Locale.Tr "repo.migrate.clone_local_path"}}{{end}} + </span> + </div> - <div class="inline field {{if .Err_Auth}}error{{end}}"> - <label for="auth_username">{{ctx.Locale.Tr "username"}}</label> - <input id="auth_username" name="auth_username" value="{{.auth_username}}" {{if not .auth_username}}data-need-clear="true"{{end}}> - </div> - <div class="inline field {{if .Err_Auth}}error{{end}}"> - <label for="auth_password">{{ctx.Locale.Tr "password"}}</label> - <input id="auth_password" name="auth_password" type="password" value="{{.auth_password}}"> - </div> + <div class="inline field {{if .Err_Auth}}error{{end}}"> + <label for="auth_username">{{ctx.Locale.Tr "username"}}</label> + <input id="auth_username" name="auth_username" value="{{.auth_username}}" {{if not .auth_username}}data-need-clear="true"{{end}}> + </div> + <div class="inline field {{if .Err_Auth}}error{{end}}"> + <label for="auth_password">{{ctx.Locale.Tr "password"}}</label> + <input id="auth_password" name="auth_password" type="password" value="{{.auth_password}}"> + </div> - {{template "repo/migrate/options" .}} + {{template "repo/migrate/options" .}} - <div id="migrate_items"> - <div class="inline field"> - <label>{{ctx.Locale.Tr "repo.migrate_items"}}</label> - <div class="ui checkbox"> - <input name="milestones" type="checkbox" {{if .milestones}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_milestones"}}</label> - </div> - <div class="ui checkbox"> - <input name="labels" type="checkbox" {{if .labels}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_labels"}}</label> - </div> + <div id="migrate_items"> + <div class="inline field"> + <label>{{ctx.Locale.Tr "repo.migrate_items"}}</label> + <div class="ui checkbox"> + <input name="milestones" type="checkbox" {{if .milestones}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_milestones"}}</label> </div> - <div class="inline field"> - <label></label> - <div class="ui checkbox"> - <input name="issues" type="checkbox" {{if .issues}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_issues"}}</label> - </div> - <div class="ui checkbox"> - <input name="pull_requests" type="checkbox" {{if .pull_requests}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_merge_requests"}}</label> - </div> + <div class="ui checkbox"> + <input name="labels" type="checkbox" {{if .labels}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_labels"}}</label> </div> </div> - - <div class="divider"></div> - - <div class="inline required field {{if .Err_Owner}}error{{end}}"> - <label>{{ctx.Locale.Tr "repo.owner"}}</label> - <div class="ui selection owner dropdown"> - <input type="hidden" id="uid" name="uid" value="{{.ContextUser.ID}}" required> - <span class="text truncated-item-container" title="{{.ContextUser.Name}}"> - {{ctx.AvatarUtils.Avatar .ContextUser 28 "mini"}} - <span class="truncated-item-name">{{.ContextUser.ShortName 40}}</span> - </span> - {{svg "octicon-triangle-down" 14 "dropdown icon"}} - <div class="menu" title="{{.SignedUser.Name}}"> - <div class="item truncated-item-container" data-value="{{.SignedUser.ID}}"> - {{ctx.AvatarUtils.Avatar .SignedUser 28 "mini"}} - <span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span> - </div> - {{range .Orgs}} - <div class="item truncated-item-container" data-value="{{.ID}}" title="{{.Name}}"> - {{ctx.AvatarUtils.Avatar . 28 "mini"}} - <span class="truncated-item-name">{{.ShortName 40}}</span> - </div> - {{end}} - </div> + <div class="inline field"> + <label></label> + <div class="ui checkbox"> + <input name="issues" type="checkbox" {{if .issues}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_issues"}}</label> + </div> + <div class="ui checkbox"> + <input name="pull_requests" type="checkbox" {{if .pull_requests}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_merge_requests"}}</label> </div> </div> + </div> - <div class="inline required field {{if .Err_RepoName}}error{{end}}"> - <label for="repo_name">{{ctx.Locale.Tr "repo.repo_name"}}</label> - <input id="repo_name" name="repo_name" value="{{.repo_name}}" required maxlength="100"> - </div> - <div class="inline field"> - <label>{{ctx.Locale.Tr "repo.visibility"}}</label> - <div class="ui checkbox"> - {{if .IsForcedPrivate}} - <input name="private" type="checkbox" checked disabled> - <label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label> - {{else}} - <input name="private" type="checkbox" {{if .private}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label> + <div class="divider"></div> + + <div class="inline required field {{if .Err_Owner}}error{{end}}"> + <label>{{ctx.Locale.Tr "repo.owner"}}</label> + <div class="ui selection owner dropdown"> + <input type="hidden" id="uid" name="uid" value="{{.ContextUser.ID}}" required> + <span class="text truncated-item-container" title="{{.ContextUser.Name}}"> + {{ctx.AvatarUtils.Avatar .ContextUser 28 "mini"}} + <span class="truncated-item-name">{{.ContextUser.ShortName 40}}</span> + </span> + {{svg "octicon-triangle-down" 14 "dropdown icon"}} + <div class="menu" title="{{.SignedUser.Name}}"> + <div class="item truncated-item-container" data-value="{{.SignedUser.ID}}"> + {{ctx.AvatarUtils.Avatar .SignedUser 28 "mini"}} + <span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span> + </div> + {{range .Orgs}} + <div class="item truncated-item-container" data-value="{{.ID}}" title="{{.Name}}"> + {{ctx.AvatarUtils.Avatar . 28 "mini"}} + <span class="truncated-item-name">{{.ShortName 40}}</span> + </div> {{end}} </div> </div> - <div class="inline field {{if .Err_Description}}error{{end}}"> - <label for="description">{{ctx.Locale.Tr "repo.repo_desc"}}</label> - <textarea id="description" name="description" maxlength="2048">{{.description}}</textarea> - </div> + </div> - <div class="inline field"> - <label></label> - <button class="ui primary button"> - {{ctx.Locale.Tr "repo.migrate_repo"}} - </button> + <div class="inline required field {{if .Err_RepoName}}error{{end}}"> + <label for="repo_name">{{ctx.Locale.Tr "repo.repo_name"}}</label> + <input id="repo_name" name="repo_name" value="{{.repo_name}}" required maxlength="100"> + </div> + <div class="inline field"> + <label>{{ctx.Locale.Tr "repo.visibility"}}</label> + <div class="ui checkbox"> + {{if .IsForcedPrivate}} + <input name="private" type="checkbox" checked disabled> + <label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label> + {{else}} + <input name="private" type="checkbox" {{if .private}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label> + {{end}} </div> </div> + <div class="inline field {{if .Err_Description}}error{{end}}"> + <label for="description">{{ctx.Locale.Tr "repo.repo_desc"}}</label> + <textarea id="description" name="description" maxlength="2048">{{.description}}</textarea> + </div> + + <div class="inline field"> + <label></label> + <button class="ui primary button"> + {{ctx.Locale.Tr "repo.migrate_repo"}} + </button> + </div> </form> </div> </div> diff --git a/templates/repo/migrate/codecommit.tmpl b/templates/repo/migrate/codecommit.tmpl index d1cebd0e48..f75112f896 100644 --- a/templates/repo/migrate/codecommit.tmpl +++ b/templates/repo/migrate/codecommit.tmpl @@ -1,115 +1,113 @@ {{template "base/head" .}} <div role="main" aria-label="{{.Title}}" class="page-content repository new migrate"> - <div class="ui middle very relaxed page grid"> - <div class="column"> - <form class="ui form" action="{{.Link}}" method="post"> + <div class="ui container medium-width"> + <h3 class="ui top attached header"> + {{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}} + <input id="service_type" type="hidden" name="service" value="{{.service}}"> + </h3> + <div class="ui attached segment"> + {{template "base/alert" .}} + <form class="ui form left-right-form" action="{{.Link}}" method="post"> {{template "base/disable_form_autofill"}} {{.CsrfTokenHtml}} - <h3 class="ui top attached header"> - {{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}} - <input id="service_type" type="hidden" name="service" value="{{.service}}"> - </h3> - <div class="ui attached segment"> - {{template "base/alert" .}} - <div class="inline required field {{if .Err_CloneAddr}}error{{end}}"> - <label for="clone_addr">{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label> - <input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required> - <span class="help"> - {{ctx.Locale.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{ctx.Locale.Tr "repo.migrate.clone_local_path"}}{{end}} - </span> - </div> + <div class="inline required field {{if .Err_CloneAddr}}error{{end}}"> + <label for="clone_addr">{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label> + <input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required> + <span class="help"> + {{ctx.Locale.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{ctx.Locale.Tr "repo.migrate.clone_local_path"}}{{end}} + </span> + </div> - <div class="inline required field {{if .Err_Auth}}error{{end}}"> - <label for="aws_access_key_id">{{ctx.Locale.Tr "repo.migrate.codecommit.aws_access_key_id"}}</label> - <input id="aws_access_key_id" name="aws_access_key_id" value="{{.aws_access_key_id}}" required> - </div> - <div class="inline required field {{if .Err_Auth}}error{{end}}"> - <label for="aws_secret_access_key">{{ctx.Locale.Tr "repo.migrate.codecommit.aws_secret_access_key"}}</label> - <input id="aws_secret_access_key" name="aws_secret_access_key" type="password" value="{{.aws_secret_access_key}}" required> - </div> - <div class="inline required field {{if .Err_Auth}}error{{end}}"> - <label for="auth_username">{{ctx.Locale.Tr "repo.migrate.codecommit.https_git_credentials_username"}}</label> - <input id="auth_username" name="auth_username" value="{{.auth_username}}" required> - </div> - <div class="inline required field {{if .Err_Auth}}error{{end}}"> - <label for="auth_password">{{ctx.Locale.Tr "repo.migrate.codecommit.https_git_credentials_password"}}</label> - <input id="auth_password" name="auth_password" type="password" value="{{.auth_password}}" required> + <div class="inline required field {{if .Err_Auth}}error{{end}}"> + <label for="aws_access_key_id">{{ctx.Locale.Tr "repo.migrate.codecommit.aws_access_key_id"}}</label> + <input id="aws_access_key_id" name="aws_access_key_id" value="{{.aws_access_key_id}}" required> + </div> + <div class="inline required field {{if .Err_Auth}}error{{end}}"> + <label for="aws_secret_access_key">{{ctx.Locale.Tr "repo.migrate.codecommit.aws_secret_access_key"}}</label> + <input id="aws_secret_access_key" name="aws_secret_access_key" type="password" value="{{.aws_secret_access_key}}" required> + </div> + <div class="inline required field {{if .Err_Auth}}error{{end}}"> + <label for="auth_username">{{ctx.Locale.Tr "repo.migrate.codecommit.https_git_credentials_username"}}</label> + <input id="auth_username" name="auth_username" value="{{.auth_username}}" required> + </div> + <div class="inline required field {{if .Err_Auth}}error{{end}}"> + <label for="auth_password">{{ctx.Locale.Tr "repo.migrate.codecommit.https_git_credentials_password"}}</label> + <input id="auth_password" name="auth_password" type="password" value="{{.auth_password}}" required> + </div> + + {{if not .DisableNewPullMirrors}} + <div class="inline field"> + <label>{{ctx.Locale.Tr "repo.migrate_options"}}</label> + <div class="ui checkbox"> + <input id="mirror" name="mirror" type="checkbox" {{if .mirror}} checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_options_mirror_helper"}}</label> </div> + </div> + {{end}} - {{if not .DisableNewPullMirrors}} + <div id="migrate_items"> <div class="inline field"> - <label>{{ctx.Locale.Tr "repo.migrate_options"}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items"}}</label> <div class="ui checkbox"> - <input id="mirror" name="mirror" type="checkbox" {{if .mirror}} checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_options_mirror_helper"}}</label> - </div> - </div> - {{end}} - - <div id="migrate_items"> - <div class="inline field"> - <label>{{ctx.Locale.Tr "repo.migrate_items"}}</label> - <div class="ui checkbox"> - <input name="pull_requests" type="checkbox" {{if .pull_requests}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_pullrequests"}}</label> - </div> + <input name="pull_requests" type="checkbox" {{if .pull_requests}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_pullrequests"}}</label> </div> </div> + </div> - <div class="divider"></div> + <div class="divider"></div> - <div class="inline required field {{if .Err_Owner}}error{{end}}"> - <label>{{ctx.Locale.Tr "repo.owner"}}</label> - <div class="ui selection owner dropdown"> - <input type="hidden" id="uid" name="uid" value="{{.ContextUser.ID}}" required> - <span class="text truncated-item-container" title="{{.ContextUser.Name}}"> - {{ctx.AvatarUtils.Avatar .ContextUser 28 "mini"}} - <span class="truncated-item-name">{{.ContextUser.ShortName 40}}</span> - </span> - {{svg "octicon-triangle-down" 14 "dropdown icon"}} - <div class="menu" title="{{.SignedUser.Name}}"> - <div class="item truncated-item-container" data-value="{{.SignedUser.ID}}"> - {{ctx.AvatarUtils.Avatar .SignedUser 28 "mini"}} - <span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span> - </div> - {{range .Orgs}} - <div class="item truncated-item-container" data-value="{{.ID}}" title="{{.Name}}"> - {{ctx.AvatarUtils.Avatar . 28 "mini"}} - <span class="truncated-item-name">{{.ShortName 40}}</span> - </div> - {{end}} + <div class="inline required field {{if .Err_Owner}}error{{end}}"> + <label>{{ctx.Locale.Tr "repo.owner"}}</label> + <div class="ui selection owner dropdown"> + <input type="hidden" id="uid" name="uid" value="{{.ContextUser.ID}}" required> + <span class="text truncated-item-container" title="{{.ContextUser.Name}}"> + {{ctx.AvatarUtils.Avatar .ContextUser 28 "mini"}} + <span class="truncated-item-name">{{.ContextUser.ShortName 40}}</span> + </span> + {{svg "octicon-triangle-down" 14 "dropdown icon"}} + <div class="menu" title="{{.SignedUser.Name}}"> + <div class="item truncated-item-container" data-value="{{.SignedUser.ID}}"> + {{ctx.AvatarUtils.Avatar .SignedUser 28 "mini"}} + <span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span> </div> - </div> - </div> - - <div class="inline required field {{if .Err_RepoName}}error{{end}}"> - <label for="repo_name">{{ctx.Locale.Tr "repo.repo_name"}}</label> - <input id="repo_name" name="repo_name" value="{{.repo_name}}" required maxlength="100"> - </div> - <div class="inline field"> - <label>{{ctx.Locale.Tr "repo.visibility"}}</label> - <div class="ui checkbox"> - {{if .IsForcedPrivate}} - <input name="private" type="checkbox" checked disabled> - <label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label> - {{else}} - <input name="private" type="checkbox" {{if .private}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label> + {{range .Orgs}} + <div class="item truncated-item-container" data-value="{{.ID}}" title="{{.Name}}"> + {{ctx.AvatarUtils.Avatar . 28 "mini"}} + <span class="truncated-item-name">{{.ShortName 40}}</span> + </div> {{end}} </div> </div> - <div class="inline field {{if .Err_Description}}error{{end}}"> - <label for="description">{{ctx.Locale.Tr "repo.repo_desc"}}</label> - <textarea id="description" name="description" maxlength="2048">{{.description}}</textarea> - </div> + </div> - <div class="inline field"> - <label></label> - <button class="ui primary button"> - {{ctx.Locale.Tr "repo.migrate_repo"}} - </button> + <div class="inline required field {{if .Err_RepoName}}error{{end}}"> + <label for="repo_name">{{ctx.Locale.Tr "repo.repo_name"}}</label> + <input id="repo_name" name="repo_name" value="{{.repo_name}}" required maxlength="100"> + </div> + <div class="inline field"> + <label>{{ctx.Locale.Tr "repo.visibility"}}</label> + <div class="ui checkbox"> + {{if .IsForcedPrivate}} + <input name="private" type="checkbox" checked disabled> + <label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label> + {{else}} + <input name="private" type="checkbox" {{if .private}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label> + {{end}} </div> </div> + <div class="inline field {{if .Err_Description}}error{{end}}"> + <label for="description">{{ctx.Locale.Tr "repo.repo_desc"}}</label> + <textarea id="description" name="description" maxlength="2048">{{.description}}</textarea> + </div> + + <div class="inline field"> + <label></label> + <button class="ui primary button"> + {{ctx.Locale.Tr "repo.migrate_repo"}} + </button> + </div> </form> </div> </div> diff --git a/templates/repo/migrate/git.tmpl b/templates/repo/migrate/git.tmpl index 9c5f0d7d6d..b10c49c10e 100644 --- a/templates/repo/migrate/git.tmpl +++ b/templates/repo/migrate/git.tmpl @@ -1,88 +1,86 @@ {{template "base/head" .}} <div role="main" aria-label="{{.Title}}" class="page-content repository new migrate"> - <div class="ui middle very relaxed page grid"> - <div class="column"> - <form class="ui form" action="{{.Link}}" method="post"> + <div class="ui container medium-width"> + <h3 class="ui top attached header"> + {{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}} + <input id="service_type" type="hidden" name="service" value="{{.service}}"> + </h3> + <div class="ui attached segment"> + {{template "base/alert" .}} + <form class="ui form left-right-form" action="{{.Link}}" method="post"> {{template "base/disable_form_autofill"}} {{.CsrfTokenHtml}} - <h3 class="ui top attached header"> - {{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}} - <input id="service_type" type="hidden" name="service" value="{{.service}}"> - </h3> - <div class="ui attached segment"> - {{template "base/alert" .}} - <div class="inline required field {{if .Err_CloneAddr}}error{{end}}"> - <label for="clone_addr">{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label> - <input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required> - <span class="help"> - {{ctx.Locale.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{ctx.Locale.Tr "repo.migrate.clone_local_path"}}{{end}} - </span> - </div> - <div class="inline field {{if .Err_Auth}}error{{end}}"> - <label for="auth_username">{{ctx.Locale.Tr "username"}}</label> - <input id="auth_username" name="auth_username" value="{{.auth_username}}" {{if not .auth_username}}data-need-clear="true"{{end}}> - </div> - <div class="inline field {{if .Err_Auth}}error{{end}}"> - <label for="auth_password">{{ctx.Locale.Tr "password"}}</label> - <input id="auth_password" name="auth_password" type="password" value="{{.auth_password}}"> - </div> + <div class="inline required field {{if .Err_CloneAddr}}error{{end}}"> + <label for="clone_addr">{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label> + <input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required> + <span class="help"> + {{ctx.Locale.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{ctx.Locale.Tr "repo.migrate.clone_local_path"}}{{end}} + </span> + </div> + <div class="inline field {{if .Err_Auth}}error{{end}}"> + <label for="auth_username">{{ctx.Locale.Tr "username"}}</label> + <input id="auth_username" name="auth_username" value="{{.auth_username}}" {{if not .auth_username}}data-need-clear="true"{{end}}> + </div> + <div class="inline field {{if .Err_Auth}}error{{end}}"> + <label for="auth_password">{{ctx.Locale.Tr "password"}}</label> + <input id="auth_password" name="auth_password" type="password" value="{{.auth_password}}"> + </div> - {{template "repo/migrate/options" .}} + {{template "repo/migrate/options" .}} - <div class="divider"></div> + <div class="divider"></div> - <div class="inline required field {{if .Err_Owner}}error{{end}}"> - <label>{{ctx.Locale.Tr "repo.owner"}}</label> - <div class="ui selection owner dropdown"> - <input type="hidden" id="uid" name="uid" value="{{.ContextUser.ID}}" required> - <span class="text truncated-item-container" title="{{.ContextUser.Name}}"> - {{ctx.AvatarUtils.Avatar .ContextUser}} - <span class="truncated-item-name">{{.ContextUser.ShortName 40}}</span> - </span> - {{svg "octicon-triangle-down" 14 "dropdown icon"}} - <div class="menu" title="{{.SignedUser.Name}}"> - <div class="item truncated-item-container" data-value="{{.SignedUser.ID}}"> - {{ctx.AvatarUtils.Avatar .SignedUser}} - <span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span> - </div> - {{range .Orgs}} - <div class="item truncated-item-container" data-value="{{.ID}}" title="{{.Name}}"> - {{ctx.AvatarUtils.Avatar .}} - <span class="truncated-item-name">{{.ShortName 40}}</span> - </div> - {{end}} + <div class="inline required field {{if .Err_Owner}}error{{end}}"> + <label>{{ctx.Locale.Tr "repo.owner"}}</label> + <div class="ui selection owner dropdown"> + <input type="hidden" id="uid" name="uid" value="{{.ContextUser.ID}}" required> + <span class="text truncated-item-container" title="{{.ContextUser.Name}}"> + {{ctx.AvatarUtils.Avatar .ContextUser}} + <span class="truncated-item-name">{{.ContextUser.ShortName 40}}</span> + </span> + {{svg "octicon-triangle-down" 14 "dropdown icon"}} + <div class="menu" title="{{.SignedUser.Name}}"> + <div class="item truncated-item-container" data-value="{{.SignedUser.ID}}"> + {{ctx.AvatarUtils.Avatar .SignedUser}} + <span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span> </div> - </div> - </div> - - <div class="inline required field {{if .Err_RepoName}}error{{end}}"> - <label for="repo_name">{{ctx.Locale.Tr "repo.repo_name"}}</label> - <input id="repo_name" name="repo_name" value="{{.repo_name}}" required maxlength="100"> - </div> - <div class="inline field"> - <label>{{ctx.Locale.Tr "repo.visibility"}}</label> - <div class="ui checkbox"> - {{if .IsForcedPrivate}} - <input name="private" type="checkbox" checked disabled> - <label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label> - {{else}} - <input name="private" type="checkbox" {{if .private}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label> + {{range .Orgs}} + <div class="item truncated-item-container" data-value="{{.ID}}" title="{{.Name}}"> + {{ctx.AvatarUtils.Avatar .}} + <span class="truncated-item-name">{{.ShortName 40}}</span> + </div> {{end}} </div> </div> - <div class="inline field {{if .Err_Description}}error{{end}}"> - <label for="description">{{ctx.Locale.Tr "repo.repo_desc"}}</label> - <textarea id="description" name="description" maxlength="2048">{{.description}}</textarea> - </div> + </div> - <div class="inline field"> - <label></label> - <button class="ui primary button"> - {{ctx.Locale.Tr "repo.migrate_repo"}} - </button> + <div class="inline required field {{if .Err_RepoName}}error{{end}}"> + <label for="repo_name">{{ctx.Locale.Tr "repo.repo_name"}}</label> + <input id="repo_name" name="repo_name" value="{{.repo_name}}" required maxlength="100"> + </div> + <div class="inline field"> + <label>{{ctx.Locale.Tr "repo.visibility"}}</label> + <div class="ui checkbox"> + {{if .IsForcedPrivate}} + <input name="private" type="checkbox" checked disabled> + <label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label> + {{else}} + <input name="private" type="checkbox" {{if .private}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label> + {{end}} </div> </div> + <div class="inline field {{if .Err_Description}}error{{end}}"> + <label for="description">{{ctx.Locale.Tr "repo.repo_desc"}}</label> + <textarea id="description" name="description" maxlength="2048">{{.description}}</textarea> + </div> + + <div class="inline field"> + <label></label> + <button class="ui primary button"> + {{ctx.Locale.Tr "repo.migrate_repo"}} + </button> + </div> </form> </div> </div> diff --git a/templates/repo/migrate/gitbucket.tmpl b/templates/repo/migrate/gitbucket.tmpl index b667fa828a..80d2491e91 100644 --- a/templates/repo/migrate/gitbucket.tmpl +++ b/templates/repo/migrate/gitbucket.tmpl @@ -1,130 +1,128 @@ {{template "base/head" .}} <div role="main" aria-label="{{.Title}}" class="page-content repository new migrate"> - <div class="ui middle very relaxed page grid"> - <div class="column"> - <form class="ui form" action="{{.Link}}" method="post"> + <div class="ui container medium-width"> + <h3 class="ui top attached header"> + {{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}} + <input id="service_type" type="hidden" name="service" value="{{.service}}"> + </h3> + <div class="ui attached segment"> + {{template "base/alert" .}} + <form class="ui form left-right-form" action="{{.Link}}" method="post"> {{template "base/disable_form_autofill"}} {{.CsrfTokenHtml}} - <h3 class="ui top attached header"> - {{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}} - <input id="service_type" type="hidden" name="service" value="{{.service}}"> - </h3> - <div class="ui attached segment"> - {{template "base/alert" .}} - <div class="inline required field {{if .Err_CloneAddr}}error{{end}}"> - <label for="clone_addr">{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label> - <input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required> - <span class="help"> - {{ctx.Locale.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{ctx.Locale.Tr "repo.migrate.clone_local_path"}}{{end}} - </span> - </div> + <div class="inline required field {{if .Err_CloneAddr}}error{{end}}"> + <label for="clone_addr">{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label> + <input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required> + <span class="help"> + {{ctx.Locale.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{ctx.Locale.Tr "repo.migrate.clone_local_path"}}{{end}} + </span> + </div> - <div class="inline field {{if .Err_Auth}}error{{end}}"> - <label for="auth_username">{{ctx.Locale.Tr "username"}}</label> - <input id="auth_username" name="auth_username" value="{{.auth_username}}" {{if not .auth_username}}data-need-clear="true"{{end}}> - </div> - <div class="inline field {{if .Err_Auth}}error{{end}}"> - <label for="auth_password">{{ctx.Locale.Tr "password"}}</label> - <input id="auth_password" name="auth_password" type="password" value="{{.auth_password}}"> - </div> + <div class="inline field {{if .Err_Auth}}error{{end}}"> + <label for="auth_username">{{ctx.Locale.Tr "username"}}</label> + <input id="auth_username" name="auth_username" value="{{.auth_username}}" {{if not .auth_username}}data-need-clear="true"{{end}}> + </div> + <div class="inline field {{if .Err_Auth}}error{{end}}"> + <label for="auth_password">{{ctx.Locale.Tr "password"}}</label> + <input id="auth_password" name="auth_password" type="password" value="{{.auth_password}}"> + </div> + + {{template "repo/migrate/options" .}} - {{template "repo/migrate/options" .}} + <div class="inline field"> + <label>{{ctx.Locale.Tr "repo.migrate_items"}}</label> + <div class="ui checkbox"> + <input name="wiki" type="checkbox" {{if .wiki}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_wiki"}}</label> + </div> + </div> + <div id="migrate_items" class="inline field"> + <span class="help">{{ctx.Locale.Tr "repo.migrate.migrate_items_options"}}</span> <div class="inline field"> - <label>{{ctx.Locale.Tr "repo.migrate_items"}}</label> + <label></label> <div class="ui checkbox"> - <input name="wiki" type="checkbox" {{if .wiki}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_wiki"}}</label> + <input name="labels" type="checkbox" {{if .labels}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_labels"}}</label> + </div> + <div class="ui checkbox"> + <input name="issues" type="checkbox" {{if .issues}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_issues"}}</label> </div> </div> - - <div id="migrate_items"> - <span class="help">{{ctx.Locale.Tr "repo.migrate.migrate_items_options"}}</span> - <div class="inline field"> - <label></label> - <div class="ui checkbox"> - <input name="labels" type="checkbox" {{if .labels}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_labels"}}</label> - </div> - <div class="ui checkbox"> - <input name="issues" type="checkbox" {{if .issues}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_issues"}}</label> - </div> + <div class="inline field"> + <label></label> + <div class="ui checkbox"> + <input name="pull_requests" type="checkbox" {{if .pull_requests}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_pullrequests"}}</label> </div> - <div class="inline field"> - <label></label> - <div class="ui checkbox"> - <input name="pull_requests" type="checkbox" {{if .pull_requests}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_pullrequests"}}</label> - </div> - <div class="ui checkbox"> - <input name="releases" type="checkbox" {{if .releases}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_releases"}}</label> - </div> + <div class="ui checkbox"> + <input name="releases" type="checkbox" {{if .releases}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_releases"}}</label> </div> - <div class="inline field"> - <label></label> - <div class="ui checkbox"> - <input name="milestones" type="checkbox" {{if .milestones}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_milestones"}}</label> - </div> + </div> + <div class="inline field"> + <label></label> + <div class="ui checkbox"> + <input name="milestones" type="checkbox" {{if .milestones}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_milestones"}}</label> </div> </div> + </div> - <div class="divider"></div> + <div class="divider"></div> - <div class="inline required field {{if .Err_Owner}}error{{end}}"> - <label>{{ctx.Locale.Tr "repo.owner"}}</label> - <div class="ui selection owner dropdown"> - <input type="hidden" id="uid" name="uid" value="{{.ContextUser.ID}}" required> - <span class="text truncated-item-container" title="{{.ContextUser.Name}}"> - {{ctx.AvatarUtils.Avatar .ContextUser 28 "mini"}} - <span class="truncated-item-name">{{.ContextUser.ShortName 40}}</span> - </span> - {{svg "octicon-triangle-down" 14 "dropdown icon"}} - <div class="menu" title="{{.SignedUser.Name}}"> - <div class="item truncated-item-container" data-value="{{.SignedUser.ID}}"> - {{ctx.AvatarUtils.Avatar .SignedUser 28 "mini"}} - <span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span> - </div> - {{range .Orgs}} - <div class="item truncated-item-container" data-value="{{.ID}}" title="{{.Name}}"> - {{ctx.AvatarUtils.Avatar . 28 "mini"}} - <span class="truncated-item-name">{{.ShortName 40}}</span> - </div> - {{end}} + <div class="inline required field {{if .Err_Owner}}error{{end}}"> + <label>{{ctx.Locale.Tr "repo.owner"}}</label> + <div class="ui selection owner dropdown"> + <input type="hidden" id="uid" name="uid" value="{{.ContextUser.ID}}" required> + <span class="text truncated-item-container" title="{{.ContextUser.Name}}"> + {{ctx.AvatarUtils.Avatar .ContextUser 28 "mini"}} + <span class="truncated-item-name">{{.ContextUser.ShortName 40}}</span> + </span> + {{svg "octicon-triangle-down" 14 "dropdown icon"}} + <div class="menu" title="{{.SignedUser.Name}}"> + <div class="item truncated-item-container" data-value="{{.SignedUser.ID}}"> + {{ctx.AvatarUtils.Avatar .SignedUser 28 "mini"}} + <span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span> </div> - </div> - </div> - - <div class="inline required field {{if .Err_RepoName}}error{{end}}"> - <label for="repo_name">{{ctx.Locale.Tr "repo.repo_name"}}</label> - <input id="repo_name" name="repo_name" value="{{.repo_name}}" required maxlength="100"> - </div> - <div class="inline field"> - <label>{{ctx.Locale.Tr "repo.visibility"}}</label> - <div class="ui checkbox"> - {{if .IsForcedPrivate}} - <input name="private" type="checkbox" checked disabled> - <label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label> - {{else}} - <input name="private" type="checkbox" {{if .private}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label> + {{range .Orgs}} + <div class="item truncated-item-container" data-value="{{.ID}}" title="{{.Name}}"> + {{ctx.AvatarUtils.Avatar . 28 "mini"}} + <span class="truncated-item-name">{{.ShortName 40}}</span> + </div> {{end}} </div> </div> - <div class="inline field {{if .Err_Description}}error{{end}}"> - <label for="description">{{ctx.Locale.Tr "repo.repo_desc"}}</label> - <textarea id="description" name="description" maxlength="2048">{{.description}}</textarea> - </div> + </div> - <div class="inline field"> - <label></label> - <button class="ui primary button"> - {{ctx.Locale.Tr "repo.migrate_repo"}} - </button> + <div class="inline required field {{if .Err_RepoName}}error{{end}}"> + <label for="repo_name">{{ctx.Locale.Tr "repo.repo_name"}}</label> + <input id="repo_name" name="repo_name" value="{{.repo_name}}" required maxlength="100"> + </div> + <div class="inline field"> + <label>{{ctx.Locale.Tr "repo.visibility"}}</label> + <div class="ui checkbox"> + {{if .IsForcedPrivate}} + <input name="private" type="checkbox" checked disabled> + <label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label> + {{else}} + <input name="private" type="checkbox" {{if .private}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label> + {{end}} </div> </div> + <div class="inline field {{if .Err_Description}}error{{end}}"> + <label for="description">{{ctx.Locale.Tr "repo.repo_desc"}}</label> + <textarea id="description" name="description" maxlength="2048">{{.description}}</textarea> + </div> + + <div class="inline field"> + <label></label> + <button class="ui primary button"> + {{ctx.Locale.Tr "repo.migrate_repo"}} + </button> + </div> </form> </div> </div> diff --git a/templates/repo/migrate/gitea.tmpl b/templates/repo/migrate/gitea.tmpl index 3b8f377096..220295662e 100644 --- a/templates/repo/migrate/gitea.tmpl +++ b/templates/repo/migrate/gitea.tmpl @@ -1,126 +1,124 @@ {{template "base/head" .}} <div role="main" aria-label="{{.Title}}" class="page-content repository new migrate"> - <div class="ui middle very relaxed page grid"> - <div class="column"> - <form class="ui form" action="{{.Link}}" method="post"> + <div class="ui container medium-width"> + <h3 class="ui top attached header"> + {{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}} + <input id="service_type" type="hidden" name="service" value="{{.service}}"> + </h3> + <div class="ui attached segment"> + {{template "base/alert" .}} + <form class="ui form left-right-form" action="{{.Link}}" method="post"> {{.CsrfTokenHtml}} - <h3 class="ui top attached header"> - {{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}} - <input id="service_type" type="hidden" name="service" value="{{.service}}"> - </h3> - <div class="ui attached segment"> - {{template "base/alert" .}} - <div class="inline required field {{if .Err_CloneAddr}}error{{end}}"> - <label for="clone_addr">{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label> - <input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required> - <span class="help"> - {{ctx.Locale.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{ctx.Locale.Tr "repo.migrate.clone_local_path"}}{{end}} - </span> - </div> + <div class="inline required field {{if .Err_CloneAddr}}error{{end}}"> + <label for="clone_addr">{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label> + <input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required> + <span class="help"> + {{ctx.Locale.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{ctx.Locale.Tr "repo.migrate.clone_local_path"}}{{end}} + </span> + </div> - <div class="inline field {{if .Err_Auth}}error{{end}}"> - <label for="auth_token">{{ctx.Locale.Tr "access_token"}}</label> - <input id="auth_token" name="auth_token" type="password" autocomplete="new-password" value="{{.auth_token}}" {{if not .auth_token}} data-need-clear="true" {{end}}> - <a target="_blank" href="https://docs.gitea.com/development/api-usage">{{svg "octicon-question"}}</a> - </div> + <div class="inline field {{if .Err_Auth}}error{{end}}"> + <label for="auth_token">{{ctx.Locale.Tr "access_token"}}</label> + <input id="auth_token" name="auth_token" type="password" autocomplete="new-password" value="{{.auth_token}}" {{if not .auth_token}} data-need-clear="true" {{end}}> + <a target="_blank" href="https://docs.gitea.com/development/api-usage">{{svg "octicon-question"}}</a> + </div> - {{template "repo/migrate/options" .}} + {{template "repo/migrate/options" .}} + <div class="inline field"> + <label>{{ctx.Locale.Tr "repo.migrate_items"}}</label> + <div class="ui checkbox"> + <input name="wiki" type="checkbox" {{if .wiki}} checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_wiki"}}</label> + </div> + </div> + + <div id="migrate_items" class="inline field"> + <span class="help">{{ctx.Locale.Tr "repo.migrate.migrate_items_options"}}</span> <div class="inline field"> - <label>{{ctx.Locale.Tr "repo.migrate_items"}}</label> + <label></label> + <div class="ui checkbox"> + <input name="labels" type="checkbox" {{if .labels}} checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_labels"}}</label> + </div> <div class="ui checkbox"> - <input name="wiki" type="checkbox" {{if .wiki}} checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_wiki"}}</label> + <input name="issues" type="checkbox" {{if .issues}} checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_issues"}}</label> </div> </div> - - <div id="migrate_items"> - <span class="help">{{ctx.Locale.Tr "repo.migrate.migrate_items_options"}}</span> - <div class="inline field"> - <label></label> - <div class="ui checkbox"> - <input name="labels" type="checkbox" {{if .labels}} checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_labels"}}</label> - </div> - <div class="ui checkbox"> - <input name="issues" type="checkbox" {{if .issues}} checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_issues"}}</label> - </div> + <div class="inline field"> + <label></label> + <div class="ui checkbox"> + <input name="pull_requests" type="checkbox" {{if .pull_requests}} checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_pullrequests"}}</label> </div> - <div class="inline field"> - <label></label> - <div class="ui checkbox"> - <input name="pull_requests" type="checkbox" {{if .pull_requests}} checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_pullrequests"}}</label> - </div> - <div class="ui checkbox"> - <input name="releases" type="checkbox" {{if .releases}} checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_releases"}}</label> - </div> + <div class="ui checkbox"> + <input name="releases" type="checkbox" {{if .releases}} checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_releases"}}</label> </div> - <div class="inline field"> - <label></label> - <div class="ui checkbox"> - <input name="milestones" type="checkbox" {{if .milestones}} checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_milestones"}}</label> - </div> + </div> + <div class="inline field"> + <label></label> + <div class="ui checkbox"> + <input name="milestones" type="checkbox" {{if .milestones}} checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_milestones"}}</label> </div> </div> + </div> - <div class="divider"></div> + <div class="divider"></div> - <div class="inline required field {{if .Err_Owner}}error{{end}}"> - <label>{{ctx.Locale.Tr "repo.owner"}}</label> - <div class="ui selection owner dropdown"> - <input type="hidden" id="uid" name="uid" value="{{.ContextUser.ID}}" required> - <span class="text truncated-item-container" title="{{.ContextUser.Name}}"> - {{ctx.AvatarUtils.Avatar .ContextUser}} - <span class="truncated-item-name">{{.ContextUser.ShortName 40}}</span> - </span> - {{svg "octicon-triangle-down" 14 "dropdown icon"}} - <div class="menu" title="{{.SignedUser.Name}}"> - <div class="item truncated-item-container" data-value="{{.SignedUser.ID}}"> - {{ctx.AvatarUtils.Avatar .SignedUser}} - <span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span> - </div> - {{range .Orgs}} - <div class="item truncated-item-container" data-value="{{.ID}}" title="{{.Name}}"> - {{ctx.AvatarUtils.Avatar .}} - <span class="truncated-item-name">{{.ShortName 40}}</span> - </div> - {{end}} + <div class="inline required field {{if .Err_Owner}}error{{end}}"> + <label>{{ctx.Locale.Tr "repo.owner"}}</label> + <div class="ui selection owner dropdown"> + <input type="hidden" id="uid" name="uid" value="{{.ContextUser.ID}}" required> + <span class="text truncated-item-container" title="{{.ContextUser.Name}}"> + {{ctx.AvatarUtils.Avatar .ContextUser}} + <span class="truncated-item-name">{{.ContextUser.ShortName 40}}</span> + </span> + {{svg "octicon-triangle-down" 14 "dropdown icon"}} + <div class="menu" title="{{.SignedUser.Name}}"> + <div class="item truncated-item-container" data-value="{{.SignedUser.ID}}"> + {{ctx.AvatarUtils.Avatar .SignedUser}} + <span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span> + </div> + {{range .Orgs}} + <div class="item truncated-item-container" data-value="{{.ID}}" title="{{.Name}}"> + {{ctx.AvatarUtils.Avatar .}} + <span class="truncated-item-name">{{.ShortName 40}}</span> </div> - </div> - </div> - - <div class="inline required field {{if .Err_RepoName}}error{{end}}"> - <label for="repo_name">{{ctx.Locale.Tr "repo.repo_name"}}</label> - <input id="repo_name" name="repo_name" value="{{.repo_name}}" required maxlength="100"> - </div> - <div class="inline field"> - <label>{{ctx.Locale.Tr "repo.visibility"}}</label> - <div class="ui checkbox"> - {{if .IsForcedPrivate}} - <input name="private" type="checkbox" checked disabled> - <label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label> - {{else}} - <input name="private" type="checkbox" {{if .private}} checked{{end}}> - <label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label> {{end}} </div> </div> - <div class="inline field {{if .Err_Description}}error{{end}}"> - <label for="description">{{ctx.Locale.Tr "repo.repo_desc"}}</label> - <textarea id="description" name="description" maxlength="2048">{{.description}}</textarea> - </div> + </div> - <div class="inline field"> - <label></label> - <button class="ui primary button"> - {{ctx.Locale.Tr "repo.migrate_repo"}} - </button> + <div class="inline required field {{if .Err_RepoName}}error{{end}}"> + <label for="repo_name">{{ctx.Locale.Tr "repo.repo_name"}}</label> + <input id="repo_name" name="repo_name" value="{{.repo_name}}" required maxlength="100"> + </div> + <div class="inline field"> + <label>{{ctx.Locale.Tr "repo.visibility"}}</label> + <div class="ui checkbox"> + {{if .IsForcedPrivate}} + <input name="private" type="checkbox" checked disabled> + <label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label> + {{else}} + <input name="private" type="checkbox" {{if .private}} checked{{end}}> + <label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label> + {{end}} </div> </div> + <div class="inline field {{if .Err_Description}}error{{end}}"> + <label for="description">{{ctx.Locale.Tr "repo.repo_desc"}}</label> + <textarea id="description" name="description" maxlength="2048">{{.description}}</textarea> + </div> + + <div class="inline field"> + <label></label> + <button class="ui primary button"> + {{ctx.Locale.Tr "repo.migrate_repo"}} + </button> + </div> </form> </div> </div> diff --git a/templates/repo/migrate/github.tmpl b/templates/repo/migrate/github.tmpl index 3535eddfc2..d1aa4c1f29 100644 --- a/templates/repo/migrate/github.tmpl +++ b/templates/repo/migrate/github.tmpl @@ -1,128 +1,126 @@ {{template "base/head" .}} <div role="main" aria-label="{{.Title}}" class="page-content repository new migrate"> - <div class="ui middle very relaxed page grid"> - <div class="column"> - <form class="ui form" action="{{.Link}}" method="post"> + <div class="ui container medium-width"> + <h3 class="ui top attached header"> + {{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}} + <input id="service_type" type="hidden" name="service" value="{{.service}}"> + </h3> + <div class="ui attached segment"> + {{template "base/alert" .}} + <form class="ui form left-right-form" action="{{.Link}}" method="post"> {{.CsrfTokenHtml}} - <h3 class="ui top attached header"> - {{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}} - <input id="service_type" type="hidden" name="service" value="{{.service}}"> - </h3> - <div class="ui attached segment"> - {{template "base/alert" .}} - <div class="inline required field {{if .Err_CloneAddr}}error{{end}}"> - <label for="clone_addr">{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label> - <input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required> - <span class="help"> - {{ctx.Locale.Tr "repo.migrate.clone_address_desc"}} - </span> - </div> + <div class="inline required field {{if .Err_CloneAddr}}error{{end}}"> + <label for="clone_addr">{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label> + <input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required> + <span class="help"> + {{ctx.Locale.Tr "repo.migrate.clone_address_desc"}} + </span> + </div> - <div class="inline field {{if .Err_Auth}}error{{end}}"> - <label for="auth_token">{{ctx.Locale.Tr "access_token"}}</label> - <input id="auth_token" name="auth_token" type="password" autocomplete="new-password" value="{{.auth_token}}" {{if not .auth_token}}data-need-clear="true"{{end}}> - <a target="_blank" href="https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token">{{svg "octicon-question"}}</a> - <span class="help"> - {{ctx.Locale.Tr "repo.migrate.github_token_desc"}} - </span> - </div> + <div class="inline field {{if .Err_Auth}}error{{end}}"> + <label for="auth_token">{{ctx.Locale.Tr "access_token"}}</label> + <input id="auth_token" name="auth_token" type="password" autocomplete="new-password" value="{{.auth_token}}" {{if not .auth_token}}data-need-clear="true"{{end}}> + <a target="_blank" href="https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token">{{svg "octicon-question"}}</a> + <span class="help"> + {{ctx.Locale.Tr "repo.migrate.github_token_desc"}} + </span> + </div> - {{template "repo/migrate/options" .}} + {{template "repo/migrate/options" .}} + <div class="inline field"> + <label>{{ctx.Locale.Tr "repo.migrate_items"}}</label> + <div class="ui checkbox"> + <input name="wiki" type="checkbox" {{if .wiki}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_wiki"}}</label> + </div> + </div> + <div id="migrate_items" class="inline field"> + <span class="help">{{ctx.Locale.Tr "repo.migrate.migrate_items_options"}}</span> <div class="inline field"> - <label>{{ctx.Locale.Tr "repo.migrate_items"}}</label> + <label></label> + <div class="ui checkbox"> + <input name="labels" type="checkbox" {{if .labels}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_labels"}}</label> + </div> <div class="ui checkbox"> - <input name="wiki" type="checkbox" {{if .wiki}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_wiki"}}</label> + <input name="issues" type="checkbox" {{if .issues}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_issues"}}</label> </div> </div> - <div id="migrate_items"> - <span class="help">{{ctx.Locale.Tr "repo.migrate.migrate_items_options"}}</span> - <div class="inline field"> - <label></label> - <div class="ui checkbox"> - <input name="labels" type="checkbox" {{if .labels}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_labels"}}</label> - </div> - <div class="ui checkbox"> - <input name="issues" type="checkbox" {{if .issues}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_issues"}}</label> - </div> + <div class="inline field"> + <label></label> + <div class="ui checkbox"> + <input name="pull_requests" type="checkbox" {{if .pull_requests}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_pullrequests"}}</label> </div> - <div class="inline field"> - <label></label> - <div class="ui checkbox"> - <input name="pull_requests" type="checkbox" {{if .pull_requests}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_pullrequests"}}</label> - </div> - <div class="ui checkbox"> - <input name="releases" type="checkbox" {{if .releases}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_releases"}}</label> - </div> + <div class="ui checkbox"> + <input name="releases" type="checkbox" {{if .releases}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_releases"}}</label> </div> - <div class="inline field"> - <label></label> - <div class="ui checkbox"> - <input name="milestones" type="checkbox" {{if .milestones}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_milestones"}}</label> - </div> + </div> + <div class="inline field"> + <label></label> + <div class="ui checkbox"> + <input name="milestones" type="checkbox" {{if .milestones}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_milestones"}}</label> </div> </div> + </div> - <div class="divider"></div> + <div class="divider"></div> - <div class="inline required field {{if .Err_Owner}}error{{end}}"> - <label>{{ctx.Locale.Tr "repo.owner"}}</label> - <div class="ui selection owner dropdown"> - <input type="hidden" id="uid" name="uid" value="{{.ContextUser.ID}}" required> - <span class="text truncated-item-container" title="{{.ContextUser.Name}}"> - {{ctx.AvatarUtils.Avatar .ContextUser 28 "mini"}} - <span class="truncated-item-name">{{.ContextUser.ShortName 40}}</span> - </span> - {{svg "octicon-triangle-down" 14 "dropdown icon"}} - <div class="menu" title="{{.SignedUser.Name}}"> - <div class="item truncated-item-container" data-value="{{.SignedUser.ID}}"> - {{ctx.AvatarUtils.Avatar .SignedUser 28 "mini"}} - <span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span> - </div> - {{range .Orgs}} - <div class="item truncated-item-container" data-value="{{.ID}}" title="{{.Name}}"> - {{ctx.AvatarUtils.Avatar . 28 "mini"}} - <span class="truncated-item-name">{{.ShortName 40}}</span> - </div> - {{end}} + <div class="inline required field {{if .Err_Owner}}error{{end}}"> + <label>{{ctx.Locale.Tr "repo.owner"}}</label> + <div class="ui selection owner dropdown"> + <input type="hidden" id="uid" name="uid" value="{{.ContextUser.ID}}" required> + <span class="text truncated-item-container" title="{{.ContextUser.Name}}"> + {{ctx.AvatarUtils.Avatar .ContextUser 28 "mini"}} + <span class="truncated-item-name">{{.ContextUser.ShortName 40}}</span> + </span> + {{svg "octicon-triangle-down" 14 "dropdown icon"}} + <div class="menu" title="{{.SignedUser.Name}}"> + <div class="item truncated-item-container" data-value="{{.SignedUser.ID}}"> + {{ctx.AvatarUtils.Avatar .SignedUser 28 "mini"}} + <span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span> </div> - </div> - </div> - - <div class="inline required field {{if .Err_RepoName}}error{{end}}"> - <label for="repo_name">{{ctx.Locale.Tr "repo.repo_name"}}</label> - <input id="repo_name" name="repo_name" value="{{.repo_name}}" required maxlength="100"> - </div> - <div class="inline field"> - <label>{{ctx.Locale.Tr "repo.visibility"}}</label> - <div class="ui checkbox"> - {{if .IsForcedPrivate}} - <input name="private" type="checkbox" checked disabled> - <label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label> - {{else}} - <input name="private" type="checkbox" {{if .private}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label> + {{range .Orgs}} + <div class="item truncated-item-container" data-value="{{.ID}}" title="{{.Name}}"> + {{ctx.AvatarUtils.Avatar . 28 "mini"}} + <span class="truncated-item-name">{{.ShortName 40}}</span> + </div> {{end}} </div> </div> - <div class="inline field {{if .Err_Description}}error{{end}}"> - <label for="description">{{ctx.Locale.Tr "repo.repo_desc"}}</label> - <textarea id="description" name="description" maxlength="2048">{{.description}}</textarea> - </div> + </div> - <div class="inline field"> - <label></label> - <button class="ui primary button"> - {{ctx.Locale.Tr "repo.migrate_repo"}} - </button> + <div class="inline required field {{if .Err_RepoName}}error{{end}}"> + <label for="repo_name">{{ctx.Locale.Tr "repo.repo_name"}}</label> + <input id="repo_name" name="repo_name" value="{{.repo_name}}" required maxlength="100"> + </div> + <div class="inline field"> + <label>{{ctx.Locale.Tr "repo.visibility"}}</label> + <div class="ui checkbox"> + {{if .IsForcedPrivate}} + <input name="private" type="checkbox" checked disabled> + <label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label> + {{else}} + <input name="private" type="checkbox" {{if .private}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label> + {{end}} </div> </div> + <div class="inline field {{if .Err_Description}}error{{end}}"> + <label for="description">{{ctx.Locale.Tr "repo.repo_desc"}}</label> + <textarea id="description" name="description" maxlength="2048">{{.description}}</textarea> + </div> + + <div class="inline field"> + <label></label> + <button class="ui primary button"> + {{ctx.Locale.Tr "repo.migrate_repo"}} + </button> + </div> </form> </div> </div> diff --git a/templates/repo/migrate/gitlab.tmpl b/templates/repo/migrate/gitlab.tmpl index f705fb3090..87a04d7849 100644 --- a/templates/repo/migrate/gitlab.tmpl +++ b/templates/repo/migrate/gitlab.tmpl @@ -1,125 +1,123 @@ {{template "base/head" .}} <div role="main" aria-label="{{.Title}}" class="page-content repository new migrate"> - <div class="ui middle very relaxed page grid"> - <div class="column"> - <form class="ui form" action="{{.Link}}" method="post"> + <div class="ui container medium-width"> + <h3 class="ui top attached header"> + {{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}} + <input id="service_type" type="hidden" name="service" value="{{.service}}"> + </h3> + <div class="ui attached segment"> + {{template "base/alert" .}} + <form class="ui form left-right-form" action="{{.Link}}" method="post"> {{.CsrfTokenHtml}} - <h3 class="ui top attached header"> - {{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}} - <input id="service_type" type="hidden" name="service" value="{{.service}}"> - </h3> - <div class="ui attached segment"> - {{template "base/alert" .}} - <div class="inline required field {{if .Err_CloneAddr}}error{{end}}"> - <label for="clone_addr">{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label> - <input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required> - <span class="help"> - {{ctx.Locale.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{ctx.Locale.Tr "repo.migrate.clone_local_path"}}{{end}} - </span> - </div> + <div class="inline required field {{if .Err_CloneAddr}}error{{end}}"> + <label for="clone_addr">{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label> + <input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required> + <span class="help"> + {{ctx.Locale.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{ctx.Locale.Tr "repo.migrate.clone_local_path"}}{{end}} + </span> + </div> - <div class="inline field {{if .Err_Auth}}error{{end}}"> - <label for="auth_token">{{ctx.Locale.Tr "access_token"}}</label> - <input id="auth_token" name="auth_token" type="password" autocomplete="new-password" value="{{.auth_token}}" {{if not .auth_token}}data-need-clear="true"{{end}}> - <a target="_blank" href="https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html">{{svg "octicon-question"}}</a> - </div> + <div class="inline field {{if .Err_Auth}}error{{end}}"> + <label for="auth_token">{{ctx.Locale.Tr "access_token"}}</label> + <input id="auth_token" name="auth_token" type="password" autocomplete="new-password" value="{{.auth_token}}" {{if not .auth_token}}data-need-clear="true"{{end}}> + <a target="_blank" href="https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html">{{svg "octicon-question"}}</a> + </div> - {{template "repo/migrate/options" .}} + {{template "repo/migrate/options" .}} + <div class="inline field"> + <label>{{ctx.Locale.Tr "repo.migrate_items"}}</label> + <div class="ui checkbox"> + <input name="wiki" type="checkbox" {{if .wiki}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_wiki"}}</label> + </div> + </div> + <div id="migrate_items" class="inline field"> + <span class="help">{{ctx.Locale.Tr "repo.migrate.migrate_items_options"}}</span> <div class="inline field"> - <label>{{ctx.Locale.Tr "repo.migrate_items"}}</label> + <label></label> <div class="ui checkbox"> - <input name="wiki" type="checkbox" {{if .wiki}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_wiki"}}</label> + <input name="labels" type="checkbox" {{if .labels}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_labels"}}</label> + </div> + <div class="ui checkbox"> + <input name="issues" type="checkbox" {{if .issues}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_issues"}}</label> </div> </div> - <div id="migrate_items"> - <span class="help">{{ctx.Locale.Tr "repo.migrate.migrate_items_options"}}</span> - <div class="inline field"> - <label></label> - <div class="ui checkbox"> - <input name="labels" type="checkbox" {{if .labels}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_labels"}}</label> - </div> - <div class="ui checkbox"> - <input name="issues" type="checkbox" {{if .issues}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_issues"}}</label> - </div> + <div class="inline field"> + <label></label> + <div class="ui checkbox"> + <input name="pull_requests" type="checkbox" {{if .pull_requests}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_merge_requests"}}</label> </div> - <div class="inline field"> - <label></label> - <div class="ui checkbox"> - <input name="pull_requests" type="checkbox" {{if .pull_requests}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_merge_requests"}}</label> - </div> - <div class="ui checkbox"> - <input name="releases" type="checkbox" {{if .releases}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_releases"}}</label> - </div> + <div class="ui checkbox"> + <input name="releases" type="checkbox" {{if .releases}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_releases"}}</label> </div> - <div class="inline field"> - <label></label> - <div class="ui checkbox"> - <input name="milestones" type="checkbox" {{if .milestones}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_milestones"}}</label> - </div> + </div> + <div class="inline field"> + <label></label> + <div class="ui checkbox"> + <input name="milestones" type="checkbox" {{if .milestones}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_milestones"}}</label> </div> </div> + </div> - <div class="divider"></div> + <div class="divider"></div> - <div class="inline required field {{if .Err_Owner}}error{{end}}"> - <label>{{ctx.Locale.Tr "repo.owner"}}</label> - <div class="ui selection owner dropdown"> - <input type="hidden" id="uid" name="uid" value="{{.ContextUser.ID}}" required> - <span class="text truncated-item-container" title="{{.ContextUser.Name}}"> - {{ctx.AvatarUtils.Avatar .ContextUser 28 "mini"}} - <span class="truncated-item-name">{{.ContextUser.ShortName 40}}</span> - </span> - {{svg "octicon-triangle-down" 14 "dropdown icon"}} - <div class="menu" title="{{.SignedUser.Name}}"> - <div class="item truncated-item-container" data-value="{{.SignedUser.ID}}"> - {{ctx.AvatarUtils.Avatar .SignedUser 28 "mini"}} - <span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span> - </div> - {{range .Orgs}} - <div class="item truncated-item-container" data-value="{{.ID}}" title="{{.Name}}"> - {{ctx.AvatarUtils.Avatar . 28 "mini"}} - <span class="truncated-item-name">{{.ShortName 40}}</span> - </div> - {{end}} + <div class="inline required field {{if .Err_Owner}}error{{end}}"> + <label>{{ctx.Locale.Tr "repo.owner"}}</label> + <div class="ui selection owner dropdown"> + <input type="hidden" id="uid" name="uid" value="{{.ContextUser.ID}}" required> + <span class="text truncated-item-container" title="{{.ContextUser.Name}}"> + {{ctx.AvatarUtils.Avatar .ContextUser 28 "mini"}} + <span class="truncated-item-name">{{.ContextUser.ShortName 40}}</span> + </span> + {{svg "octicon-triangle-down" 14 "dropdown icon"}} + <div class="menu" title="{{.SignedUser.Name}}"> + <div class="item truncated-item-container" data-value="{{.SignedUser.ID}}"> + {{ctx.AvatarUtils.Avatar .SignedUser 28 "mini"}} + <span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span> </div> - </div> - </div> - - <div class="inline required field {{if .Err_RepoName}}error{{end}}"> - <label for="repo_name">{{ctx.Locale.Tr "repo.repo_name"}}</label> - <input id="repo_name" name="repo_name" value="{{.repo_name}}" required maxlength="100"> - </div> - <div class="inline field"> - <label>{{ctx.Locale.Tr "repo.visibility"}}</label> - <div class="ui checkbox"> - {{if .IsForcedPrivate}} - <input name="private" type="checkbox" checked disabled> - <label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label> - {{else}} - <input name="private" type="checkbox" {{if .private}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label> + {{range .Orgs}} + <div class="item truncated-item-container" data-value="{{.ID}}" title="{{.Name}}"> + {{ctx.AvatarUtils.Avatar . 28 "mini"}} + <span class="truncated-item-name">{{.ShortName 40}}</span> + </div> {{end}} </div> </div> - <div class="inline field {{if .Err_Description}}error{{end}}"> - <label for="description">{{ctx.Locale.Tr "repo.repo_desc"}}</label> - <textarea id="description" name="description" maxlength="2048">{{.description}}</textarea> - </div> + </div> - <div class="inline field"> - <label></label> - <button class="ui primary button"> - {{ctx.Locale.Tr "repo.migrate_repo"}} - </button> + <div class="inline required field {{if .Err_RepoName}}error{{end}}"> + <label for="repo_name">{{ctx.Locale.Tr "repo.repo_name"}}</label> + <input id="repo_name" name="repo_name" value="{{.repo_name}}" required maxlength="100"> + </div> + <div class="inline field"> + <label>{{ctx.Locale.Tr "repo.visibility"}}</label> + <div class="ui checkbox"> + {{if .IsForcedPrivate}} + <input name="private" type="checkbox" checked disabled> + <label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label> + {{else}} + <input name="private" type="checkbox" {{if .private}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label> + {{end}} </div> </div> + <div class="inline field {{if .Err_Description}}error{{end}}"> + <label for="description">{{ctx.Locale.Tr "repo.repo_desc"}}</label> + <textarea id="description" name="description" maxlength="2048">{{.description}}</textarea> + </div> + + <div class="inline field"> + <label></label> + <button class="ui primary button"> + {{ctx.Locale.Tr "repo.migrate_repo"}} + </button> + </div> </form> </div> </div> diff --git a/templates/repo/migrate/gogs.tmpl b/templates/repo/migrate/gogs.tmpl index eca83b1636..a4d05e8acd 100644 --- a/templates/repo/migrate/gogs.tmpl +++ b/templates/repo/migrate/gogs.tmpl @@ -1,128 +1,126 @@ {{template "base/head" .}} <div role="main" aria-label="{{.Title}}" class="page-content repository new migrate"> - <div class="ui middle very relaxed page grid"> - <div class="column"> - <form class="ui form" action="{{.Link}}" method="post"> + <div class="ui container medium-width"> + <h3 class="ui top attached header"> + {{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}} + <input id="service_type" type="hidden" name="service" value="{{.service}}"> + </h3> + <div class="ui attached segment"> + {{template "base/alert" .}} + <form class="ui form left-right-form" action="{{.Link}}" method="post"> {{.CsrfTokenHtml}} - <h3 class="ui top attached header"> - {{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}} - <input id="service_type" type="hidden" name="service" value="{{.service}}"> - </h3> - <div class="ui attached segment"> - {{template "base/alert" .}} - <div class="inline required field {{if .Err_CloneAddr}}error{{end}}"> - <label for="clone_addr">{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label> - <input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required> - <span class="help"> - {{ctx.Locale.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{ctx.Locale.Tr "repo.migrate.clone_local_path"}}{{end}} - </span> - </div> + <div class="inline required field {{if .Err_CloneAddr}}error{{end}}"> + <label for="clone_addr">{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label> + <input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required> + <span class="help"> + {{ctx.Locale.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{ctx.Locale.Tr "repo.migrate.clone_local_path"}}{{end}} + </span> + </div> - <div class="inline field {{if .Err_Auth}}error{{end}}"> - <label for="auth_token">{{ctx.Locale.Tr "access_token"}}</label> - <input id="auth_token" name="auth_token" type="password" autocomplete="new-password" value="{{.auth_token}}" {{if not .auth_token}} data-need-clear="true" {{end}}> - <!-- <a target="_blank" href="https://docs.gitea.com/development/api-usage">{{svg "octicon-question"}}</a> --> - </div> + <div class="inline field {{if .Err_Auth}}error{{end}}"> + <label for="auth_token">{{ctx.Locale.Tr "access_token"}}</label> + <input id="auth_token" name="auth_token" type="password" autocomplete="new-password" value="{{.auth_token}}" {{if not .auth_token}} data-need-clear="true" {{end}}> + <!-- <a target="_blank" href="https://docs.gitea.com/development/api-usage">{{svg "octicon-question"}}</a> --> + </div> - {{template "repo/migrate/options" .}} + {{template "repo/migrate/options" .}} + <div class="inline field"> + <label>{{ctx.Locale.Tr "repo.migrate_items"}}</label> + <div class="ui checkbox"> + <input name="wiki" type="checkbox" {{if .wiki}} checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_wiki"}}</label> + </div> + </div> + + <div id="migrate_items" class="inline field"> + <span class="help">{{ctx.Locale.Tr "repo.migrate.migrate_items_options"}}</span> <div class="inline field"> - <label>{{ctx.Locale.Tr "repo.migrate_items"}}</label> + <label></label> + <div class="ui checkbox"> + <input name="labels" type="checkbox" {{if .labels}} checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_labels"}}</label> + </div> <div class="ui checkbox"> - <input name="wiki" type="checkbox" {{if .wiki}} checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_wiki"}}</label> + <input name="issues" type="checkbox" {{if .issues}} checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_issues"}}</label> </div> </div> - - <div id="migrate_items"> - <span class="help">{{ctx.Locale.Tr "repo.migrate.migrate_items_options"}}</span> - <div class="inline field"> - <label></label> - <div class="ui checkbox"> - <input name="labels" type="checkbox" {{if .labels}} checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_labels"}}</label> - </div> - <div class="ui checkbox"> - <input name="issues" type="checkbox" {{if .issues}} checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_issues"}}</label> - </div> + <div class="inline field"> + <label></label> + <div class="ui checkbox"> + <input name="milestones" type="checkbox" {{if .milestones}} checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_milestones"}}</label> </div> - <div class="inline field"> - <label></label> - <div class="ui checkbox"> - <input name="milestones" type="checkbox" {{if .milestones}} checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_milestones"}}</label> - </div> + </div> + <!-- Gogs do not support it + <div class="inline field"> + <label></label> + <div class="ui checkbox"> + <input name="pull_requests" type="checkbox" {{if .pull_requests}} checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_merge_requests"}}</label> </div> - <!-- Gogs do not support it - <div class="inline field"> - <label></label> - <div class="ui checkbox"> - <input name="pull_requests" type="checkbox" {{if .pull_requests}} checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_merge_requests"}}</label> - </div> - <div class="ui checkbox"> - <input name="releases" type="checkbox" {{if .releases}} checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_releases"}}</label> - </div> + <div class="ui checkbox"> + <input name="releases" type="checkbox" {{if .releases}} checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_releases"}}</label> </div> - --> </div> + --> + </div> - <div class="divider"></div> + <div class="divider"></div> - <div class="inline required field {{if .Err_Owner}}error{{end}}"> - <label>{{ctx.Locale.Tr "repo.owner"}}</label> - <div class="ui selection owner dropdown"> - <input type="hidden" id="uid" name="uid" value="{{.ContextUser.ID}}" required> - <span class="text truncated-item-container" title="{{.ContextUser.Name}}"> - {{ctx.AvatarUtils.Avatar .ContextUser}} - <span class="truncated-item-name">{{.ContextUser.ShortName 40}}</span> - </span> - {{svg "octicon-triangle-down" 14 "dropdown icon"}} - <div class="menu" title="{{.SignedUser.Name}}"> - <div class="item truncated-item-container" data-value="{{.SignedUser.ID}}"> - {{ctx.AvatarUtils.Avatar .SignedUser}} - <span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span> - </div> - {{range .Orgs}} - <div class="item truncated-item-container" data-value="{{.ID}}" title="{{.Name}}"> - {{ctx.AvatarUtils.Avatar .}} - <span class="truncated-item-name">{{.ShortName 40}}</span> - </div> - {{end}} + <div class="inline required field {{if .Err_Owner}}error{{end}}"> + <label>{{ctx.Locale.Tr "repo.owner"}}</label> + <div class="ui selection owner dropdown"> + <input type="hidden" id="uid" name="uid" value="{{.ContextUser.ID}}" required> + <span class="text truncated-item-container" title="{{.ContextUser.Name}}"> + {{ctx.AvatarUtils.Avatar .ContextUser}} + <span class="truncated-item-name">{{.ContextUser.ShortName 40}}</span> + </span> + {{svg "octicon-triangle-down" 14 "dropdown icon"}} + <div class="menu" title="{{.SignedUser.Name}}"> + <div class="item truncated-item-container" data-value="{{.SignedUser.ID}}"> + {{ctx.AvatarUtils.Avatar .SignedUser}} + <span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span> + </div> + {{range .Orgs}} + <div class="item truncated-item-container" data-value="{{.ID}}" title="{{.Name}}"> + {{ctx.AvatarUtils.Avatar .}} + <span class="truncated-item-name">{{.ShortName 40}}</span> </div> - </div> - </div> - - <div class="inline required field {{if .Err_RepoName}}error{{end}}"> - <label for="repo_name">{{ctx.Locale.Tr "repo.repo_name"}}</label> - <input id="repo_name" name="repo_name" value="{{.repo_name}}" required maxlength="100"> - </div> - <div class="inline field"> - <label>{{ctx.Locale.Tr "repo.visibility"}}</label> - <div class="ui checkbox"> - {{if .IsForcedPrivate}} - <input name="private" type="checkbox" checked disabled> - <label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label> - {{else}} - <input name="private" type="checkbox" {{if .private}} checked{{end}}> - <label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label> {{end}} </div> </div> - <div class="inline field {{if .Err_Description}}error{{end}}"> - <label for="description">{{ctx.Locale.Tr "repo.repo_desc"}}</label> - <textarea id="description" name="description" maxlength="2048">{{.description}}</textarea> - </div> + </div> - <div class="inline field"> - <label></label> - <button class="ui primary button"> - {{ctx.Locale.Tr "repo.migrate_repo"}} - </button> + <div class="inline required field {{if .Err_RepoName}}error{{end}}"> + <label for="repo_name">{{ctx.Locale.Tr "repo.repo_name"}}</label> + <input id="repo_name" name="repo_name" value="{{.repo_name}}" required maxlength="100"> + </div> + <div class="inline field"> + <label>{{ctx.Locale.Tr "repo.visibility"}}</label> + <div class="ui checkbox"> + {{if .IsForcedPrivate}} + <input name="private" type="checkbox" checked disabled> + <label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label> + {{else}} + <input name="private" type="checkbox" {{if .private}} checked{{end}}> + <label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label> + {{end}} </div> </div> + <div class="inline field {{if .Err_Description}}error{{end}}"> + <label for="description">{{ctx.Locale.Tr "repo.repo_desc"}}</label> + <textarea id="description" name="description" maxlength="2048">{{.description}}</textarea> + </div> + + <div class="inline field"> + <label></label> + <button class="ui primary button"> + {{ctx.Locale.Tr "repo.migrate_repo"}} + </button> + </div> </form> </div> </div> diff --git a/templates/repo/migrate/onedev.tmpl b/templates/repo/migrate/onedev.tmpl index e1aad96ba4..a27188ed24 100644 --- a/templates/repo/migrate/onedev.tmpl +++ b/templates/repo/migrate/onedev.tmpl @@ -1,114 +1,113 @@ {{template "base/head" .}} <div role="main" aria-label="{{.Title}}" class="page-content repository new migrate"> - <div class="ui middle very relaxed page grid"> - <div class="column"> - <form class="ui form" action="{{.Link}}" method="post"> + <div class="ui container medium-width"> + <h3 class="ui top attached header"> + {{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}} + <input id="service_type" type="hidden" name="service" value="{{.service}}"> + </h3> + <div class="ui attached segment"> + {{template "base/alert" .}} + <form class="ui form left-right-form" action="{{.Link}}" method="post"> {{template "base/disable_form_autofill"}} {{.CsrfTokenHtml}} - <h3 class="ui top attached header"> - {{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}} - <input id="service_type" type="hidden" name="service" value="{{.service}}"> - </h3> - <div class="ui attached segment"> - {{template "base/alert" .}} - <div class="inline required field {{if .Err_CloneAddr}}error{{end}}"> - <label for="clone_addr">{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label> - <input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required> - <span class="help"> - {{ctx.Locale.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{ctx.Locale.Tr "repo.migrate.clone_local_path"}}{{end}} - </span> - </div> + <div class="inline required field {{if .Err_CloneAddr}}error{{end}}"> + <label for="clone_addr">{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label> + <input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required> + <span class="help"> + {{ctx.Locale.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{ctx.Locale.Tr "repo.migrate.clone_local_path"}}{{end}} + </span> + </div> - <div class="inline field {{if .Err_Auth}}error{{end}}"> - <label for="auth_username">{{ctx.Locale.Tr "username"}}</label> - <input id="auth_username" name="auth_username" value="{{.auth_username}}" {{if not .auth_username}}data-need-clear="true"{{end}}> - </div> - <div class="inline field {{if .Err_Auth}}error{{end}}"> - <label for="auth_password">{{ctx.Locale.Tr "password"}}</label> - <input id="auth_password" name="auth_password" type="password" value="{{.auth_password}}"> - </div> + <div class="inline field {{if .Err_Auth}}error{{end}}"> + <label for="auth_username">{{ctx.Locale.Tr "username"}}</label> + <input id="auth_username" name="auth_username" value="{{.auth_username}}" {{if not .auth_username}}data-need-clear="true"{{end}}> + </div> + <div class="inline field {{if .Err_Auth}}error{{end}}"> + <label for="auth_password">{{ctx.Locale.Tr "password"}}</label> + <input id="auth_password" name="auth_password" type="password" value="{{.auth_password}}"> + </div> - {{template "repo/migrate/options" .}} + {{template "repo/migrate/options" .}} - <div id="migrate_items"> - <div class="inline field"> - <label>{{ctx.Locale.Tr "repo.migrate_items"}}</label> - <div class="ui checkbox"> - <input name="milestones" type="checkbox" {{if .milestones}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_milestones"}}</label> - </div> - <div class="ui checkbox"> - <input name="labels" type="checkbox" {{if .labels}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_labels"}}</label> - </div> + <div id="migrate_items" class="inline field"> + <label></label> + <div class="inline field"> + <label>{{ctx.Locale.Tr "repo.migrate_items"}}</label> + <div class="ui checkbox"> + <input name="milestones" type="checkbox" {{if .milestones}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_milestones"}}</label> </div> - <div class="inline field"> - <label></label> - <div class="ui checkbox"> - <input name="issues" type="checkbox" {{if .issues}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_issues"}}</label> - </div> - <div class="ui checkbox"> - <input name="pull_requests" type="checkbox" {{if .pull_requests}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_pullrequests"}}</label> - </div> + <div class="ui checkbox"> + <input name="labels" type="checkbox" {{if .labels}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_labels"}}</label> </div> </div> - - <div class="divider"></div> - - <div class="inline required field {{if .Err_Owner}}error{{end}}"> - <label>{{ctx.Locale.Tr "repo.owner"}}</label> - <div class="ui selection owner dropdown"> - <input type="hidden" id="uid" name="uid" value="{{.ContextUser.ID}}" required> - <span class="text truncated-item-container" title="{{.ContextUser.Name}}"> - {{ctx.AvatarUtils.Avatar .ContextUser 28 "mini"}} - <span class="truncated-item-name">{{.ContextUser.ShortName 40}}</span> - </span> - {{svg "octicon-triangle-down" 14 "dropdown icon"}} - <div class="menu" title="{{.SignedUser.Name}}"> - <div class="item truncated-item-container" data-value="{{.SignedUser.ID}}"> - {{ctx.AvatarUtils.Avatar .SignedUser 28 "mini"}} - <span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span> - </div> - {{range .Orgs}} - <div class="item truncated-item-container" data-value="{{.ID}}" title="{{.Name}}"> - {{ctx.AvatarUtils.Avatar . 28 "mini"}} - <span class="truncated-item-name">{{.ShortName 40}}</span> - </div> - {{end}} - </div> + <div class="inline field"> + <label></label> + <div class="ui checkbox"> + <input name="issues" type="checkbox" {{if .issues}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_issues"}}</label> + </div> + <div class="ui checkbox"> + <input name="pull_requests" type="checkbox" {{if .pull_requests}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.migrate_items_pullrequests"}}</label> </div> </div> + </div> - <div class="inline required field {{if .Err_RepoName}}error{{end}}"> - <label for="repo_name">{{ctx.Locale.Tr "repo.repo_name"}}</label> - <input id="repo_name" name="repo_name" value="{{.repo_name}}" required maxlength="100"> - </div> - <div class="inline field"> - <label>{{ctx.Locale.Tr "repo.visibility"}}</label> - <div class="ui checkbox"> - {{if .IsForcedPrivate}} - <input name="private" type="checkbox" checked disabled> - <label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label> - {{else}} - <input name="private" type="checkbox" {{if .private}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label> + <div class="divider"></div> + + <div class="inline required field {{if .Err_Owner}}error{{end}}"> + <label>{{ctx.Locale.Tr "repo.owner"}}</label> + <div class="ui selection owner dropdown"> + <input type="hidden" id="uid" name="uid" value="{{.ContextUser.ID}}" required> + <span class="text truncated-item-container" title="{{.ContextUser.Name}}"> + {{ctx.AvatarUtils.Avatar .ContextUser 28 "mini"}} + <span class="truncated-item-name">{{.ContextUser.ShortName 40}}</span> + </span> + {{svg "octicon-triangle-down" 14 "dropdown icon"}} + <div class="menu" title="{{.SignedUser.Name}}"> + <div class="item truncated-item-container" data-value="{{.SignedUser.ID}}"> + {{ctx.AvatarUtils.Avatar .SignedUser 28 "mini"}} + <span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span> + </div> + {{range .Orgs}} + <div class="item truncated-item-container" data-value="{{.ID}}" title="{{.Name}}"> + {{ctx.AvatarUtils.Avatar . 28 "mini"}} + <span class="truncated-item-name">{{.ShortName 40}}</span> + </div> {{end}} </div> </div> - <div class="inline field {{if .Err_Description}}error{{end}}"> - <label for="description">{{ctx.Locale.Tr "repo.repo_desc"}}</label> - <textarea id="description" name="description" maxlength="2048">{{.description}}</textarea> - </div> + </div> - <div class="inline field"> - <label></label> - <button class="ui primary button"> - {{ctx.Locale.Tr "repo.migrate_repo"}} - </button> + <div class="inline required field {{if .Err_RepoName}}error{{end}}"> + <label for="repo_name">{{ctx.Locale.Tr "repo.repo_name"}}</label> + <input id="repo_name" name="repo_name" value="{{.repo_name}}" required maxlength="100"> + </div> + <div class="inline field"> + <label>{{ctx.Locale.Tr "repo.visibility"}}</label> + <div class="ui checkbox"> + {{if .IsForcedPrivate}} + <input name="private" type="checkbox" checked disabled> + <label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label> + {{else}} + <input name="private" type="checkbox" {{if .private}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label> + {{end}} </div> </div> + <div class="inline field {{if .Err_Description}}error{{end}}"> + <label for="description">{{ctx.Locale.Tr "repo.repo_desc"}}</label> + <textarea id="description" name="description" maxlength="2048">{{.description}}</textarea> + </div> + + <div class="inline field"> + <label></label> + <button class="ui primary button"> + {{ctx.Locale.Tr "repo.migrate_repo"}} + </button> + </div> </form> </div> </div> diff --git a/templates/repo/pulls/fork.tmpl b/templates/repo/pulls/fork.tmpl index 7af535f1d3..879e5328f2 100644 --- a/templates/repo/pulls/fork.tmpl +++ b/templates/repo/pulls/fork.tmpl @@ -1,85 +1,83 @@ {{template "base/head" .}} <div role="main" aria-label="{{.Title}}" class="page-content repository new fork"> - <div class="ui middle very relaxed page grid"> - <div class="column"> - <form class="ui form" action="{{.Link}}" method="post"> + <div class="ui container medium-width"> + <h3 class="ui top attached header"> + {{ctx.Locale.Tr "new_fork"}} + </h3> + <div class="ui attached segment"> + {{template "base/alert" .}} + <form class="ui form left-right-form" action="{{.Link}}" method="post"> {{.CsrfTokenHtml}} - <h3 class="ui top attached header"> - {{ctx.Locale.Tr "new_fork"}} - </h3> - <div class="ui attached segment"> - {{template "base/alert" .}} - <div class="inline required field {{if .Err_Owner}}error{{end}}"> - <label>{{ctx.Locale.Tr "repo.owner"}}</label> - <div class="ui selection owner dropdown"> - <input type="hidden" id="uid" name="uid" value="{{.ContextUser.ID}}" required> - <span class="text truncated-item-container" title="{{.ContextUser.Name}}"> - {{ctx.AvatarUtils.Avatar .ContextUser 28 "mini"}} - <span class="truncated-item-name">{{.ContextUser.ShortName 40}}</span> - </span> - {{svg "octicon-triangle-down" 14 "dropdown icon"}} - <div class="menu"> - {{if .CanForkToUser}} - <div class="item truncated-item-container" data-value="{{.SignedUser.ID}}" title="{{.SignedUser.Name}}"> - {{ctx.AvatarUtils.Avatar .SignedUser 28 "mini"}} - <span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span> - </div> - {{end}} - {{range .Orgs}} - <div class="item truncated-item-container" data-value="{{.ID}}" title="{{.Name}}"> - {{ctx.AvatarUtils.Avatar . 28 "mini"}} - <span class="truncated-item-name">{{.ShortName 40}}</span> - </div> - {{end}} - </div> + <div class="inline required field {{if .Err_Owner}}error{{end}}"> + <label>{{ctx.Locale.Tr "repo.owner"}}</label> + <div class="ui selection owner dropdown"> + <input type="hidden" id="uid" name="uid" value="{{.ContextUser.ID}}" required> + <span class="text truncated-item-container" title="{{.ContextUser.Name}}"> + {{ctx.AvatarUtils.Avatar .ContextUser 28 "mini"}} + <span class="truncated-item-name">{{.ContextUser.ShortName 40}}</span> + </span> + {{svg "octicon-triangle-down" 14 "dropdown icon"}} + <div class="menu"> + {{if .CanForkToUser}} + <div class="item truncated-item-container" data-value="{{.SignedUser.ID}}" title="{{.SignedUser.Name}}"> + {{ctx.AvatarUtils.Avatar .SignedUser 28 "mini"}} + <span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span> + </div> + {{end}} + {{range .Orgs}} + <div class="item truncated-item-container" data-value="{{.ID}}" title="{{.Name}}"> + {{ctx.AvatarUtils.Avatar . 28 "mini"}} + <span class="truncated-item-name">{{.ShortName 40}}</span> + </div> + {{end}} </div> </div> + </div> - <div class="inline field"> - <label>{{ctx.Locale.Tr "repo.fork_from"}}</label> - <a href="{{.ForkRepo.Link}}" class="tw-inline-block">{{.ForkRepo.FullName}}</a> - </div> - <div class="inline required field {{if .Err_RepoName}}error{{end}}"> - <label for="repo_name">{{ctx.Locale.Tr "repo.repo_name"}}</label> - <input id="repo_name" name="repo_name" value="{{.repo_name}}" required> + <div class="inline field"> + <label>{{ctx.Locale.Tr "repo.fork_from"}}</label> + <a href="{{.ForkRepo.Link}}" class="tw-inline-block">{{.ForkRepo.FullName}}</a> + </div> + <div class="inline required field {{if .Err_RepoName}}error{{end}}"> + <label for="repo_name">{{ctx.Locale.Tr "repo.repo_name"}}</label> + <input id="repo_name" name="repo_name" value="{{.repo_name}}" required> + </div> + <div class="inline field"> + <label>{{ctx.Locale.Tr "repo.visibility"}}</label> + <div class="ui disabled checkbox"> + <input type="checkbox" disabled {{if .IsPrivate}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label> </div> - <div class="inline field"> - <label>{{ctx.Locale.Tr "repo.visibility"}}</label> - <div class="ui disabled checkbox"> - <input type="checkbox" disabled {{if .IsPrivate}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label> + <span class="help">{{ctx.Locale.Tr "repo.fork_visibility_helper"}}</span> + </div> + <div class="inline field"> + <label>{{ctx.Locale.Tr "repo.fork_branch"}}</label> + <div class="ui selection dropdown ellipsis-items-nowrap"> + <input type="hidden" id="fork_single_branch" name="fork_single_branch" value="" required> + <div class="text" title="{{ctx.Locale.Tr "repo.all_branches"}}"> + <span class="truncated-item-name">{{ctx.Locale.Tr "repo.all_branches"}}</span> </div> - <span class="help">{{ctx.Locale.Tr "repo.fork_visibility_helper"}}</span> - </div> - <div class="inline field"> - <label>{{ctx.Locale.Tr "repo.fork_branch"}}</label> - <div class="ui selection dropdown ellipsis-items-nowrap"> - <input type="hidden" id="fork_single_branch" name="fork_single_branch" value="" required> - <div class="text" title="{{ctx.Locale.Tr "repo.all_branches"}}"> - <span class="truncated-item-name">{{ctx.Locale.Tr "repo.all_branches"}}</span> - </div> - {{svg "octicon-triangle-down" 14 "dropdown icon"}} - <div class="menu"> - <div class="item" data-value="" title="{{ctx.Locale.Tr "repo.all_branches"}}"> - {{ctx.Locale.Tr "repo.all_branches"}} - </div> - {{range .Branches}} - <div class="item" data-value="{{.}}" title="{{.}}">{{.}}</div> - {{end}} + {{svg "octicon-triangle-down" 14 "dropdown icon"}} + <div class="menu"> + <div class="item" data-value="" title="{{ctx.Locale.Tr "repo.all_branches"}}"> + {{ctx.Locale.Tr "repo.all_branches"}} </div> + {{range .Branches}} + <div class="item" data-value="{{.}}" title="{{.}}">{{.}}</div> + {{end}} </div> </div> - <div class="inline field {{if .Err_Description}}error{{end}}"> - <label for="description">{{ctx.Locale.Tr "repo.repo_desc"}}</label> - <textarea id="description" name="description">{{.description}}</textarea> - </div> + </div> + <div class="inline field {{if .Err_Description}}error{{end}}"> + <label for="description">{{ctx.Locale.Tr "repo.repo_desc"}}</label> + <textarea id="description" name="description">{{.description}}</textarea> + </div> - <div class="inline field"> - <label></label> - <button class="ui primary button{{if not .CanForkRepo}} disabled{{end}}"> - {{ctx.Locale.Tr "repo.fork_repo"}} - </button> - </div> + <div class="inline field"> + <label></label> + <button class="ui primary button{{if not .CanForkRepo}} disabled{{end}}"> + {{ctx.Locale.Tr "repo.fork_repo"}} + </button> </div> </form> </div> diff --git a/templates/repo/pulls/status.tmpl b/templates/repo/pulls/status.tmpl index e8636ba1b8..96030f9422 100644 --- a/templates/repo/pulls/status.tmpl +++ b/templates/repo/pulls/status.tmpl @@ -1,5 +1,4 @@ -{{/* -Template Attributes: +{{/* Template Attributes: * CommitStatus: summary of all commit status state * CommitStatuses: all commit status elements * MissingRequiredChecks: commit check contexts that are required by branch protection but not present diff --git a/templates/repo/release/label.tmpl b/templates/repo/release/label.tmpl index eacb3e36f4..2381a15351 100644 --- a/templates/repo/release/label.tmpl +++ b/templates/repo/release/label.tmpl @@ -1,5 +1,4 @@ -{{/* -Template Attributes: +{{/* Template Attributes: * Release: the release * IsLatest: boolean indicating whether this is the latest release, optional */}} diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl index 1a7884bde2..21eaf6a651 100644 --- a/templates/repo/settings/options.tmpl +++ b/templates/repo/settings/options.tmpl @@ -733,7 +733,7 @@ <span class="field"> {{if .CodeIndexerStatus}} <a rel="nofollow" class="ui sha label" href="{{.RepoLink}}/commit/{{.CodeIndexerStatus.CommitSha}}"> - <span class="shortsha">{{ShortSha .CodeIndexerStatus.CommitSha}}</span> + {{ShortSha .CodeIndexerStatus.CommitSha}} </a> {{else}} <span>{{ctx.Locale.Tr "repo.settings.admin_indexer_unindexed"}}</span> @@ -752,7 +752,7 @@ <span class="field"> {{if and .StatsIndexerStatus .StatsIndexerStatus.CommitSha}} <a rel="nofollow" class="ui sha label" href="{{.RepoLink}}/commit/{{.StatsIndexerStatus.CommitSha}}"> - <span class="shortsha">{{ShortSha .StatsIndexerStatus.CommitSha}}</span> + {{ShortSha .StatsIndexerStatus.CommitSha}} </a> {{else}} <span>{{ctx.Locale.Tr "repo.settings.admin_indexer_unindexed"}}</span> diff --git a/templates/repo/shabox_badge.tmpl b/templates/repo/shabox_badge.tmpl deleted file mode 100644 index 36fc9e04b1..0000000000 --- a/templates/repo/shabox_badge.tmpl +++ /dev/null @@ -1,15 +0,0 @@ -<div class="ui detail icon button"> - {{if .verification.Verified}} - <div title="{{if eq .verification.TrustStatus "trusted"}}{{else if eq .verification.TrustStatus "untrusted"}}{{ctx.Locale.Tr "repo.commits.signed_by_untrusted_user"}}: {{else}}{{ctx.Locale.Tr "repo.commits.signed_by_untrusted_user_unmatched"}}: {{end}}{{.verification.Reason}}"> - {{if ne .verification.SigningUser.ID 0}} - {{svg "gitea-lock"}} - {{ctx.AvatarUtils.Avatar .verification.SigningUser 16 "signature"}} - {{else}} - <span title="{{ctx.Locale.Tr "gpg.default_key"}}">{{svg "gitea-lock-cog"}}</span> - {{ctx.AvatarUtils.AvatarByEmail .verification.SigningEmail "" 16 "signature"}} - {{end}} - </div> - {{else}} - <span title="{{ctx.Locale.Tr .verification.Reason}}">{{svg "gitea-unlock"}}</span> - {{end}} -</div> diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl index 86366ae053..0a43e3db54 100644 --- a/templates/repo/view_file.tmpl +++ b/templates/repo/view_file.tmpl @@ -12,9 +12,7 @@ {{if not .ReadmeInList}} <div id="repo-file-commit-box" class="ui segment list-header tw-mb-4 tw-flex tw-justify-between"> - <div class="latest-commit"> - {{template "repo/latest_commit" .}} - </div> + {{template "repo/latest_commit" .}} {{if .LatestCommit}} {{if .LatestCommit.Committer}} <div class="text grey age"> diff --git a/templates/repo/view_list.tmpl b/templates/repo/view_list.tmpl index 2d555e4c2e..7540931010 100644 --- a/templates/repo/view_list.tmpl +++ b/templates/repo/view_list.tmpl @@ -1,7 +1,7 @@ {{/* use grid layout, still use the old ID because there are many other CSS styles depending on this ID */}} <div id="repo-files-table" {{if .HasFilesWithoutLatestCommit}}hx-indicator="#repo-files-table .repo-file-cell.message" hx-trigger="load" hx-swap="morph" hx-post="{{.LastCommitLoaderURL}}"{{end}}> <div class="repo-file-line repo-file-last-commit"> - <div class="latest-commit">{{template "repo/latest_commit" .}}</div> + {{template "repo/latest_commit" .}} <div>{{if and .LatestCommit .LatestCommit.Committer}}{{DateUtils.TimeSince .LatestCommit.Committer.When}}{{end}}</div> </div> {{if .HasParentPath}} @@ -13,15 +13,15 @@ <div class="repo-file-item"> {{$entry := $item.Entry}} {{$commit := $item.Commit}} - {{$subModuleFile := $item.SubModuleFile}} + {{$submoduleFile := $item.SubmoduleFile}} <div class="repo-file-cell name {{if not $commit}}notready{{end}}"> {{if $entry.IsSubModule}} {{svg "octicon-file-submodule"}} - {{$refURL := $subModuleFile.RefURL AppUrl $.Repository.FullName $.SSHDomain}} {{/* FIXME: the usage of AppUrl seems incorrect, it would be fixed in the future, use AppSubUrl instead */}} - {{if $refURL}} - <a class="muted" href="{{$refURL}}">{{$entry.Name}}</a><span class="at">@</span><a href="{{$refURL}}/commit/{{PathEscape $subModuleFile.RefID}}">{{ShortSha $subModuleFile.RefID}}</a> + {{$submoduleLink := $submoduleFile.SubmoduleWebLink ctx}} + {{if $submoduleLink}} + <a class="muted" href="{{$submoduleLink.RepoWebLink}}">{{$entry.Name}}</a> <span class="at">@</span> <a href="{{$submoduleLink.CommitWebLink}}">{{ShortSha $submoduleFile.RefID}}</a> {{else}} - {{$entry.Name}}<span class="at">@</span>{{ShortSha $subModuleFile.RefID}} + {{$entry.Name}} <span class="at">@</span> {{ShortSha $submoduleFile.RefID}} {{end}} {{else}} {{if $entry.IsDir}} diff --git a/templates/shared/actions/runner_list.tmpl b/templates/shared/actions/runner_list.tmpl index f652d56e09..e5907da8e8 100644 --- a/templates/shared/actions/runner_list.tmpl +++ b/templates/shared/actions/runner_list.tmpl @@ -3,7 +3,7 @@ <h4 class="ui top attached header"> {{ctx.Locale.Tr "actions.runners.runner_manage_panel"}} ({{ctx.Locale.Tr "admin.total" .Total}}) <div class="ui right"> - <div class="ui top right pointing dropdown"> + <div class="ui top right pointing dropdown jump"> <button class="ui primary tiny button"> {{ctx.Locale.Tr "actions.runners.new"}} {{svg "octicon-triangle-down" 14 "dropdown icon"}} @@ -17,14 +17,18 @@ Registration Token </div> <div class="ui input"> - <input type="text" value="{{.RegistrationToken}}"> + <input type="text" value="{{.RegistrationToken}}" readonly> <button class="ui basic label button" aria-label="{{ctx.Locale.Tr "copy"}}" data-clipboard-text="{{.RegistrationToken}}"> {{svg "octicon-copy" 14}} </button> </div> <div class="divider"></div> <div class="item"> - <a href="{{$.Link}}/reset_registration_token">{{ctx.Locale.Tr "actions.runners.reset_registration_token"}}</a> + <a class="link-action" data-url="{{$.Link}}/reset_registration_token" + data-modal-confirm="{{ctx.Locale.Tr "actions.runners.reset_registration_token_confirm"}}" + > + {{ctx.Locale.Tr "actions.runners.reset_registration_token"}} + </a> </div> </div> </div> diff --git a/templates/shared/combomarkdowneditor.tmpl b/templates/shared/combomarkdowneditor.tmpl index 3191346f59..b1c3b29cf3 100644 --- a/templates/shared/combomarkdowneditor.tmpl +++ b/templates/shared/combomarkdowneditor.tmpl @@ -1,5 +1,4 @@ -{{/* -Template Attributes: +{{/* Template Attributes: * CustomInit: do not initialize the editor automatically * ContainerId: id attribute for the container element * ContainerClasses: additional classes for the container element diff --git a/templates/shared/issueicon.tmpl b/templates/shared/issueicon.tmpl index a62714e988..f828de5c66 100644 --- a/templates/shared/issueicon.tmpl +++ b/templates/shared/issueicon.tmpl @@ -1,25 +1,25 @@ -{{if .IsPull}} - {{if not .PullRequest}} +{{- if .IsPull -}} + {{- if not .PullRequest -}} No PullRequest - {{else}} - {{if .IsClosed}} - {{if .PullRequest.HasMerged}} - {{svg "octicon-git-merge" 16 "text purple"}} - {{else}} - {{svg "octicon-git-pull-request" 16 "text red"}} - {{end}} - {{else}} - {{if .PullRequest.IsWorkInProgress ctx}} - {{svg "octicon-git-pull-request-draft" 16 "text grey"}} - {{else}} - {{svg "octicon-git-pull-request" 16 "text green"}} - {{end}} - {{end}} - {{end}} -{{else}} - {{if .IsClosed}} - {{svg "octicon-issue-closed" 16 "text red"}} - {{else}} - {{svg "octicon-issue-opened" 16 "text green"}} - {{end}} -{{end}} + {{- else -}} + {{- if .IsClosed -}} + {{- if .PullRequest.HasMerged -}} + {{- svg "octicon-git-merge" 16 "text purple" -}} + {{- else -}} + {{- svg "octicon-git-pull-request" 16 "text red" -}} + {{- end -}} + {{- else -}} + {{- if .PullRequest.IsWorkInProgress ctx -}} + {{- svg "octicon-git-pull-request-draft" 16 "text grey" -}} + {{- else -}} + {{- svg "octicon-git-pull-request" 16 "text green" -}} + {{- end -}} + {{- end -}} + {{- end -}} +{{- else -}} + {{- if .IsClosed -}} + {{- svg "octicon-issue-closed" 16 "text red" -}} + {{- else -}} + {{- svg "octicon-issue-opened" 16 "text green" -}} + {{- end -}} +{{- end -}} diff --git a/templates/shared/issuelist.tmpl b/templates/shared/issuelist.tmpl index a2b802f2a2..e8015b40ea 100644 --- a/templates/shared/issuelist.tmpl +++ b/templates/shared/issuelist.tmpl @@ -25,30 +25,10 @@ {{end}} </span> </div> - {{if or .TotalTrackedTime .Assignees .NumComments}} - <div class="flex-item-trailing"> - {{if .TotalTrackedTime}} - <div class="text grey flex-text-block"> - {{svg "octicon-clock" 16}} - {{.TotalTrackedTime | Sec2Time}} - </div> - {{end}} - {{if .Assignees}} - <div class="text grey"> - {{range .Assignees}} - <a class="ui assignee tw-no-underline" href="{{.HomeLink}}" data-tooltip-content="{{.GetDisplayName}}"> - {{ctx.AvatarUtils.Avatar . 20}} - </a> - {{end}} - </div> - {{end}} - {{if .NumComments}} - <div class="text grey"> - <a class="tw-no-underline muted flex-text-block" href="{{if .Link}}{{.Link}}{{else}}{{$.Link}}/{{.Index}}{{end}}"> - {{svg "octicon-comment" 16}}{{.NumComments}} - </a> - </div> - {{end}} + {{if .TotalTrackedTime}} + <div class="text grey flex-text-block"> + {{svg "octicon-clock" 16}} + {{.TotalTrackedTime | Sec2Time}} </div> {{end}} </div> @@ -152,6 +132,26 @@ {{end}} </div> </div> + {{if or .Assignees .NumComments}} + <div class="flex-item-trailing"> + {{if .Assignees}} + <div class="text grey"> + {{range .Assignees}} + <a class="ui assignee tw-no-underline" href="{{.HomeLink}}" data-tooltip-content="{{.GetDisplayName}}"> + {{ctx.AvatarUtils.Avatar . 20}} + </a> + {{end}} + </div> + {{end}} + {{if .NumComments}} + <div class="text grey"> + <a class="tw-no-underline muted flex-text-block" href="{{if .Link}}{{.Link}}{{else}}{{$.Link}}/{{.Index}}{{end}}"> + {{svg "octicon-comment" 16}}{{.NumComments}} + </a> + </div> + {{end}} + </div> + {{end}} </div> {{end}} {{if .IssueIndexerUnavailable}} diff --git a/templates/shared/search/code/search.tmpl b/templates/shared/search/code/search.tmpl index e49ea47e03..dde45c0fbf 100644 --- a/templates/shared/search/code/search.tmpl +++ b/templates/shared/search/code/search.tmpl @@ -2,7 +2,8 @@ {{template "shared/search/combo_fuzzy" dict "Value" .Keyword "Disabled" .CodeIndexerUnavailable "IsFuzzy" .IsFuzzy "Placeholder" (ctx.Locale.Tr "search.code_kind")}} </form> <div class="divider"></div> -<div class="ui user list"> +<div class="ui list"> + {{template "base/alert" .}} {{if .CodeIndexerUnavailable}} <div class="ui error message"> <p>{{ctx.Locale.Tr "search.code_search_unavailable"}}</p> diff --git a/templates/shared/user/profile_big_avatar.tmpl b/templates/shared/user/profile_big_avatar.tmpl index f04f1ef6c4..91f04e0b53 100644 --- a/templates/shared/user/profile_big_avatar.tmpl +++ b/templates/shared/user/profile_big_avatar.tmpl @@ -92,6 +92,9 @@ </li> {{end}} {{end}} + {{if .ShowMoreOrgs}} + <li><a class="tw-align-center" href="{{.ContextUser.HomeLink}}?tab=organizations" data-tooltip-content="{{ctx.Locale.Tr "user.show_more"}}">{{svg "octicon-kebab-horizontal" 28 "icon tw-p-1"}}</a></li> + {{end}} </ul> </li> {{end}} diff --git a/templates/user/auth/finalize_openid.tmpl b/templates/user/auth/finalize_openid.tmpl deleted file mode 100644 index 1c1dcdb825..0000000000 --- a/templates/user/auth/finalize_openid.tmpl +++ /dev/null @@ -1,47 +0,0 @@ -{{template "base/head" .}} -<div role="main" aria-label="{{.Title}}" class="page-content user signin"> - <div class="ui container"> - <div class="ui grid"> - {{template "user/auth/finalize_openid_navbar" .}} - <div class="twelve wide column content"> - {{template "base/alert" .}} - <h4 class="ui top attached header"> - {{ctx.Locale.Tr "auth.login_userpass"}} - </h4> - <div class="ui attached segment"> - <form class="ui form" action="{{.Link}}" method="post"> - {{.CsrfTokenHtml}} - <div class="required inline field {{if .Err_UserName}}error{{end}}"> - <label for="user_name">{{ctx.Locale.Tr "home.uname_holder"}}</label> - <input id="user_name" type="text" name="user_name" value="{{.user_name}}" autofocus required> - </div> - <div class="required inline field {{if .Err_Password}}error{{end}}"> - <label for="password">{{ctx.Locale.Tr "password"}}</label> - <input id="password" name="password" type="password" value="{{.password}}" autocomplete="off" required> - </div> - <div class="inline field"> - <label></label> - <div class="ui checkbox"> - <label>{{ctx.Locale.Tr "auth.remember_me"}}</label> - <input name="remember" type="checkbox"> - </div> - </div> - - <div class="inline field"> - <label></label> - <button class="ui primary button">{{ctx.Locale.Tr "sign_in"}}</button> - <a href="{{AppSubUrl}}/user/forget_password">{{ctx.Locale.Tr "auth.forget_password"}}</a> - </div> - {{if .ShowRegistrationButton}} - <div class="inline field"> - <label></label> - <a href="{{AppSubUrl}}/user/sign_up">{{ctx.Locale.Tr "auth.sign_up_now"}}</a> - </div> - {{end}} - </form> - </div> - </div> - </div> - </div> -</div> -{{template "base/footer" .}} diff --git a/templates/user/auth/reset_passwd.tmpl b/templates/user/auth/reset_passwd.tmpl index f8303feef3..37a23b3e55 100644 --- a/templates/user/auth/reset_passwd.tmpl +++ b/templates/user/auth/reset_passwd.tmpl @@ -34,18 +34,18 @@ {{ctx.Locale.Tr "twofa"}} </h4> <div class="ui warning visible message">{{ctx.Locale.Tr "settings.twofa_is_enrolled"}}</div> - {{if .scratch_code}} - <div class="required inline field {{if .Err_Token}}error{{end}}"> - <label for="token">{{ctx.Locale.Tr "auth.scratch_code"}}</label> - <input id="token" name="token" type="text" autocomplete="off" autofocus required> - </div> - <input type="hidden" name="scratch_code" value="true"> - {{else}} - <div class="required field {{if .Err_Passcode}}error{{end}}"> - <label for="passcode">{{ctx.Locale.Tr "passcode"}}</label> - <input id="passcode" name="passcode" type="number" autocomplete="off" autofocus required> - </div> - {{end}} + {{if .scratch_code}} + <div class="required inline field {{if .Err_Token}}error{{end}}"> + <label for="token">{{ctx.Locale.Tr "auth.scratch_code"}}</label> + <input id="token" name="token" type="text" autocomplete="off" autofocus required> + </div> + <input type="hidden" name="scratch_code" value="true"> + {{else}} + <div class="required field {{if .Err_Passcode}}error{{end}}"> + <label for="passcode">{{ctx.Locale.Tr "passcode"}}</label> + <input id="passcode" name="passcode" type="number" autocomplete="off" autofocus required> + </div> + {{end}} {{end}} <div class="divider"></div> <div class="inline field"> diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl index dd608e5aa1..fbf86a92bf 100644 --- a/templates/user/auth/signin_inner.tmpl +++ b/templates/user/auth/signin_inner.tmpl @@ -18,9 +18,9 @@ <input id="user_name" type="text" name="user_name" value="{{.user_name}}" autofocus required tabindex="1"> </div> {{if or (not .DisablePassword) .LinkAccountMode}} - <div class="required field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}} form-field-content-aside-label"> - <label for="password">{{ctx.Locale.Tr "password"}}</label> - <div> + <div class="required field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}"> + <div class="tw-flex tw-mb-1"> + <label for="password" class="tw-flex-1">{{ctx.Locale.Tr "password"}}</label> <a href="{{AppSubUrl}}/user/forgot_password" tabindex="4">{{ctx.Locale.Tr "auth.forgot_password"}}</a> </div> <input id="password" name="password" type="password" value="{{.password}}" autocomplete="current-password" required tabindex="2"> diff --git a/templates/user/auth/signup_openid_connect.tmpl b/templates/user/auth/signup_openid_connect.tmpl index e4b7936374..67e99b2f89 100644 --- a/templates/user/auth/signup_openid_connect.tmpl +++ b/templates/user/auth/signup_openid_connect.tmpl @@ -1,36 +1,36 @@ {{template "base/head" .}} <div role="main" aria-label="{{.Title}}" class="page-content user signup"> {{template "user/auth/signup_openid_navbar" .}} - <div class="ui container"> - {{template "base/alert" .}} - <h4 class="ui top attached header"> - {{ctx.Locale.Tr "auth.openid_connect_title"}} - </h4> - <div class="ui attached segment"> - <p> - {{ctx.Locale.Tr "auth.openid_connect_desc"}} - </p> - <form class="ui form" action="{{.Link}}" method="post"> - {{.CsrfTokenHtml}} - <div class="required inline field {{if .Err_UserName}}error{{end}}"> - <label for="user_name">{{ctx.Locale.Tr "home.uname_holder"}}</label> - <input id="user_name" type="text" name="user_name" value="{{.user_name}}" autofocus required> - </div> - <div class="required inline field {{if .Err_Password}}error{{end}}"> - <label for="password">{{ctx.Locale.Tr "password"}}</label> - <input id="password" name="password" type="password" value="{{.password}}" autocomplete="off" required> - </div> - <div class="inline field"> - <label for="openid">OpenID URI</label> - <input id="openid" value="{{.OpenID}}" readonly> - </div> - <div class="inline field"> - <label></label> - <button class="ui primary button">{{ctx.Locale.Tr "auth.openid_connect_submit"}}</button> - <a href="{{AppSubUrl}}/user/forgot_password">{{ctx.Locale.Tr "auth.forgot_password"}}</a> - </div> - </form> + <div class="ui container medium-width"> + {{template "base/alert" .}} + <h4 class="ui top attached header"> + {{ctx.Locale.Tr "auth.openid_connect_title"}} + </h4> + <div class="ui attached segment"> + <form class="ui form left-right-form" action="{{.Link}}" method="post"> + {{.CsrfTokenHtml}} + <div class="inline field"> + <span class="help">{{ctx.Locale.Tr "auth.openid_connect_desc"}}</span> </div> + <div class="required inline field {{if .Err_UserName}}error{{end}}"> + <label for="user_name">{{ctx.Locale.Tr "home.uname_holder"}}</label> + <input id="user_name" type="text" name="user_name" value="{{.user_name}}" autofocus required> + </div> + <div class="required inline field {{if .Err_Password}}error{{end}}"> + <label for="password">{{ctx.Locale.Tr "password"}}</label> + <input id="password" name="password" type="password" value="{{.password}}" autocomplete="off" required> + </div> + <div class="inline field"> + <label for="openid">OpenID URI</label> + <input id="openid" value="{{.OpenID}}" readonly> + </div> + <div class="inline field"> + <label></label> + <button class="ui primary button">{{ctx.Locale.Tr "auth.openid_connect_submit"}}</button> + <a href="{{AppSubUrl}}/user/forgot_password">{{ctx.Locale.Tr "auth.forgot_password"}}</a> + </div> + </form> + </div> </div> </div> {{template "base/footer" .}} diff --git a/templates/user/auth/signup_openid_register.tmpl b/templates/user/auth/signup_openid_register.tmpl index c017a0e65b..df6268d151 100644 --- a/templates/user/auth/signup_openid_register.tmpl +++ b/templates/user/auth/signup_openid_register.tmpl @@ -7,7 +7,7 @@ {{ctx.Locale.Tr "auth.openid_register_title"}} </h4> <div class="ui attached segment"> - <p class="tw-max-w-2xl tw-mx-auto"> + <p> {{ctx.Locale.Tr "auth.openid_register_desc"}} </p> <form class="ui form" action="{{.Link}}" method="post"> diff --git a/templates/user/dashboard/navbar.tmpl b/templates/user/dashboard/navbar.tmpl index 7982cbd950..a828bc90e4 100644 --- a/templates/user/dashboard/navbar.tmpl +++ b/templates/user/dashboard/navbar.tmpl @@ -12,7 +12,7 @@ {{svg "octicon-triangle-down" 14 "dropdown icon tw-ml-1"}} </span> <div class="context user overflow menu"> - <div class="ui header"> + <div class="header"> {{ctx.Locale.Tr "home.switch_dashboard_context"}} </div> <div class="scrolling menu items"> @@ -56,7 +56,7 @@ </span> {{svg "octicon-triangle-down" 14 "dropdown icon"}} <div class="context user overflow menu"> - <div class="ui header"> + <div class="header"> {{ctx.Locale.Tr "home.filter_by_team_repositories"}} </div> <div class="scrolling menu items"> diff --git a/templates/user/overview/header.tmpl b/templates/user/overview/header.tmpl index 275c4e295e..f4664c704d 100644 --- a/templates/user/overview/header.tmpl +++ b/templates/user/overview/header.tmpl @@ -1,6 +1,6 @@ <overflow-menu class="ui secondary pointing tabular borderless menu"> <div class="overflow-menu-items"> - {{if and .HasProfileReadme .ContextUser.IsIndividual}} + {{if and .HasUserProfileReadme .ContextUser.IsIndividual}} <a class="{{if eq .TabName "overview"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=overview"> {{svg "octicon-info"}} {{ctx.Locale.Tr "user.overview"}} </a> diff --git a/templates/user/profile.tmpl b/templates/user/profile.tmpl index cf61bb906a..345872b00d 100644 --- a/templates/user/profile.tmpl +++ b/templates/user/profile.tmpl @@ -26,7 +26,9 @@ {{else if eq .TabName "followers"}} {{template "repo/user_cards" .}} {{else if eq .TabName "overview"}} - <div id="readme_profile" class="markup">{{.ProfileReadme}}</div> + <div id="readme_profile" class="markup">{{.ProfileReadmeContent}}</div> + {{else if eq .TabName "organizations"}} + {{template "repo/user_cards" .}} {{else}} {{template "shared/repo_search" .}} {{template "explore/repo_list" .}} diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index 78e42777bb..ece136be50 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -40,13 +40,6 @@ func TestMain(m *testing.M) { tests.InitTest(false) testE2eWebRoutes = routers.NormalRoutes() - os.Unsetenv("GIT_AUTHOR_NAME") - os.Unsetenv("GIT_AUTHOR_EMAIL") - os.Unsetenv("GIT_AUTHOR_DATE") - os.Unsetenv("GIT_COMMITTER_NAME") - os.Unsetenv("GIT_COMMITTER_EMAIL") - os.Unsetenv("GIT_COMMITTER_DATE") - err := unittest.InitFixtures( unittest.FixturesOptions{ Dir: filepath.Join(filepath.Dir(setting.AppPath), "models/fixtures/"), diff --git a/tests/gitea-repositories-meta/user2/git_hooks_test.git/hooks/pre-receive.d/pre-receive b/tests/gitea-repositories-meta/user2/git_hooks_test.git/hooks/pre-receive.d/pre-receive index b26a3b9b68..205086810d 100755 --- a/tests/gitea-repositories-meta/user2/git_hooks_test.git/hooks/pre-receive.d/pre-receive +++ b/tests/gitea-repositories-meta/user2/git_hooks_test.git/hooks/pre-receive.d/pre-receive @@ -1,3 +1,2 @@ #!/bin/bash - -echo Hello, World! +echo "TestGitHookScript" diff --git a/tests/integration/actions_job_test.go b/tests/integration/actions_job_test.go new file mode 100644 index 0000000000..e13277678d --- /dev/null +++ b/tests/integration/actions_job_test.go @@ -0,0 +1,410 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "encoding/base64" + "fmt" + "net/http" + "net/url" + "testing" + "time" + + actions_model "code.gitea.io/gitea/models/actions" + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + api "code.gitea.io/gitea/modules/structs" + + runnerv1 "code.gitea.io/actions-proto-go/runner/v1" + "github.com/stretchr/testify/assert" +) + +func TestJobWithNeeds(t *testing.T) { + testCases := []struct { + treePath string + fileContent string + outcomes map[string]*mockTaskOutcome + expectedStatuses map[string]string + }{ + { + treePath: ".gitea/workflows/job-with-needs.yml", + fileContent: `name: job-with-needs +on: + push: + paths: + - '.gitea/workflows/job-with-needs.yml' +jobs: + job1: + runs-on: ubuntu-latest + steps: + - run: echo job1 + job2: + runs-on: ubuntu-latest + needs: [job1] + steps: + - run: echo job2 +`, + outcomes: map[string]*mockTaskOutcome{ + "job1": { + result: runnerv1.Result_RESULT_SUCCESS, + }, + "job2": { + result: runnerv1.Result_RESULT_SUCCESS, + }, + }, + expectedStatuses: map[string]string{ + "job1": actions_model.StatusSuccess.String(), + "job2": actions_model.StatusSuccess.String(), + }, + }, + { + treePath: ".gitea/workflows/job-with-needs-fail.yml", + fileContent: `name: job-with-needs-fail +on: + push: + paths: + - '.gitea/workflows/job-with-needs-fail.yml' +jobs: + job1: + runs-on: ubuntu-latest + steps: + - run: echo job1 + job2: + runs-on: ubuntu-latest + needs: [job1] + steps: + - run: echo job2 +`, + outcomes: map[string]*mockTaskOutcome{ + "job1": { + result: runnerv1.Result_RESULT_FAILURE, + }, + }, + expectedStatuses: map[string]string{ + "job1": actions_model.StatusFailure.String(), + "job2": actions_model.StatusSkipped.String(), + }, + }, + { + treePath: ".gitea/workflows/job-with-needs-fail-if.yml", + fileContent: `name: job-with-needs-fail-if +on: + push: + paths: + - '.gitea/workflows/job-with-needs-fail-if.yml' +jobs: + job1: + runs-on: ubuntu-latest + steps: + - run: echo job1 + job2: + runs-on: ubuntu-latest + if: ${{ always() }} + needs: [job1] + steps: + - run: echo job2 +`, + outcomes: map[string]*mockTaskOutcome{ + "job1": { + result: runnerv1.Result_RESULT_FAILURE, + }, + "job2": { + result: runnerv1.Result_RESULT_SUCCESS, + }, + }, + expectedStatuses: map[string]string{ + "job1": actions_model.StatusFailure.String(), + "job2": actions_model.StatusSuccess.String(), + }, + }, + } + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, user2.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + apiRepo := createActionsTestRepo(t, token, "actions-jobs-with-needs", false) + runner := newMockRunner() + runner.registerAsRepoRunner(t, user2.Name, apiRepo.Name, "mock-runner", []string{"ubuntu-latest"}) + + for _, tc := range testCases { + t.Run(fmt.Sprintf("test %s", tc.treePath), func(t *testing.T) { + // create the workflow file + opts := getWorkflowCreateFileOptions(user2, apiRepo.DefaultBranch, fmt.Sprintf("create %s", tc.treePath), tc.fileContent) + fileResp := createWorkflowFile(t, token, user2.Name, apiRepo.Name, tc.treePath, opts) + + // fetch and execute task + for i := 0; i < len(tc.outcomes); i++ { + task := runner.fetchTask(t) + jobName := getTaskJobNameByTaskID(t, token, user2.Name, apiRepo.Name, task.Id) + outcome := tc.outcomes[jobName] + assert.NotNil(t, outcome) + runner.execTask(t, task, outcome) + } + + // check result + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/tasks", user2.Name, apiRepo.Name)). + AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var actionTaskRespAfter api.ActionTaskResponse + DecodeJSON(t, resp, &actionTaskRespAfter) + for _, apiTask := range actionTaskRespAfter.Entries { + if apiTask.HeadSHA != fileResp.Commit.SHA { + continue + } + status := apiTask.Status + assert.Equal(t, status, tc.expectedStatuses[apiTask.Name]) + } + }) + } + + httpContext := NewAPITestContext(t, user2.Name, apiRepo.Name, auth_model.AccessTokenScopeWriteRepository) + doAPIDeleteRepository(httpContext)(t) + }) +} + +func TestJobNeedsMatrix(t *testing.T) { + testCases := []struct { + treePath string + fileContent string + outcomes map[string]*mockTaskOutcome + expectedTaskNeeds map[string]*runnerv1.TaskNeed // jobID => TaskNeed + }{ + { + treePath: ".gitea/workflows/jobs-outputs-with-matrix.yml", + fileContent: `name: jobs-outputs-with-matrix +on: + push: + paths: + - '.gitea/workflows/jobs-outputs-with-matrix.yml' +jobs: + job1: + runs-on: ubuntu-latest + outputs: + output_1: ${{ steps.gen_output.outputs.output_1 }} + output_2: ${{ steps.gen_output.outputs.output_2 }} + output_3: ${{ steps.gen_output.outputs.output_3 }} + strategy: + matrix: + version: [1, 2, 3] + steps: + - name: Generate output + id: gen_output + run: | + version="${{ matrix.version }}" + echo "output_${version}=${version}" >> "$GITHUB_OUTPUT" + job2: + runs-on: ubuntu-latest + needs: [job1] + steps: + - run: echo '${{ toJSON(needs.job1.outputs) }}' +`, + outcomes: map[string]*mockTaskOutcome{ + "job1 (1)": { + result: runnerv1.Result_RESULT_SUCCESS, + outputs: map[string]string{ + "output_1": "1", + "output_2": "", + "output_3": "", + }, + }, + "job1 (2)": { + result: runnerv1.Result_RESULT_SUCCESS, + outputs: map[string]string{ + "output_1": "", + "output_2": "2", + "output_3": "", + }, + }, + "job1 (3)": { + result: runnerv1.Result_RESULT_SUCCESS, + outputs: map[string]string{ + "output_1": "", + "output_2": "", + "output_3": "3", + }, + }, + }, + expectedTaskNeeds: map[string]*runnerv1.TaskNeed{ + "job1": { + Result: runnerv1.Result_RESULT_SUCCESS, + Outputs: map[string]string{ + "output_1": "1", + "output_2": "2", + "output_3": "3", + }, + }, + }, + }, + { + treePath: ".gitea/workflows/jobs-outputs-with-matrix-failure.yml", + fileContent: `name: jobs-outputs-with-matrix-failure +on: + push: + paths: + - '.gitea/workflows/jobs-outputs-with-matrix-failure.yml' +jobs: + job1: + runs-on: ubuntu-latest + outputs: + output_1: ${{ steps.gen_output.outputs.output_1 }} + output_2: ${{ steps.gen_output.outputs.output_2 }} + output_3: ${{ steps.gen_output.outputs.output_3 }} + strategy: + matrix: + version: [1, 2, 3] + steps: + - name: Generate output + id: gen_output + run: | + version="${{ matrix.version }}" + echo "output_${version}=${version}" >> "$GITHUB_OUTPUT" + job2: + runs-on: ubuntu-latest + if: ${{ always() }} + needs: [job1] + steps: + - run: echo '${{ toJSON(needs.job1.outputs) }}' +`, + outcomes: map[string]*mockTaskOutcome{ + "job1 (1)": { + result: runnerv1.Result_RESULT_SUCCESS, + outputs: map[string]string{ + "output_1": "1", + "output_2": "", + "output_3": "", + }, + }, + "job1 (2)": { + result: runnerv1.Result_RESULT_FAILURE, + outputs: map[string]string{ + "output_1": "", + "output_2": "", + "output_3": "", + }, + }, + "job1 (3)": { + result: runnerv1.Result_RESULT_SUCCESS, + outputs: map[string]string{ + "output_1": "", + "output_2": "", + "output_3": "3", + }, + }, + }, + expectedTaskNeeds: map[string]*runnerv1.TaskNeed{ + "job1": { + Result: runnerv1.Result_RESULT_FAILURE, + Outputs: map[string]string{ + "output_1": "1", + "output_2": "", + "output_3": "3", + }, + }, + }, + }, + } + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, user2.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + apiRepo := createActionsTestRepo(t, token, "actions-jobs-outputs-with-matrix", false) + runner := newMockRunner() + runner.registerAsRepoRunner(t, user2.Name, apiRepo.Name, "mock-runner", []string{"ubuntu-latest"}) + + for _, tc := range testCases { + t.Run(fmt.Sprintf("test %s", tc.treePath), func(t *testing.T) { + opts := getWorkflowCreateFileOptions(user2, apiRepo.DefaultBranch, fmt.Sprintf("create %s", tc.treePath), tc.fileContent) + createWorkflowFile(t, token, user2.Name, apiRepo.Name, tc.treePath, opts) + + for i := 0; i < len(tc.outcomes); i++ { + task := runner.fetchTask(t) + jobName := getTaskJobNameByTaskID(t, token, user2.Name, apiRepo.Name, task.Id) + outcome := tc.outcomes[jobName] + assert.NotNil(t, outcome) + runner.execTask(t, task, outcome) + } + + task := runner.fetchTask(t) + actualTaskNeeds := task.Needs + assert.Len(t, actualTaskNeeds, len(tc.expectedTaskNeeds)) + for jobID, tn := range tc.expectedTaskNeeds { + actualNeed := actualTaskNeeds[jobID] + assert.Equal(t, tn.Result, actualNeed.Result) + assert.Len(t, actualNeed.Outputs, len(tn.Outputs)) + for outputKey, outputValue := range tn.Outputs { + assert.Equal(t, outputValue, actualNeed.Outputs[outputKey]) + } + } + }) + } + + httpContext := NewAPITestContext(t, user2.Name, apiRepo.Name, auth_model.AccessTokenScopeWriteRepository) + doAPIDeleteRepository(httpContext)(t) + }) +} + +func createActionsTestRepo(t *testing.T, authToken, repoName string, isPrivate bool) *api.Repository { + req := NewRequestWithJSON(t, "POST", "/api/v1/user/repos", &api.CreateRepoOption{ + Name: repoName, + Private: isPrivate, + Readme: "Default", + AutoInit: true, + DefaultBranch: "main", + }).AddTokenAuth(authToken) + resp := MakeRequest(t, req, http.StatusCreated) + var apiRepo api.Repository + DecodeJSON(t, resp, &apiRepo) + return &apiRepo +} + +func getWorkflowCreateFileOptions(u *user_model.User, branch, msg, content string) *api.CreateFileOptions { + return &api.CreateFileOptions{ + FileOptions: api.FileOptions{ + BranchName: branch, + Message: msg, + Author: api.Identity{ + Name: u.Name, + Email: u.Email, + }, + Committer: api.Identity{ + Name: u.Name, + Email: u.Email, + }, + Dates: api.CommitDateOptions{ + Author: time.Now(), + Committer: time.Now(), + }, + }, + ContentBase64: base64.StdEncoding.EncodeToString([]byte(content)), + } +} + +func createWorkflowFile(t *testing.T, authToken, ownerName, repoName, treePath string, opts *api.CreateFileOptions) *api.FileResponse { + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", ownerName, repoName, treePath), opts). + AddTokenAuth(authToken) + resp := MakeRequest(t, req, http.StatusCreated) + var fileResponse api.FileResponse + DecodeJSON(t, resp, &fileResponse) + return &fileResponse +} + +// getTaskJobNameByTaskID get the job name of the task by task ID +// there is currently not an API for querying a task by ID so we have to list all the tasks +func getTaskJobNameByTaskID(t *testing.T, authToken, ownerName, repoName string, taskID int64) string { + // FIXME: we may need to query several pages + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/tasks", ownerName, repoName)). + AddTokenAuth(authToken) + resp := MakeRequest(t, req, http.StatusOK) + var taskRespBefore api.ActionTaskResponse + DecodeJSON(t, resp, &taskRespBefore) + for _, apiTask := range taskRespBefore.Entries { + if apiTask.ID == taskID { + return apiTask.Name + } + } + return "" +} diff --git a/tests/integration/actions_log_test.go b/tests/integration/actions_log_test.go new file mode 100644 index 0000000000..fd055fc4c4 --- /dev/null +++ b/tests/integration/actions_log_test.go @@ -0,0 +1,159 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "fmt" + "net/http" + "net/url" + "strings" + "testing" + "time" + + auth_model "code.gitea.io/gitea/models/auth" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/storage" + "code.gitea.io/gitea/modules/test" + + runnerv1 "code.gitea.io/actions-proto-go/runner/v1" + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestDownloadTaskLogs(t *testing.T) { + now := time.Now() + testCases := []struct { + treePath string + fileContent string + outcome *mockTaskOutcome + zstdEnabled bool + }{ + { + treePath: ".gitea/workflows/download-task-logs-zstd.yml", + fileContent: `name: download-task-logs-zstd +on: + push: + paths: + - '.gitea/workflows/download-task-logs-zstd.yml' +jobs: + job1: + runs-on: ubuntu-latest + steps: + - run: echo job1 with zstd enabled +`, + outcome: &mockTaskOutcome{ + result: runnerv1.Result_RESULT_SUCCESS, + logRows: []*runnerv1.LogRow{ + { + Time: timestamppb.New(now.Add(1 * time.Second)), + Content: " \U0001F433 docker create image", + }, + { + Time: timestamppb.New(now.Add(2 * time.Second)), + Content: "job1 zstd enabled", + }, + { + Time: timestamppb.New(now.Add(3 * time.Second)), + Content: "\U0001F3C1 Job succeeded", + }, + }, + }, + zstdEnabled: true, + }, + { + treePath: ".gitea/workflows/download-task-logs-no-zstd.yml", + fileContent: `name: download-task-logs-no-zstd +on: + push: + paths: + - '.gitea/workflows/download-task-logs-no-zstd.yml' +jobs: + job1: + runs-on: ubuntu-latest + steps: + - run: echo job1 with zstd disabled +`, + outcome: &mockTaskOutcome{ + result: runnerv1.Result_RESULT_SUCCESS, + logRows: []*runnerv1.LogRow{ + { + Time: timestamppb.New(now.Add(4 * time.Second)), + Content: " \U0001F433 docker create image", + }, + { + Time: timestamppb.New(now.Add(5 * time.Second)), + Content: "job1 zstd disabled", + }, + { + Time: timestamppb.New(now.Add(6 * time.Second)), + Content: "\U0001F3C1 Job succeeded", + }, + }, + }, + zstdEnabled: false, + }, + } + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, user2.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + apiRepo := createActionsTestRepo(t, token, "actions-download-task-logs", false) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID}) + runner := newMockRunner() + runner.registerAsRepoRunner(t, user2.Name, repo.Name, "mock-runner", []string{"ubuntu-latest"}) + + for _, tc := range testCases { + t.Run(fmt.Sprintf("test %s", tc.treePath), func(t *testing.T) { + var resetFunc func() + if tc.zstdEnabled { + resetFunc = test.MockVariableValue(&setting.Actions.LogCompression, "zstd") + assert.True(t, setting.Actions.LogCompression.IsZstd()) + } else { + resetFunc = test.MockVariableValue(&setting.Actions.LogCompression, "none") + assert.False(t, setting.Actions.LogCompression.IsZstd()) + } + + // create the workflow file + opts := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, fmt.Sprintf("create %s", tc.treePath), tc.fileContent) + createWorkflowFile(t, token, user2.Name, repo.Name, tc.treePath, opts) + + // fetch and execute task + task := runner.fetchTask(t) + runner.execTask(t, task, tc.outcome) + + // check whether the log file exists + logFileName := fmt.Sprintf("%s/%02x/%d.log", repo.FullName(), task.Id%256, task.Id) + if setting.Actions.LogCompression.IsZstd() { + logFileName += ".zst" + } + _, err := storage.Actions.Stat(logFileName) + assert.NoError(t, err) + + // download task logs and check content + runIndex := task.Context.GetFields()["run_number"].GetStringValue() + req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/0/logs", user2.Name, repo.Name, runIndex)). + AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + logTextLines := strings.Split(strings.TrimSpace(resp.Body.String()), "\n") + assert.Len(t, logTextLines, len(tc.outcome.logRows)) + for idx, lr := range tc.outcome.logRows { + assert.Equal( + t, + fmt.Sprintf("%s %s", lr.Time.AsTime().Format("2006-01-02T15:04:05.0000000Z07:00"), lr.Content), + logTextLines[idx], + ) + } + + resetFunc() + }) + } + + httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository) + doAPIDeleteRepository(httpContext)(t) + }) +} diff --git a/tests/integration/actions_runner_test.go b/tests/integration/actions_runner_test.go new file mode 100644 index 0000000000..355ea1705e --- /dev/null +++ b/tests/integration/actions_runner_test.go @@ -0,0 +1,157 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" + + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/modules/setting" + + pingv1 "code.gitea.io/actions-proto-go/ping/v1" + "code.gitea.io/actions-proto-go/ping/v1/pingv1connect" + runnerv1 "code.gitea.io/actions-proto-go/runner/v1" + "code.gitea.io/actions-proto-go/runner/v1/runnerv1connect" + "connectrpc.com/connect" + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/types/known/timestamppb" +) + +type mockRunner struct { + client *mockRunnerClient +} + +type mockRunnerClient struct { + pingServiceClient pingv1connect.PingServiceClient + runnerServiceClient runnerv1connect.RunnerServiceClient +} + +func newMockRunner() *mockRunner { + client := newMockRunnerClient("", "") + return &mockRunner{client: client} +} + +func newMockRunnerClient(uuid, token string) *mockRunnerClient { + baseURL := fmt.Sprintf("%sapi/actions", setting.AppURL) + + opt := connect.WithInterceptors(connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { + if uuid != "" { + req.Header().Set("x-runner-uuid", uuid) + } + if token != "" { + req.Header().Set("x-runner-token", token) + } + return next(ctx, req) + } + })) + + client := &mockRunnerClient{ + pingServiceClient: pingv1connect.NewPingServiceClient(http.DefaultClient, baseURL, opt), + runnerServiceClient: runnerv1connect.NewRunnerServiceClient(http.DefaultClient, baseURL, opt), + } + + return client +} + +func (r *mockRunner) doPing(t *testing.T) { + resp, err := r.client.pingServiceClient.Ping(context.Background(), connect.NewRequest(&pingv1.PingRequest{ + Data: "mock-runner", + })) + assert.NoError(t, err) + assert.Equal(t, "Hello, mock-runner!", resp.Msg.Data) +} + +func (r *mockRunner) doRegister(t *testing.T, name, token string, labels []string) { + r.doPing(t) + resp, err := r.client.runnerServiceClient.Register(context.Background(), connect.NewRequest(&runnerv1.RegisterRequest{ + Name: name, + Token: token, + Version: "mock-runner-version", + Labels: labels, + })) + assert.NoError(t, err) + r.client = newMockRunnerClient(resp.Msg.Runner.Uuid, resp.Msg.Runner.Token) +} + +func (r *mockRunner) registerAsRepoRunner(t *testing.T, ownerName, repoName, runnerName string, labels []string) { + session := loginUser(t, ownerName) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners/registration-token", ownerName, repoName)).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var registrationToken struct { + Token string `json:"token"` + } + DecodeJSON(t, resp, ®istrationToken) + r.doRegister(t, runnerName, registrationToken.Token, labels) +} + +func (r *mockRunner) fetchTask(t *testing.T, timeout ...time.Duration) *runnerv1.Task { + fetchTimeout := 10 * time.Second + if len(timeout) > 0 { + fetchTimeout = timeout[0] + } + ddl := time.Now().Add(fetchTimeout) + var task *runnerv1.Task + for time.Now().Before(ddl) { + resp, err := r.client.runnerServiceClient.FetchTask(context.Background(), connect.NewRequest(&runnerv1.FetchTaskRequest{ + TasksVersion: 0, + })) + assert.NoError(t, err) + if resp.Msg.Task != nil { + task = resp.Msg.Task + break + } + time.Sleep(time.Second) + } + assert.NotNil(t, task, "failed to fetch a task") + return task +} + +type mockTaskOutcome struct { + result runnerv1.Result + outputs map[string]string + logRows []*runnerv1.LogRow + execTime time.Duration +} + +func (r *mockRunner) execTask(t *testing.T, task *runnerv1.Task, outcome *mockTaskOutcome) { + for idx, lr := range outcome.logRows { + resp, err := r.client.runnerServiceClient.UpdateLog(context.Background(), connect.NewRequest(&runnerv1.UpdateLogRequest{ + TaskId: task.Id, + Index: int64(idx), + Rows: []*runnerv1.LogRow{lr}, + NoMore: idx == len(outcome.logRows)-1, + })) + assert.NoError(t, err) + assert.EqualValues(t, idx+1, resp.Msg.AckIndex) + } + sentOutputKeys := make([]string, 0, len(outcome.outputs)) + for outputKey, outputValue := range outcome.outputs { + resp, err := r.client.runnerServiceClient.UpdateTask(context.Background(), connect.NewRequest(&runnerv1.UpdateTaskRequest{ + State: &runnerv1.TaskState{ + Id: task.Id, + Result: runnerv1.Result_RESULT_UNSPECIFIED, + }, + Outputs: map[string]string{outputKey: outputValue}, + })) + assert.NoError(t, err) + sentOutputKeys = append(sentOutputKeys, outputKey) + assert.ElementsMatch(t, sentOutputKeys, resp.Msg.SentOutputs) + } + time.Sleep(outcome.execTime) + resp, err := r.client.runnerServiceClient.UpdateTask(context.Background(), connect.NewRequest(&runnerv1.UpdateTaskRequest{ + State: &runnerv1.TaskState{ + Id: task.Id, + Result: outcome.result, + StoppedAt: timestamppb.Now(), + }, + })) + assert.NoError(t, err) + assert.Equal(t, outcome.result, resp.Msg.State.Result) +} diff --git a/tests/integration/actions_trigger_test.go b/tests/integration/actions_trigger_test.go index 4718aa7e73..f18ad9e85d 100644 --- a/tests/integration/actions_trigger_test.go +++ b/tests/integration/actions_trigger_test.go @@ -17,17 +17,20 @@ import ( issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/perm" repo_model "code.gitea.io/gitea/models/repo" - unit_model "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" actions_module "code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/test" + "code.gitea.io/gitea/modules/timeutil" + issue_service "code.gitea.io/gitea/services/issue" pull_service "code.gitea.io/gitea/services/pull" release_service "code.gitea.io/gitea/services/release" repo_service "code.gitea.io/gitea/services/repository" + commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus" files_service "code.gitea.io/gitea/services/repository/files" "github.com/stretchr/testify/assert" @@ -52,13 +55,6 @@ func TestPullRequestTargetEvent(t *testing.T) { assert.NoError(t, err) assert.NotEmpty(t, baseRepo) - // enable actions - err = repo_service.UpdateRepositoryUnits(db.DefaultContext, baseRepo, []repo_model.RepoUnit{{ - RepoID: baseRepo.ID, - Type: unit_model.TypeActions, - }}, nil) - assert.NoError(t, err) - // add user4 as the collaborator ctx := NewAPITestContext(t, baseRepo.OwnerName, baseRepo.Name, auth_model.AccessTokenScopeWriteRepository) t.Run("AddUser4AsCollaboratorWithReadAccess", doAPIAddCollaborator(ctx, "user4", perm.AccessModeRead)) @@ -228,13 +224,6 @@ func TestSkipCI(t *testing.T) { assert.NoError(t, err) assert.NotEmpty(t, repo) - // enable actions - err = repo_service.UpdateRepositoryUnits(db.DefaultContext, repo, []repo_model.RepoUnit{{ - RepoID: repo.ID, - Type: unit_model.TypeActions, - }}, nil) - assert.NoError(t, err) - // add workflow file to the repo addWorkflowToBaseResp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{ Files: []*files_service.ChangeRepoFile{ @@ -354,13 +343,6 @@ func TestCreateDeleteRefEvent(t *testing.T) { assert.NoError(t, err) assert.NotEmpty(t, repo) - // enable actions - err = repo_service.UpdateRepositoryUnits(db.DefaultContext, repo, []repo_model.RepoUnit{{ - RepoID: repo.ID, - Type: unit_model.TypeActions, - }}, nil) - assert.NoError(t, err) - // add workflow file to the repo addWorkflowToBaseResp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{ Files: []*files_service.ChangeRepoFile{ @@ -451,3 +433,221 @@ func TestCreateDeleteRefEvent(t *testing.T) { assert.NotNil(t, run) }) } + +func TestPullRequestCommitStatusEvent(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the repo + user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) // contributor of the repo + + // create a repo + repo, err := repo_service.CreateRepository(db.DefaultContext, user2, user2, repo_service.CreateRepoOptions{ + Name: "repo-pull-request", + Description: "test pull-request event", + AutoInit: true, + Gitignores: "Go", + License: "MIT", + Readme: "Default", + DefaultBranch: "main", + IsPrivate: false, + }) + assert.NoError(t, err) + assert.NotEmpty(t, repo) + + // add user4 as the collaborator + ctx := NewAPITestContext(t, repo.OwnerName, repo.Name, auth_model.AccessTokenScopeWriteRepository) + t.Run("AddUser4AsCollaboratorWithReadAccess", doAPIAddCollaborator(ctx, "user4", perm.AccessModeRead)) + + // add the workflow file to the repo + addWorkflow, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{ + Files: []*files_service.ChangeRepoFile{ + { + Operation: "create", + TreePath: ".gitea/workflows/pr.yml", + ContentReader: strings.NewReader("name: test\non:\n pull_request:\n types: [assigned, unassigned, labeled, unlabeled, opened, edited, closed, reopened, synchronize, milestoned, demilestoned, review_requested, review_request_removed]\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - run: echo helloworld\n"), + }, + }, + Message: "add workflow", + OldBranch: "main", + NewBranch: "main", + Author: &files_service.IdentityOptions{ + Name: user2.Name, + Email: user2.Email, + }, + Committer: &files_service.IdentityOptions{ + Name: user2.Name, + Email: user2.Email, + }, + Dates: &files_service.CommitDateOptions{ + Author: time.Now(), + Committer: time.Now(), + }, + }) + assert.NoError(t, err) + assert.NotEmpty(t, addWorkflow) + sha := addWorkflow.Commit.SHA + + // create a new branch + testBranch := "test-branch" + gitRepo, err := git.OpenRepository(git.DefaultContext, ".") + assert.NoError(t, err) + err = repo_service.CreateNewBranch(git.DefaultContext, user2, repo, gitRepo, "main", testBranch) + assert.NoError(t, err) + + // create Pull + pullIssue := &issues_model.Issue{ + RepoID: repo.ID, + Title: "A test PR", + PosterID: user2.ID, + Poster: user2, + IsPull: true, + } + pullRequest := &issues_model.PullRequest{ + HeadRepoID: repo.ID, + BaseRepoID: repo.ID, + HeadBranch: testBranch, + BaseBranch: "main", + HeadRepo: repo, + BaseRepo: repo, + Type: issues_model.PullRequestGitea, + } + prOpts := &pull_service.NewPullRequestOptions{Repo: repo, Issue: pullIssue, PullRequest: pullRequest} + err = pull_service.NewPullRequest(db.DefaultContext, prOpts) + assert.NoError(t, err) + + // opened + checkCommitStatusAndInsertFakeStatus(t, repo, sha) + + // edited + err = issue_service.ChangeContent(db.DefaultContext, pullIssue, user2, "test", 0) + assert.NoError(t, err) + checkCommitStatusAndInsertFakeStatus(t, repo, sha) + + // closed + err = issue_service.CloseIssue(db.DefaultContext, pullIssue, user2, "") + assert.NoError(t, err) + checkCommitStatusAndInsertFakeStatus(t, repo, sha) + + // reopened + err = issue_service.ReopenIssue(db.DefaultContext, pullIssue, user2, "") + assert.NoError(t, err) + checkCommitStatusAndInsertFakeStatus(t, repo, sha) + + // assign + removed, _, err := issue_service.ToggleAssigneeWithNotify(db.DefaultContext, pullIssue, user2, user4.ID) + assert.False(t, removed) + assert.NoError(t, err) + checkCommitStatusAndInsertFakeStatus(t, repo, sha) + + // unassign + removed, _, err = issue_service.ToggleAssigneeWithNotify(db.DefaultContext, pullIssue, user2, user4.ID) + assert.True(t, removed) + assert.NoError(t, err) + checkCommitStatusAndInsertFakeStatus(t, repo, sha) + + // labeled + label := &issues_model.Label{ + RepoID: repo.ID, + Name: "test", + Exclusive: false, + Description: "test", + Color: "#e11d21", + } + err = issues_model.NewLabel(db.DefaultContext, label) + assert.NoError(t, err) + err = issue_service.AddLabel(db.DefaultContext, pullIssue, user2, label) + assert.NoError(t, err) + checkCommitStatusAndInsertFakeStatus(t, repo, sha) + + // unlabeled + err = issue_service.RemoveLabel(db.DefaultContext, pullIssue, user2, label) + assert.NoError(t, err) + checkCommitStatusAndInsertFakeStatus(t, repo, sha) + + // synchronize + addFileResp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{ + Files: []*files_service.ChangeRepoFile{ + { + Operation: "create", + TreePath: "test.txt", + ContentReader: strings.NewReader("test"), + }, + }, + Message: "add file", + OldBranch: testBranch, + NewBranch: testBranch, + Author: &files_service.IdentityOptions{ + Name: user2.Name, + Email: user2.Email, + }, + Committer: &files_service.IdentityOptions{ + Name: user2.Name, + Email: user2.Email, + }, + Dates: &files_service.CommitDateOptions{ + Author: time.Now(), + Committer: time.Now(), + }, + }) + assert.NoError(t, err) + assert.NotEmpty(t, addFileResp) + sha = addFileResp.Commit.SHA + assert.Eventually(t, func() bool { + latestCommitStatuses, _, err := git_model.GetLatestCommitStatus(db.DefaultContext, repo.ID, sha, db.ListOptionsAll) + assert.NoError(t, err) + if len(latestCommitStatuses) == 0 { + return false + } + if latestCommitStatuses[0].State == api.CommitStatusPending { + insertFakeStatus(t, repo, sha, latestCommitStatuses[0].TargetURL, latestCommitStatuses[0].Context) + return true + } + return false + }, 1*time.Second, 100*time.Millisecond) + + // milestoned + milestone := &issues_model.Milestone{ + RepoID: repo.ID, + Name: "test", + Content: "test", + DeadlineUnix: timeutil.TimeStampNow(), + } + err = issues_model.NewMilestone(db.DefaultContext, milestone) + assert.NoError(t, err) + err = issue_service.ChangeMilestoneAssign(db.DefaultContext, pullIssue, user2, milestone.ID) + assert.NoError(t, err) + checkCommitStatusAndInsertFakeStatus(t, repo, sha) + + // demilestoned + err = issue_service.ChangeMilestoneAssign(db.DefaultContext, pullIssue, user2, milestone.ID) + assert.NoError(t, err) + checkCommitStatusAndInsertFakeStatus(t, repo, sha) + + // review_requested + _, err = issue_service.ReviewRequest(db.DefaultContext, pullIssue, user2, nil, user4, true) + assert.NoError(t, err) + checkCommitStatusAndInsertFakeStatus(t, repo, sha) + + // review_request_removed + _, err = issue_service.ReviewRequest(db.DefaultContext, pullIssue, user2, nil, user4, false) + assert.NoError(t, err) + checkCommitStatusAndInsertFakeStatus(t, repo, sha) + }) +} + +func checkCommitStatusAndInsertFakeStatus(t *testing.T, repo *repo_model.Repository, sha string) { + latestCommitStatuses, _, err := git_model.GetLatestCommitStatus(db.DefaultContext, repo.ID, sha, db.ListOptionsAll) + assert.NoError(t, err) + assert.Len(t, latestCommitStatuses, 1) + assert.Equal(t, api.CommitStatusPending, latestCommitStatuses[0].State) + + insertFakeStatus(t, repo, sha, latestCommitStatuses[0].TargetURL, latestCommitStatuses[0].Context) +} + +func insertFakeStatus(t *testing.T, repo *repo_model.Repository, sha, targetURL, context string) { + err := commitstatus_service.CreateCommitStatus(db.DefaultContext, repo, user_model.NewActionsUser(), sha, &git_model.CommitStatus{ + State: api.CommitStatusSuccess, + TargetURL: targetURL, + Context: context, + }) + assert.NoError(t, err) +} diff --git a/tests/integration/api_issue_label_test.go b/tests/integration/api_issue_label_test.go index 0e4cd8243b..c9cdd46b9a 100644 --- a/tests/integration/api_issue_label_test.go +++ b/tests/integration/api_issue_label_test.go @@ -117,27 +117,33 @@ func TestAPIAddIssueLabels(t *testing.T) { func TestAPIAddIssueLabelsWithLabelNames(t *testing.T) { assert.NoError(t, unittest.LoadFixtures()) - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repo.ID}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 6, RepoID: repo.ID}) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + repoLabel := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 10, RepoID: repo.ID}) + orgLabel := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 4, OrgID: owner.ID}) - session := loginUser(t, owner.Name) - token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue) - urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels", - repo.OwnerName, repo.Name, issue.Index) + user1Session := loginUser(t, "user1") + token := getTokenForLoggedInUser(t, user1Session, auth_model.AccessTokenScopeWriteIssue) + + // add the org label and the repo label to the issue + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels", owner.Name, repo.Name, issue.Index) req := NewRequestWithJSON(t, "POST", urlStr, &api.IssueLabelsOption{ - Labels: []any{"label1", "label2"}, + Labels: []any{repoLabel.Name, orgLabel.Name}, }).AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusOK) var apiLabels []*api.Label DecodeJSON(t, resp, &apiLabels) assert.Len(t, apiLabels, unittest.GetCount(t, &issues_model.IssueLabel{IssueID: issue.ID})) - var apiLabelNames []string for _, label := range apiLabels { apiLabelNames = append(apiLabelNames, label.Name) } - assert.ElementsMatch(t, apiLabelNames, []string{"label1", "label2"}) + assert.ElementsMatch(t, apiLabelNames, []string{repoLabel.Name, orgLabel.Name}) + + // delete labels + req = NewRequest(t, "DELETE", urlStr).AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) } func TestAPIReplaceIssueLabels(t *testing.T) { diff --git a/tests/integration/api_packages_maven_test.go b/tests/integration/api_packages_maven_test.go index e54238858c..486a5af93e 100644 --- a/tests/integration/api_packages_maven_test.go +++ b/tests/integration/api_packages_maven_test.go @@ -6,6 +6,7 @@ package integration import ( "fmt" "net/http" + "net/url" "strconv" "strings" "sync" @@ -20,6 +21,7 @@ import ( "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestPackageMaven(t *testing.T) { @@ -29,16 +31,14 @@ func TestPackageMaven(t *testing.T) { groupID := "com.gitea" artifactID := "test-project" - packageName := groupID + "-" + artifactID packageVersion := "1.0.1" packageDescription := "Test Description" - root := fmt.Sprintf("/api/packages/%s/maven/%s/%s", user.Name, strings.ReplaceAll(groupID, ".", "/"), artifactID) - filename := fmt.Sprintf("%s-%s.jar", packageName, packageVersion) + root := "/api/packages/user2/maven/com/gitea/test-project" + filename := "any-name.jar" putFile := func(t *testing.T, path, content string, expectedStatus int) { - req := NewRequestWithBody(t, "PUT", root+path, strings.NewReader(content)). - AddBasicAuth(user.Name) + req := NewRequestWithBody(t, "PUT", root+path, strings.NewReader(content)).AddBasicAuth(user.Name) MakeRequest(t, req, expectedStatus) } @@ -56,27 +56,67 @@ func TestPackageMaven(t *testing.T) { putFile(t, "/maven-metadata.xml", "test", http.StatusOK) pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeMaven) - assert.NoError(t, err) + require.NoError(t, err) assert.Len(t, pvs, 1) pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0]) - assert.NoError(t, err) + require.NoError(t, err) assert.Nil(t, pd.SemVer) assert.Nil(t, pd.Metadata) - assert.Equal(t, packageName, pd.Package.Name) + assert.Equal(t, groupID+":"+artifactID, pd.Package.Name) assert.Equal(t, packageVersion, pd.Version.Version) pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID) - assert.NoError(t, err) + require.NoError(t, err) assert.Len(t, pfs, 1) assert.Equal(t, filename, pfs[0].Name) assert.False(t, pfs[0].IsLead) pb, err := packages.GetBlobByID(db.DefaultContext, pfs[0].BlobID) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, int64(4), pb.Size) }) + t.Run("UploadLegacy", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + legacyRootLink := "/api/packages/user2/maven/com/gitea/legacy-project" + req := NewRequestWithBody(t, "PUT", legacyRootLink+"/1.0.2/any-file-name?use_legacy_package_name=1", strings.NewReader("test-content")).AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusCreated) + p, err := packages.GetPackageByName(db.DefaultContext, user.ID, packages.TypeMaven, "com.gitea-legacy-project") + require.NoError(t, err) + assert.Equal(t, "com.gitea-legacy-project", p.Name) + + req = NewRequest(t, "HEAD", legacyRootLink+"/1.0.2/any-file-name").AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusOK) + + req = NewRequest(t, "GET", "/user2/-/packages/maven/com.gitea-legacy-project/1.0.2") + MakeRequest(t, req, http.StatusOK) + req = NewRequest(t, "GET", "/user2/-/packages/maven/com.gitea:legacy-project/1.0.2") + MakeRequest(t, req, http.StatusNotFound) + req = NewRequest(t, "GET", "/user2/-/packages/maven/com.gitea%3Alegacy-project/1.0.2") + MakeRequest(t, req, http.StatusNotFound) + + req = NewRequestWithBody(t, "PUT", legacyRootLink+"/1.0.3/any-file-name", strings.NewReader("test-content")).AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusCreated) + _, err = packages.GetPackageByName(db.DefaultContext, user.ID, packages.TypeMaven, "com.gitea-legacy-project") + require.ErrorIs(t, err, packages.ErrPackageNotExist) + p, err = packages.GetPackageByName(db.DefaultContext, user.ID, packages.TypeMaven, "com.gitea:legacy-project") + require.NoError(t, err) + assert.Equal(t, "com.gitea:legacy-project", p.Name) + req = NewRequest(t, "HEAD", legacyRootLink+"/1.0.2/any-file-name").AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusOK) + + req = NewRequest(t, "GET", "/user2/-/packages/maven/com.gitea-legacy-project/1.0.2") + MakeRequest(t, req, http.StatusNotFound) + req = NewRequest(t, "GET", "/user2/-/packages/maven/com.gitea:legacy-project/1.0.2") + MakeRequest(t, req, http.StatusOK) + req = NewRequest(t, "GET", "/user2/-/packages/maven/com.gitea%3Alegacy-project/1.0.2") + MakeRequest(t, req, http.StatusOK) + + require.NoError(t, packages.DeletePackageByID(db.DefaultContext, p.ID)) + }) + t.Run("UploadExists", func(t *testing.T) { defer tests.PrintCurrentTest(t)() @@ -86,14 +126,12 @@ func TestPackageMaven(t *testing.T) { t.Run("Download", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - req := NewRequest(t, "HEAD", fmt.Sprintf("%s/%s/%s", root, packageVersion, filename)). - AddBasicAuth(user.Name) + req := NewRequest(t, "HEAD", fmt.Sprintf("%s/%s/%s", root, packageVersion, filename)).AddBasicAuth(user.Name) resp := MakeRequest(t, req, http.StatusOK) checkHeaders(t, resp.Header(), "application/java-archive", 4) - req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s", root, packageVersion, filename)). - AddBasicAuth(user.Name) + req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s", root, packageVersion, filename)).AddBasicAuth(user.Name) resp = MakeRequest(t, req, http.StatusOK) checkHeaders(t, resp.Header(), "application/java-archive", 4) @@ -101,7 +139,7 @@ func TestPackageMaven(t *testing.T) { assert.Equal(t, []byte("test"), resp.Body.Bytes()) pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeMaven) - assert.NoError(t, err) + require.NoError(t, err) assert.Len(t, pvs, 1) assert.Equal(t, int64(0), pvs[0].DownloadCount) }) @@ -133,26 +171,26 @@ func TestPackageMaven(t *testing.T) { defer tests.PrintCurrentTest(t)() pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeMaven) - assert.NoError(t, err) + require.NoError(t, err) assert.Len(t, pvs, 1) pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0]) - assert.NoError(t, err) + require.NoError(t, err) assert.Nil(t, pd.Metadata) putFile(t, fmt.Sprintf("/%s/%s.pom", packageVersion, filename), pomContent, http.StatusCreated) pvs, err = packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeMaven) - assert.NoError(t, err) + require.NoError(t, err) assert.Len(t, pvs, 1) pd, err = packages.GetPackageDescriptor(db.DefaultContext, pvs[0]) - assert.NoError(t, err) + require.NoError(t, err) assert.IsType(t, &maven.Metadata{}, pd.Metadata) assert.Equal(t, packageDescription, pd.Metadata.(*maven.Metadata).Description) pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID) - assert.NoError(t, err) + require.NoError(t, err) assert.Len(t, pfs, 2) for _, pf := range pfs { if strings.HasSuffix(pf.Name, ".pom") { @@ -167,14 +205,12 @@ func TestPackageMaven(t *testing.T) { t.Run("DownloadPOM", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - req := NewRequest(t, "HEAD", fmt.Sprintf("%s/%s/%s.pom", root, packageVersion, filename)). - AddBasicAuth(user.Name) + req := NewRequest(t, "HEAD", fmt.Sprintf("%s/%s/%s.pom", root, packageVersion, filename)).AddBasicAuth(user.Name) resp := MakeRequest(t, req, http.StatusOK) checkHeaders(t, resp.Header(), "text/xml", int64(len(pomContent))) - req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s.pom", root, packageVersion, filename)). - AddBasicAuth(user.Name) + req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s.pom", root, packageVersion, filename)).AddBasicAuth(user.Name) resp = MakeRequest(t, req, http.StatusOK) checkHeaders(t, resp.Header(), "text/xml", int64(len(pomContent))) @@ -182,7 +218,7 @@ func TestPackageMaven(t *testing.T) { assert.Equal(t, []byte(pomContent), resp.Body.Bytes()) pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeMaven) - assert.NoError(t, err) + require.NoError(t, err) assert.Len(t, pvs, 1) assert.Equal(t, int64(1), pvs[0].DownloadCount) }) @@ -190,8 +226,7 @@ func TestPackageMaven(t *testing.T) { t.Run("DownloadChecksums", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - req := NewRequest(t, "GET", fmt.Sprintf("%s/1.2.3/%s", root, filename)). - AddBasicAuth(user.Name) + req := NewRequest(t, "GET", fmt.Sprintf("%s/1.2.3/%s", root, filename)).AddBasicAuth(user.Name) MakeRequest(t, req, http.StatusNotFound) for key, checksum := range map[string]string{ @@ -200,8 +235,7 @@ func TestPackageMaven(t *testing.T) { "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "sha512": "ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff", } { - req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s.%s", root, packageVersion, filename, key)). - AddBasicAuth(user.Name) + req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s.%s", root, packageVersion, filename, key)).AddBasicAuth(user.Name) resp := MakeRequest(t, req, http.StatusOK) assert.Equal(t, checksum, resp.Body.String()) @@ -211,8 +245,7 @@ func TestPackageMaven(t *testing.T) { t.Run("DownloadMetadata", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - req := NewRequest(t, "GET", root+"/maven-metadata.xml"). - AddBasicAuth(user.Name) + req := NewRequest(t, "GET", root+"/maven-metadata.xml").AddBasicAuth(user.Name) resp := MakeRequest(t, req, http.StatusOK) expectedMetadata := `<?xml version="1.0" encoding="UTF-8"?>` + "\n<metadata><groupId>com.gitea</groupId><artifactId>test-project</artifactId><versioning><release>1.0.1</release><latest>1.0.1</latest><versions><version>1.0.1</version></versions></versioning></metadata>" @@ -227,8 +260,7 @@ func TestPackageMaven(t *testing.T) { "sha256": "3f48322f81c4b2c3bb8649ae1e5c9801476162b520e1c2734ac06b2c06143208", "sha512": "cb075aa2e2ef1a83cdc14dd1e08c505b72d633399b39e73a21f00f0deecb39a3e2c79f157c1163f8a3854828750706e0dec3a0f5e4778e91f8ec2cf351a855f2", } { - req := NewRequest(t, "GET", fmt.Sprintf("%s/maven-metadata.xml.%s", root, key)). - AddBasicAuth(user.Name) + req := NewRequest(t, "GET", fmt.Sprintf("%s/maven-metadata.xml.%s", root, key)).AddBasicAuth(user.Name) resp := MakeRequest(t, req, http.StatusOK) assert.Equal(t, checksum, resp.Body.String()) @@ -245,9 +277,10 @@ func TestPackageMaven(t *testing.T) { }) t.Run("InvalidFile", func(t *testing.T) { - ver := packageVersion + "-invalid" - putFile(t, fmt.Sprintf("/%s/%s", ver, filename), "any invalid content", http.StatusCreated) - req := NewRequestf(t, "GET", "/%s/-/packages/maven/%s-%s/%s", user.Name, groupID, artifactID, ver) + invalidVersion := packageVersion + "-invalid" + putFile(t, fmt.Sprintf("/%s/%s", invalidVersion, filename), "any invalid content", http.StatusCreated) + + req := NewRequestf(t, "GET", "/%s/-/packages/maven/%s/%s", user.Name, url.QueryEscape(groupID+":"+artifactID), invalidVersion) resp := MakeRequest(t, req, http.StatusOK) assert.Contains(t, resp.Body.String(), "No metadata.") assert.True(t, test.IsNormalPageCompleted(resp.Body.String())) @@ -266,8 +299,7 @@ func TestPackageMavenConcurrent(t *testing.T) { root := fmt.Sprintf("/api/packages/%s/maven/%s/%s", user.Name, strings.ReplaceAll(groupID, ".", "/"), artifactID) putFile := func(t *testing.T, path, content string, expectedStatus int) { - req := NewRequestWithBody(t, "PUT", root+path, strings.NewReader(content)). - AddBasicAuth(user.Name) + req := NewRequestWithBody(t, "PUT", root+path, strings.NewReader(content)).AddBasicAuth(user.Name) MakeRequest(t, req, expectedStatus) } diff --git a/tests/integration/api_packages_npm_test.go b/tests/integration/api_packages_npm_test.go index b9660aeeb9..ae1dd876f7 100644 --- a/tests/integration/api_packages_npm_test.go +++ b/tests/integration/api_packages_npm_test.go @@ -52,24 +52,33 @@ func TestPackageNpm(t *testing.T) { "` + packageTag + `": "` + version + `" }, "versions": { - "` + version + `": { - "name": "` + packageName + `", - "version": "` + version + `", - "description": "` + packageDescription + `", - "author": { - "name": "` + packageAuthor + `" - }, - "bin": { - "` + packageBinName + `": "` + packageBinPath + `" - }, - "dist": { - "integrity": "sha512-yA4FJsVhetynGfOC1jFf79BuS+jrHbm0fhh+aHzCQkOaOBXKf9oBnC4a6DnLLnEsHQDRLYd00cwj8sCXpC+wIg==", - "shasum": "aaa7eaf852a948b0aa05afeda35b1badca155d90" - }, - "repository": { - "type": "` + repoType + `", - "url": "` + repoURL + `" - } + "` + version + `": { + "name": "` + packageName + `", + "version": "` + version + `", + "description": "` + packageDescription + `", + "author": { + "name": "` + packageAuthor + `" + }, + "bin": { + "` + packageBinName + `": "` + packageBinPath + `" + }, + "dist": { + "integrity": "sha512-yA4FJsVhetynGfOC1jFf79BuS+jrHbm0fhh+aHzCQkOaOBXKf9oBnC4a6DnLLnEsHQDRLYd00cwj8sCXpC+wIg==", + "shasum": "aaa7eaf852a948b0aa05afeda35b1badca155d90" + }, + "repository": { + "type": "` + repoType + `", + "url": "` + repoURL + `" + }, + "peerDependencies": { + "tea": "2.x", + "soy-milk": "1.2" + }, + "peerDependenciesMeta": { + "soy-milk": { + "optional": true + } + } } }, "_attachments": { @@ -178,6 +187,8 @@ func TestPackageNpm(t *testing.T) { assert.Equal(t, fmt.Sprintf("%s%s/-/%s/%s", setting.AppURL, root[1:], packageVersion, filename), pmv.Dist.Tarball) assert.Equal(t, repoType, result.Repository.Type) assert.Equal(t, repoURL, result.Repository.URL) + assert.Equal(t, map[string]string{"tea": "2.x", "soy-milk": "1.2"}, pmv.PeerDependencies) + assert.Equal(t, map[string]any{"soy-milk": map[string]any{"optional": true}}, pmv.PeerDependenciesMeta) }) t.Run("AddTag", func(t *testing.T) { diff --git a/tests/integration/api_packages_pypi_test.go b/tests/integration/api_packages_pypi_test.go index e973f6a52a..2dabb5005b 100644 --- a/tests/integration/api_packages_pypi_test.go +++ b/tests/integration/api_packages_pypi_test.go @@ -32,15 +32,16 @@ func TestPackagePyPI(t *testing.T) { packageVersion := "1!1.0.1+r1234" packageAuthor := "KN4CK3R" packageDescription := "Test Description" + projectURL := "https://example.com" content := "test" hashSHA256 := "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" root := fmt.Sprintf("/api/packages/%s/pypi", user.Name) - uploadFile := func(t *testing.T, filename, content string, expectedStatus int) { - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) + createBasicMultipartFile := func(filename, packageName, content string) (body *bytes.Buffer, writer *multipart.Writer, closer func() error) { + body = &bytes.Buffer{} + writer = multipart.NewWriter(body) part, _ := writer.CreateFormFile("content", filename) _, _ = io.Copy(part, strings.NewReader(content)) @@ -52,14 +53,27 @@ func TestPackagePyPI(t *testing.T) { writer.WriteField("sha256_digest", hashSHA256) writer.WriteField("requires_python", "3.6") - _ = writer.Close() + return body, writer, writer.Close + } + uploadHelper := func(t *testing.T, body *bytes.Buffer, contentType string, expectedStatus int) { req := NewRequestWithBody(t, "POST", root, body). - SetHeader("Content-Type", writer.FormDataContentType()). + SetHeader("Content-Type", contentType). AddBasicAuth(user.Name) MakeRequest(t, req, expectedStatus) } + uploadFile := func(t *testing.T, filename, content string, expectedStatus int) { + body, writer, closeFunc := createBasicMultipartFile(filename, packageName, content) + + writer.WriteField("project_urls", "DOCUMENTATION , https://readthedocs.org") + writer.WriteField("project_urls", fmt.Sprintf("Home-page, %s", projectURL)) + + _ = closeFunc() + + uploadHelper(t, body, writer.FormDataContentType(), expectedStatus) + } + t.Run("Upload", func(t *testing.T) { defer tests.PrintCurrentTest(t)() @@ -74,6 +88,7 @@ func TestPackagePyPI(t *testing.T) { assert.NoError(t, err) assert.Nil(t, pd.SemVer) assert.IsType(t, &pypi.Metadata{}, pd.Metadata) + assert.Equal(t, projectURL, pd.Metadata.(*pypi.Metadata).ProjectURL) assert.Equal(t, packageName, pd.Package.Name) assert.Equal(t, packageVersion, pd.Version.Version) @@ -133,6 +148,48 @@ func TestPackagePyPI(t *testing.T) { uploadFile(t, "test.tar.gz", content, http.StatusConflict) }) + t.Run("UploadUsingDeprecatedHomepageMetadata", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + pkgName := "homepage-package" + body, writer, closeFunc := createBasicMultipartFile("test.whl", pkgName, content) + + writer.WriteField("home_page", projectURL) + + _ = closeFunc() + + uploadHelper(t, body, writer.FormDataContentType(), http.StatusCreated) + + pvs, err := packages.GetVersionsByPackageName(db.DefaultContext, user.ID, packages.TypePyPI, pkgName) + assert.NoError(t, err) + assert.Len(t, pvs, 1) + + pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0]) + assert.NoError(t, err) + assert.IsType(t, &pypi.Metadata{}, pd.Metadata) + assert.Equal(t, projectURL, pd.Metadata.(*pypi.Metadata).ProjectURL) + }) + + t.Run("UploadWithoutAnyHomepageURLMetadata", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + pkgName := "no-project-url-or-homepage-package" + body, writer, closeFunc := createBasicMultipartFile("test.whl", pkgName, content) + + _ = closeFunc() + + uploadHelper(t, body, writer.FormDataContentType(), http.StatusCreated) + + pvs, err := packages.GetVersionsByPackageName(db.DefaultContext, user.ID, packages.TypePyPI, pkgName) + assert.NoError(t, err) + assert.Len(t, pvs, 1) + + pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0]) + assert.NoError(t, err) + assert.IsType(t, &pypi.Metadata{}, pd.Metadata) + assert.Empty(t, pd.Metadata.(*pypi.Metadata).ProjectURL) + }) + t.Run("Download", func(t *testing.T) { defer tests.PrintCurrentTest(t)() @@ -147,7 +204,7 @@ func TestPackagePyPI(t *testing.T) { downloadFile("test.whl") downloadFile("test.tar.gz") - pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypePyPI) + pvs, err := packages.GetVersionsByPackageName(db.DefaultContext, user.ID, packages.TypePyPI, packageName) assert.NoError(t, err) assert.Len(t, pvs, 1) assert.Equal(t, int64(2), pvs[0].DownloadCount) diff --git a/tests/integration/api_repo_git_hook_test.go b/tests/integration/api_repo_git_hook_test.go index 9917b41790..c28c4336e2 100644 --- a/tests/integration/api_repo_git_hook_test.go +++ b/tests/integration/api_repo_git_hook_test.go @@ -12,185 +12,190 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" ) -const testHookContent = `#!/bin/bash +func TestAPIGitHooks(t *testing.T) { + defer tests.PrepareTestEnv(t)() + defer test.MockVariableValue(&setting.DisableGitHooks, false)() -echo Hello, World! + const testHookContent = `#!/bin/bash +echo "TestGitHookScript" ` -func TestAPIListGitHooks(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 37}) - owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) - - // user1 is an admin user - session := loginUser(t, "user1") - token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git", owner.Name, repo.Name). - AddTokenAuth(token) - resp := MakeRequest(t, req, http.StatusOK) - var apiGitHooks []*api.GitHook - DecodeJSON(t, resp, &apiGitHooks) - assert.Len(t, apiGitHooks, 3) - for _, apiGitHook := range apiGitHooks { - if apiGitHook.Name == "pre-receive" { - assert.True(t, apiGitHook.IsActive) - assert.Equal(t, testHookContent, apiGitHook.Content) - } else { + t.Run("ListGitHooks", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 37}) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + + // user1 is an admin user + session := loginUser(t, "user1") + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git", owner.Name, repo.Name). + AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var apiGitHooks []*api.GitHook + DecodeJSON(t, resp, &apiGitHooks) + assert.Len(t, apiGitHooks, 3) + for _, apiGitHook := range apiGitHooks { + if apiGitHook.Name == "pre-receive" { + assert.True(t, apiGitHook.IsActive) + assert.Equal(t, testHookContent, apiGitHook.Content) + } else { + assert.False(t, apiGitHook.IsActive) + assert.Empty(t, apiGitHook.Content) + } + } + }) + + t.Run("NoGitHooks", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + + // user1 is an admin user + session := loginUser(t, "user1") + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git", owner.Name, repo.Name). + AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var apiGitHooks []*api.GitHook + DecodeJSON(t, resp, &apiGitHooks) + assert.Len(t, apiGitHooks, 3) + for _, apiGitHook := range apiGitHooks { assert.False(t, apiGitHook.IsActive) assert.Empty(t, apiGitHook.Content) } - } -} - -func TestAPIListGitHooksNoHooks(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) - - // user1 is an admin user - session := loginUser(t, "user1") - token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git", owner.Name, repo.Name). - AddTokenAuth(token) - resp := MakeRequest(t, req, http.StatusOK) - var apiGitHooks []*api.GitHook - DecodeJSON(t, resp, &apiGitHooks) - assert.Len(t, apiGitHooks, 3) - for _, apiGitHook := range apiGitHooks { - assert.False(t, apiGitHook.IsActive) - assert.Empty(t, apiGitHook.Content) - } -} - -func TestAPIListGitHooksNoAccess(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) - - session := loginUser(t, owner.Name) - token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git", owner.Name, repo.Name). - AddTokenAuth(token) - MakeRequest(t, req, http.StatusForbidden) -} - -func TestAPIGetGitHook(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 37}) - owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) - - // user1 is an admin user - session := loginUser(t, "user1") - token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git/pre-receive", owner.Name, repo.Name). - AddTokenAuth(token) - resp := MakeRequest(t, req, http.StatusOK) - var apiGitHook *api.GitHook - DecodeJSON(t, resp, &apiGitHook) - assert.True(t, apiGitHook.IsActive) - assert.Equal(t, testHookContent, apiGitHook.Content) -} - -func TestAPIGetGitHookNoAccess(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) - - session := loginUser(t, owner.Name) - token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git/pre-receive", owner.Name, repo.Name). - AddTokenAuth(token) - MakeRequest(t, req, http.StatusForbidden) -} - -func TestAPIEditGitHook(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) - - // user1 is an admin user - session := loginUser(t, "user1") - token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) - - urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/hooks/git/pre-receive", - owner.Name, repo.Name) - req := NewRequestWithJSON(t, "PATCH", urlStr, &api.EditGitHookOption{ - Content: testHookContent, - }).AddTokenAuth(token) - resp := MakeRequest(t, req, http.StatusOK) - var apiGitHook *api.GitHook - DecodeJSON(t, resp, &apiGitHook) - assert.True(t, apiGitHook.IsActive) - assert.Equal(t, testHookContent, apiGitHook.Content) - - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git/pre-receive", owner.Name, repo.Name). - AddTokenAuth(token) - resp = MakeRequest(t, req, http.StatusOK) - var apiGitHook2 *api.GitHook - DecodeJSON(t, resp, &apiGitHook2) - assert.True(t, apiGitHook2.IsActive) - assert.Equal(t, testHookContent, apiGitHook2.Content) -} - -func TestAPIEditGitHookNoAccess(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) - - session := loginUser(t, owner.Name) - token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) - urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/hooks/git/pre-receive", owner.Name, repo.Name) - req := NewRequestWithJSON(t, "PATCH", urlStr, &api.EditGitHookOption{ - Content: testHookContent, - }).AddTokenAuth(token) - MakeRequest(t, req, http.StatusForbidden) -} - -func TestAPIDeleteGitHook(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 37}) - owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) - - // user1 is an admin user - session := loginUser(t, "user1") - token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) - - req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/hooks/git/pre-receive", owner.Name, repo.Name). - AddTokenAuth(token) - MakeRequest(t, req, http.StatusNoContent) - - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git/pre-receive", owner.Name, repo.Name). - AddTokenAuth(token) - resp := MakeRequest(t, req, http.StatusOK) - var apiGitHook2 *api.GitHook - DecodeJSON(t, resp, &apiGitHook2) - assert.False(t, apiGitHook2.IsActive) - assert.Empty(t, apiGitHook2.Content) -} - -func TestAPIDeleteGitHookNoAccess(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) - - session := loginUser(t, owner.Name) - token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) - req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/hooks/git/pre-receive", owner.Name, repo.Name). - AddTokenAuth(token) - MakeRequest(t, req, http.StatusForbidden) + }) + + t.Run("ListGitHooksNoAccess", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + + session := loginUser(t, owner.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git", owner.Name, repo.Name). + AddTokenAuth(token) + MakeRequest(t, req, http.StatusForbidden) + }) + + t.Run("GetGitHook", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 37}) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + + // user1 is an admin user + session := loginUser(t, "user1") + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git/pre-receive", owner.Name, repo.Name). + AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var apiGitHook *api.GitHook + DecodeJSON(t, resp, &apiGitHook) + assert.True(t, apiGitHook.IsActive) + assert.Equal(t, testHookContent, apiGitHook.Content) + }) + t.Run("GetGitHookNoAccess", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + + session := loginUser(t, owner.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git/pre-receive", owner.Name, repo.Name). + AddTokenAuth(token) + MakeRequest(t, req, http.StatusForbidden) + }) + + t.Run("EditGitHook", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + + // user1 is an admin user + session := loginUser(t, "user1") + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/hooks/git/pre-receive", + owner.Name, repo.Name) + req := NewRequestWithJSON(t, "PATCH", urlStr, &api.EditGitHookOption{ + Content: testHookContent, + }).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var apiGitHook *api.GitHook + DecodeJSON(t, resp, &apiGitHook) + assert.True(t, apiGitHook.IsActive) + assert.Equal(t, testHookContent, apiGitHook.Content) + + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git/pre-receive", owner.Name, repo.Name). + AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + var apiGitHook2 *api.GitHook + DecodeJSON(t, resp, &apiGitHook2) + assert.True(t, apiGitHook2.IsActive) + assert.Equal(t, testHookContent, apiGitHook2.Content) + }) + + t.Run("EditGitHookNoAccess", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + + session := loginUser(t, owner.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/hooks/git/pre-receive", owner.Name, repo.Name) + req := NewRequestWithJSON(t, "PATCH", urlStr, &api.EditGitHookOption{ + Content: testHookContent, + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusForbidden) + }) + + t.Run("DeleteGitHook", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 37}) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + + // user1 is an admin user + session := loginUser(t, "user1") + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + + req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/hooks/git/pre-receive", owner.Name, repo.Name). + AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git/pre-receive", owner.Name, repo.Name). + AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var apiGitHook2 *api.GitHook + DecodeJSON(t, resp, &apiGitHook2) + assert.False(t, apiGitHook2.IsActive) + assert.Empty(t, apiGitHook2.Content) + }) + + t.Run("DeleteGitHookNoAccess", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + + session := loginUser(t, owner.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/hooks/git/pre-receive", owner.Name, repo.Name). + AddTokenAuth(token) + MakeRequest(t, req, http.StatusForbidden) + }) } diff --git a/tests/integration/auth_ldap_test.go b/tests/integration/auth_ldap_test.go index 5d37244331..5c50fd0288 100644 --- a/tests/integration/auth_ldap_test.go +++ b/tests/integration/auth_ldap_test.go @@ -332,7 +332,7 @@ func TestLDAPUserSyncWithGroupFilter(t *testing.T) { // Assert members of LDAP group "cn=git" are added for _, gitLDAPUser := range te.gitLDAPUsers { - unittest.BeanExists(t, &user_model.User{ + unittest.AssertExistsAndLoadBean(t, &user_model.User{ Name: gitLDAPUser.UserName, }) } diff --git a/tests/integration/compare_test.go b/tests/integration/compare_test.go index d960416b3a..cbf927813e 100644 --- a/tests/integration/compare_test.go +++ b/tests/integration/compare_test.go @@ -27,7 +27,7 @@ func TestCompareTag(t *testing.T) { req := NewRequest(t, "GET", "/user2/repo1/compare/v1.1...master") resp := session.MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) - selection := htmlDoc.doc.Find(".choose.branch .filter.dropdown") + selection := htmlDoc.doc.Find(".ui.dropdown.select-branch") // A dropdown for both base and head. assert.Lenf(t, selection.Nodes, 2, "The template has changed") @@ -44,7 +44,7 @@ func TestCompareDefault(t *testing.T) { req := NewRequest(t, "GET", "/user2/repo1/compare/v1.1") resp := session.MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) - selection := htmlDoc.doc.Find(".choose.branch .filter.dropdown") + selection := htmlDoc.doc.Find(".ui.dropdown.select-branch") assert.Lenf(t, selection.Nodes, 2, "The template has changed") } diff --git a/tests/integration/dump_restore_test.go b/tests/integration/dump_restore_test.go index 47bb6f76e9..abec8f300c 100644 --- a/tests/integration/dump_restore_test.go +++ b/tests/integration/dump_restore_test.go @@ -66,7 +66,7 @@ func TestDumpRestore(t *testing.T) { Milestones: true, Comments: true, AuthToken: token, - CloneAddr: repo.CloneLink().HTTPS, + CloneAddr: repo.CloneLinkGeneral(context.Background()).HTTPS, RepoName: reponame, } err = migrations.DumpRepository(ctx, basePath, repoOwner.Name, opts) @@ -96,7 +96,7 @@ func TestDumpRestore(t *testing.T) { // Phase 3: dump restored from the Gitea instance to the filesystem // opts.RepoName = newreponame - opts.CloneAddr = newrepo.CloneLink().HTTPS + opts.CloneAddr = newrepo.CloneLinkGeneral(context.Background()).HTTPS err = migrations.DumpRepository(ctx, basePath, repoOwner.Name, opts) assert.NoError(t, err) diff --git a/tests/integration/empty_repo_test.go b/tests/integration/empty_repo_test.go index 630a3c03af..0801b093df 100644 --- a/tests/integration/empty_repo_test.go +++ b/tests/integration/empty_repo_test.go @@ -58,8 +58,12 @@ func TestEmptyRepoAddFile(t *testing.T) { defer tests.PrepareTestEnv(t)() session := loginUser(t, "user30") - req := NewRequest(t, "GET", "/user30/empty/_new/"+setting.Repository.DefaultBranch) + req := NewRequest(t, "GET", "/user30/empty") resp := session.MakeRequest(t, req, http.StatusOK) + assert.Contains(t, resp.Body.String(), "empty-repo-guide") + + req = NewRequest(t, "GET", "/user30/empty/_new/"+setting.Repository.DefaultBranch) + resp = session.MakeRequest(t, req, http.StatusOK) doc := NewHTMLParser(t, resp.Body).Find(`input[name="commit_choice"]`) assert.Empty(t, doc.AttrOr("checked", "_no_")) req = NewRequestWithValues(t, "POST", "/user30/empty/_new/"+setting.Repository.DefaultBranch, map[string]string{ diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index 9b3b2f2b92..46b93b0a10 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -88,18 +88,6 @@ func TestMain(m *testing.M) { tests.InitTest(true) testWebRoutes = routers.NormalRoutes() - os.Unsetenv("GIT_AUTHOR_NAME") - os.Unsetenv("GIT_AUTHOR_EMAIL") - os.Unsetenv("GIT_AUTHOR_DATE") - os.Unsetenv("GIT_COMMITTER_NAME") - os.Unsetenv("GIT_COMMITTER_EMAIL") - os.Unsetenv("GIT_COMMITTER_DATE") - - // Avoid loading the default system config. On MacOS, this config - // sets the osxkeychain credential helper, which will cause tests - // to freeze with a dialog. - os.Setenv("GIT_CONFIG_NOSYSTEM", "true") - err := unittest.InitFixtures( unittest.FixturesOptions{ Dir: filepath.Join(filepath.Dir(setting.AppPath), "models/fixtures/"), diff --git a/tests/integration/org_profile_test.go b/tests/integration/org_profile_test.go new file mode 100644 index 0000000000..73cafd85c2 --- /dev/null +++ b/tests/integration/org_profile_test.go @@ -0,0 +1,132 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "encoding/base64" + "fmt" + "net/http" + "net/url" + "testing" + "time" + + auth_model "code.gitea.io/gitea/models/auth" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/routers/web/shared/user" + + "github.com/stretchr/testify/assert" +) + +func getCreateProfileReadmeFileOptions(content string) api.CreateFileOptions { + contentEncoded := base64.StdEncoding.EncodeToString([]byte(content)) + return api.CreateFileOptions{ + FileOptions: api.FileOptions{ + BranchName: "main", + NewBranchName: "main", + Message: "create the profile README.md", + Dates: api.CommitDateOptions{ + Author: time.Unix(946684810, 0), + Committer: time.Unix(978307190, 0), + }, + }, + ContentBase64: contentEncoded, + } +} + +func createTestProfile(t *testing.T, orgName, profileRepoName, readmeContent string) { + isPrivate := profileRepoName == user.RepoNameProfilePrivate + + ctx := NewAPITestContext(t, "user1", profileRepoName, auth_model.AccessTokenScopeAll) + session := loginUser(t, "user1") + tokenAdmin := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll) + + // create repo + doAPICreateOrganizationRepository(ctx, orgName, &api.CreateRepoOption{Name: profileRepoName, Private: isPrivate})(t) + + // create readme + createFileOptions := getCreateProfileReadmeFileOptions(readmeContent) + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", orgName, profileRepoName, "README.md"), &createFileOptions). + AddTokenAuth(tokenAdmin) + MakeRequest(t, req, http.StatusCreated) +} + +func TestOrgProfile(t *testing.T) { + onGiteaRun(t, testOrgProfile) +} + +func testOrgProfile(t *testing.T, u *url.URL) { + const contentPublicReadme = "Public Readme Content" + const contentPrivateReadme = "Private Readme Content" + // HTML: "#org-home-view-as-dropdown" (indicate whether the view as dropdown menu is present) + + // PART 1: Test Both Private and Public + createTestProfile(t, "org3", user.RepoNameProfile, contentPublicReadme) + createTestProfile(t, "org3", user.RepoNameProfilePrivate, contentPrivateReadme) + + // Anonymous User + req := NewRequest(t, "GET", "org3") + resp := MakeRequest(t, req, http.StatusOK) + bodyString := util.UnsafeBytesToString(resp.Body.Bytes()) + assert.Contains(t, bodyString, contentPublicReadme) + assert.NotContains(t, bodyString, `id="org-home-view-as-dropdown"`) + + // Logged in but not member + session := loginUser(t, "user24") + req = NewRequest(t, "GET", "org3") + resp = session.MakeRequest(t, req, http.StatusOK) + bodyString = util.UnsafeBytesToString(resp.Body.Bytes()) + assert.Contains(t, bodyString, contentPublicReadme) + assert.NotContains(t, bodyString, `id="org-home-view-as-dropdown"`) + + // Site Admin + session = loginUser(t, "user1") + req = NewRequest(t, "GET", "/org3") + resp = session.MakeRequest(t, req, http.StatusOK) + bodyString = util.UnsafeBytesToString(resp.Body.Bytes()) + assert.Contains(t, bodyString, contentPrivateReadme) // as an org member, default to show the private profile + assert.Contains(t, bodyString, `id="org-home-view-as-dropdown"`) + + req = NewRequest(t, "GET", "/org3?view_as=member") + resp = session.MakeRequest(t, req, http.StatusOK) + bodyString = util.UnsafeBytesToString(resp.Body.Bytes()) + assert.Contains(t, bodyString, contentPrivateReadme) + assert.Contains(t, bodyString, `id="org-home-view-as-dropdown"`) + + req = NewRequest(t, "GET", "/org3?view_as=public") + resp = session.MakeRequest(t, req, http.StatusOK) + bodyString = util.UnsafeBytesToString(resp.Body.Bytes()) + assert.Contains(t, bodyString, contentPublicReadme) + assert.Contains(t, bodyString, `id="org-home-view-as-dropdown"`) + + // PART 2: Each org has either one of private pr public profile + createTestProfile(t, "org41", user.RepoNameProfile, contentPublicReadme) + createTestProfile(t, "org42", user.RepoNameProfilePrivate, contentPrivateReadme) + + // Anonymous User + req = NewRequest(t, "GET", "/org41") + resp = MakeRequest(t, req, http.StatusOK) + bodyString = util.UnsafeBytesToString(resp.Body.Bytes()) + assert.Contains(t, bodyString, contentPublicReadme) + assert.NotContains(t, bodyString, `id="org-home-view-as-dropdown"`) + + req = NewRequest(t, "GET", "/org42") + resp = MakeRequest(t, req, http.StatusOK) + bodyString = util.UnsafeBytesToString(resp.Body.Bytes()) + assert.NotContains(t, bodyString, contentPrivateReadme) + assert.NotContains(t, bodyString, `id="org-home-view-as-dropdown"`) + + // Site Admin + req = NewRequest(t, "GET", "/org41") + resp = session.MakeRequest(t, req, http.StatusOK) + bodyString = util.UnsafeBytesToString(resp.Body.Bytes()) + assert.Contains(t, bodyString, contentPublicReadme) + assert.NotContains(t, bodyString, `id="org-home-view-as-dropdown"`) + + req = NewRequest(t, "GET", "/org42") + resp = session.MakeRequest(t, req, http.StatusOK) + bodyString = util.UnsafeBytesToString(resp.Body.Bytes()) + assert.Contains(t, bodyString, contentPrivateReadme) + assert.NotContains(t, bodyString, `id="org-home-view-as-dropdown"`) +} diff --git a/tests/integration/org_team_invite_test.go b/tests/integration/org_team_invite_test.go index 274fde4085..4c1053702e 100644 --- a/tests/integration/org_team_invite_test.go +++ b/tests/integration/org_team_invite_test.go @@ -274,7 +274,8 @@ func TestOrgTeamEmailInviteRedirectsNewUserWithActivation(t *testing.T) { user, err := user_model.GetUserByName(db.DefaultContext, "doesnotexist") assert.NoError(t, err) - activateURL := fmt.Sprintf("/user/activate?code=%s", user.GenerateEmailActivateCode("doesnotexist@example.com")) + activationCode := user_model.GenerateUserTimeLimitCode(&user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateAccount}, user) + activateURL := fmt.Sprintf("/user/activate?code=%s", activationCode) req = NewRequestWithValues(t, "POST", activateURL, map[string]string{ "password": "examplePassword!1", }) diff --git a/tests/integration/pull_create_test.go b/tests/integration/pull_create_test.go index 9812d2073d..162ea532c8 100644 --- a/tests/integration/pull_create_test.go +++ b/tests/integration/pull_create_test.go @@ -12,6 +12,8 @@ import ( "strings" "testing" + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/tests" @@ -82,6 +84,30 @@ func testPullCreateDirectly(t *testing.T, session *TestSession, baseRepoOwner, b return resp } +func testPullCreateFailure(t *testing.T, session *TestSession, baseRepoOwner, baseRepoName, baseBranch, headRepoOwner, headRepoName, headBranch, title string) *httptest.ResponseRecorder { + headCompare := headBranch + if headRepoOwner != "" { + if headRepoName != "" { + headCompare = fmt.Sprintf("%s/%s:%s", headRepoOwner, headRepoName, headBranch) + } else { + headCompare = fmt.Sprintf("%s:%s", headRepoOwner, headBranch) + } + } + req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/compare/%s...%s", baseRepoOwner, baseRepoName, baseBranch, headCompare)) + resp := session.MakeRequest(t, req, http.StatusOK) + + // Submit the form for creating the pull + htmlDoc := NewHTMLParser(t, resp.Body) + link, exists := htmlDoc.doc.Find("form.ui.form").Attr("action") + assert.True(t, exists, "The template has changed") + req = NewRequestWithValues(t, "POST", link, map[string]string{ + "_csrf": htmlDoc.GetCSRF(), + "title": title, + }) + resp = session.MakeRequest(t, req, http.StatusBadRequest) + return resp +} + func TestPullCreate(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user1") @@ -226,3 +252,64 @@ func TestPullCreatePrFromBaseToFork(t *testing.T) { assert.Regexp(t, "^/user1/repo1/pulls/[0-9]*$", url) }) } + +func TestCreateAgitPullWithReadPermission(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + dstPath := t.TempDir() + + u.Path = "user2/repo1.git" + u.User = url.UserPassword("user4", userPassword) + + t.Run("Clone", doGitClone(dstPath, u)) + + t.Run("add commit", doGitAddSomeCommits(dstPath, "master")) + + t.Run("do agit pull create", func(t *testing.T) { + err := git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master", "-o").AddDynamicArguments("topic=" + "test-topic").Run(&git.RunOpts{Dir: dstPath}) + assert.NoError(t, err) + }) + }) +} + +/* +Setup: user2 has repository, user1 forks it +--- + +1. User2 blocks User1 +2. User1 adds changes to fork +3. User1 attempts to create a pull request +4. User1 sees alert that the action is not allowed because of the block +*/ +func TestCreatePullWhenBlocked(t *testing.T) { + RepoOwner := "user2" + ForkOwner := "user16" + onGiteaRun(t, func(t *testing.T, u *url.URL) { + // Setup + // User1 forks repo1 from User2 + sessionFork := loginUser(t, ForkOwner) + testRepoFork(t, sessionFork, RepoOwner, "repo1", ForkOwner, "forkrepo1", "") + + // 1. User2 blocks user1 + // sessionBase := loginUser(t, "user2") + token := getUserToken(t, RepoOwner, auth_model.AccessTokenScopeWriteUser) + + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/blocks/%s", ForkOwner)). + AddTokenAuth(token) + MakeRequest(t, req, http.StatusNotFound) + req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/blocks/%s", ForkOwner)). + AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + + // 2. User1 adds changes to fork + testEditFile(t, sessionFork, ForkOwner, "forkrepo1", "master", "README.md", "Hello, World (Edited)\n") + + // 3. User1 attempts to create a pull request + testPullCreateFailure(t, sessionFork, RepoOwner, "repo1", "master", ForkOwner, "forkrepo1", "master", "This is a pull title") + + // Teardown + // Unblock user + req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/user/blocks/%s", ForkOwner)). + AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + }) +} diff --git a/tests/integration/pull_review_test.go b/tests/integration/pull_review_test.go index 5ecf3ef469..68de421413 100644 --- a/tests/integration/pull_review_test.go +++ b/tests/integration/pull_review_test.go @@ -92,7 +92,7 @@ func TestPullView_CodeOwner(t *testing.T) { testPullCreate(t, session, "user2", "test_codeowner", false, repo.DefaultBranch, "codeowner-basebranch", "Test Pull Request") pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadRepoID: repo.ID, HeadBranch: "codeowner-basebranch"}) - unittest.AssertExistsIf(t, true, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 5}) + unittest.AssertExistsAndLoadBean(t, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 5}) assert.NoError(t, pr.LoadIssue(db.DefaultContext)) err := issue_service.ChangeTitle(db.DefaultContext, pr.Issue, user2, "[WIP] Test Pull Request") @@ -139,7 +139,7 @@ func TestPullView_CodeOwner(t *testing.T) { testPullCreate(t, session, "user2", "test_codeowner", false, repo.DefaultBranch, "codeowner-basebranch2", "Test Pull Request2") pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadBranch: "codeowner-basebranch2"}) - unittest.AssertExistsIf(t, true, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8}) + unittest.AssertExistsAndLoadBean(t, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8}) }) t.Run("Forked Repo Pull Request", func(t *testing.T) { @@ -169,13 +169,13 @@ func TestPullView_CodeOwner(t *testing.T) { testPullCreateDirectly(t, session, "user5", "test_codeowner", forkedRepo.DefaultBranch, "", "", "codeowner-basebranch-forked", "Test Pull Request on Forked Repository") pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: forkedRepo.ID, HeadBranch: "codeowner-basebranch-forked"}) - unittest.AssertExistsIf(t, false, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8}) + unittest.AssertNotExistsBean(t, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8}) // create a pull request to base repository, code reviewers should be mentioned testPullCreateDirectly(t, session, repo.OwnerName, repo.Name, repo.DefaultBranch, forkedRepo.OwnerName, forkedRepo.Name, "codeowner-basebranch-forked", "Test Pull Request3") pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadRepoID: forkedRepo.ID, HeadBranch: "codeowner-basebranch-forked"}) - unittest.AssertExistsIf(t, true, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8}) + unittest.AssertExistsAndLoadBean(t, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8}) }) }) } diff --git a/tests/integration/repo_commits_test.go b/tests/integration/repo_commits_test.go index bb65d9e04a..fc066e06d3 100644 --- a/tests/integration/repo_commits_test.go +++ b/tests/integration/repo_commits_test.go @@ -30,7 +30,7 @@ func TestRepoCommits(t *testing.T) { resp := session.MakeRequest(t, req, http.StatusOK) doc := NewHTMLParser(t, resp.Body) - commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Attr("href") + commitURL, exists := doc.doc.Find("#commits-table .commit-id-short").Attr("href") assert.True(t, exists) assert.NotEmpty(t, commitURL) } @@ -46,7 +46,7 @@ func doTestRepoCommitWithStatus(t *testing.T, state string, classes ...string) { doc := NewHTMLParser(t, resp.Body) // Get first commit URL - commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Attr("href") + commitURL, exists := doc.doc.Find("#commits-table .commit-id-short").Attr("href") assert.True(t, exists) assert.NotEmpty(t, commitURL) @@ -64,7 +64,7 @@ func doTestRepoCommitWithStatus(t *testing.T, state string, classes ...string) { doc = NewHTMLParser(t, resp.Body) // Check if commit status is displayed in message column (.tippy-target to ignore the tippy trigger) - sel := doc.doc.Find("#commits-table tbody tr td.message .tippy-target .commit-status") + sel := doc.doc.Find("#commits-table .message .tippy-target .commit-status") assert.Equal(t, 1, sel.Length()) for _, class := range classes { assert.True(t, sel.HasClass(class)) @@ -140,7 +140,7 @@ func TestRepoCommitsStatusParallel(t *testing.T) { doc := NewHTMLParser(t, resp.Body) // Get first commit URL - commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Attr("href") + commitURL, exists := doc.doc.Find("#commits-table .commit-id-short").Attr("href") assert.True(t, exists) assert.NotEmpty(t, commitURL) @@ -175,7 +175,7 @@ func TestRepoCommitsStatusMultiple(t *testing.T) { doc := NewHTMLParser(t, resp.Body) // Get first commit URL - commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Attr("href") + commitURL, exists := doc.doc.Find("#commits-table .commit-id-short").Attr("href") assert.True(t, exists) assert.NotEmpty(t, commitURL) @@ -200,6 +200,6 @@ func TestRepoCommitsStatusMultiple(t *testing.T) { doc = NewHTMLParser(t, resp.Body) // Check that the data-tippy="commit-statuses" (for trigger) and commit-status (svg) are present - sel := doc.doc.Find("#commits-table tbody tr td.message [data-tippy=\"commit-statuses\"] .commit-status") + sel := doc.doc.Find("#commits-table .message [data-tippy=\"commit-statuses\"] .commit-status") assert.Equal(t, 1, sel.Length()) } diff --git a/tests/integration/repofiles_change_test.go b/tests/integration/repofiles_change_test.go index 9f938c4099..d86dcc01fe 100644 --- a/tests/integration/repofiles_change_test.go +++ b/tests/integration/repofiles_change_test.go @@ -247,7 +247,7 @@ func TestChangeRepoFilesForCreate(t *testing.T) { // setup onGiteaRun(t, func(t *testing.T, u *url.URL) { 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) @@ -284,7 +284,7 @@ func TestChangeRepoFilesForUpdate(t *testing.T) { // setup onGiteaRun(t, func(t *testing.T, u *url.URL) { 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) @@ -318,7 +318,7 @@ func TestChangeRepoFilesForUpdateWithFileMove(t *testing.T) { // setup onGiteaRun(t, func(t *testing.T, u *url.URL) { 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) @@ -369,7 +369,7 @@ func TestChangeRepoFilesWithoutBranchNames(t *testing.T) { // setup onGiteaRun(t, func(t *testing.T, u *url.URL) { 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) @@ -405,7 +405,7 @@ func testDeleteRepoFiles(t *testing.T, u *url.URL) { // setup 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) @@ -444,7 +444,7 @@ func testDeleteRepoFilesWithoutBranchNames(t *testing.T, u *url.URL) { // setup 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) @@ -474,7 +474,7 @@ func TestChangeRepoFilesErrors(t *testing.T) { // setup onGiteaRun(t, func(t *testing.T, u *url.URL) { 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/tests/integration/signin_test.go b/tests/integration/signin_test.go index abad9eb5e5..d7c0b1bcd3 100644 --- a/tests/integration/signin_test.go +++ b/tests/integration/signin_test.go @@ -9,6 +9,7 @@ import ( "strings" "testing" + "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" @@ -17,6 +18,7 @@ import ( "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func testLoginFailed(t *testing.T, username, password, message string) { @@ -42,7 +44,7 @@ func TestSignin(t *testing.T) { user.Name = "testuser" user.LowerName = strings.ToLower(user.Name) user.ID = 0 - unittest.AssertSuccessfulInsert(t, user) + require.NoError(t, db.Insert(db.DefaultContext, user)) samples := []struct { username string diff --git a/tests/integration/signup_test.go b/tests/integration/signup_test.go index e9a05201ee..e86851352e 100644 --- a/tests/integration/signup_test.go +++ b/tests/integration/signup_test.go @@ -9,6 +9,7 @@ import ( "strings" "testing" + "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" @@ -99,34 +100,39 @@ func TestSignupEmailActive(t *testing.T) { // try to sign up and send the activation email req := NewRequestWithValues(t, "POST", "/user/sign_up", map[string]string{ - "user_name": "test-user-1", - "email": "email-1@example.com", + "user_name": "Test-User-1", + "email": "EmAiL-1@example.com", "password": "password1", "retype": "password1", }) resp := MakeRequest(t, req, http.StatusOK) - assert.Contains(t, resp.Body.String(), `A new confirmation email has been sent to <b>email-1@example.com</b>.`) + assert.Contains(t, resp.Body.String(), `A new confirmation email has been sent to <b>EmAiL-1@example.com</b>.`) // access "user/activate" means trying to re-send the activation email session := loginUserWithPassword(t, "test-user-1", "password1") resp = session.MakeRequest(t, NewRequest(t, "GET", "/user/activate"), http.StatusOK) assert.Contains(t, resp.Body.String(), "You have already requested an activation email recently") - // access anywhere else will see a "Activate Your Account" prompt, and there is a chance to change email + // access anywhere else will see an "Activate Your Account" prompt, and there is a chance to change email resp = session.MakeRequest(t, NewRequest(t, "GET", "/user/issues"), http.StatusOK) assert.Contains(t, resp.Body.String(), `<input id="change-email" name="change_email" `) // post to "user/activate" with a new email session.MakeRequest(t, NewRequestWithValues(t, "POST", "/user/activate", map[string]string{"change_email": "email-changed@example.com"}), http.StatusSeeOther) - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "test-user-1"}) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "Test-User-1"}) assert.Equal(t, "email-changed@example.com", user.Email) email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "email-changed@example.com"}) assert.False(t, email.IsActivated) assert.True(t, email.IsPrimary) + // generate an activation code from lower-cased email + activationCode := user_model.GenerateUserTimeLimitCode(&user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateAccount}, user) + // and update the user email to case-sensitive, it shouldn't affect the verification later + _, _ = db.Exec(db.DefaultContext, "UPDATE `user` SET email=? WHERE id=?", "EmAiL-changed@example.com", user.ID) + user = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "Test-User-1"}) + assert.Equal(t, "EmAiL-changed@example.com", user.Email) + // access "user/activate" with a valid activation code, then get the "verify password" page - user = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "test-user-1"}) - activationCode := user.GenerateEmailActivateCode(user.Email) resp = session.MakeRequest(t, NewRequest(t, "GET", "/user/activate?code="+activationCode), http.StatusOK) assert.Contains(t, resp.Body.String(), `<input id="verify-password"`) @@ -138,7 +144,7 @@ func TestSignupEmailActive(t *testing.T) { resp = session.MakeRequest(t, req, http.StatusOK) assert.Contains(t, resp.Body.String(), `Your password does not match`) assert.Contains(t, resp.Body.String(), `<input id="verify-password"`) - user = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "test-user-1"}) + user = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "Test-User-1"}) assert.False(t, user.IsActive) // then use a correct password, the user should be activated @@ -148,6 +154,6 @@ func TestSignupEmailActive(t *testing.T) { }) resp = session.MakeRequest(t, req, http.StatusSeeOther) assert.Equal(t, "/", test.RedirectURL(resp)) - user = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "test-user-1"}) + user = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "Test-User-1"}) assert.True(t, user.IsActive) } diff --git a/tests/mssql.ini.tmpl b/tests/mssql.ini.tmpl index 77c969e813..b50816b2cd 100644 --- a/tests/mssql.ini.tmpl +++ b/tests/mssql.ini.tmpl @@ -93,7 +93,6 @@ COLORIZE = true LEVEL = Debug [security] -DISABLE_GIT_HOOKS = false INSTALL_LOCK = true SECRET_KEY = 9pCviYTWSb INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0OTU1NTE2MTh9.hhSVGOANkaKk3vfCd2jDOIww4pUk0xtg9JRde5UogyQ diff --git a/tests/mysql.ini.tmpl b/tests/mysql.ini.tmpl index 0fddde46de..ec8307acc3 100644 --- a/tests/mysql.ini.tmpl +++ b/tests/mysql.ini.tmpl @@ -94,7 +94,6 @@ COLORIZE = true LEVEL = Debug [security] -DISABLE_GIT_HOOKS = false INSTALL_LOCK = true SECRET_KEY = 9pCviYTWSb INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0OTU1NTE2MTh9.hhSVGOANkaKk3vfCd2jDOIww4pUk0xtg9JRde5UogyQ diff --git a/tests/pgsql.ini.tmpl b/tests/pgsql.ini.tmpl index 695662c2e9..139ea9c2b7 100644 --- a/tests/pgsql.ini.tmpl +++ b/tests/pgsql.ini.tmpl @@ -94,7 +94,6 @@ COLORIZE = true LEVEL = Debug [security] -DISABLE_GIT_HOOKS = false INSTALL_LOCK = true SECRET_KEY = 9pCviYTWSb INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0OTU1NTE2MTh9.hhSVGOANkaKk3vfCd2jDOIww4pUk0xtg9JRde5UogyQ diff --git a/tests/sqlite.ini.tmpl b/tests/sqlite.ini.tmpl index 1cbcd8b2e5..2f7a3e8182 100644 --- a/tests/sqlite.ini.tmpl +++ b/tests/sqlite.ini.tmpl @@ -93,7 +93,6 @@ COLORIZE = true LEVEL = Debug [security] -DISABLE_GIT_HOOKS = false INSTALL_LOCK = true SECRET_KEY = 9pCviYTWSb INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0OTI3OTU5ODN9.OQkH5UmzID2XBdwQ9TAI6Jj2t1X-wElVTjbE7aoN4I8 diff --git a/tests/test_utils.go b/tests/test_utils.go index 0fe0200ea7..96eb5731b4 100644 --- a/tests/test_utils.go +++ b/tests/test_utils.go @@ -58,7 +58,7 @@ func InitTest(requireGitea bool) { _ = os.Setenv("GITEA_CONF", giteaConf) fmt.Printf("Environment variable $GITEA_CONF not set, use default: %s\n", giteaConf) if !setting.EnableSQLite3 { - testlogger.Fatalf(`sqlite3 requires: import _ "github.com/mattn/go-sqlite3" or -tags sqlite,sqlite_unlock_notify` + "\n") + testlogger.Fatalf(`sqlite3 requires: -tags sqlite,sqlite_unlock_notify` + "\n") } } if !filepath.IsAbs(giteaConf) { diff --git a/web_src/css/base.css b/web_src/css/base.css index 04f3678f3a..a1ee7044ec 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -336,8 +336,13 @@ a.label, border-color: var(--color-secondary); } +.ui.dropdown .menu > .header { + text-transform: none; /* reset fomantic's "uppercase" */ +} + .ui.dropdown .menu > .header:not(.ui) { color: var(--color-text); + font-size: 0.95em; /* reset fomantic's small font-size */ } .ui.dropdown .menu > .item { @@ -691,10 +696,6 @@ input:-webkit-autofill:active, box-shadow: 0 6px 18px var(--color-shadow) !important; } -.ui.dropdown .menu > .header { - font-size: 0.8em; -} - .ui .text.left { text-align: left !important; } @@ -742,15 +743,10 @@ input:-webkit-autofill:active, font-family: var(--fonts-monospace); font-size: 13px; font-weight: var(--font-weight-normal); - margin: 0 6px; - padding: 5px 10px; + padding: 3px 5px; flex-shrink: 0; } -.ui .sha.label .shortsha { - display: inline-block; /* not sure whether it is still needed */ -} - .ui .button.truncate { display: inline-block; max-width: 100%; @@ -933,7 +929,8 @@ strong.attention-caution, svg.attention-caution { color: var(--color-red-dark-1); } -.center:not(.popup) { +/* FIXME: this is a longstanding dirty patch since 2015, it only makes the pages more messy and shouldn't be used */ +.center { text-align: center; } @@ -989,22 +986,6 @@ overflow-menu .ui.label { margin-top: 3em !important; } -/* multiple radio or checkboxes as inline element */ -.inline-grouped-list { - display: inline-block; - vertical-align: top; -} - -.inline-grouped-list > .ui { - display: block; - margin-top: 5px; - margin-bottom: 10px; -} - -.inline-grouped-list > .ui:first-child { - margin-top: 1px; -} - .lines-blame-btn { padding: 0 0 0 5px; display: flex; diff --git a/web_src/css/explore.css b/web_src/css/explore.css index 5cdee823c0..968a103ce9 100644 --- a/web_src/css/explore.css +++ b/web_src/css/explore.css @@ -1,13 +1,4 @@ -.explore .secondary-nav { - border-width: 1px !important; -} - -.explore .secondary-nav .svg { - width: 16px; - text-align: center; - margin-right: 5px; -} - +/* FIXME: need to refactor the repo branches list page and move these styles to proper place */ .ui.repository.branches .info { font-size: 12px; color: var(--color-text-light); @@ -20,12 +11,3 @@ overflow: hidden; text-overflow: ellipsis; } - -.ui.repository.branches .overflow-visible { - overflow: visible; -} - -/* fix alignment of PR popup in branches table */ -.ui.repository.branches table .ui.popup { - text-align: left; -} diff --git a/web_src/css/features/gitgraph.css b/web_src/css/features/gitgraph.css index f8f7e35cdc..1ed541a695 100644 --- a/web_src/css/features/gitgraph.css +++ b/web_src/css/features/gitgraph.css @@ -57,6 +57,12 @@ white-space: nowrap; display: flex; align-items: center; + gap: 0.25em; +} + +#git-graph-container li .ui.label.commit-id-short { + padding-top: 2px; + padding-bottom: 2px; } #git-graph-container li .node-relation { @@ -112,17 +118,6 @@ text-overflow: ellipsis; } -#git-graph-container #rev-list .sha.label { - padding-top: 5px; - padding-bottom: 3px; -} - -#git-graph-container #rev-list .sha.label .ui.detail.icon.button { - padding-top: 3px; - margin-top: -5px; - padding-bottom: 1px; -} - #git-graph-container #graph-raw-list { margin: 0; } diff --git a/web_src/css/form.css b/web_src/css/form.css index 5dd5e05bec..42b97413fa 100644 --- a/web_src/css/form.css +++ b/web_src/css/form.css @@ -38,11 +38,6 @@ textarea, color: var(--color-input-text); } -/* fix fomantic small dropdown having inconsistent padding with input */ -.ui.small.selection.dropdown { - padding: .67857143em 1.6em .67857143em 1em; -} - input:hover, textarea:hover, .ui.input input:hover, @@ -109,9 +104,8 @@ textarea:focus, color: var(--color-input-text); } -/* match <select> padding to <input> */ -.ui.form select { - padding: 0.67857143em 1em; +.ui.form .field > .selection.dropdown { + min-width: 14em; /* matches the default min width */ } .form .help { @@ -120,47 +114,6 @@ textarea:focus, display: inline-block; } -#create-page-form form { - margin: auto; -} - -#create-page-form form .ui.message { - text-align: center; -} - -@media (min-width: 768px) { - #create-page-form form { - width: 800px !important; - } - #create-page-form form .header { - padding-left: 280px !important; - } - #create-page-form form .inline.field > label { - text-align: right; - width: 250px !important; - word-wrap: break-word; - } - #create-page-form form .help { - margin-left: 265px !important; - } - #create-page-form form .optional .title { - margin-left: 250px !important; - } - #create-page-form form .inline.field > input, - #create-page-form form .inline.field > textarea { - width: 50%; - } -} - -@media (max-width: 767.98px) { - #create-page-form form .optional .title { - margin-left: 15px; - } - #create-page-form form .inline.field > label { - display: block; - } -} - .m-captcha-style { width: 100%; height: 5em; @@ -187,7 +140,7 @@ textarea:focus, } @media (max-height: 575px) { - #rc-imageselect, + #rc-imageselect, /* google recaptcha */ .g-recaptcha-style, .h-captcha-style { transform: scale(0.77); @@ -195,295 +148,41 @@ textarea:focus, } } -.user.forgot.password form, -.user.reset.password form, -.user.signup form { - margin: auto; - width: 700px !important; -} - -.user.activate form .ui.message, -.user.forgot.password form .ui.message, -.user.reset.password form .ui.message, -.user.link-account form .ui.message, -.user.signin form .ui.message, -.user.signup form .ui.message { - text-align: center; -} - -@media (min-width: 768px) { - .user.activate form, - .user.forgot.password form, - .user.reset.password form, - .user.link-account form, - .user.signin form, - .user.signup form { - width: 800px !important; - } - .user.activate form .header, - .user.forgot.password form .header, - .user.reset.password form .header, - .user.link-account form .header, - .user.signin form .header, - .user.signup form .header { - padding-left: 280px !important; - } - .user.activate form .inline.field > label { - text-align: right; - width: 250px !important; - word-wrap: break-word; - } - .user.activate form .help, - .user.forgot.password form .help, - .user.reset.password form .help, - .user.link-account form .help, - .user.signin form .help, - .user.signup form .help { - margin-left: 265px !important; - } - .user.activate form .optional .title, - .user.forgot.password form .optional .title, - .user.reset.password form .optional .title, - .user.link-account form .optional .title, - .user.signin form .optional .title, - .user.signup form .optional .title { - margin-left: 250px !important; - } -} - -@media (max-width: 767.98px) { - .user.activate form .optional .title, - .user.forgot.password form .optional .title, - .user.reset.password form .optional .title, - .user.link-account form .optional .title, - .user.signin form .optional .title, - .user.signup form .optional .title { - margin-left: 15px; - } - .user.activate form .inline.field > label, - .user.forgot.password form .inline.field > label, - .user.reset.password form .inline.field > label, - .user.link-account form .inline.field > label, - .user.signin form .inline.field > label, - .user.signup form .inline.field > label { - display: block; - } -} - -.user.activate form .header, -.user.forgot.password form .header, -.user.reset.password form .header, -.user.link-account form .header, -.user.signin form .header, -.user.signup form .header { - padding-left: 0 !important; - text-align: center; -} - -.user.activate form .inline.field > label, -.user.forgot.password form .inline.field > label, -.user.reset.password form .inline.field > label, -.user.link-account form .inline.field > label, -.user.signin form .inline.field > label, -.user.signup form .inline.field > label { - width: 200px; -} - -@media (max-width: 767.98px) { - .user.activate form .inline.field > label, - .user.forgot.password form .inline.field > label, - .user.reset.password form .inline.field > label, - .user.link-account form .inline.field > label, - .user.signin form .inline.field > label, - .user.signup form .inline.field > label { - width: 100% !important; - } -} - -.user.activate form input[type="number"], -.user.forgot.password form input[type="number"], -.user.reset.password form input[type="number"], -.user.link-account form input[type="number"], -.user.signin form input[type="number"], -.user.signup form input[type="number"] { - -moz-appearance: textfield; -} - -.user.activate form input::-webkit-outer-spin-button, -.user.forgot.password form input::-webkit-outer-spin-button, -.user.reset.password form input::-webkit-outer-spin-button, -.user.link-account form input::-webkit-outer-spin-button, -.user.signin form input::-webkit-outer-spin-button, -.user.signup form input::-webkit-outer-spin-button, -.user.activate form input::-webkit-inner-spin-button, -.user.forgot.password form input::-webkit-inner-spin-button, -.user.reset.password form input::-webkit-inner-spin-button, -.user.link-account form input::-webkit-inner-spin-button, -.user.signin form input::-webkit-inner-spin-button, -.user.signup form input::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; -} - -.repository.new.repo form, -.repository.new.migrate form, -.repository.new.fork form { - margin: auto; -} - -.repository.new.repo form .ui.message, -.repository.new.migrate form .ui.message, -.repository.new.fork form .ui.message { - text-align: center; -} - -@media (min-width: 768px) { - .repository.new.repo form, - .repository.new.migrate form, - .repository.new.fork form { - width: 800px !important; - } - .repository.new.repo form .header, - .repository.new.migrate form .header, - .repository.new.fork form .header { - padding-left: 280px !important; - } - .repository.new.repo form .inline.field > label, - .repository.new.migrate form .inline.field > label, - .repository.new.fork form .inline.field > label { - text-align: right; - width: 250px !important; - word-wrap: break-word; - } - .repository.new.repo form .help, - .repository.new.migrate form .help, - .repository.new.fork form .help { - margin-left: 265px !important; - } - .repository.new.repo form .optional .title, - .repository.new.migrate form .optional .title, - .repository.new.fork form .optional .title { - margin-left: 250px !important; - } - .repository.new.repo form .inline.field > input, - .repository.new.migrate form .inline.field > input, - .repository.new.fork form .inline.field > input, - .repository.new.repo form .inline.field > textarea, - .repository.new.migrate form .inline.field > textarea, - .repository.new.fork form .inline.field > textarea { - width: 50%; - } -} - -@media (max-width: 767.98px) { - .repository.new.repo form .optional .title, - .repository.new.migrate form .optional .title, - .repository.new.fork form .optional .title { - margin-left: 15px; - } - .repository.new.repo form .inline.field > label, - .repository.new.migrate form .inline.field > label, - .repository.new.fork form .inline.field > label { - display: block; - } +.ui.form.left-right-form .inline.field > label { + text-align: right; + width: 250px; + margin-right: 10px; } -.repository.new.repo form .dropdown .text, -.repository.new.migrate form .dropdown .text, -.repository.new.fork form .dropdown .text { - margin-right: 0 !important; +.ui.form.left-right-form .inline.field > .help { + display: block; + margin-left: calc(250px + 15px); } -.repository.new.repo form .header, -.repository.new.migrate form .header, -.repository.new.fork form .header { - padding-left: 0 !important; - text-align: center; +.ui.form.left-right-form .inline.field input:not([type="checkbox"], [type="radio"]), +.ui.form.left-right-form .inline.field .ui.dropdown, +.ui.form.left-right-form .inline.field textarea { + width: 50%; } -.repository.new.repo form .selection.dropdown, -.repository.new.migrate form .selection.dropdown, -.repository.new.fork form .selection.dropdown, -.repository.new.fork form .field a { - vertical-align: middle; - width: 50% !important; +.ui.form.left-right-form .inline.field .inline-right { + display: inline-flex; + flex-direction: column; + gap: 0.5em; } @media (max-width: 767.98px) { - .repository.new.repo form label, - .repository.new.migrate form label, - .repository.new.fork form label, - .repository.new.repo form .inline.field > input, - .repository.new.migrate form .inline.field > input, - .repository.new.fork form .inline.field > input, - .repository.new.fork form .field a, - .repository.new.repo form .selection.dropdown, - .repository.new.migrate form .selection.dropdown, - .repository.new.fork form .selection.dropdown { - width: 100% !important; - } - .repository.new.repo form .field button, - .repository.new.migrate form .field button, - .repository.new.fork form .field button, - .repository.new.repo form .field a, - .repository.new.migrate form .field a { - margin-bottom: 1em; + .ui.form.left-right-form .inline.field > label { width: 100%; + margin: 0; + text-align: left; } -} - -@media (min-width: 768px) { - .repository.new.repo .ui.form #auto-init { - margin-left: 265px !important; - } -} - -.repository.new.repo .ui.form .selection.dropdown:not(.owner) { - width: 50% !important; -} - -@media (max-width: 767.98px) { - .repository.new.repo .ui.form .selection.dropdown:not(.owner) { - width: 100% !important; + .ui.form.left-right-form .inline.field > .help { + margin: 0; } -} - -/* form fields with additional content besides their label, used on login form - * use like <div class="field"><label/><a/><input/></div> */ -.form-field-content-aside-label { - display: grid; - grid-template-columns: 1fr 1fr; -} -.form-field-content-aside-label > *:nth-child(2) { - text-align: right; -} -.form-field-content-aside-label input { - grid-column: span 2; -} - -.ui.form .field > .selection.dropdown { - min-width: 14em; /* matches the default min width */ -} - -.new.webhook form .help { - margin-left: 25px; -} - -.new.webhook .events.fields .column { - padding-left: 40px; -} - -.githook textarea { - font-family: var(--fonts-monospace); -} - -@media (max-width: 767.98px) { - .new.org .ui.form .field button, - .new.org .ui.form .field a { - margin-bottom: 1em; + .ui.form.left-right-form .inline.field input:not([type="checkbox"], [type="radio"]), + .ui.form.left-right-form .inline.field .ui.dropdown, + .ui.form.left-right-form .inline.field textarea { width: 100%; } - .new.org .ui.form .field input { - width: 100% !important; - } } diff --git a/web_src/css/index.css b/web_src/css/index.css index 02513aebc1..ce1a23b245 100644 --- a/web_src/css/index.css +++ b/web_src/css/index.css @@ -82,5 +82,6 @@ @import "./review.css"; @import "./actions.css"; -@tailwind utilities; @import "./helpers.css"; + +@tailwind utilities; diff --git a/web_src/css/modules/container.css b/web_src/css/modules/container.css index 4a442c35b1..69eab17cfd 100644 --- a/web_src/css/modules/container.css +++ b/web_src/css/modules/container.css @@ -11,6 +11,9 @@ .ui.fluid.container { width: 100%; } +.ui.container.medium-width { + width: 800px; +} .ui[class*="center aligned"].container { text-align: center; diff --git a/web_src/css/modules/navbar.css b/web_src/css/modules/navbar.css index b5bc95b058..b09b271ad4 100644 --- a/web_src/css/modules/navbar.css +++ b/web_src/css/modules/navbar.css @@ -48,7 +48,8 @@ align-items: stretch; } /* hide all items */ - #navbar .item { + #navbar .navbar-left > .item, + #navbar .navbar-right > .item { display: none; } #navbar #navbar-logo { @@ -103,11 +104,11 @@ #navbar .ui.dropdown .navbar-profile-admin { display: block; position: absolute; - font-size: 10px; + font-size: 9px; font-weight: var(--font-weight-bold); color: var(--color-nav-bg); background: var(--color-primary); - padding: 2px 4px; + padding: 2px 3px; border-radius: 10px; top: -1px; left: 18px; diff --git a/web_src/css/org.css b/web_src/css/org.css index 1082625041..48b41de297 100644 --- a/web_src/css/org.css +++ b/web_src/css/org.css @@ -1,94 +1,7 @@ -#create-page-form form { - margin: auto; -} - -#create-page-form form .ui.message { - text-align: center; -} - -@media (min-width: 768px) { - #create-page-form form { - width: 800px !important; - } - #create-page-form form .header { - padding-left: 280px !important; - } - #create-page-form form .inline.field > label { - text-align: right; - width: 250px !important; - word-wrap: break-word; - } - #create-page-form form .help { - margin-left: 265px !important; - } - #create-page-form form .optional .title { - margin-left: 250px !important; - } - #create-page-form form .inline.field > input, - #create-page-form form .inline.field > textarea { - width: 50%; - } -} - -@media (max-width: 767.98px) { - #create-page-form form .optional .title { - margin-left: 15px; - } - #create-page-form form .inline.field > label { - display: block; - } -} - .organization .head .ui.header .ui.right { margin-top: 5px; } -.organization.new.org form { - margin: auto; -} - -.organization.new.org form .ui.message { - text-align: center; -} - -@media (min-width: 768px) { - .organization.new.org form { - width: 800px !important; - } - .organization.new.org form .header { - padding-left: 280px !important; - } - .organization.new.org form .inline.field > label { - text-align: right; - width: 250px !important; - word-wrap: break-word; - } - .organization.new.org form .help { - margin-left: 265px !important; - } - .organization.new.org form .optional .title { - margin-left: 250px !important; - } - .organization.new.org form .inline.field > input, - .organization.new.org form .inline.field > textarea { - width: 50%; - } -} - -@media (max-width: 767.98px) { - .organization.new.org form .optional .title { - margin-left: 15px; - } - .organization.new.org form .inline.field > label { - display: block; - } -} - -.organization.new.org form .header { - padding-left: 0 !important; - text-align: center; -} - .page-content.organization .org-avatar { margin-right: 15px; } diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 6fdc9ec2a8..22bbe3cc23 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -120,15 +120,13 @@ td .commit-summary { align-items: center; overflow: hidden; text-overflow: ellipsis; + gap: 0.25em; } @media (max-width: 767.98px) { - .latest-commit .sha { + .latest-commit .commit-id-short { display: none; } - .latest-commit .commit-summary { - margin-left: 8px; - } } .repo-path { @@ -605,15 +603,6 @@ td .commit-summary { margin-right: 0.25em; } -.singular-commit { - display: flex; - align-items: center; -} - -.singular-commit .badge { - height: 30px !important; -} - .repository.view.issue .comment-list .timeline-item.event > .commit-status-link { float: right; margin-right: 8px; @@ -836,10 +825,6 @@ td .commit-summary { height: 10px; } -.repository.compare.pull .show-form-container { - text-align: left; -} - .repository .choose.branch { display: flex; align-items: center; @@ -877,11 +862,6 @@ td .commit-summary { margin-top: -8px; } -.repository.compare.pull .pullrequest-form { - margin-top: 16px; - margin-bottom: 16px; -} - .repository.compare.pull .markup { font-size: 14px; } @@ -936,14 +916,6 @@ td .commit-summary { width: 200px; } -.repository #commits-table thead .shatd { - text-align: center; -} - -.repository #commits-table td.sha .sha.label { - margin: 0; -} - .repository #commits-table.ui.basic.striped.table tbody tr:nth-child(2n) { background-color: var(--color-light) !important; } @@ -1440,12 +1412,6 @@ td .commit-summary { padding-top: 15px; } -.commit-header-row { - min-height: 50px !important; - padding-top: 0 !important; - padding-bottom: 0 !important; -} - .commit-header-buttons { display: flex; gap: 4px; @@ -1622,7 +1588,7 @@ td .commit-summary { align-items: center; } -.labels-list .label { +.labels-list .label, .scope-parent > .label { padding: 0 6px; min-height: 20px; line-height: 1.3; /* there is a `font-size: 1.25em` for inside emoji, so here the line-height needs to be larger slightly */ @@ -1701,6 +1667,10 @@ tbody.commit-list { white-space: nowrap; } +.latest-commit .message-wrapper { + max-width: calc(100% - 2.5rem); +} + /* in the commit list, messages can wrap so we can use inline */ .commit-list .message-wrapper { display: inline; @@ -2128,18 +2098,6 @@ tbody.commit-list { .repository.view.issue .comment-list .timeline .comment-header-right .role-label { display: none; } - .commit-header-row .ui.horizontal.list { - width: 100%; - overflow-x: auto; - margin-top: 2px; - } - .commit-header-row .ui.horizontal.list .item { - align-items: center; - display: flex; - } - .commit-header-row .author { - padding: 3px 0; - } .commit-header h3 { flex-basis: auto !important; margin-bottom: 0.5rem !important; diff --git a/web_src/css/repo/commit-sign.css b/web_src/css/repo/commit-sign.css index e757030419..834fdd95d1 100644 --- a/web_src/css/repo/commit-sign.css +++ b/web_src/css/repo/commit-sign.css @@ -1,272 +1,60 @@ - -.repository .ui.attached.isSigned.isWarning { - border-left: 1px solid var(--color-error-border); - border-right: 1px solid var(--color-error-border); -} - -.repository .ui.attached.isSigned.isWarning.top, -.repository .ui.attached.isSigned.isWarning.message { - border-top: 1px solid var(--color-error-border); -} - -.repository .ui.attached.isSigned.isWarning.message { - box-shadow: none; - background-color: var(--color-error-bg); - color: var(--color-error-text); -} - -.repository .ui.attached.isSigned.isWarning.message .ui.text { - color: var(--color-error-text); -} - -.repository .ui.attached.isSigned.isWarning:last-child, -.repository .ui.attached.isSigned.isWarning.bottom { - border-bottom: 1px solid var(--color-error-border); -} - -.repository .ui.attached.isSigned.isVerified { - border-left: 1px solid var(--color-success-border); - border-right: 1px solid var(--color-success-border); -} - -.repository .ui.attached.isSigned.isVerified.top, -.repository .ui.attached.isSigned.isVerified.message { - border-top: 1px solid var(--color-success-border); -} - -.repository .ui.attached.isSigned.isVerified.message { - box-shadow: none; - background-color: var(--color-success-bg); - color: var(--color-success-text); -} - -.repository .ui.attached.isSigned.isVerified.message .pull-right { - color: var(--color-text); -} - -.repository .ui.attached.isSigned.isVerified.message .ui.text { - color: var(--color-success-text); -} - -.repository .ui.attached.isSigned.isVerified:last-child, -.repository .ui.attached.isSigned.isVerified.bottom { - border-bottom: 1px solid var(--color-success-border); -} - -.repository .ui.attached.isSigned.isVerifiedUntrusted, -.repository .ui.attached.isSigned.isVerifiedUnmatched { - border-left: 1px solid var(--color-warning-border); - border-right: 1px solid var(--color-warning-border); -} - -.repository .ui.attached.isSigned.isVerifiedUntrusted.top, -.repository .ui.attached.isSigned.isVerifiedUnmatched.top, -.repository .ui.attached.isSigned.isVerifiedUntrusted.message, -.repository .ui.attached.isSigned.isVerifiedUnmatched.message { - border-top: 1px solid var(--color-warning-border); -} - -.repository .ui.attached.isSigned.isVerifiedUntrusted.message, -.repository .ui.attached.isSigned.isVerifiedUnmatched.message { - box-shadow: none; - background-color: var(--color-warning-bg); - color: var(--color-warning-text); -} - -.repository .ui.attached.isSigned.isVerifiedUntrusted.message .ui.text, -.repository .ui.attached.isSigned.isVerifiedUnmatched.message .ui.text { - color: var(--color-warning-text); -} - -.repository .ui.attached.isSigned.isVerifiedUntrusted:last-child, -.repository .ui.attached.isSigned.isVerifiedUnmatched:last-child, -.repository .ui.attached.isSigned.isVerifiedUntrusted.bottom, -.repository .ui.attached.isSigned.isVerifiedUnmatched.bottom { - border-bottom: 1px solid var(--color-warning-border); -} - -.repository #commits-table td.sha .sha.label, -.repository #repo-files-table .sha.label, -.repository #repo-file-commit-box .sha.label, -.repository #rev-list .sha.label, -.repository .timeline-item.commits-list .singular-commit .sha.label { +.ui.label.commit-id-short, +.ui.label.commit-sign-badge { border: 1px solid var(--color-light-border); + font-size: 13px; + font-weight: var(--font-weight-normal); + padding: 3px 5px; + flex-shrink: 0; } -.repository #commits-table td.sha .sha.label .detail.icon, -.repository #repo-files-table .sha.label .detail.icon, -.repository #repo-file-commit-box .sha.label .detail.icon, -.repository #rev-list .sha.label .detail.icon, -.repository .timeline-item.commits-list .singular-commit .sha.label .detail.icon { - background: var(--color-light); - margin: -6px -10px -4px 0; - padding: 5px 4px 5px 6px; - border-left: 1px solid var(--color-light-border); - border-top: 0; - border-right: 0; - border-bottom: 0; - border-top-left-radius: 0; - border-bottom-left-radius: 0; +.ui.label.commit-id-short { + font-family: var(--fonts-monospace); } -.repository #commits-table td.sha .sha.label .detail.icon .svg, -.repository #repo-files-table .sha.label .detail.icon .svg, -.repository #repo-file-commit-box .sha.label .detail.icon .svg, -.repository #rev-list .sha.label .detail.icon .svg, -.repository .timeline-item.commits-list .singular-commit .sha.label .detail.icon .svg { - margin: 0 0.25em 0 0; -} - -.repository #commits-table td.sha .sha.label .detail.icon > div, -.repository #repo-files-table .sha.label .detail.icon > div, -.repository #repo-file-commit-box .sha.label .detail.icon > div, -.repository #rev-list .sha.label .detail.icon > div, -.repository .timeline-item.commits-list .singular-commit .sha.label .detail.icon > div { - display: flex; - align-items: center; -} - -.repository #commits-table td.sha .sha.label.isSigned.isWarning, -.repository #repo-files-table .sha.label.isSigned.isWarning, -.repository #repo-file-commit-box .sha.label.isSigned.isWarning, -.repository #rev-list .sha.label.isSigned.isWarning, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isWarning { - border: 1px solid var(--color-red-badge); - background: var(--color-red-badge-bg); -} - -.repository #commits-table td.sha .sha.label.isSigned.isWarning .detail.icon, -.repository #repo-files-table .sha.label.isSigned.isWarning .detail.icon, -.repository #repo-file-commit-box .sha.label.isSigned.isWarning .detail.icon, -.repository #rev-list .sha.label.isSigned.isWarning .detail.icon, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isWarning .detail.icon { - border-left: 1px solid var(--color-red-badge); - color: var(--color-red-badge); -} - -.repository #commits-table td.sha .sha.label.isSigned.isWarning:hover, -.repository #repo-files-table .sha.label.isSigned.isWarning:hover, -.repository #repo-file-commit-box .sha.label.isSigned.isWarning:hover, -.repository #rev-list .sha.label.isSigned.isWarning:hover, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isWarning:hover { - background: var(--color-red-badge-hover-bg) !important; +.ui.label.commit-id-short > .commit-sign-badge { + margin: 0; + padding: 0; + border: 0 !important; + border-radius: 0; + background: transparent; } -.repository #commits-table td.sha .sha.label.isSigned.isVerified, -.repository #repo-files-table .sha.label.isSigned.isVerified, -.repository #repo-file-commit-box .sha.label.isSigned.isVerified, -.repository #rev-list .sha.label.isSigned.isVerified, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerified { - border: 1px solid var(--color-green-badge); - background: var(--color-green-badge-bg); +.ui.label.commit-id-short > .commit-sign-badge:hover { + background: transparent !important; } -.repository #commits-table td.sha .sha.label.isSigned.isVerified .detail.icon, -.repository #repo-files-table .sha.label.isSigned.isVerified .detail.icon, -.repository #repo-file-commit-box .sha.label.isSigned.isVerified .detail.icon, -.repository #rev-list .sha.label.isSigned.isVerified .detail.icon, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerified .detail.icon { - border-left: 1px solid var(--color-green-badge); - color: var(--color-green-badge); +.commit-is-signed.sign-trusted { + border: 1px solid var(--color-green-badge) !important; + background: var(--color-green-badge-bg) !important; } -.repository #commits-table td.sha .sha.label.isSigned.isVerified:hover, -.repository #repo-files-table .sha.label.isSigned.isVerified:hover, -.repository #repo-file-commit-box .sha.label.isSigned.isVerified:hover, -.repository #rev-list .sha.label.isSigned.isVerified:hover, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerified:hover { +.commit-is-signed.sign-trusted:hover { background: var(--color-green-badge-hover-bg) !important; } -.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUntrusted, -.repository #repo-files-table .sha.label.isSigned.isVerifiedUntrusted, -.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUntrusted, -.repository #rev-list .sha.label.isSigned.isVerifiedUntrusted, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUntrusted { - border: 1px solid var(--color-yellow-badge); - background: var(--color-yellow-badge-bg); -} - -.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUntrusted .detail.icon, -.repository #repo-files-table .sha.label.isSigned.isVerifiedUntrusted .detail.icon, -.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUntrusted .detail.icon, -.repository #rev-list .sha.label.isSigned.isVerifiedUntrusted .detail.icon, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUntrusted .detail.icon { - border-left: 1px solid var(--color-yellow-badge); - color: var(--color-yellow-badge); +.commit-is-signed.sign-untrusted { + border: 1px solid var(--color-yellow-badge) !important; + background: var(--color-yellow-badge-bg) !important; } -.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUntrusted:hover, -.repository #repo-files-table .sha.label.isSigned.isVerifiedUntrusted:hover, -.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUntrusted:hover, -.repository #rev-list .sha.label.isSigned.isVerifiedUntrusted:hover, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUntrusted:hover { +.commit-is-signed.sign-untrusted:hover { background: var(--color-yellow-badge-hover-bg) !important; } -.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUnmatched, -.repository #repo-files-table .sha.label.isSigned.isVerifiedUnmatched, -.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUnmatched, -.repository #rev-list .sha.label.isSigned.isVerifiedUnmatched, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUnmatched { - border: 1px solid var(--color-orange-badge); - background: var(--color-orange-badge-bg); -} - -.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUnmatched .detail.icon, -.repository #repo-files-table .sha.label.isSigned.isVerifiedUnmatched .detail.icon, -.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUnmatched .detail.icon, -.repository #rev-list .sha.label.isSigned.isVerifiedUnmatched .detail.icon, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUnmatched .detail.icon { - border-left: 1px solid var(--color-orange-badge); - color: var(--color-orange-badge); +.commit-is-signed.sign-unmatched { + border: 1px solid var(--color-orange-badge) !important; + background: var(--color-orange-badge-bg) !important; } -.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUnmatched:hover, -.repository #repo-files-table .sha.label.isSigned.isVerifiedUnmatched:hover, -.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUnmatched:hover, -.repository #rev-list .sha.label.isSigned.isVerifiedUnmatched:hover, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUnmatched:hover { +.commit-is-signed.sign-unmatched:hover { background: var(--color-orange-badge-hover-bg) !important; } -.singular-commit .shabox .sha.label { - margin: 0; - border: 1px solid var(--color-light-border); +.commit-is-signed.sign-warning { + border: 1px solid var(--color-red-badge) !important; + background: var(--color-red-badge-bg) !important; } -.singular-commit .shabox .sha.label.isSigned.isWarning { - border: 1px solid var(--color-red-badge); - background: var(--color-red-badge-bg); -} - -.singular-commit .shabox .sha.label.isSigned.isWarning:hover { +.commit-is-signed.sign-warning:hover { background: var(--color-red-badge-hover-bg) !important; } - -.singular-commit .shabox .sha.label.isSigned.isVerified { - border: 1px solid var(--color-green-badge); - background: var(--color-green-badge-bg); -} - -.singular-commit .shabox .sha.label.isSigned.isVerified:hover { - background: var(--color-green-badge-hover-bg) !important; -} - -.singular-commit .shabox .sha.label.isSigned.isVerifiedUntrusted { - border: 1px solid var(--color-yellow-badge); - background: var(--color-yellow-badge-bg); -} - -.singular-commit .shabox .sha.label.isSigned.isVerifiedUntrusted:hover { - background: var(--color-yellow-badge-hover-bg) !important; -} - -.singular-commit .shabox .sha.label.isSigned.isVerifiedUnmatched { - border: 1px solid var(--color-orange-badge); - background: var(--color-orange-badge-bg); -} - -.singular-commit .shabox .sha.label.isSigned.isVerifiedUnmatched:hover { - background: var(--color-orange-badge-hover-bg) !important; -} diff --git a/web_src/js/components/DiffFileTreeItem.vue b/web_src/js/components/DiffFileTreeItem.vue index 12cafd8f1b..31ce94aacd 100644 --- a/web_src/js/components/DiffFileTreeItem.vue +++ b/web_src/js/components/DiffFileTreeItem.vue @@ -8,6 +8,7 @@ type File = { NameHash: string; Type: number; IsViewed: boolean; + IsSubmodule: boolean; } type Item = { @@ -34,6 +35,13 @@ function getIconForDiffType(pType) { }; return diffTypes[pType]; } + +function fileIcon(file) { + if (file.IsSubmodule) { + return 'octicon-file-submodule'; + } + return 'octicon-file'; +} </script> <template> @@ -44,7 +52,7 @@ function getIconForDiffType(pType) { :title="item.name" :href="'#diff-' + item.file.NameHash" > <!-- file --> - <SvgIcon name="octicon-file"/> + <SvgIcon :name="fileIcon(item.file)"/> <span class="gt-ellipsis tw-flex-1">{{ item.name }}</span> <SvgIcon :name="getIconForDiffType(item.file.Type).name" :class="getIconForDiffType(item.file.Type).classes"/> </a> diff --git a/web_src/js/components/PullRequestMergeForm.vue b/web_src/js/components/PullRequestMergeForm.vue index e8bcee70db..bafeec6c97 100644 --- a/web_src/js/components/PullRequestMergeForm.vue +++ b/web_src/js/components/PullRequestMergeForm.vue @@ -147,7 +147,7 @@ function clearMergeMessage() { </template> </span> </button> - <div class="ui dropdown icon button" @click.stop="showMergeStyleMenu = !showMergeStyleMenu" v-if="mergeStyleAllowedCount>1"> + <div class="ui dropdown icon button" @click.stop="showMergeStyleMenu = !showMergeStyleMenu"> <svg-icon name="octicon-triangle-down" :size="14"/> <div class="menu" :class="{'show':showMergeStyleMenu}"> <template v-for="msd in mergeForm.mergeStyles"> diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue index b083fb0b77..914c9e76de 100644 --- a/web_src/js/components/RepoActionView.vue +++ b/web_src/js/components/RepoActionView.vue @@ -43,6 +43,20 @@ function isLogElementInViewport(el: HTMLElement): boolean { return rect.top >= 0 && rect.bottom <= window.innerHeight; // only check height but not width } +type LocaleStorageOptions = { + autoScroll: boolean; + expandRunning: boolean; +}; + +function getLocaleStorageOptions(): LocaleStorageOptions { + try { + const optsJson = localStorage.getItem('actions-view-options'); + if (optsJson) return JSON.parse(optsJson); + } catch {} + // if no options in localStorage, or failed to parse, return default options + return {autoScroll: true, expandRunning: false}; +} + const sfc = { name: 'RepoActionView', components: { @@ -56,7 +70,17 @@ const sfc = { locale: Object, }, + watch: { + optionAlwaysAutoScroll() { + this.saveLocaleStorageOptions(); + }, + optionAlwaysExpandRunning() { + this.saveLocaleStorageOptions(); + }, + }, + data() { + const {autoScroll, expandRunning} = getLocaleStorageOptions(); return { // internal state loadingAbortController: null, @@ -70,6 +94,8 @@ const sfc = { 'log-time-stamp': false, 'log-time-seconds': false, }, + optionAlwaysAutoScroll: autoScroll ?? false, + optionAlwaysExpandRunning: expandRunning ?? false, // provided by backend run: { @@ -147,6 +173,11 @@ const sfc = { }, methods: { + saveLocaleStorageOptions() { + const opts: LocaleStorageOptions = {autoScroll: this.optionAlwaysAutoScroll, expandRunning: this.optionAlwaysExpandRunning}; + localStorage.setItem('actions-view-options', JSON.stringify(opts)); + }, + // get the job step logs container ('.job-step-logs') getJobStepLogsContainer(stepIndex: number): HTMLElement { return this.$refs.logs[stepIndex]; @@ -228,8 +259,10 @@ const sfc = { }, shouldAutoScroll(stepIndex: number): boolean { + if (!this.optionAlwaysAutoScroll) return false; const el = this.getJobStepLogsContainer(stepIndex); - if (!el.lastChild) return false; + // if the logs container is empty, then auto-scroll if the step is expanded + if (!el.lastChild) return this.currentJobStepsStates[stepIndex].expanded; return isLogElementInViewport(el.lastChild); }, @@ -280,6 +313,7 @@ const sfc = { const abortController = new AbortController(); this.loadingAbortController = abortController; try { + const isFirstLoad = !this.run.status; const job = await this.fetchJobData(abortController); if (this.loadingAbortController !== abortController) return; @@ -289,9 +323,10 @@ const sfc = { // sync the currentJobStepsStates to store the job step states for (let i = 0; i < this.currentJob.steps.length; i++) { + const expanded = isFirstLoad && this.optionAlwaysExpandRunning && this.currentJob.steps[i].status === 'running'; if (!this.currentJobStepsStates[i]) { // initial states for job steps - this.currentJobStepsStates[i] = {cursor: null, expanded: false}; + this.currentJobStepsStates[i] = {cursor: null, expanded}; } } @@ -426,6 +461,8 @@ export function initRepositoryActionView() { skipped: el.getAttribute('data-locale-status-skipped'), blocked: el.getAttribute('data-locale-status-blocked'), }, + logsAlwaysAutoScroll: el.getAttribute('data-locale-logs-always-auto-scroll'), + logsAlwaysExpandRunning: el.getAttribute('data-locale-logs-always-expand-running'), }, }); view.mount(el); @@ -528,6 +565,17 @@ export function initRepositoryActionView() { <i class="icon"><SvgIcon :name="isFullScreen ? 'octicon-check' : 'gitea-empty-checkbox'"/></i> {{ locale.showFullScreen }} </a> + + <div class="divider"/> + <a class="item" @click="optionAlwaysAutoScroll = !optionAlwaysAutoScroll"> + <i class="icon"><SvgIcon :name="optionAlwaysAutoScroll ? 'octicon-check' : 'gitea-empty-checkbox'"/></i> + {{ locale.logsAlwaysAutoScroll }} + </a> + <a class="item" @click="optionAlwaysExpandRunning = !optionAlwaysExpandRunning"> + <i class="icon"><SvgIcon :name="optionAlwaysExpandRunning ? 'octicon-check' : 'gitea-empty-checkbox'"/></i> + {{ locale.logsAlwaysExpandRunning }} + </a> + <div class="divider"/> <a :class="['item', !currentJob.steps.length ? 'disabled' : '']" :href="run.link+'/jobs/'+jobIndex+'/logs'" target="_blank"> <i class="icon"><SvgIcon name="octicon-download"/></i> diff --git a/web_src/js/features/common-button.ts b/web_src/js/features/common-button.ts index acce992b90..3162557b9b 100644 --- a/web_src/js/features/common-button.ts +++ b/web_src/js/features/common-button.ts @@ -17,7 +17,8 @@ export function initGlobalDeleteButton(): void { // Some model/form elements will be filled by `data-id` / `data-name` / `data-data-xxx` attributes. // If there is a form defined by `data-form`, then the form will be submitted as-is (without any modification). // If there is no form, then the data will be posted to `data-url`. - // TODO: it's not encouraged to use this method. `show-modal` does far better than this. + // TODO: do not use this method in new code. `show-modal` / `link-action(data-modal-confirm)` does far better than this. + // FIXME: all legacy `delete-button` should be refactored to use `show-modal` or `link-action` for (const btn of document.querySelectorAll<HTMLElement>('.delete-button')) { btn.addEventListener('click', (e) => { e.preventDefault(); diff --git a/web_src/js/features/comp/EditorMarkdown.test.ts b/web_src/js/features/comp/EditorMarkdown.test.ts index acd496bed6..7b4b44e83c 100644 --- a/web_src/js/features/comp/EditorMarkdown.test.ts +++ b/web_src/js/features/comp/EditorMarkdown.test.ts @@ -4,13 +4,24 @@ test('EditorMarkdown', () => { const textarea = document.createElement('textarea'); initTextareaMarkdown(textarea); - const testInput = (value, expected) => { - textarea.value = value; - textarea.setSelectionRange(value.length, value.length); + type ValueWithCursor = string | { + value: string; + pos: number; + } + const testInput = (input: ValueWithCursor, result: ValueWithCursor) => { + const intputValue = typeof input === 'string' ? input : input.value; + const inputPos = typeof input === 'string' ? intputValue.length : input.pos; + textarea.value = intputValue; + textarea.setSelectionRange(inputPos, inputPos); + const e = new KeyboardEvent('keydown', {key: 'Enter', cancelable: true}); textarea.dispatchEvent(e); - if (!e.defaultPrevented) textarea.value += '\n'; - expect(textarea.value).toEqual(expected); + if (!e.defaultPrevented) textarea.value += '\n'; // simulate default behavior + + const expectedValue = typeof result === 'string' ? result : result.value; + const expectedPos = typeof result === 'string' ? expectedValue.length : result.pos; + expect(textarea.value).toEqual(expectedValue); + expect(textarea.selectionStart).toEqual(expectedPos); }; testInput('-', '-\n'); @@ -18,8 +29,11 @@ test('EditorMarkdown', () => { testInput('- ', ''); testInput('1. ', ''); + testInput({value: '1. \n2. ', pos: 3}, {value: '\n2. ', pos: 0}); testInput('- x', '- x\n- '); + testInput('1. foo', '1. foo\n1. '); + testInput({value: '1. a\n2. b\n3. c', pos: 4}, {value: '1. a\n1. \n2. b\n3. c', pos: 8}); testInput('- [ ]', '- [ ]\n- '); testInput('- [ ] foo', '- [ ] foo\n- [ ] '); testInput('* [x] foo', '* [x] foo\n* [ ] '); diff --git a/web_src/js/features/comp/EditorMarkdown.ts b/web_src/js/features/comp/EditorMarkdown.ts index 2af003ccb0..5e2ef121f5 100644 --- a/web_src/js/features/comp/EditorMarkdown.ts +++ b/web_src/js/features/comp/EditorMarkdown.ts @@ -92,6 +92,7 @@ function handleNewline(textarea: HTMLTextAreaElement, e: Event) { if (!line) { // clear current line if we only have i.e. '1. ' and the user presses enter again to finish creating a list textarea.value = value.slice(0, lineStart) + value.slice(lineEnd); + textarea.setSelectionRange(selStart - prefix.length, selStart - prefix.length); } else { // start a new line with the same indention and prefix let newPrefix = prefix; diff --git a/web_src/js/features/notification.ts b/web_src/js/features/notification.ts index adfdb157a1..dc0acb0244 100644 --- a/web_src/js/features/notification.ts +++ b/web_src/js/features/notification.ts @@ -1,6 +1,5 @@ -import $ from 'jquery'; import {GET} from '../modules/fetch.ts'; -import {toggleElem, type DOMEvent} from '../utils/dom.ts'; +import {toggleElem, type DOMEvent, createElementFromHTML} from '../utils/dom.ts'; import {logoutFromWorker} from '../modules/worker.ts'; const {appSubUrl, notificationSettings, assetVersionEncoded} = window.config; @@ -158,7 +157,8 @@ async function updateNotificationTable() { } const data = await response.text(); - if ($(data).data('sequence-number') === notificationSequenceNumber) { + const el = createElementFromHTML(data); + if (parseInt(el.getAttribute('data-sequence-number')) === notificationSequenceNumber) { notificationDiv.outerHTML = data; initNotificationsTable(); } diff --git a/web_src/js/features/org-team.ts b/web_src/js/features/org-team.ts index e4e98fd990..e160f07bf2 100644 --- a/web_src/js/features/org-team.ts +++ b/web_src/js/features/org-team.ts @@ -1,35 +1,34 @@ -import $ from 'jquery'; -import {hideElem, showElem} from '../utils/dom.ts'; +import {queryElems, toggleElem} from '../utils/dom.ts'; +import {fomanticQuery} from '../modules/fomantic/base.ts'; const {appSubUrl} = window.config; -export function initOrgTeamSettings() { - // Change team access mode - $('.organization.new.team input[name=permission]').on('change', () => { - const val = $('input[name=permission]:checked', '.organization.new.team').val(); - if (val === 'admin') { - hideElem('.organization.new.team .team-units'); - } else { - showElem('.organization.new.team .team-units'); - } - }); +function initOrgTeamSettings() { + // on the page "page-content organization new team" + const pageContent = document.querySelector('.page-content.organization.new.team'); + if (!pageContent) return; + queryElems(pageContent, 'input[name=permission]', (el) => el.addEventListener('change', () => { + // Change team access mode + const val = pageContent.querySelector<HTMLInputElement>('input[name=permission]:checked')?.value; + toggleElem(pageContent.querySelectorAll('.team-units'), val !== 'admin'); + })); } -export function initOrgTeamSearchRepoBox() { - const $searchRepoBox = $('#search-repo-box'); +function initOrgTeamSearchRepoBox() { + // on the page "page-content organization teams" + const $searchRepoBox = fomanticQuery('#search-repo-box'); $searchRepoBox.search({ minCharacters: 2, apiSettings: { url: `${appSubUrl}/repo/search?q={query}&uid=${$searchRepoBox.data('uid')}`, onResponse(response) { const items = []; - $.each(response.data, (_i, item) => { + for (const item of response.data) { items.push({ title: item.repository.full_name.split('/')[1], description: item.repository.full_name, }); - }); - + } return {results: items}; }, }, @@ -37,3 +36,9 @@ export function initOrgTeamSearchRepoBox() { showNoResults: false, }); } + +export function initOrgTeam() { + if (!document.querySelector('.page-content.organization')) return; + initOrgTeamSettings(); + initOrgTeamSearchRepoBox(); +} diff --git a/web_src/js/features/repo-code.test.ts b/web_src/js/features/repo-code.test.ts deleted file mode 100644 index 27554aa847..0000000000 --- a/web_src/js/features/repo-code.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {singleAnchorRegex, rangeAnchorRegex} from './repo-code.ts'; - -test('singleAnchorRegex', () => { - expect(singleAnchorRegex.test('#L0')).toEqual(false); - expect(singleAnchorRegex.test('#L1')).toEqual(true); - expect(singleAnchorRegex.test('#L01')).toEqual(false); - expect(singleAnchorRegex.test('#n0')).toEqual(false); - expect(singleAnchorRegex.test('#n1')).toEqual(true); - expect(singleAnchorRegex.test('#n01')).toEqual(false); -}); - -test('rangeAnchorRegex', () => { - expect(rangeAnchorRegex.test('#L0-L10')).toEqual(false); - expect(rangeAnchorRegex.test('#L1-L10')).toEqual(true); - expect(rangeAnchorRegex.test('#L01-L10')).toEqual(false); - expect(rangeAnchorRegex.test('#L1-L01')).toEqual(false); -}); diff --git a/web_src/js/features/repo-code.ts b/web_src/js/features/repo-code.ts index a8d6e8f97d..207022ca42 100644 --- a/web_src/js/features/repo-code.ts +++ b/web_src/js/features/repo-code.ts @@ -1,12 +1,8 @@ -import $ from 'jquery'; import {svg} from '../svg.ts'; -import {invertFileFolding} from './file-fold.ts'; import {createTippy} from '../modules/tippy.ts'; import {clippie} from 'clippie'; import {toAbsoluteUrl} from '../utils.ts'; - -export const singleAnchorRegex = /^#(L|n)([1-9][0-9]*)$/; -export const rangeAnchorRegex = /^#(L[1-9][0-9]*)-(L[1-9][0-9]*)$/; +import {addDelegatedEventListener} from '../utils/dom.ts'; function changeHash(hash: string) { if (window.history.pushState) { @@ -16,20 +12,11 @@ function changeHash(hash: string) { } } -function isBlame() { - return Boolean(document.querySelector('div.blame')); -} +// it selects the code lines defined by range: `L1-L3` (3 lines) or `L2` (singe line) +function selectRange(range: string): Element { + for (const el of document.querySelectorAll('.code-view tr.active')) el.classList.remove('active'); + const elLineNums = document.querySelectorAll(`.code-view td.lines-num span[data-line-number]`); -function getLineEls() { - return document.querySelectorAll(`.code-view td.lines-code${isBlame() ? '.blame-code' : ''}`); -} - -function selectRange($linesEls, $selectionEndEl, $selectionStartEls?) { - for (const el of $linesEls) { - el.closest('tr').classList.remove('active'); - } - - // add hashchange to permalink const refInNewIssue = document.querySelector('a.ref-in-new-issue'); const copyPermalink = document.querySelector('a.copy-line-permalink'); const viewGitBlame = document.querySelector('a.view_git_blame'); @@ -59,37 +46,30 @@ function selectRange($linesEls, $selectionEndEl, $selectionStartEls?) { copyPermalink.setAttribute('data-url', link); }; - if ($selectionStartEls) { - let a = parseInt($selectionEndEl[0].getAttribute('rel').slice(1)); - let b = parseInt($selectionStartEls[0].getAttribute('rel').slice(1)); - let c; - if (a !== b) { - if (a > b) { - c = a; - a = b; - b = c; - } - const classes = []; - for (let i = a; i <= b; i++) { - classes.push(`[rel=L${i}]`); - } - $linesEls.filter(classes.join(',')).each(function () { - this.closest('tr').classList.add('active'); - }); - changeHash(`#L${a}-L${b}`); - - updateIssueHref(`L${a}-L${b}`); - updateViewGitBlameFragment(`L${a}-L${b}`); - updateCopyPermalinkUrl(`L${a}-L${b}`); - return; - } + const rangeFields = range ? range.split('-') : []; + const start = rangeFields[0] ?? ''; + if (!start) return null; + const stop = rangeFields[1] || start; + + // format is i.e. 'L14-L26' + let startLineNum = parseInt(start.substring(1)); + let stopLineNum = parseInt(stop.substring(1)); + if (startLineNum > stopLineNum) { + const tmp = startLineNum; + startLineNum = stopLineNum; + stopLineNum = tmp; + range = `${stop}-${start}`; } - $selectionEndEl[0].closest('tr').classList.add('active'); - changeHash(`#${$selectionEndEl[0].getAttribute('rel')}`); - updateIssueHref($selectionEndEl[0].getAttribute('rel')); - updateViewGitBlameFragment($selectionEndEl[0].getAttribute('rel')); - updateCopyPermalinkUrl($selectionEndEl[0].getAttribute('rel')); + const first = elLineNums[startLineNum - 1] ?? null; + for (let i = startLineNum - 1; i <= stopLineNum - 1 && i < elLineNums.length; i++) { + elLineNums[i].closest('tr').classList.add('active'); + } + changeHash(`#${range}`); + updateIssueHref(range); + updateViewGitBlameFragment(range); + updateCopyPermalinkUrl(range); + return first; } function showLineButton() { @@ -103,6 +83,8 @@ function showLineButton() { // find active row and add button const tr = document.querySelector('.code-view tr.active'); + if (!tr) return; + const td = tr.querySelector('td.lines-num'); const btn = document.createElement('button'); btn.classList.add('code-line-button', 'ui', 'basic', 'button'); @@ -128,62 +110,36 @@ function showLineButton() { } export function initRepoCodeView() { - if ($('.code-view .lines-num').length > 0) { - $(document).on('click', '.lines-num span', function (e) { - const linesEls = getLineEls(); - const selectedEls = Array.from(linesEls).filter((el) => { - return el.matches(`[rel=${this.getAttribute('id')}]`); - }); - - let from; - if (e.shiftKey) { - from = Array.from(linesEls).filter((el) => { - return el.closest('tr').classList.contains('active'); - }); - } - selectRange($(linesEls), $(selectedEls), from ? $(from) : null); - window.getSelection().removeAllRanges(); - showLineButton(); - }); - - $(window).on('hashchange', () => { - let m = rangeAnchorRegex.exec(window.location.hash); - const $linesEls = $(getLineEls()); - let $first; - if (m) { - $first = $linesEls.filter(`[rel=${m[1]}]`); - if ($first.length) { - selectRange($linesEls, $first, $linesEls.filter(`[rel=${m[2]}]`)); - - // show code view menu marker (don't show in blame page) - if (!isBlame()) { - showLineButton(); - } - - $('html, body').scrollTop($first.offset().top - 200); - return; - } - } - m = singleAnchorRegex.exec(window.location.hash); - if (m) { - $first = $linesEls.filter(`[rel=L${m[2]}]`); - if ($first.length) { - selectRange($linesEls, $first); - - // show code view menu marker (don't show in blame page) - if (!isBlame()) { - showLineButton(); - } - - $('html, body').scrollTop($first.offset().top - 200); - } - } - }).trigger('hashchange'); - } - $(document).on('click', '.fold-file', ({currentTarget}) => { - invertFileFolding(currentTarget.closest('.file-content'), currentTarget); + if (!document.querySelector('.code-view .lines-num')) return; + + let selRangeStart: string; + addDelegatedEventListener(document, 'click', '.lines-num span', (el: HTMLElement, e: KeyboardEvent) => { + if (!selRangeStart || !e.shiftKey) { + selRangeStart = el.getAttribute('id'); + selectRange(selRangeStart); + } else { + const selRangeStop = el.getAttribute('id'); + selectRange(`${selRangeStart}-${selRangeStop}`); + } + window.getSelection().removeAllRanges(); + showLineButton(); }); - $(document).on('click', '.copy-line-permalink', async ({currentTarget}) => { - await clippie(toAbsoluteUrl(currentTarget.getAttribute('data-url'))); + + const onHashChange = () => { + if (!window.location.hash) return; + const range = window.location.hash.substring(1); + const first = selectRange(range); + if (first) { + // set scrollRestoration to 'manual' when there is a hash in url, so that the scroll position will not be remembered after refreshing + if (window.history.scrollRestoration !== 'manual') window.history.scrollRestoration = 'manual'; + first.scrollIntoView({block: 'start'}); + showLineButton(); + } + }; + onHashChange(); + window.addEventListener('hashchange', onHashChange); + + addDelegatedEventListener(document, 'click', '.copy-line-permalink', (el) => { + clippie(toAbsoluteUrl(el.getAttribute('data-url'))); }); } diff --git a/web_src/js/features/repo-diff.ts b/web_src/js/features/repo-diff.ts index 58e0d88092..0cb2e566c0 100644 --- a/web_src/js/features/repo-diff.ts +++ b/web_src/js/features/repo-diff.ts @@ -19,6 +19,7 @@ import { import {POST, GET} from '../modules/fetch.ts'; import {fomanticQuery} from '../modules/fomantic/base.ts'; import {createTippy} from '../modules/tippy.ts'; +import {invertFileFolding} from './file-fold.ts'; const {pageData, i18n} = window.config; @@ -38,6 +39,8 @@ function initRepoDiffFileViewToggle() { } function initRepoDiffConversationForm() { + // FIXME: there could be various different form in a conversation-holder (for example: reply form, edit form). + // This listener is for "reply form" only, it should clearly distinguish different forms in the future. addDelegatedEventListener<HTMLFormElement, SubmitEvent>(document, 'submit', '.conversation-holder form', async (form, e) => { e.preventDefault(); const textArea = form.querySelector<HTMLTextAreaElement>('textarea'); @@ -242,4 +245,8 @@ export function initRepoDiffView() { initRepoDiffFileViewToggle(); initViewedCheckboxListenerFor(); initExpandAndCollapseFilesButton(); + + addDelegatedEventListener(document, 'click', '.fold-file', (el) => { + invertFileFolding(el.closest('.file-content'), el); + }); } diff --git a/web_src/js/features/repo-issue-content.ts b/web_src/js/features/repo-issue-content.ts index 88672cc255..2279c26beb 100644 --- a/web_src/js/features/repo-issue-content.ts +++ b/web_src/js/features/repo-issue-content.ts @@ -1,20 +1,17 @@ -import $ from 'jquery'; import {svg} from '../svg.ts'; import {showErrorToast} from '../modules/toast.ts'; import {GET, POST} from '../modules/fetch.ts'; -import {showElem} from '../utils/dom.ts'; +import {createElementFromHTML, showElem} from '../utils/dom.ts'; import {parseIssuePageInfo} from '../utils.ts'; +import {fomanticQuery} from '../modules/fomantic/base.ts'; -let i18nTextEdited; -let i18nTextOptions; -let i18nTextDeleteFromHistory; -let i18nTextDeleteFromHistoryConfirm; +let i18nTextEdited: string; +let i18nTextOptions: string; +let i18nTextDeleteFromHistory: string; +let i18nTextDeleteFromHistoryConfirm: string; -function showContentHistoryDetail(issueBaseUrl, commentId, historyId, itemTitleHtml) { - let $dialog = $('.content-history-detail-dialog'); - if ($dialog.length) return; - - $dialog = $(` +function showContentHistoryDetail(issueBaseUrl: string, commentId: string, historyId: string, itemTitleHtml: string) { + const elDetailDialog = createElementFromHTML(` <div class="ui modal content-history-detail-dialog"> ${svg('octicon-x', 16, 'close icon inside')} <div class="header tw-flex tw-items-center tw-justify-between"> @@ -29,8 +26,11 @@ function showContentHistoryDetail(issueBaseUrl, commentId, historyId, itemTitleH </div> <div class="comment-diff-data is-loading"></div> </div>`); - $dialog.appendTo($('body')); - $dialog.find('.dialog-header-options').dropdown({ + document.body.append(elDetailDialog); + const elOptionsDropdown = elDetailDialog.querySelector('.ui.dropdown.dialog-header-options'); + const $fomanticDialog = fomanticQuery(elDetailDialog); + const $fomanticDropdownOptions = fomanticQuery(elOptionsDropdown); + $fomanticDropdownOptions.dropdown({ showOnFocus: false, allowReselection: true, async onChange(_value, _text, $item) { @@ -46,7 +46,7 @@ function showContentHistoryDetail(issueBaseUrl, commentId, historyId, itemTitleH const resp = await response.json(); if (resp.ok) { - $dialog.modal('hide'); + $fomanticDialog.modal('hide'); } else { showErrorToast(resp.message); } @@ -60,10 +60,10 @@ function showContentHistoryDetail(issueBaseUrl, commentId, historyId, itemTitleH } }, onHide() { - $(this).dropdown('clear', true); + $fomanticDropdownOptions.dropdown('clear', true); }, }); - $dialog.modal({ + $fomanticDialog.modal({ async onShow() { try { const params = new URLSearchParams(); @@ -74,25 +74,25 @@ function showContentHistoryDetail(issueBaseUrl, commentId, historyId, itemTitleH const response = await GET(url); const resp = await response.json(); - const commentDiffData = $dialog.find('.comment-diff-data')[0]; - commentDiffData?.classList.remove('is-loading'); + const commentDiffData = elDetailDialog.querySelector('.comment-diff-data'); + commentDiffData.classList.remove('is-loading'); commentDiffData.innerHTML = resp.diffHtml; // there is only one option "item[data-option-item=delete]", so the dropdown can be entirely shown/hidden. if (resp.canSoftDelete) { - showElem($dialog.find('.dialog-header-options')); + showElem(elOptionsDropdown); } } catch (error) { console.error('Error:', error); } }, onHidden() { - $dialog.remove(); + $fomanticDialog.remove(); }, }).modal('show'); } -function showContentHistoryMenu(issueBaseUrl, $item, commentId) { - const $headerLeft = $item.find('.comment-header-left'); +function showContentHistoryMenu(issueBaseUrl: string, elCommentItem: Element, commentId: string) { + const elHeaderLeft = elCommentItem.querySelector('.comment-header-left'); const menuHtml = ` <div class="ui dropdown interact-fg content-history-menu" data-comment-id="${commentId}"> • ${i18nTextEdited}${svg('octicon-triangle-down', 14, 'dropdown icon')} @@ -100,9 +100,12 @@ function showContentHistoryMenu(issueBaseUrl, $item, commentId) { </div> </div>`; - $headerLeft.find(`.content-history-menu`).remove(); - $headerLeft.append($(menuHtml)); - $headerLeft.find('.dropdown').dropdown({ + elHeaderLeft.querySelector(`.ui.dropdown.content-history-menu`)?.remove(); // remove the old one if exists + elHeaderLeft.append(createElementFromHTML(menuHtml)); + + const elDropdown = elHeaderLeft.querySelector('.ui.dropdown.content-history-menu'); + const $fomanticDropdown = fomanticQuery(elDropdown); + $fomanticDropdown.dropdown({ action: 'hide', apiSettings: { cache: false, @@ -110,7 +113,7 @@ function showContentHistoryMenu(issueBaseUrl, $item, commentId) { }, saveRemoteData: false, onHide() { - $(this).dropdown('change values', null); + $fomanticDropdown.dropdown('change values', null); }, onChange(value, itemHtml, $item) { if (value && !$item.find('[data-history-is-deleted=1]').length) { @@ -124,9 +127,9 @@ export async function initRepoIssueContentHistory() { const issuePageInfo = parseIssuePageInfo(); if (!issuePageInfo.issueNumber) return; - const $itemIssue = $('.repository.issue .timeline-item.comment.first'); // issue(PR) main content - const $comments = $('.repository.issue .comment-list .comment'); // includes: issue(PR) comments, review comments, code comments - if (!$itemIssue.length && !$comments.length) return; + const elIssueDescription = document.querySelector('.repository.issue .timeline-item.comment.first'); // issue(PR) main content + const elComments = document.querySelectorAll('.repository.issue .comment-list .comment'); // includes: issue(PR) comments, review comments, code comments + if (!elIssueDescription && !elComments.length) return; const issueBaseUrl = `${issuePageInfo.repoLink}/issues/${issuePageInfo.issueNumber}`; @@ -139,13 +142,13 @@ export async function initRepoIssueContentHistory() { i18nTextDeleteFromHistoryConfirm = resp.i18n.textDeleteFromHistoryConfirm; i18nTextOptions = resp.i18n.textOptions; - if (resp.editedHistoryCountMap[0] && $itemIssue.length) { - showContentHistoryMenu(issueBaseUrl, $itemIssue, '0'); + if (resp.editedHistoryCountMap[0] && elIssueDescription) { + showContentHistoryMenu(issueBaseUrl, elIssueDescription, '0'); } for (const [commentId, _editedCount] of Object.entries(resp.editedHistoryCountMap)) { if (commentId === '0') continue; - const $itemComment = $(`#issuecomment-${commentId}`); - showContentHistoryMenu(issueBaseUrl, $itemComment, commentId); + const elIssueComment = document.querySelector(`#issuecomment-${commentId}`); + if (elIssueComment) showContentHistoryMenu(issueBaseUrl, elIssueComment, commentId); } } catch (error) { console.error('Error:', error); diff --git a/web_src/js/features/repo-issue-edit.ts b/web_src/js/features/repo-issue-edit.ts index cf4c223e03..38dfea4743 100644 --- a/web_src/js/features/repo-issue-edit.ts +++ b/web_src/js/features/repo-issue-edit.ts @@ -30,6 +30,9 @@ async function tryOnEditContent(e) { const saveAndRefresh = async (e) => { e.preventDefault(); + // we are already in a form, do not bubble up to the document otherwise there will be other "form submit handlers" + // at the moment, the form submit event conflicts with initRepoDiffConversationForm (global '.conversation-holder form' event handler) + e.stopPropagation(); renderContent.classList.add('is-loading'); showElem(renderContent); hideElem(editContentZone); diff --git a/web_src/js/features/repo-issue-sidebar.ts b/web_src/js/features/repo-issue-sidebar.ts index ef2b7d143c..f84bed127f 100644 --- a/web_src/js/features/repo-issue-sidebar.ts +++ b/web_src/js/features/repo-issue-sidebar.ts @@ -1,17 +1,15 @@ -import $ from 'jquery'; import {POST} from '../modules/fetch.ts'; import {queryElems, toggleElem} from '../utils/dom.ts'; import {initIssueSidebarComboList} from './repo-issue-sidebar-combolist.ts'; function initBranchSelector() { // TODO: RemoveIssueRef: see "repo/issue/branch_selector_field.tmpl" - const elSelectBranch = document.querySelector('.ui.dropdown.select-branch'); + const elSelectBranch = document.querySelector('.ui.dropdown.select-branch.branch-selector-dropdown'); if (!elSelectBranch) return; const urlUpdateIssueRef = elSelectBranch.getAttribute('data-url-update-issueref'); - const $selectBranch = $(elSelectBranch); - const $branchMenu = $selectBranch.find('.reference-list-menu'); - $branchMenu.find('.item:not(.no-select)').on('click', async function (e) { + const elBranchMenu = elSelectBranch.querySelector('.reference-list-menu'); + queryElems(elBranchMenu, '.item:not(.no-select)', (el) => el.addEventListener('click', async function (e) { e.preventDefault(); const selectedValue = this.getAttribute('data-id'); // eg: "refs/heads/my-branch" const selectedText = this.getAttribute('data-name'); // eg: "my-branch" @@ -29,7 +27,7 @@ function initBranchSelector() { document.querySelector<HTMLInputElement>(selectedHiddenSelector).value = selectedValue; elSelectBranch.querySelector('.text-branch-name').textContent = selectedText; } - }); + })); } function initRepoIssueDue() { diff --git a/web_src/js/features/repo-issue.ts b/web_src/js/features/repo-issue.ts index a9dda39a7f..d74d3f7700 100644 --- a/web_src/js/features/repo-issue.ts +++ b/web_src/js/features/repo-issue.ts @@ -373,10 +373,6 @@ export async function handleReply(el) { export function initRepoPullRequestReview() { if (window.location.hash && window.location.hash.startsWith('#issuecomment-')) { - // set scrollRestoration to 'manual' when there is a hash in url, so that the scroll position will not be remembered after refreshing - if (window.history.scrollRestoration !== 'manual') { - window.history.scrollRestoration = 'manual'; - } const commentDiv = document.querySelector(window.location.hash); if (commentDiv) { // get the name of the parent id @@ -384,14 +380,6 @@ export function initRepoPullRequestReview() { if (groupID && groupID.startsWith('code-comments-')) { const id = groupID.slice(14); const ancestorDiffBox = commentDiv.closest('.diff-file-box'); - // on pages like conversation, there is no diff header - const diffHeader = ancestorDiffBox?.querySelector('.diff-file-header'); - - // offset is for scrolling - let offset = 30; - if (diffHeader) { - offset += $('.diff-detail-box').outerHeight() + $(diffHeader).outerHeight(); - } hideElem(`#show-outdated-${id}`); showElem(`#code-comments-${id}, #code-preview-${id}, #hide-outdated-${id}`); @@ -399,12 +387,11 @@ export function initRepoPullRequestReview() { if (ancestorDiffBox?.getAttribute('data-folded') === 'true') { setFileFolding(ancestorDiffBox, ancestorDiffBox.querySelector('.fold-file'), false); } - - window.scrollTo({ - top: $(commentDiv).offset().top - offset, - behavior: 'instant', - }); } + // set scrollRestoration to 'manual' when there is a hash in url, so that the scroll position will not be remembered after refreshing + if (window.history.scrollRestoration !== 'manual') window.history.scrollRestoration = 'manual'; + // wait for a while because some elements (eg: image, editor, etc.) may change the viewport's height. + setTimeout(() => commentDiv.scrollIntoView({block: 'start'}), 100); } } diff --git a/web_src/js/features/repo-legacy.ts b/web_src/js/features/repo-legacy.ts index 04267d1dda..33f02be865 100644 --- a/web_src/js/features/repo-legacy.ts +++ b/web_src/js/features/repo-legacy.ts @@ -1,4 +1,3 @@ -import $ from 'jquery'; import { initRepoCommentFormAndSidebar, initRepoIssueBranchSelect, initRepoIssueCodeCommentCancel, initRepoIssueCommentDelete, @@ -15,7 +14,7 @@ import {initCompReactionSelector} from './comp/ReactionSelector.ts'; import {initRepoSettings} from './repo-settings.ts'; import {initRepoPullRequestMergeForm} from './repo-issue-pr-form.ts'; import {initRepoPullRequestCommitStatus} from './repo-issue-pr-status.ts'; -import {hideElem, queryElemChildren, showElem} from '../utils/dom.ts'; +import {hideElem, queryElemChildren, queryElems, showElem} from '../utils/dom.ts'; import {initRepoIssueCommentEdit} from './repo-issue-edit.ts'; import {initRepoMilestone} from './repo-milestone.ts'; import {initRepoNew} from './repo-new.ts'; @@ -29,47 +28,20 @@ function initRepoBranchTagSelector(selector: string) { } export function initBranchSelectorTabs() { - const elSelectBranch = document.querySelector('.ui.dropdown.select-branch'); - if (!elSelectBranch) return; - - $(elSelectBranch).find('.reference.column').on('click', function () { - hideElem($(elSelectBranch).find('.scrolling.reference-list-menu')); - showElem(this.getAttribute('data-target')); - queryElemChildren(this.parentNode, '.branch-tag-item', (el) => el.classList.remove('active')); - this.classList.add('active'); - return false; - }); -} - -function initRepoCommonBranchOrTagDropdown(selector: string) { - $(selector).each(function () { - const $dropdown = $(this); - $dropdown.find('.reference.column').on('click', function () { - hideElem($dropdown.find('.scrolling.reference-list-menu')); - showElem($($(this).data('target'))); - return false; - }); - }); -} - -function initRepoCommonFilterSearchDropdown(selector: string) { - const $dropdown = $(selector); - if (!$dropdown.length) return; - - $dropdown.dropdown({ - fullTextSearch: 'exact', - selectOnKeydown: false, - onChange(_text, _value, $choice) { - if ($choice[0].getAttribute('data-url')) { - window.location.href = $choice[0].getAttribute('data-url'); - } - }, - message: {noResults: $dropdown[0].getAttribute('data-no-results')}, - }); + const elSelectBranches = document.querySelectorAll('.ui.dropdown.select-branch'); + for (const elSelectBranch of elSelectBranches) { + queryElems(elSelectBranch, '.reference.column', (el) => el.addEventListener('click', () => { + hideElem(elSelectBranch.querySelectorAll('.scrolling.reference-list-menu')); + showElem(el.getAttribute('data-target')); + queryElemChildren(el.parentNode, '.branch-tag-item', (el) => el.classList.remove('active')); + el.classList.add('active'); + })); + } } export function initRepository() { - if (!$('.page-content.repository').length) return; + const pageContent = document.querySelector('.page-content.repository'); + if (!pageContent) return; initRepoBranchTagSelector('.js-branch-tag-selector'); initRepoCommentFormAndSidebar(); @@ -79,19 +51,12 @@ export function initRepository() { initRepoMilestone(); initRepoNew(); - // Compare or pull request - const $repoDiff = $('.repository.diff'); - if ($repoDiff.length) { - initRepoCommonBranchOrTagDropdown('.choose.branch .dropdown'); - initRepoCommonFilterSearchDropdown('.choose.branch .dropdown'); - } - initRepoCloneButtons(); initCitationFileCopyContent(); initRepoSettings(); // Issues - if ($('.repository.view.issue').length > 0) { + if (pageContent.matches('.page-content.repository.view.issue')) { initRepoIssueCommentEdit(); initRepoIssueBranchSelect(); @@ -112,18 +77,5 @@ export function initRepository() { initRepoPullRequestCommitStatus(); } - // Pull request - const $repoComparePull = $('.repository.compare.pull'); - if ($repoComparePull.length > 0) { - // show pull request form - $repoComparePull.find('button.show-form').on('click', function (e) { - e.preventDefault(); - hideElem($(this).parent()); - - const $form = $repoComparePull.find('.pullrequest-form'); - showElem($form); - }); - } - initUnicodeEscapeButton(); } diff --git a/web_src/js/features/repo-new.ts b/web_src/js/features/repo-new.ts index 436288325a..8a77a77b4a 100644 --- a/web_src/js/features/repo-new.ts +++ b/web_src/js/features/repo-new.ts @@ -1,14 +1,80 @@ -import $ from 'jquery'; +import {hideElem, showElem, toggleElem} from '../utils/dom.ts'; +import {htmlEscape} from 'escape-goat'; +import {fomanticQuery} from '../modules/fomantic/base.ts'; -export function initRepoNew() { - // Repo Creation - if ($('.repository.new.repo').length > 0) { - $('input[name="gitignores"], input[name="license"]').on('change', () => { - const gitignores = $('input[name="gitignores"]').val(); - const license = $('input[name="license"]').val(); - if (gitignores || license) { - document.querySelector<HTMLInputElement>('input[name="auto_init"]').checked = true; - } +const {appSubUrl} = window.config; + +function initRepoNewTemplateSearch(form: HTMLFormElement) { + const inputRepoOwnerUid = form.querySelector<HTMLInputElement>('#uid'); + const elRepoTemplateDropdown = form.querySelector<HTMLInputElement>('#repo_template_search'); + const inputRepoTemplate = form.querySelector<HTMLInputElement>('#repo_template'); + const elTemplateUnits = form.querySelector('#template_units'); + const elNonTemplate = form.querySelector('#non_template'); + const checkTemplate = function () { + const hasSelectedTemplate = inputRepoTemplate.value !== '' && inputRepoTemplate.value !== '0'; + toggleElem(elTemplateUnits, hasSelectedTemplate); + toggleElem(elNonTemplate, !hasSelectedTemplate); + }; + inputRepoTemplate.addEventListener('change', checkTemplate); + checkTemplate(); + + const $dropdown = fomanticQuery(elRepoTemplateDropdown); + const onChangeOwner = function () { + $dropdown.dropdown('setting', { + apiSettings: { + url: `${appSubUrl}/repo/search?q={query}&template=true&priority_owner_id=${inputRepoOwnerUid.value}`, + onResponse(response) { + const results = []; + results.push({name: '', value: ''}); // empty item means not using template + for (const tmplRepo of response.data) { + results.push({ + name: htmlEscape(tmplRepo.repository.full_name), + value: String(tmplRepo.repository.id), + }); + } + $dropdown.fomanticExt.onResponseKeepSelectedItem($dropdown, inputRepoTemplate.value); + return {results}; + }, + cache: false, + }, }); - } + }; + inputRepoOwnerUid.addEventListener('change', onChangeOwner); + onChangeOwner(); +} + +export function initRepoNew() { + const pageContent = document.querySelector('.page-content.repository.new-repo'); + if (!pageContent) return; + + const form = document.querySelector<HTMLFormElement>('.new-repo-form'); + const inputGitIgnores = form.querySelector<HTMLInputElement>('input[name="gitignores"]'); + const inputLicense = form.querySelector<HTMLInputElement>('input[name="license"]'); + const inputAutoInit = form.querySelector<HTMLInputElement>('input[name="auto_init"]'); + const updateUiAutoInit = () => { + inputAutoInit.checked = Boolean(inputGitIgnores.value || inputLicense.value); + }; + inputGitIgnores.addEventListener('change', updateUiAutoInit); + inputLicense.addEventListener('change', updateUiAutoInit); + updateUiAutoInit(); + + const inputRepoName = form.querySelector<HTMLInputElement>('input[name="repo_name"]'); + const inputPrivate = form.querySelector<HTMLInputElement>('input[name="private"]'); + const updateUiRepoName = () => { + const helps = form.querySelectorAll(`.help[data-help-for-repo-name]`); + hideElem(helps); + let help = form.querySelector(`.help[data-help-for-repo-name="${CSS.escape(inputRepoName.value)}"]`); + if (!help) help = form.querySelector(`.help[data-help-for-repo-name=""]`); + showElem(help); + const repoNamePreferPrivate = {'.profile': false, '.profile-private': true}; + const preferPrivate = repoNamePreferPrivate[inputRepoName.value]; + // inputPrivate might be disabled because site admin "force private" + if (preferPrivate !== undefined && !inputPrivate.closest('.disabled, [disabled]')) { + inputPrivate.checked = preferPrivate; + } + }; + inputRepoName.addEventListener('input', updateUiRepoName); + updateUiRepoName(); + + initRepoNewTemplateSearch(form); } diff --git a/web_src/js/features/repo-settings.ts b/web_src/js/features/repo-settings.ts index 1d2d447205..90b0219f3e 100644 --- a/web_src/js/features/repo-settings.ts +++ b/web_src/js/features/repo-settings.ts @@ -107,7 +107,7 @@ function initRepoSettingsBranches() { let matched = false; const statusCheck = el.getAttribute('data-status-check'); for (const pattern of validPatterns) { - if (minimatch(statusCheck, pattern)) { + if (minimatch(statusCheck, pattern, {noext: true})) { // https://github.com/go-gitea/gitea/issues/33121 disable extended glob syntax matched = true; break; } diff --git a/web_src/js/features/repo-template.ts b/web_src/js/features/repo-template.ts deleted file mode 100644 index fbd7b656ed..0000000000 --- a/web_src/js/features/repo-template.ts +++ /dev/null @@ -1,51 +0,0 @@ -import $ from 'jquery'; -import {htmlEscape} from 'escape-goat'; -import {hideElem, showElem} from '../utils/dom.ts'; - -const {appSubUrl} = window.config; - -export function initRepoTemplateSearch() { - const $repoTemplate = $('#repo_template'); - const checkTemplate = function () { - const $templateUnits = $('#template_units'); - const $nonTemplate = $('#non_template'); - if ($repoTemplate.val() !== '' && $repoTemplate.val() !== '0') { - showElem($templateUnits); - hideElem($nonTemplate); - } else { - hideElem($templateUnits); - showElem($nonTemplate); - } - }; - $repoTemplate.on('change', checkTemplate); - checkTemplate(); - - const changeOwner = function () { - $('#repo_template_search') - .dropdown({ - apiSettings: { - url: `${appSubUrl}/repo/search?q={query}&template=true&priority_owner_id=${$('#uid').val()}`, - onResponse(response) { - const filteredResponse = {success: true, results: []}; - filteredResponse.results.push({ - name: '', - value: '', - }); - // Parse the response from the api to work with our dropdown - $.each(response.data, (_r, repo) => { - filteredResponse.results.push({ - name: htmlEscape(repo.repository.full_name), - value: repo.repository.id, - }); - }); - return filteredResponse; - }, - cache: false, - }, - - fullTextSearch: true, - }); - }; - $('#uid').on('change', changeOwner); - changeOwner(); -} diff --git a/web_src/js/globals.d.ts b/web_src/js/globals.d.ts index c08ff9976b..0c540ac296 100644 --- a/web_src/js/globals.d.ts +++ b/web_src/js/globals.d.ts @@ -36,8 +36,9 @@ declare module 'swagger-ui-dist/swagger-ui-es-bundle.js' { } interface JQuery { - api: any, // fomantic areYouSure: any, // jquery.are-you-sure + fomanticExt: any; // fomantic extension + api: any, // fomantic dimmer: any, // fomantic dropdown: any; // fomantic modal: any; // fomantic diff --git a/web_src/js/index.ts b/web_src/js/index.ts index 51d8c96fbd..022be033da 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -34,14 +34,13 @@ import { import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.ts'; import {initRepoTopicBar} from './features/repo-home.ts'; import {initAdminCommon} from './features/admin/common.ts'; -import {initRepoTemplateSearch} from './features/repo-template.ts'; import {initRepoCodeView} from './features/repo-code.ts'; import {initSshKeyFormParser} from './features/sshkey-helper.ts'; import {initUserSettings} from './features/user-settings.ts'; import {initRepoActivityTopAuthorsChart, initRepoArchiveLinks} from './features/repo-common.ts'; import {initRepoMigrationStatusChecker} from './features/repo-migrate.ts'; import {initRepoDiffView} from './features/repo-diff.ts'; -import {initOrgTeamSearchRepoBox, initOrgTeamSettings} from './features/org-team.ts'; +import {initOrgTeam} from './features/org-team.ts'; import {initUserAuthWebAuthn, initUserAuthWebAuthnRegister} from './features/user-auth-webauthn.ts'; import {initRepoRelease, initRepoReleaseNew} from './features/repo-release.ts'; import {initRepoEditor} from './features/repo-editor.ts'; @@ -167,8 +166,7 @@ onDomReady(() => { initNotificationCount, initNotificationsTable, - initOrgTeamSearchRepoBox, - initOrgTeamSettings, + initOrgTeam, initRepoActivityTopAuthorsChart, initRepoArchiveLinks, @@ -193,7 +191,6 @@ onDomReady(() => { initRepoPullRequestReview, initRepoRelease, initRepoReleaseNew, - initRepoTemplateSearch, initRepoTopicBar, initRepoWikiForm, initRepository, diff --git a/web_src/js/modules/fomantic.ts b/web_src/js/modules/fomantic.ts index af47c8fb51..18a3c18c9c 100644 --- a/web_src/js/modules/fomantic.ts +++ b/web_src/js/modules/fomantic.ts @@ -11,9 +11,10 @@ import {svg} from '../svg.ts'; export const fomanticMobileScreen = window.matchMedia('only screen and (max-width: 767.98px)'); export function initGiteaFomantic() { + // our extensions + $.fn.fomanticExt = {}; // Silence fomantic's error logging when tabs are used without a target content element $.fn.tab.settings.silent = true; - // By default, use "exact match" for full text search $.fn.dropdown.settings.fullTextSearch = 'exact'; // Do not use "cursor: pointer" for dropdown labels diff --git a/web_src/js/modules/fomantic/dropdown.ts b/web_src/js/modules/fomantic/dropdown.ts index 6d0f12cb43..9bdc9bfc33 100644 --- a/web_src/js/modules/fomantic/dropdown.ts +++ b/web_src/js/modules/fomantic/dropdown.ts @@ -1,6 +1,7 @@ import $ from 'jquery'; import {generateAriaId} from './base.ts'; import type {FomanticInitFunction} from '../../types.ts'; +import {queryElems} from '../../utils/dom.ts'; const ariaPatchKey = '_giteaAriaPatchDropdown'; const fomanticDropdownFn = $.fn.dropdown; @@ -9,6 +10,7 @@ const fomanticDropdownFn = $.fn.dropdown; export function initAriaDropdownPatch() { if ($.fn.dropdown === ariaDropdownFn) throw new Error('initAriaDropdownPatch could only be called once'); $.fn.dropdown = ariaDropdownFn; + $.fn.fomanticExt.onResponseKeepSelectedItem = onResponseKeepSelectedItem; (ariaDropdownFn as FomanticInitFunction).settings = fomanticDropdownFn.settings; } @@ -351,3 +353,19 @@ export function hideScopedEmptyDividers(container: Element) { if (item.nextElementSibling?.matches('.divider')) hideDivider(item); } } + +function onResponseKeepSelectedItem(dropdown: typeof $|HTMLElement, selectedValue: string) { + // There is a bug in fomantic dropdown when using "apiSettings" to fetch data + // * when there is a selected item, the dropdown insists on hiding the selected one from the list: + // * in the "filter" function: ('[data-value="'+value+'"]').addClass(className.filtered) + // + // When user selects one item, and click the dropdown again, + // then the dropdown only shows other items and will select another (wrong) one. + // It can't be easily fix by using setTimeout(patch, 0) in `onResponse` because the `onResponse` is called before another `setTimeout(..., timeLeft)` + // Fortunately, the "timeLeft" is controlled by "loadingDuration" which is always zero at the moment, so we can use `setTimeout(..., 10)` + const elDropdown = (dropdown instanceof HTMLElement) ? dropdown : dropdown[0]; + setTimeout(() => { + queryElems(elDropdown, `.menu .item[data-value="${CSS.escape(selectedValue)}"].filtered`, (el) => el.classList.remove('filtered')); + $(elDropdown).dropdown('set selected', selectedValue ?? ''); + }, 10); +} diff --git a/web_src/js/svg.ts b/web_src/js/svg.ts index 90b12fa87d..6a8246fa1b 100644 --- a/web_src/js/svg.ts +++ b/web_src/js/svg.ts @@ -28,6 +28,7 @@ import octiconEye from '../../public/assets/img/svg/octicon-eye.svg'; import octiconFile from '../../public/assets/img/svg/octicon-file.svg'; import octiconFileDirectoryFill from '../../public/assets/img/svg/octicon-file-directory-fill.svg'; import octiconFileDirectoryOpenFill from '../../public/assets/img/svg/octicon-file-directory-open-fill.svg'; +import octiconFileSubmodule from '../../public/assets/img/svg/octicon-file-submodule.svg'; import octiconFilter from '../../public/assets/img/svg/octicon-filter.svg'; import octiconGear from '../../public/assets/img/svg/octicon-gear.svg'; import octiconGitBranch from '../../public/assets/img/svg/octicon-git-branch.svg'; @@ -104,6 +105,7 @@ const svgs = { 'octicon-file': octiconFile, 'octicon-file-directory-fill': octiconFileDirectoryFill, 'octicon-file-directory-open-fill': octiconFileDirectoryOpenFill, + 'octicon-file-submodule': octiconFileSubmodule, 'octicon-filter': octiconFilter, 'octicon-gear': octiconGear, 'octicon-git-branch': octiconGitBranch, diff --git a/web_src/js/utils.test.ts b/web_src/js/utils.test.ts index ac9d4fab91..b527111533 100644 --- a/web_src/js/utils.test.ts +++ b/web_src/js/utils.test.ts @@ -49,6 +49,8 @@ test('parseIssueNewHref', () => { expect(parseIssueNewHref('/owner/repo/issues/new')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues'}); expect(parseIssueNewHref('/owner/repo/issues/new?query')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues'}); expect(parseIssueNewHref('/sub/owner/repo/issues/new#hash')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues'}); + expect(parseIssueNewHref('/sub/owner/repo/compare/feature/branch-1...fix/branch-2')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'pulls'}); + expect(parseIssueNewHref('/other')).toEqual({}); }); test('parseUrl', () => { diff --git a/web_src/js/utils.ts b/web_src/js/utils.ts index 997a4d1ff3..2a2bdc60f9 100644 --- a/web_src/js/utils.ts +++ b/web_src/js/utils.ts @@ -39,8 +39,9 @@ export function parseIssueHref(href: string): IssuePathInfo { export function parseIssueNewHref(href: string): IssuePathInfo { const path = (href || '').replace(/[#?].*$/, ''); - const [_, ownerName, repoName, pathType, indexString] = /([^/]+)\/([^/]+)\/(issues|pulls)\/new/.exec(path) || []; - return {ownerName, repoName, pathType, indexString}; + const [_, ownerName, repoName, pathTypeField] = /([^/]+)\/([^/]+)\/(issues\/new|compare\/.+\.\.\.)/.exec(path) || []; + const pathType = pathTypeField ? (pathTypeField.startsWith('issues/new') ? 'issues' : 'pulls') : undefined; + return {ownerName, repoName, pathType}; } export function parseIssuePageInfo(): IssuePageInfo { |