diff --git a/.deadcode-out b/.deadcode-out index ec08a0ca64..7efe6f8406 100644 --- a/.deadcode-out +++ b/.deadcode-out @@ -100,6 +100,8 @@ package "code.gitea.io/gitea/models/unittest" func LoadFixtures func Copy func CopyDir + func NewMockWebServer + func NormalizedFullPath func FixturesDir func fatalTestError func InitSettings @@ -322,6 +324,7 @@ package "code.gitea.io/gitea/services/pull" package "code.gitea.io/gitea/services/repository" func IsErrForkAlreadyExist + func UpdateRepositoryUnits package "code.gitea.io/gitea/services/repository/archiver" func ArchiveRepository diff --git a/.golangci.yml b/.golangci.yml index d6ce37f49a..6d52f99401 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -90,6 +90,8 @@ linters-settings: desc: do not use the internal package, use AddXxx function instead - pkg: gopkg.in/ini.v1 desc: do not use the ini package, use gitea's config system instead + - pkg: github.com/minio/sha256-simd + desc: use crypto/sha256 instead, see https://codeberg.org/forgejo/forgejo/pulls/1528 issues: max-issues-per-linter: 0 diff --git a/assets/go-licenses.json b/assets/go-licenses.json index 8ce1dbad9a..48dfabaeae 100644 --- a/assets/go-licenses.json +++ b/assets/go-licenses.json @@ -89,11 +89,6 @@ "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" }, - { - "name": "github.com/NYTimes/gziphandler", - "path": "github.com/NYTimes/gziphandler/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 2016-2017 The New York Times Company\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/ProtonMail/go-crypto", "path": "github.com/ProtonMail/go-crypto/LICENSE", @@ -664,6 +659,11 @@ "path": "github.com/klauspost/compress/LICENSE", "licenseText": "Copyright (c) 2012 The Go Authors. All rights reserved.\nCopyright (c) 2019 Klaus Post. 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\n------------------\n\nFiles: gzhttp/*\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-2017 The New York Times Company\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n\n------------------\n\nFiles: s2/cmd/internal/readahead/*\n\nThe MIT License (MIT)\n\nCopyright (c) 2015 Klaus Post\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---------------------\nFiles: snappy/*\nFiles: internal/snapref/*\n\nCopyright (c) 2011 The Snappy-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\n-----------------\n\nFiles: s2/cmd/internal/filepathx/*\n\nCopyright 2016 The filepathx Authors\n\nPermission 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:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE 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.\n" }, + { + "name": "github.com/klauspost/compress/gzhttp", + "path": "github.com/klauspost/compress/gzhttp/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 2016-2017 The New York Times Company\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/klauspost/compress/internal/snapref", "path": "github.com/klauspost/compress/internal/snapref/LICENSE", diff --git a/cmd/generate.go b/cmd/generate.go index 4ab10da22a..806946244b 100644 --- a/cmd/generate.go +++ b/cmd/generate.go @@ -70,7 +70,7 @@ func runGenerateInternalToken(c *cli.Context) error { } func runGenerateLfsJwtSecret(c *cli.Context) error { - _, jwtSecretBase64, err := generate.NewJwtSecretBase64() + _, jwtSecretBase64, err := generate.NewJwtSecret() if err != nil { return err } diff --git a/contrib/ide/vscode/settings.json b/contrib/ide/vscode/settings.json index e33bccf902..2ec666f3c1 100644 --- a/contrib/ide/vscode/settings.json +++ b/contrib/ide/vscode/settings.json @@ -1,4 +1,4 @@ { - "go.buildTags": "'sqlite sqlite_unlock_notify'", + "go.buildTags": "sqlite,sqlite_unlock_notify", "go.testFlags": ["-v"] -} \ No newline at end of file +} diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 5ce4e6ea5b..6d7b3bedf6 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -412,6 +412,10 @@ USER = root ;; ;; Whether execute database models migrations automatically ;AUTO_MIGRATION = true +;; +;; Threshold value (in seconds) beyond which query execution time is logged as a warning in the xorm logger +;; +;SLOW_QUERY_TRESHOLD = 5s ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -817,6 +821,11 @@ LEVEL = Info ;; Every new user will have restricted permissions depending on this setting ;DEFAULT_USER_IS_RESTRICTED = false ;; +;; Users will be able to use dots when choosing their username. Disabling this is +;; helpful if your usersare having issues with e.g. RSS feeds or advanced third-party +;; extensions that use strange regex patterns. +; ALLOW_DOTS_IN_USERNAMES = true +;; ;; Either "public", "limited" or "private", default is "public" ;; Limited is for users visible only to signed users ;; Private is for users visible only to members of their organizations @@ -903,6 +912,14 @@ LEVEL = Info ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;[badges] +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Enable repository badges (via shields.io or a similar generator) +;ENABLED = true +;; Template for the badge generator. +;GENERATOR_URL_TEMPLATE = https://img.shields.io/badge/{{.label}}-{{.text}}-{{.color}} + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;[repository] ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -1470,6 +1487,8 @@ LEVEL = Info ;; ;; Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled ;DEFAULT_EMAIL_NOTIFICATIONS = enabled +;; Send an email to all admins when a new user signs up to inform the admins about this act. Options: true, false +;SEND_NOTIFICATION_EMAIL_ON_NEW_USER = false ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -1783,9 +1802,6 @@ LEVEL = Info ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; -;AVATAR_UPLOAD_PATH = data/avatars -;REPOSITORY_AVATAR_UPLOAD_PATH = data/repo-avatars -;; ;; How Gitea deals with missing repository avatars ;; none = no avatar will be displayed; random = random avatar will be displayed; image = default image will be used ;REPOSITORY_AVATAR_FALLBACK = none diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md index 33732d080b..b7c6ceb431 100644 --- a/docs/content/administration/config-cheat-sheet.en-us.md +++ b/docs/content/administration/config-cheat-sheet.en-us.md @@ -458,6 +458,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a - `MAX_IDLE_CONNS` **2**: Max idle database connections on connection pool, default is 2 - this will be capped to `MAX_OPEN_CONNS`. - `CONN_MAX_LIFETIME` **0 or 3s**: Sets the maximum amount of time a DB connection may be reused - default is 0, meaning there is no limit (except on MySQL where it is 3s - see #6804 & #7071). - `AUTO_MIGRATION` **true**: Whether execute database models migrations automatically. +- `SLOW_QUERY_THRESHOLD` **5s**: Threshold value in seconds beyond which query execution time is logged as a warning in the xorm logger. [^1]: It may be necessary to specify a hostport even when listening on a unix socket, as the port is part of the socket name. see [#24552](https://github.com/go-gitea/gitea/issues/24552#issuecomment-1681649367) for additional details. @@ -517,6 +518,7 @@ And the following unique queues: - `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**: Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled - `DISABLE_REGULAR_ORG_CREATION`: **false**: Disallow regular (non-admin) users from creating organizations. +- `SEND_NOTIFICATION_EMAIL_ON_NEW_USER`: **false**: Send an email to all admins when a new user signs up to inform the admins about this act. ## Security (`security`) diff --git a/go.mod b/go.mod index 7292347b96..3741a413e1 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,6 @@ require ( gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121 github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 - github.com/NYTimes/gziphandler v1.1.1 github.com/PuerkitoBio/goquery v1.8.1 github.com/alecthomas/chroma/v2 v2.12.0 github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb @@ -77,14 +76,12 @@ require ( github.com/mholt/archiver/v3 v3.5.1 github.com/microcosm-cc/bluemonday v1.0.26 github.com/minio/minio-go/v7 v7.0.66 - github.com/minio/sha256-simd v1.0.1 github.com/msteinert/pam v1.2.0 github.com/nektos/act v0.2.52 github.com/niklasfasching/go-org v1.7.0 github.com/olivere/elastic/v7 v7.0.32 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.0-rc6 - github.com/pkg/errors v0.9.1 github.com/pquerna/otp v1.4.0 github.com/prometheus/client_golang v1.18.0 github.com/quasoft/websspi v1.1.2 @@ -100,7 +97,6 @@ require ( github.com/ulikunitz/xz v0.5.11 github.com/urfave/cli/v2 v2.27.1 github.com/xanzy/go-gitlab v0.96.0 - github.com/xeipuuv/gojsonschema v1.2.0 github.com/yohcop/openid-go v1.0.1 github.com/yuin/goldmark v1.6.0 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc @@ -230,6 +226,7 @@ require ( github.com/mholt/acmez v1.2.0 // indirect github.com/miekg/dns v1.1.58 // indirect github.com/minio/md5-simd v1.1.2 // indirect + github.com/minio/sha256-simd v1.0.1 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect @@ -245,6 +242,7 @@ require ( github.com/pelletier/go-toml/v2 v2.1.1 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.46.0 // indirect @@ -275,8 +273,6 @@ require ( github.com/valyala/fastjson v1.6.4 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect - github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect - github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect github.com/zeebo/blake3 v0.2.3 // indirect diff --git a/go.sum b/go.sum index de29622eca..c43b51cd38 100644 --- a/go.sum +++ b/go.sum @@ -93,8 +93,6 @@ github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBa github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= -github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= 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/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= @@ -832,13 +830,6 @@ github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23n github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= -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= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= -github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI= diff --git a/models/actions/run.go b/models/actions/run.go index fcac58d515..9c7f049bbc 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -171,14 +171,13 @@ func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) err } // CancelRunningJobs cancels all running and waiting jobs associated with a specific workflow. -func CancelRunningJobs(ctx context.Context, repoID int64, ref, workflowID string, event webhook_module.HookEventType) error { +func CancelRunningJobs(ctx context.Context, repoID int64, ref, workflowID string) error { // Find all runs in the specified repository, reference, and workflow with statuses 'Running' or 'Waiting'. runs, total, err := db.FindAndCount[ActionRun](ctx, FindRunOptions{ - RepoID: repoID, - Ref: ref, - WorkflowID: workflowID, - TriggerEvent: event, - Status: []Status{StatusRunning, StatusWaiting}, + RepoID: repoID, + Ref: ref, + WorkflowID: workflowID, + Status: []Status{StatusRunning, StatusWaiting}, }) if err != nil { return err @@ -312,6 +311,32 @@ func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWork return commiter.Commit() } +func GetLatestRun(ctx context.Context, repoID int64) (*ActionRun, error) { + var run ActionRun + has, err := db.GetEngine(ctx).Where("repo_id=?", repoID).OrderBy("id DESC").Limit(1).Get(&run) + if err != nil { + return nil, err + } else if !has { + return nil, fmt.Errorf("latest run: %w", util.ErrNotExist) + } + return &run, nil +} + +func GetLatestRunForBranchAndWorkflow(ctx context.Context, repoID int64, branch, workflowFile, event string) (*ActionRun, error) { + var run ActionRun + q := db.GetEngine(ctx).Where("repo_id=?", repoID).And("ref=?", branch).And("workflow_id=?", workflowFile) + if event != "" { + q = q.And("event=?", event) + } + has, err := q.Desc("id").Get(&run) + if err != nil { + return nil, err + } else if !has { + return nil, util.NewNotExistErrorf("run with repo_id %d, ref %s, workflow_id %s", repoID, branch, workflowFile) + } + return &run, nil +} + func GetRunByID(ctx context.Context, id int64) (*ActionRun, error) { var run ActionRun has, err := db.GetEngine(ctx).Where("id=?", id).Get(&run) diff --git a/models/actions/run_list.go b/models/actions/run_list.go index 388bfc4f86..375c46221b 100644 --- a/models/actions/run_list.go +++ b/models/actions/run_list.go @@ -10,7 +10,6 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/container" - webhook_module "code.gitea.io/gitea/modules/webhook" "xorm.io/builder" ) @@ -72,7 +71,6 @@ type FindRunOptions struct { WorkflowID string Ref string // the commit/tag/… that caused this workflow TriggerUserID int64 - TriggerEvent webhook_module.HookEventType Approved bool // not util.OptionalBool, it works only when it's true Status []Status } @@ -100,9 +98,6 @@ func (opts FindRunOptions) ToConds() builder.Cond { if opts.Ref != "" { cond = cond.And(builder.Eq{"ref": opts.Ref}) } - if opts.TriggerEvent != "" { - cond = cond.And(builder.Eq{"trigger_event": opts.TriggerEvent}) - } return cond } diff --git a/models/actions/schedule.go b/models/actions/schedule.go index d450e7aa07..34d23f1c01 100644 --- a/models/actions/schedule.go +++ b/models/actions/schedule.go @@ -5,7 +5,6 @@ package actions import ( "context" - "fmt" "time" "code.gitea.io/gitea/models/db" @@ -119,22 +118,3 @@ func DeleteScheduleTaskByRepo(ctx context.Context, id int64) error { return committer.Commit() } - -func CleanRepoScheduleTasks(ctx context.Context, repo *repo_model.Repository) error { - // If actions disabled when there is schedule task, this will remove the outdated schedule tasks - // There is no other place we can do this because the app.ini will be changed manually - if err := DeleteScheduleTaskByRepo(ctx, repo.ID); err != nil { - return fmt.Errorf("DeleteCronTaskByRepo: %v", err) - } - // cancel running cron jobs of this repository and delete old schedules - if err := CancelRunningJobs( - ctx, - repo.ID, - repo.DefaultBranch, - "", - webhook_module.HookEventSchedule, - ); err != nil { - return fmt.Errorf("CancelRunningJobs: %v", err) - } - return nil -} diff --git a/models/asymkey/main_test.go b/models/asymkey/main_test.go index be71f848d9..87b5c22c4a 100644 --- a/models/asymkey/main_test.go +++ b/models/asymkey/main_test.go @@ -14,6 +14,7 @@ func TestMain(m *testing.M) { FixtureFiles: []string{ "gpg_key.yml", "public_key.yml", + "TestParseCommitWithSSHSignature/public_key.yml", "deploy_key.yml", "gpg_key_import.yml", "user.yml", diff --git a/models/asymkey/ssh_key_authorized_keys.go b/models/asymkey/ssh_key_authorized_keys.go index 267ab252c8..2b15450c98 100644 --- a/models/asymkey/ssh_key_authorized_keys.go +++ b/models/asymkey/ssh_key_authorized_keys.go @@ -169,7 +169,12 @@ func RewriteAllPublicKeys(ctx context.Context) error { return err } - t.Close() + if err := t.Sync(); err != nil { + return err + } + if err := t.Close(); err != nil { + return err + } return util.Rename(tmpPath, fPath) } diff --git a/models/asymkey/ssh_key_authorized_principals.go b/models/asymkey/ssh_key_authorized_principals.go index 107d70c766..f3017c3089 100644 --- a/models/asymkey/ssh_key_authorized_principals.go +++ b/models/asymkey/ssh_key_authorized_principals.go @@ -92,7 +92,12 @@ func RewriteAllPrincipalKeys(ctx context.Context) error { return err } - t.Close() + if err := t.Sync(); err != nil { + return err + } + if err := t.Close(); err != nil { + return err + } return util.Rename(tmpPath, fPath) } diff --git a/models/asymkey/ssh_key_commit_verification.go b/models/asymkey/ssh_key_commit_verification.go index 27c6df3578..2b802710a8 100644 --- a/models/asymkey/ssh_key_commit_verification.go +++ b/models/asymkey/ssh_key_commit_verification.go @@ -39,6 +39,12 @@ func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer * log.Error("GetEmailAddresses: %v", err) } + // Add the noreply email address as verified address. + committerEmailAddresses = append(committerEmailAddresses, &user_model.EmailAddress{ + IsActivated: true, + Email: committer.GetPlaceholderEmail(), + }) + activated := false for _, e := range committerEmailAddresses { if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) { diff --git a/models/asymkey/ssh_key_commit_verification_test.go b/models/asymkey/ssh_key_commit_verification_test.go new file mode 100644 index 0000000000..e1ed409bd7 --- /dev/null +++ b/models/asymkey/ssh_key_commit_verification_test.go @@ -0,0 +1,146 @@ +// Copyright 2023 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package asymkey + +import ( + "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/git" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/assert" +) + +func TestParseCommitWithSSHSignature(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + sshKey := unittest.AssertExistsAndLoadBean(t, &PublicKey{ID: 1000, OwnerID: 2}) + + t.Run("No commiter", func(t *testing.T) { + commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, &git.Commit{}, &user_model.User{}) + assert.False(t, commitVerification.Verified) + assert.Equal(t, NoKeyFound, commitVerification.Reason) + }) + + t.Run("Commiter without keys", func(t *testing.T) { + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + + commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, &git.Commit{Committer: &git.Signature{Email: user.Email}}, user) + assert.False(t, commitVerification.Verified) + assert.Equal(t, NoKeyFound, commitVerification.Reason) + }) + + t.Run("Correct signature with wrong email", func(t *testing.T) { + gitCommit := &git.Commit{ + Committer: &git.Signature{ + Email: "non-existent", + }, + Signature: &git.CommitGPGSignature{ + Payload: `tree 2d491b2985a7ff848d5c02748e7ea9f9f7619f9f +parent 45b03601635a1f463b81963a4022c7f87ce96ef9 +author user2 1699710556 +0100 +committer user2 1699710556 +0100 + +Using email that isn't known to Forgejo +`, + Signature: `-----BEGIN SSH SIGNATURE----- +U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgoGSe9Zy7Ez9bSJcaTNjh/Y7p95 +f5DujjqkpzFRtw6CEAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 +AAAAQIMufOuSjZeDUujrkVK4sl7ICa0WwEftas8UAYxx0Thdkiw2qWjR1U1PKfTLm16/w8 +/bS1LX1lZNuzm2LR2qEgw= +-----END SSH SIGNATURE----- +`, + }, + } + commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, gitCommit, user2) + assert.False(t, commitVerification.Verified) + assert.Equal(t, NoKeyFound, commitVerification.Reason) + }) + + t.Run("Incorrect signature with correct email", func(t *testing.T) { + gitCommit := &git.Commit{ + Committer: &git.Signature{ + Email: "user2@example.com", + }, + Signature: &git.CommitGPGSignature{ + Payload: `tree 853694aae8816094a0d875fee7ea26278dbf5d0f +parent c2780d5c313da2a947eae22efd7dacf4213f4e7f +author user2 1699707877 +0100 +committer user2 1699707877 +0100 + +Add content +`, + Signature: `-----BEGIN SSH SIGNATURE-----`, + }, + } + + commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, gitCommit, user2) + assert.False(t, commitVerification.Verified) + assert.Equal(t, NoKeyFound, commitVerification.Reason) + }) + + t.Run("Valid signature with correct email", func(t *testing.T) { + gitCommit := &git.Commit{ + Committer: &git.Signature{ + Email: "user2@example.com", + }, + Signature: &git.CommitGPGSignature{ + Payload: `tree 853694aae8816094a0d875fee7ea26278dbf5d0f +parent c2780d5c313da2a947eae22efd7dacf4213f4e7f +author user2 1699707877 +0100 +committer user2 1699707877 +0100 + +Add content +`, + Signature: `-----BEGIN SSH SIGNATURE----- +U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgoGSe9Zy7Ez9bSJcaTNjh/Y7p95 +f5DujjqkpzFRtw6CEAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 +AAAAQBe2Fwk/FKY3SBCnG6jSYcO6ucyahp2SpQ/0P+otslzIHpWNW8cQ0fGLdhhaFynJXQ +fs9cMpZVM9BfIKNUSO8QY= +-----END SSH SIGNATURE----- +`, + }, + } + + commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, gitCommit, user2) + assert.True(t, commitVerification.Verified) + assert.Equal(t, "user2 / SHA256:TKfwbZMR7e9OnlV2l1prfah1TXH8CmqR0PvFEXVCXA4", commitVerification.Reason) + assert.Equal(t, sshKey, commitVerification.SigningSSHKey) + }) + + t.Run("Valid signature with noreply email", func(t *testing.T) { + defer test.MockVariableValue(&setting.Service.NoReplyAddress, "noreply.example.com")() + + gitCommit := &git.Commit{ + Committer: &git.Signature{ + Email: "user2@noreply.example.com", + }, + Signature: &git.CommitGPGSignature{ + Payload: `tree 4836c7f639f37388bab4050ef5c97bbbd54272fc +parent 795be1b0117ea5c65456050bb9fd84744d4fd9c6 +author user2 1699709594 +0100 +committer user2 1699709594 +0100 + +Commit with noreply +`, + Signature: `-----BEGIN SSH SIGNATURE----- +U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgoGSe9Zy7Ez9bSJcaTNjh/Y7p95 +f5DujjqkpzFRtw6CEAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 +AAAAQJz83KKxD6Bz/ZvNpqkA3RPOSQ4LQ5FfEItbtoONkbwV9wAWMnmBqgggo/lnXCJ3oq +muPLbvEduU+Ze/1Ol1pgk= +-----END SSH SIGNATURE----- +`, + }, + } + + commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, gitCommit, user2) + assert.True(t, commitVerification.Verified) + assert.Equal(t, "user2 / SHA256:TKfwbZMR7e9OnlV2l1prfah1TXH8CmqR0PvFEXVCXA4", commitVerification.Reason) + assert.Equal(t, sshKey, commitVerification.SigningSSHKey) + }) +} diff --git a/models/auth/access_token_scope.go b/models/auth/access_token_scope.go index fe57276700..003ca5c9ab 100644 --- a/models/auth/access_token_scope.go +++ b/models/auth/access_token_scope.go @@ -250,7 +250,7 @@ func (s AccessTokenScope) parse() (accessTokenScopeBitmap, error) { remainingScopes = remainingScopes[i+1:] } singleScope := AccessTokenScope(v) - if singleScope == "" { + if singleScope == "" || singleScope == "sudo" { continue } if singleScope == AccessTokenScopeAll { diff --git a/models/auth/access_token_scope_test.go b/models/auth/access_token_scope_test.go index a6097e45d7..d11c5e6a3d 100644 --- a/models/auth/access_token_scope_test.go +++ b/models/auth/access_token_scope_test.go @@ -20,7 +20,7 @@ func TestAccessTokenScope_Normalize(t *testing.T) { tests := []scopeTestNormalize{ {"", "", nil}, {"write:misc,write:notification,read:package,write:notification,public-only", "public-only,write:misc,write:notification,read:package", nil}, - {"all", "all", nil}, + {"all,sudo", "all", nil}, {"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user", "all", nil}, {"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user,public-only", "public-only,all", nil}, } diff --git a/models/auth/session_test.go b/models/auth/session_test.go new file mode 100644 index 0000000000..3475fdd2cd --- /dev/null +++ b/models/auth/session_test.go @@ -0,0 +1,142 @@ +// Copyright 2023 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package auth_test + +import ( + "testing" + "time" + + "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/timeutil" + + "github.com/stretchr/testify/assert" +) + +func TestAuthSession(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + defer timeutil.MockUnset() + + key := "I-Like-Free-Software" + + t.Run("Create Session", func(t *testing.T) { + // Ensure it doesn't exist. + ok, err := auth.ExistSession(db.DefaultContext, key) + assert.NoError(t, err) + assert.False(t, ok) + + preCount, err := auth.CountSessions(db.DefaultContext) + assert.NoError(t, err) + + now := time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC) + timeutil.MockSet(now) + + // New session is created. + sess, err := auth.ReadSession(db.DefaultContext, key) + assert.NoError(t, err) + assert.EqualValues(t, key, sess.Key) + assert.Empty(t, sess.Data) + assert.EqualValues(t, now.Unix(), sess.Expiry) + + // Ensure it exists. + ok, err = auth.ExistSession(db.DefaultContext, key) + assert.NoError(t, err) + assert.True(t, ok) + + // Ensure the session is taken into account for count.. + postCount, err := auth.CountSessions(db.DefaultContext) + assert.NoError(t, err) + assert.Greater(t, postCount, preCount) + }) + + t.Run("Update session", func(t *testing.T) { + data := []byte{0xba, 0xdd, 0xc0, 0xde} + now := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC) + timeutil.MockSet(now) + + // Update session. + err := auth.UpdateSession(db.DefaultContext, key, data) + assert.NoError(t, err) + + timeutil.MockSet(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)) + + // Read updated session. + // Ensure data is updated and expiry is set from the update session call. + sess, err := auth.ReadSession(db.DefaultContext, key) + assert.NoError(t, err) + assert.EqualValues(t, key, sess.Key) + assert.EqualValues(t, data, sess.Data) + assert.EqualValues(t, now.Unix(), sess.Expiry) + + timeutil.MockSet(now) + }) + + t.Run("Delete session", func(t *testing.T) { + // Ensure it't exist. + ok, err := auth.ExistSession(db.DefaultContext, key) + assert.NoError(t, err) + assert.True(t, ok) + + preCount, err := auth.CountSessions(db.DefaultContext) + assert.NoError(t, err) + + err = auth.DestroySession(db.DefaultContext, key) + assert.NoError(t, err) + + // Ensure it doens't exists. + ok, err = auth.ExistSession(db.DefaultContext, key) + assert.NoError(t, err) + assert.False(t, ok) + + // Ensure the session is taken into account for count.. + postCount, err := auth.CountSessions(db.DefaultContext) + assert.NoError(t, err) + assert.Less(t, postCount, preCount) + }) + + t.Run("Cleanup sessions", func(t *testing.T) { + timeutil.MockSet(time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)) + + _, err := auth.ReadSession(db.DefaultContext, "sess-1") + assert.NoError(t, err) + + // One minute later. + timeutil.MockSet(time.Date(2023, 1, 1, 0, 1, 0, 0, time.UTC)) + _, err = auth.ReadSession(db.DefaultContext, "sess-2") + assert.NoError(t, err) + + // 5 minutes, shouldn't clean up anything. + err = auth.CleanupSessions(db.DefaultContext, 5*60) + assert.NoError(t, err) + + ok, err := auth.ExistSession(db.DefaultContext, "sess-1") + assert.NoError(t, err) + assert.True(t, ok) + + ok, err = auth.ExistSession(db.DefaultContext, "sess-2") + assert.NoError(t, err) + assert.True(t, ok) + + // 1 minute, should clean up sess-1. + err = auth.CleanupSessions(db.DefaultContext, 60) + assert.NoError(t, err) + + ok, err = auth.ExistSession(db.DefaultContext, "sess-1") + assert.NoError(t, err) + assert.False(t, ok) + + ok, err = auth.ExistSession(db.DefaultContext, "sess-2") + assert.NoError(t, err) + assert.True(t, ok) + + // Now, should clean up sess-2. + err = auth.CleanupSessions(db.DefaultContext, 0) + assert.NoError(t, err) + + ok, err = auth.ExistSession(db.DefaultContext, "sess-2") + assert.NoError(t, err) + assert.False(t, ok) + }) +} diff --git a/models/auth/twofactor.go b/models/auth/twofactor.go index 51061e5205..d0c341a192 100644 --- a/models/auth/twofactor.go +++ b/models/auth/twofactor.go @@ -6,6 +6,7 @@ package auth import ( "context" "crypto/md5" + "crypto/sha256" "crypto/subtle" "encoding/base32" "encoding/base64" @@ -18,7 +19,6 @@ import ( "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" - "github.com/minio/sha256-simd" "github.com/pquerna/otp/totp" "golang.org/x/crypto/pbkdf2" ) diff --git a/models/db/engine.go b/models/db/engine.go index b2fbdcfbf0..41207674e4 100755 --- a/models/db/engine.go +++ b/models/db/engine.go @@ -11,10 +11,13 @@ import ( "io" "reflect" "strings" + "time" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "xorm.io/xorm" + "xorm.io/xorm/contexts" "xorm.io/xorm/names" "xorm.io/xorm/schemas" @@ -144,6 +147,16 @@ func InitEngine(ctx context.Context) error { xormEngine.SetConnMaxLifetime(setting.Database.ConnMaxLifetime) xormEngine.SetDefaultContext(ctx) + if setting.Database.SlowQueryThreshold > 0 { + xormEngine.AddHook(&SlowQueryHook{ + Treshold: setting.Database.SlowQueryThreshold, + Logger: log.GetLogger("xorm"), + }) + } + xormEngine.AddHook(&ErrorQueryHook{ + Logger: log.GetLogger("xorm"), + }) + SetDefaultEngine(ctx, xormEngine) return nil } @@ -299,3 +312,38 @@ func SetLogSQL(ctx context.Context, on bool) { sess.Engine().ShowSQL(on) } } + +type SlowQueryHook struct { + Treshold time.Duration + Logger log.Logger +} + +var _ contexts.Hook = &SlowQueryHook{} + +func (SlowQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) { + return c.Ctx, nil +} + +func (h *SlowQueryHook) AfterProcess(c *contexts.ContextHook) error { + if c.ExecuteTime >= h.Treshold { + h.Logger.Log(8, log.WARN, "[Slow SQL Query] %s %v - %v", c.SQL, c.Args, c.ExecuteTime) + } + return nil +} + +type ErrorQueryHook struct { + Logger log.Logger +} + +var _ contexts.Hook = &ErrorQueryHook{} + +func (ErrorQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) { + return c.Ctx, nil +} + +func (h *ErrorQueryHook) AfterProcess(c *contexts.ContextHook) error { + if c.Err != nil { + h.Logger.Log(8, log.ERROR, "[Error SQL Query] %s %v - %v", c.SQL, c.Args, c.Err) + } + return nil +} diff --git a/models/db/engine_test.go b/models/db/engine_test.go index c9ae5f1542..f050c5ca28 100644 --- a/models/db/engine_test.go +++ b/models/db/engine_test.go @@ -6,15 +6,19 @@ package db_test import ( "path/filepath" "testing" + "time" "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" _ "code.gitea.io/gitea/cmd" // for TestPrimaryKeys "github.com/stretchr/testify/assert" + "xorm.io/xorm" ) func TestDumpDatabase(t *testing.T) { @@ -85,3 +89,65 @@ func TestPrimaryKeys(t *testing.T) { } } } + +func TestSlowQuery(t *testing.T) { + lc, cleanup := test.NewLogChecker("slow-query", log.INFO) + lc.StopMark("[Slow SQL Query]") + defer cleanup() + + e := db.GetEngine(db.DefaultContext) + engine, ok := e.(*xorm.Engine) + assert.True(t, ok) + + // It's not possible to clean this up with XORM, but it's luckily not harmful + // to leave around. + engine.AddHook(&db.SlowQueryHook{ + Treshold: time.Second * 10, + Logger: log.GetLogger("slow-query"), + }) + + // NOOP query. + e.Exec("SELECT 1 WHERE false;") + + _, stopped := lc.Check(100 * time.Millisecond) + assert.False(t, stopped) + + engine.AddHook(&db.SlowQueryHook{ + Treshold: 0, // Every query should be logged. + Logger: log.GetLogger("slow-query"), + }) + + // NOOP query. + e.Exec("SELECT 1 WHERE false;") + + _, stopped = lc.Check(100 * time.Millisecond) + assert.True(t, stopped) +} + +func TestErrorQuery(t *testing.T) { + lc, cleanup := test.NewLogChecker("error-query", log.INFO) + lc.StopMark("[Error SQL Query]") + defer cleanup() + + e := db.GetEngine(db.DefaultContext) + engine, ok := e.(*xorm.Engine) + assert.True(t, ok) + + // It's not possible to clean this up with XORM, but it's luckily not harmful + // to leave around. + engine.AddHook(&db.ErrorQueryHook{ + Logger: log.GetLogger("error-query"), + }) + + // Valid query. + e.Exec("SELECT 1 WHERE false;") + + _, stopped := lc.Check(100 * time.Millisecond) + assert.False(t, stopped) + + // Table doesn't exist. + e.Exec("SELECT column FROM table;") + + _, stopped = lc.Check(100 * time.Millisecond) + assert.True(t, stopped) +} diff --git a/models/fixtures/TestParseCommitWithSSHSignature/public_key.yml b/models/fixtures/TestParseCommitWithSSHSignature/public_key.yml new file mode 100644 index 0000000000..f76dabb1c1 --- /dev/null +++ b/models/fixtures/TestParseCommitWithSSHSignature/public_key.yml @@ -0,0 +1,13 @@ +- + id: 1000 + owner_id: 2 + name: user2@localhost + fingerprint: "SHA256:TKfwbZMR7e9OnlV2l1prfah1TXH8CmqR0PvFEXVCXA4" + content: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKBknvWcuxM/W0iXGkzY4f2O6feX+Q7o46pKcxUbcOgh user2@localhost" + # private key (base64-ed) LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0KYjNCbGJuTnphQzFyWlhrdGRqRUFBQUFBQkc1dmJtVUFBQUFFYm05dVpRQUFBQUFBQUFBQkFBQUFNd0FBQUF0emMyZ3RaVwpReU5UVXhPUUFBQUNDZ1pKNzFuTHNUUDF0SWx4cE0yT0g5anVuM2wva082T09xU25NVkczRG9JUUFBQUpocG43YTZhWisyCnVnQUFBQXR6YzJndFpXUXlOVFV4T1FBQUFDQ2daSjcxbkxzVFAxdElseHBNMk9IOWp1bjNsL2tPNk9PcVNuTVZHM0RvSVEKQUFBRUFxVm12bmo1LzZ5TW12ck9Ub29xa3F5MmUrc21aK0tBcEtKR0crRnY5MlA2QmtudldjdXhNL1cwaVhHa3pZNGYyTwo2ZmVYK1E3bzQ2cEtjeFViY09naEFBQUFFMmQxYzNSbFpFQm5kWE4wWldRdFltVmhjM1FCQWc9PQotLS0tLUVORCBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0= + mode: 2 + type: 1 + verified: true + created_unix: 1559593109 + updated_unix: 1565224552 + login_source_id: 0 diff --git a/models/fixtures/release.yml b/models/fixtures/release.yml index 372a79509f..01635064c5 100644 --- a/models/fixtures/release.yml +++ b/models/fixtures/release.yml @@ -150,3 +150,17 @@ is_prerelease: false is_tag: false created_unix: 946684803 + +- id: 12 + repo_id: 1059 + publisher_id: 2 + tag_name: "v1.0" + lower_tag_name: "v1.0" + target: "main" + title: "v1.0" + sha1: "d8f53dfb33f6ccf4169c34970b5e747511c18beb" + num_commits: 1 + is_draft: false + is_prerelease: false + is_tag: false + created_unix: 946684803 diff --git a/models/fixtures/repo_unit.yml b/models/fixtures/repo_unit.yml index e6c59f527a..e3590b06f0 100644 --- a/models/fixtures/repo_unit.yml +++ b/models/fixtures/repo_unit.yml @@ -608,6 +608,38 @@ type: 1 created_unix: 946684810 +# BEGIN Forgejo [GITEA] Improve HTML title on repositories +- + id: 1093 + repo_id: 1059 + type: 1 + created_unix: 946684810 + +- + id: 1094 + repo_id: 1059 + type: 2 + created_unix: 946684810 + +- + id: 1095 + repo_id: 1059 + type: 3 + created_unix: 946684810 + +- + id: 1096 + repo_id: 1059 + type: 4 + created_unix: 946684810 + +- + id: 1097 + repo_id: 1059 + type: 5 + created_unix: 946684810 +# END Forgejo [GITEA] Improve HTML title on repositories + - id: 91 repo_id: 58 diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml index 66eff2eee8..8ef58b64ac 100644 --- a/models/fixtures/repository.yml +++ b/models/fixtures/repository.yml @@ -1467,6 +1467,7 @@ owner_name: user27 lower_name: repo49 name: repo49 + description: A wonderful repository with more than just a README.md default_branch: master num_watches: 0 num_stars: 0 @@ -1694,6 +1695,19 @@ is_fsck_enabled: true close_issues_via_commit_in_any_branch: false +- + id: 1059 + owner_id: 2 + owner_name: user2 + lower_name: repo59 + name: repo59 + default_branch: master + is_empty: false + is_archived: false + is_private: false + status: 0 + num_issues: 0 + - id: 59 owner_id: 2 diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml index aa0daedd85..16aa2a3ff1 100644 --- a/models/fixtures/user.yml +++ b/models/fixtures/user.yml @@ -66,7 +66,7 @@ num_followers: 2 num_following: 1 num_stars: 2 - num_repos: 15 + num_repos: 16 num_teams: 0 num_members: 0 visibility: 0 diff --git a/models/forgejo_migrations/migrate.go b/models/forgejo_migrations/migrate.go index 58f158bd17..da417cc08c 100644 --- a/models/forgejo_migrations/migrate.go +++ b/models/forgejo_migrations/migrate.go @@ -10,6 +10,7 @@ import ( "code.gitea.io/gitea/models/forgejo/semver" forgejo_v1_20 "code.gitea.io/gitea/models/forgejo_migrations/v1_20" + forgejo_v1_22 "code.gitea.io/gitea/models/forgejo_migrations/v1_22" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -43,6 +44,12 @@ var migrations = []*Migration{ NewMigration("create the forgejo_sem_ver table", forgejo_v1_20.CreateSemVerTable), // v2 -> v3 NewMigration("create the forgejo_auth_token table", forgejo_v1_20.CreateAuthorizationTokenTable), + // v3 -> v4 + NewMigration("Add default_permissions to repo_unit", forgejo_v1_22.AddDefaultPermissionsToRepoUnit), + // v4 -> v5 + NewMigration("create the forgejo_repo_flag table", forgejo_v1_22.CreateRepoFlagTable), + // v5 -> v6 + NewMigration("Add wiki_branch to repository", forgejo_v1_22.AddWikiBranchToRepository), } // GetCurrentDBVersion returns the current Forgejo database version. diff --git a/models/forgejo_migrations/v1_22/v4.go b/models/forgejo_migrations/v1_22/v4.go new file mode 100644 index 0000000000..f1195f5f66 --- /dev/null +++ b/models/forgejo_migrations/v1_22/v4.go @@ -0,0 +1,17 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_22 //nolint + +import ( + "xorm.io/xorm" +) + +func AddDefaultPermissionsToRepoUnit(x *xorm.Engine) error { + type RepoUnit struct { + ID int64 + DefaultPermissions int `xorm:"NOT NULL DEFAULT 0"` + } + + return x.Sync(&RepoUnit{}) +} diff --git a/models/forgejo_migrations/v1_22/v5.go b/models/forgejo_migrations/v1_22/v5.go new file mode 100644 index 0000000000..55f9fe1338 --- /dev/null +++ b/models/forgejo_migrations/v1_22/v5.go @@ -0,0 +1,22 @@ +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_22 //nolint + +import ( + "xorm.io/xorm" +) + +type RepoFlag struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"UNIQUE(s) INDEX"` + Name string `xorm:"UNIQUE(s) INDEX"` +} + +func (RepoFlag) TableName() string { + return "forgejo_repo_flag" +} + +func CreateRepoFlagTable(x *xorm.Engine) error { + return x.Sync(new(RepoFlag)) +} diff --git a/models/forgejo_migrations/v1_22/v6.go b/models/forgejo_migrations/v1_22/v6.go new file mode 100644 index 0000000000..70feef033d --- /dev/null +++ b/models/forgejo_migrations/v1_22/v6.go @@ -0,0 +1,24 @@ +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_22 //nolint + +import ( + "xorm.io/xorm" +) + +func AddWikiBranchToRepository(x *xorm.Engine) error { + type Repository struct { + ID int64 + WikiBranch string + } + + if err := x.Sync(&Repository{}); err != nil { + return err + } + + // Update existing repositories to use `master` as the wiki branch, for + // compatilibty's sake. + _, err := x.Exec("UPDATE repository SET wiki_branch = 'master' WHERE wiki_branch = '' OR wiki_branch IS NULL") + return err +} diff --git a/models/git/branch.go b/models/git/branch.go index db02fc9b28..6baad65ab4 100644 --- a/models/git/branch.go +++ b/models/git/branch.go @@ -128,6 +128,10 @@ func (b *Branch) LoadDeletedBy(ctx context.Context) (err error) { return err } +func (b *Branch) GetRepo(ctx context.Context) (*repo_model.Repository, error) { + return repo_model.GetRepositoryByID(ctx, b.RepoID) +} + func (b *Branch) LoadPusher(ctx context.Context) (err error) { if b.Pusher == nil && b.PusherID > 0 { b.Pusher, err = user_model.GetUserByID(ctx, b.PusherID) @@ -283,7 +287,7 @@ func FindRenamedBranch(ctx context.Context, repoID int64, from string) (branch * } // RenameBranch rename a branch -func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to string, gitAction func(ctx context.Context, isDefault bool) error) (err error) { +func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to string, gitAction func(isDefault bool) error) (err error) { ctx, committer, err := db.TxContext(ctx) if err != nil { return err @@ -358,7 +362,7 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str } // 5. do git action - if err = gitAction(ctx, isDefault); err != nil { + if err = gitAction(isDefault); err != nil { return err } diff --git a/models/git/branch_test.go b/models/git/branch_test.go index fd5d6519e9..d480e2ec30 100644 --- a/models/git/branch_test.go +++ b/models/git/branch_test.go @@ -4,7 +4,6 @@ package git_test import ( - "context" "testing" "code.gitea.io/gitea/models/db" @@ -133,7 +132,7 @@ func TestRenameBranch(t *testing.T) { }, git_model.WhitelistOptions{})) assert.NoError(t, committer.Commit()) - assert.NoError(t, git_model.RenameBranch(db.DefaultContext, repo1, "master", "main", func(ctx context.Context, isDefault bool) error { + assert.NoError(t, git_model.RenameBranch(db.DefaultContext, repo1, "master", "main", func(isDefault bool) error { _isDefault = isDefault return nil })) diff --git a/models/issues/comment_test.go b/models/issues/comment_test.go index c5bbfdedc2..e08bd7fbf5 100644 --- a/models/issues/comment_test.go +++ b/models/issues/comment_test.go @@ -12,6 +12,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/structs" "github.com/stretchr/testify/assert" ) @@ -97,3 +98,29 @@ func TestMigrate_InsertIssueComments(t *testing.T) { unittest.CheckConsistencyFor(t, &issues_model.Issue{}) } + +func TestUpdateCommentsMigrationsByType(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}) + comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1, IssueID: issue.ID}) + + // Set repository to migrated from Gitea. + repo.OriginalServiceType = structs.GiteaService + repo_model.UpdateRepositoryCols(db.DefaultContext, repo, "original_service_type") + + // Set comment to have an original author. + comment.OriginalAuthor = "Example User" + comment.OriginalAuthorID = 1 + comment.PosterID = 0 + _, err := db.GetEngine(db.DefaultContext).ID(comment.ID).Cols("original_author", "original_author_id", "poster_id").Update(comment) + assert.NoError(t, err) + + assert.NoError(t, issues_model.UpdateCommentsMigrationsByType(db.DefaultContext, structs.GiteaService, "1", 513)) + + comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1, IssueID: issue.ID}) + assert.Empty(t, comment.OriginalAuthor) + assert.Empty(t, comment.OriginalAuthorID) + assert.EqualValues(t, 513, comment.PosterID) +} diff --git a/models/issues/issue_index.go b/models/issues/issue_index.go index 16274d0ef0..9386027f74 100644 --- a/models/issues/issue_index.go +++ b/models/issues/issue_index.go @@ -9,6 +9,14 @@ import ( "code.gitea.io/gitea/models/db" ) +func GetMaxIssueIndexForRepo(ctx context.Context, repoID int64) (int64, error) { + var max int64 + if _, err := db.GetEngine(ctx).Select("MAX(`index`)").Table("issue").Where("repo_id=?", repoID).Get(&max); err != nil { + return 0, err + } + return max, nil +} + // RecalculateIssueIndexForRepo create issue_index for repo if not exist and // update it based on highest index of existing issues assigned to a repo func RecalculateIssueIndexForRepo(ctx context.Context, repoID int64) error { @@ -18,8 +26,8 @@ func RecalculateIssueIndexForRepo(ctx context.Context, repoID int64) error { } defer committer.Close() - var max int64 - if _, err = db.GetEngine(ctx).Select(" MAX(`index`)").Table("issue").Where("repo_id=?", repoID).Get(&max); err != nil { + max, err := GetMaxIssueIndexForRepo(ctx, repoID) + if err != nil { return err } diff --git a/models/issues/issue_index_test.go b/models/issues/issue_index_test.go new file mode 100644 index 0000000000..9937aac70e --- /dev/null +++ b/models/issues/issue_index_test.go @@ -0,0 +1,38 @@ +// Copyright 2024 The Forgejo Authors +// SPDX-License-Identifier: MIT + +package issues_test + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + + "github.com/stretchr/testify/assert" +) + +func TestGetMaxIssueIndexForRepo(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + + maxPR, err := issues_model.GetMaxIssueIndexForRepo(db.DefaultContext, repo.ID) + assert.NoError(t, err) + + issue := testCreateIssue(t, repo.ID, repo.OwnerID, "title1", "content1", false) + assert.Greater(t, issue.Index, maxPR) + + maxPR, err = issues_model.GetMaxIssueIndexForRepo(db.DefaultContext, repo.ID) + assert.NoError(t, err) + + pull := testCreateIssue(t, repo.ID, repo.OwnerID, "title2", "content2", true) + assert.Greater(t, pull.Index, maxPR) + + maxPR, err = issues_model.GetMaxIssueIndexForRepo(db.DefaultContext, repo.ID) + assert.NoError(t, err) + + assert.Equal(t, maxPR, pull.Index) +} diff --git a/models/issues/pull_list.go b/models/issues/pull_list.go index c209386e2e..d539ed5403 100644 --- a/models/issues/pull_list.go +++ b/models/issues/pull_list.go @@ -50,6 +50,14 @@ func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullR return sess, nil } +func GetUnmergedPullRequestsByHeadInfoMax(ctx context.Context, repoID, maxIndex int64, branch string) ([]*PullRequest, error) { + prs := make([]*PullRequest, 0, 2) + sess := db.GetEngine(ctx). + Join("INNER", "issue", "issue.id = `pull_request`.issue_id"). + Where("`pull_request`.head_repo_id = ? AND `pull_request`.head_branch = ? AND `pull_request`.has_merged = ? AND `issue`.is_closed = ? AND `pull_request`.flow = ? AND `issue`.`index` <= ?", repoID, branch, false, false, PullRequestFlowGithub, maxIndex) + return prs, sess.Find(&prs) +} + // GetUnmergedPullRequestsByHeadInfo returns all pull requests that are open and has not been merged func GetUnmergedPullRequestsByHeadInfo(ctx context.Context, repoID int64, branch string) ([]*PullRequest, error) { prs := make([]*PullRequest, 0, 2) diff --git a/models/issues/pull_test.go b/models/issues/pull_test.go index 173417136c..4702049af2 100644 --- a/models/issues/pull_test.go +++ b/models/issues/pull_test.go @@ -4,6 +4,7 @@ package issues_test import ( + "fmt" "testing" "code.gitea.io/gitea/models/db" @@ -158,6 +159,91 @@ func TestGetUnmergedPullRequestsByHeadInfo(t *testing.T) { } } +func TestGetUnmergedPullRequestsByHeadInfoMax(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + repoID := int64(1) + maxPR := int64(0) + prs, err := issues_model.GetUnmergedPullRequestsByHeadInfoMax(db.DefaultContext, repoID, maxPR, "branch2") + assert.NoError(t, err) + assert.Len(t, prs, 0) + maxPR, err = issues_model.GetMaxIssueIndexForRepo(db.DefaultContext, repoID) + assert.NoError(t, err) + prs, err = issues_model.GetUnmergedPullRequestsByHeadInfoMax(db.DefaultContext, repoID, maxPR, "branch2") + assert.NoError(t, err) + assert.Len(t, prs, 1) + for _, pr := range prs { + assert.Equal(t, int64(1), pr.HeadRepoID) + assert.Equal(t, "branch2", pr.HeadBranch) + } + pr := prs[0] + + for _, testCase := range []struct { + table string + field string + id int64 + match any + nomatch any + }{ + { + table: "issue", + field: "is_closed", + id: pr.IssueID, + match: false, + nomatch: true, + }, + { + table: "pull_request", + field: "flow", + id: pr.ID, + match: issues_model.PullRequestFlowGithub, + nomatch: issues_model.PullRequestFlowAGit, + }, + { + table: "pull_request", + field: "head_repo_id", + id: pr.ID, + match: pr.HeadRepoID, + nomatch: 0, + }, + { + table: "pull_request", + field: "head_branch", + id: pr.ID, + match: pr.HeadBranch, + nomatch: "something else", + }, + { + table: "pull_request", + field: "has_merged", + id: pr.ID, + match: false, + nomatch: true, + }, + } { + t.Run(testCase.field, func(t *testing.T) { + update := fmt.Sprintf("UPDATE `%s` SET `%s` = ? WHERE `id` = ?", testCase.table, testCase.field) + + // expect no match + _, err = db.GetEngine(db.DefaultContext).Exec(update, testCase.nomatch, testCase.id) + assert.NoError(t, err) + prs, err = issues_model.GetUnmergedPullRequestsByHeadInfoMax(db.DefaultContext, repoID, maxPR, "branch2") + assert.NoError(t, err) + assert.Len(t, prs, 0) + + // expect one match + _, err = db.GetEngine(db.DefaultContext).Exec(update, testCase.match, testCase.id) + assert.NoError(t, err) + prs, err = issues_model.GetUnmergedPullRequestsByHeadInfoMax(db.DefaultContext, repoID, maxPR, "branch2") + assert.NoError(t, err) + assert.Len(t, prs, 1) + + // identical to the known PR + assert.Equal(t, pr.ID, prs[0].ID) + }) + } +} + func TestGetUnmergedPullRequestsByBaseInfo(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) prs, err := issues_model.GetUnmergedPullRequestsByBaseInfo(db.DefaultContext, 1, "master") diff --git a/models/migrations/base/hash.go b/models/migrations/base/hash.go index 0debec272b..00fd1efd4a 100644 --- a/models/migrations/base/hash.go +++ b/models/migrations/base/hash.go @@ -4,9 +4,9 @@ package base import ( + "crypto/sha256" "encoding/hex" - "github.com/minio/sha256-simd" "golang.org/x/crypto/pbkdf2" ) diff --git a/models/migrations/v1_14/v166.go b/models/migrations/v1_14/v166.go index 78f33e8f9b..e5731582fd 100644 --- a/models/migrations/v1_14/v166.go +++ b/models/migrations/v1_14/v166.go @@ -4,9 +4,9 @@ package v1_14 //nolint import ( + "crypto/sha256" "encoding/hex" - "github.com/minio/sha256-simd" "golang.org/x/crypto/argon2" "golang.org/x/crypto/bcrypt" "golang.org/x/crypto/pbkdf2" diff --git a/models/migrations/v1_21/v276.go b/models/migrations/v1_21/v276.go index ed1bc3bda5..67e950177d 100644 --- a/models/migrations/v1_21/v276.go +++ b/models/migrations/v1_21/v276.go @@ -4,13 +4,7 @@ package v1_21 //nolint import ( - "context" - "fmt" - "path/filepath" - "strings" - - "code.gitea.io/gitea/modules/git" - giturl "code.gitea.io/gitea/modules/git/url" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/setting" "xorm.io/xorm" @@ -73,7 +67,7 @@ func migratePullMirrors(x *xorm.Engine) error { start += len(mirrors) for _, m := range mirrors { - remoteAddress, err := getRemoteAddress(m.RepoOwner, m.RepoName, "origin") + remoteAddress, err := repo_model.GetPushMirrorRemoteAddress(m.RepoOwner, m.RepoName, "origin") if err != nil { return err } @@ -136,7 +130,7 @@ func migratePushMirrors(x *xorm.Engine) error { start += len(mirrors) for _, m := range mirrors { - remoteAddress, err := getRemoteAddress(m.RepoOwner, m.RepoName, m.RemoteName) + remoteAddress, err := repo_model.GetPushMirrorRemoteAddress(m.RepoOwner, m.RepoName, m.RemoteName) if err != nil { return err } @@ -160,20 +154,3 @@ func migratePushMirrors(x *xorm.Engine) error { return sess.Commit() } - -func getRemoteAddress(ownerName, repoName, remoteName string) (string, error) { - repoPath := filepath.Join(setting.RepoRootPath, strings.ToLower(ownerName), strings.ToLower(repoName)+".git") - - remoteURL, err := git.GetRemoteAddress(context.Background(), repoPath, remoteName) - if err != nil { - return "", fmt.Errorf("get remote %s's address of %s/%s failed: %v", remoteName, ownerName, repoName, err) - } - - u, err := giturl.Parse(remoteURL) - if err != nil { - return "", err - } - u.User = nil - - return u.String(), nil -} diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go index 395ecdf1a5..0b66e62d7d 100644 --- a/models/perm/access/repo_permission.go +++ b/models/perm/access/repo_permission.go @@ -33,6 +33,16 @@ func (p *Permission) IsAdmin() bool { return p.AccessMode >= perm_model.AccessModeAdmin } +// IsGloballyWriteable returns true if the unit is writeable by all users of the instance. +func (p *Permission) IsGloballyWriteable(unitType unit.Type) bool { + for _, u := range p.Units { + if u.Type == unitType { + return u.DefaultPermissions == repo_model.UnitAccessModeWrite + } + } + return false +} + // HasAccess returns true if the current user has at least read access to any unit of this repository func (p *Permission) HasAccess() bool { if p.UnitsMode == nil { @@ -198,7 +208,19 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use if err := repo.LoadOwner(ctx); err != nil { return perm, err } + if !repo.Owner.IsOrganization() { + // for a public repo, different repo units may have different default + // permissions for non-restricted users. + if !repo.IsPrivate && !user.IsRestricted && len(repo.Units) > 0 { + perm.UnitsMode = make(map[unit.Type]perm_model.AccessMode) + for _, u := range repo.Units { + if _, ok := perm.UnitsMode[u.Type]; !ok { + perm.UnitsMode[u.Type] = u.DefaultPermissions.ToAccessMode(perm.AccessMode) + } + } + } + return perm, nil } @@ -239,10 +261,12 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use } } - // for a public repo on an organization, a non-restricted user has read permission on non-team defined units. + // for a public repo on an organization, a non-restricted user should + // have the same permission on non-team defined units as the default + // permissions for the repo unit. if !found && !repo.IsPrivate && !user.IsRestricted { if _, ok := perm.UnitsMode[u.Type]; !ok { - perm.UnitsMode[u.Type] = perm_model.AccessModeRead + perm.UnitsMode[u.Type] = u.DefaultPermissions.ToAccessMode(perm_model.AccessModeRead) } } } diff --git a/models/pull/automerge.go b/models/pull/automerge.go index f69fcb60d1..f31159a8d8 100644 --- a/models/pull/automerge.go +++ b/models/pull/automerge.go @@ -74,7 +74,7 @@ func GetScheduledMergeByPullID(ctx context.Context, pullID int64) (bool, *AutoMe return false, nil, err } - doer, err := user_model.GetUserByID(ctx, scheduledPRM.DoerID) + doer, err := user_model.GetPossibleUserByID(ctx, scheduledPRM.DoerID) if err != nil { return false, nil, err } diff --git a/models/repo/pushmirror.go b/models/repo/pushmirror.go index bf134abfb1..e08333511c 100644 --- a/models/repo/pushmirror.go +++ b/models/repo/pushmirror.go @@ -5,10 +5,16 @@ package repo import ( "context" + "fmt" + "path/filepath" + "strings" "time" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/git" + giturl "code.gitea.io/gitea/modules/git/url" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -132,3 +138,21 @@ func PushMirrorsIterate(ctx context.Context, limit int, f func(idx int, bean any } return sess.Iterate(new(PushMirror), f) } + +// GetPushMirrorRemoteAddress returns the address of associated with a repository's given remote. +func GetPushMirrorRemoteAddress(ownerName, repoName, remoteName string) (string, error) { + repoPath := filepath.Join(setting.RepoRootPath, strings.ToLower(ownerName), strings.ToLower(repoName)+".git") + + remoteURL, err := git.GetRemoteAddress(context.Background(), repoPath, remoteName) + if err != nil { + return "", fmt.Errorf("get remote %s's address of %s/%s failed: %v", remoteName, ownerName, repoName, err) + } + + u, err := giturl.Parse(remoteURL) + if err != nil { + return "", err + } + u.User = nil + + return u.String(), nil +} diff --git a/models/repo/repo.go b/models/repo/repo.go index 13493ba6e8..a7bc4b3c72 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -135,6 +135,7 @@ type Repository struct { OriginalServiceType api.GitServiceType `xorm:"index"` OriginalURL string `xorm:"VARCHAR(2048)"` DefaultBranch string + WikiBranch string NumWatches int NumStars int @@ -204,6 +205,13 @@ func (repo *Repository) GetOwnerName() string { return repo.OwnerName } +func (repo *Repository) GetWikiBranchName() string { + if repo.WikiBranch == "" { + return setting.Repository.DefaultBranch + } + return repo.WikiBranch +} + // SanitizedOriginalURL returns a sanitized OriginalURL func (repo *Repository) SanitizedOriginalURL() string { if repo.OriginalURL == "" { diff --git a/models/repo/repo_flags.go b/models/repo/repo_flags.go new file mode 100644 index 0000000000..de76ed2b37 --- /dev/null +++ b/models/repo/repo_flags.go @@ -0,0 +1,102 @@ +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "context" + + "code.gitea.io/gitea/models/db" + + "xorm.io/builder" +) + +// RepoFlag represents a single flag against a repository +type RepoFlag struct { //revive:disable-line:exported + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"UNIQUE(s) INDEX"` + Name string `xorm:"UNIQUE(s) INDEX"` +} + +func init() { + db.RegisterModel(new(RepoFlag)) +} + +// TableName provides the real table name +func (RepoFlag) TableName() string { + return "forgejo_repo_flag" +} + +// ListFlags returns the array of flags on the repo. +func (repo *Repository) ListFlags(ctx context.Context) ([]RepoFlag, error) { + var flags []RepoFlag + err := db.GetEngine(ctx).Table(&RepoFlag{}).Where("repo_id = ?", repo.ID).Find(&flags) + if err != nil { + return nil, err + } + return flags, nil +} + +// IsFlagged returns whether a repo has any flags or not +func (repo *Repository) IsFlagged(ctx context.Context) bool { + has, _ := db.Exist[RepoFlag](ctx, builder.Eq{"repo_id": repo.ID}) + return has +} + +// GetFlag returns a single RepoFlag based on its name +func (repo *Repository) GetFlag(ctx context.Context, flagName string) (bool, *RepoFlag, error) { + flag, has, err := db.Get[RepoFlag](ctx, builder.Eq{"repo_id": repo.ID, "name": flagName}) + if err != nil { + return false, nil, err + } + return has, flag, nil +} + +// HasFlag returns true if a repo has a given flag, false otherwise +func (repo *Repository) HasFlag(ctx context.Context, flagName string) bool { + has, _ := db.Exist[RepoFlag](ctx, builder.Eq{"repo_id": repo.ID, "name": flagName}) + return has +} + +// AddFlag adds a new flag to the repo +func (repo *Repository) AddFlag(ctx context.Context, flagName string) error { + return db.Insert(ctx, RepoFlag{ + RepoID: repo.ID, + Name: flagName, + }) +} + +// DeleteFlag removes a flag from the repo +func (repo *Repository) DeleteFlag(ctx context.Context, flagName string) (int64, error) { + return db.DeleteByBean(ctx, &RepoFlag{RepoID: repo.ID, Name: flagName}) +} + +// ReplaceAllFlags replaces all flags of a repo with a new set +func (repo *Repository) ReplaceAllFlags(ctx context.Context, flagNames []string) error { + ctx, committer, err := db.TxContext(ctx) + if err != nil { + return err + } + defer committer.Close() + + if err := db.DeleteBeans(ctx, &RepoFlag{RepoID: repo.ID}); err != nil { + return err + } + + if len(flagNames) == 0 { + return committer.Commit() + } + + var flags []RepoFlag + for _, name := range flagNames { + flags = append(flags, RepoFlag{ + RepoID: repo.ID, + Name: name, + }) + } + if err := db.Insert(ctx, &flags); err != nil { + return err + } + + return committer.Commit() +} diff --git a/models/repo/repo_flags_test.go b/models/repo/repo_flags_test.go new file mode 100644 index 0000000000..0e4f5c1ba9 --- /dev/null +++ b/models/repo/repo_flags_test.go @@ -0,0 +1,114 @@ +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo_test + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + + "github.com/stretchr/testify/assert" +) + +func TestRepositoryFlags(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10}) + + // ******************** + // ** NEGATIVE TESTS ** + // ******************** + + // Unless we add flags, the repo has none + flags, err := repo.ListFlags(db.DefaultContext) + assert.NoError(t, err) + assert.Empty(t, flags) + + // If the repo has no flags, it is not flagged + flagged := repo.IsFlagged(db.DefaultContext) + assert.False(t, flagged) + + // Trying to find a flag when there is none + has := repo.HasFlag(db.DefaultContext, "foo") + assert.False(t, has) + + // Trying to retrieve a non-existent flag indicates not found + has, _, err = repo.GetFlag(db.DefaultContext, "foo") + assert.NoError(t, err) + assert.False(t, has) + + // Deleting a non-existent flag fails + deleted, err := repo.DeleteFlag(db.DefaultContext, "no-such-flag") + assert.NoError(t, err) + assert.Equal(t, int64(0), deleted) + + // ******************** + // ** POSITIVE TESTS ** + // ******************** + + // Adding a flag works + err = repo.AddFlag(db.DefaultContext, "foo") + assert.NoError(t, err) + + // Adding it again fails + err = repo.AddFlag(db.DefaultContext, "foo") + assert.Error(t, err) + + // Listing flags includes the one we added + flags, err = repo.ListFlags(db.DefaultContext) + assert.NoError(t, err) + assert.Len(t, flags, 1) + assert.Equal(t, "foo", flags[0].Name) + + // With a flag added, the repo is flagged + flagged = repo.IsFlagged(db.DefaultContext) + assert.True(t, flagged) + + // The flag can be found + has = repo.HasFlag(db.DefaultContext, "foo") + assert.True(t, has) + + // Added flag can be retrieved + _, flag, err := repo.GetFlag(db.DefaultContext, "foo") + assert.NoError(t, err) + assert.Equal(t, "foo", flag.Name) + + // Deleting a flag works + deleted, err = repo.DeleteFlag(db.DefaultContext, "foo") + assert.NoError(t, err) + assert.Equal(t, int64(1), deleted) + + // The list is now empty + flags, err = repo.ListFlags(db.DefaultContext) + assert.NoError(t, err) + assert.Empty(t, flags) + + // Replacing an empty list works + err = repo.ReplaceAllFlags(db.DefaultContext, []string{"bar"}) + assert.NoError(t, err) + + // The repo is now flagged with "bar" + has = repo.HasFlag(db.DefaultContext, "bar") + assert.True(t, has) + + // Replacing a tag set with another works + err = repo.ReplaceAllFlags(db.DefaultContext, []string{"baz", "quux"}) + assert.NoError(t, err) + + // The repo now has two tags + flags, err = repo.ListFlags(db.DefaultContext) + assert.NoError(t, err) + assert.Len(t, flags, 2) + assert.Equal(t, "baz", flags[0].Name) + assert.Equal(t, "quux", flags[1].Name) + + // Replacing flags with an empty set deletes all flags + err = repo.ReplaceAllFlags(db.DefaultContext, []string{}) + assert.NoError(t, err) + + // The repo is now unflagged + flagged = repo.IsFlagged(db.DefaultContext) + assert.False(t, flagged) +} diff --git a/models/repo/repo_list_test.go b/models/repo/repo_list_test.go index 8a1799aac0..a8b958109c 100644 --- a/models/repo/repo_list_test.go +++ b/models/repo/repo_list_test.go @@ -138,12 +138,12 @@ func getTestCases() []struct { { name: "AllPublic/PublicRepositoriesOfUserIncludingCollaborative", opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, AllPublic: true, Template: util.OptionalBoolFalse}, - count: 31, + count: 32, }, { name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborative", opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true, AllLimited: true, Template: util.OptionalBoolFalse}, - count: 36, + count: 37, }, { name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborativeByName", @@ -158,7 +158,7 @@ func getTestCases() []struct { { name: "AllPublic/PublicRepositoriesOfOrganization", opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, AllPublic: true, Collaborate: util.OptionalBoolFalse, Template: util.OptionalBoolFalse}, - count: 31, + count: 32, }, { name: "AllTemplates", diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go index 8a3ba1ee89..3df5236ea7 100644 --- a/models/repo/repo_unit.go +++ b/models/repo/repo_unit.go @@ -10,6 +10,7 @@ import ( "strings" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/setting" @@ -39,13 +40,43 @@ func (err ErrUnitTypeNotExist) Unwrap() error { return util.ErrNotExist } +// RepoUnitAccessMode specifies the users access mode to a repo unit +type UnitAccessMode int + +const ( + // UnitAccessModeUnset - no unit mode set + UnitAccessModeUnset UnitAccessMode = iota // 0 + // UnitAccessModeNone no access + UnitAccessModeNone // 1 + // UnitAccessModeRead read access + UnitAccessModeRead // 2 + // UnitAccessModeWrite write access + UnitAccessModeWrite // 3 +) + +func (mode UnitAccessMode) ToAccessMode(modeIfUnset perm.AccessMode) perm.AccessMode { + switch mode { + case UnitAccessModeUnset: + return modeIfUnset + case UnitAccessModeNone: + return perm.AccessModeNone + case UnitAccessModeRead: + return perm.AccessModeRead + case UnitAccessModeWrite: + return perm.AccessModeWrite + default: + return perm.AccessModeNone + } +} + // RepoUnit describes all units of a repository type RepoUnit struct { //revive:disable-line:exported - ID int64 - RepoID int64 `xorm:"INDEX(s)"` - Type unit.Type `xorm:"INDEX(s)"` - Config convert.Conversion `xorm:"TEXT"` - CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"` + ID int64 + RepoID int64 `xorm:"INDEX(s)"` + Type unit.Type `xorm:"INDEX(s)"` + Config convert.Conversion `xorm:"TEXT"` + CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"` + DefaultPermissions UnitAccessMode `xorm:"NOT NULL DEFAULT 0"` } func init() { @@ -283,3 +314,29 @@ func UpdateRepoUnit(ctx context.Context, unit *RepoUnit) error { _, err := db.GetEngine(ctx).ID(unit.ID).Update(unit) return err } + +// UpdateRepositoryUnits updates a repository's units +func UpdateRepositoryUnits(ctx context.Context, repo *Repository, units []RepoUnit, deleteUnitTypes []unit.Type) (err error) { + ctx, committer, err := db.TxContext(ctx) + if err != nil { + return err + } + defer committer.Close() + + // Delete existing settings of units before adding again + for _, u := range units { + deleteUnitTypes = append(deleteUnitTypes, u.Type) + } + + if _, err = db.GetEngine(ctx).Where("repo_id = ?", repo.ID).In("type", deleteUnitTypes).Delete(new(RepoUnit)); err != nil { + return err + } + + if len(units) > 0 { + if err = db.Insert(ctx, units); err != nil { + return err + } + } + + return committer.Commit() +} diff --git a/models/repo/repo_unit_test.go b/models/repo/repo_unit_test.go index a760594013..27a34fd0eb 100644 --- a/models/repo/repo_unit_test.go +++ b/models/repo/repo_unit_test.go @@ -6,6 +6,8 @@ package repo import ( "testing" + "code.gitea.io/gitea/models/perm" + "github.com/stretchr/testify/assert" ) @@ -28,3 +30,10 @@ func TestActionsConfig(t *testing.T) { cfg.DisableWorkflow("test3.yaml") assert.EqualValues(t, "test1.yaml,test2.yaml,test3.yaml", cfg.ToString()) } + +func TestRepoUnitAccessMode(t *testing.T) { + assert.Equal(t, UnitAccessModeNone.ToAccessMode(perm.AccessModeAdmin), perm.AccessModeNone) + assert.Equal(t, UnitAccessModeRead.ToAccessMode(perm.AccessModeAdmin), perm.AccessModeRead) + assert.Equal(t, UnitAccessModeWrite.ToAccessMode(perm.AccessModeAdmin), perm.AccessModeWrite) + assert.Equal(t, UnitAccessModeUnset.ToAccessMode(perm.AccessModeRead), perm.AccessModeRead) +} diff --git a/models/repo/topic.go b/models/repo/topic.go index 79b13e320d..6db6c8aef8 100644 --- a/models/repo/topic.go +++ b/models/repo/topic.go @@ -199,7 +199,7 @@ func FindTopics(ctx context.Context, opts *FindTopicOptions) ([]*Topic, int64, e sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id") orderBy = "topic.name" // when render topics for a repo, it's better to sort them by name, to get consistent result } - if opts.PageSize != 0 && opts.Page != 0 { + if opts.PageSize > 0 { sess = db.SetSessionPagination(sess, opts) } topics := make([]*Topic, 0, 10) diff --git a/models/unittest/mock_http.go b/models/unittest/mock_http.go new file mode 100644 index 0000000000..afdc5bed21 --- /dev/null +++ b/models/unittest/mock_http.go @@ -0,0 +1,113 @@ +// Copyright 2017 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package unittest + +import ( + "bufio" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "slices" + "strings" + "testing" + + "code.gitea.io/gitea/modules/log" + + "github.com/stretchr/testify/assert" +) + +// Mocks HTTP responses of a third-party service (such as GitHub, GitLab…) +// This has two modes: +// - live mode: the requests made to the mock HTTP server are transmitted to the live +// service, and responses are saved as test data files +// - test mode: the responses to requests to the mock HTTP server are read from the +// test data files +func NewMockWebServer(t *testing.T, liveServerBaseURL, testDataDir string, liveMode bool) *httptest.Server { + mockServerBaseURL := "" + ignoredHeaders := []string{"cf-ray", "server", "date", "report-to", "nel", "x-request-id"} + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + path := NormalizedFullPath(r.URL) + log.Info("Mock HTTP Server: got request for path %s", r.URL.Path) + // TODO check request method (support POST?) + fixturePath := fmt.Sprintf("%s/%s", testDataDir, strings.NewReplacer("/", "_", "?", "!").Replace(path)) + if liveMode { + liveURL := fmt.Sprintf("%s%s", liveServerBaseURL, path) + + request, err := http.NewRequest(r.Method, liveURL, nil) + assert.NoError(t, err, "constructing an HTTP request to %s failed", liveURL) + for headerName, headerValues := range r.Header { + // do not pass on the encoding: let the Transport of the HTTP client handle that for us + if strings.ToLower(headerName) != "accept-encoding" { + for _, headerValue := range headerValues { + request.Header.Add(headerName, headerValue) + } + } + } + + response, err := http.DefaultClient.Do(request) + assert.NoError(t, err, "HTTP request to %s failed: %s", liveURL) + + fixture, err := os.Create(fixturePath) + assert.NoError(t, err, "failed to open the fixture file %s for writing", fixturePath) + defer fixture.Close() + fixtureWriter := bufio.NewWriter(fixture) + + for headerName, headerValues := range response.Header { + for _, headerValue := range headerValues { + if !slices.Contains(ignoredHeaders, strings.ToLower(headerName)) { + _, err := fixtureWriter.WriteString(fmt.Sprintf("%s: %s\n", headerName, headerValue)) + assert.NoError(t, err, "writing the header of the HTTP response to the fixture file failed") + } + } + } + _, err = fixtureWriter.WriteString("\n") + assert.NoError(t, err, "writing the header of the HTTP response to the fixture file failed") + fixtureWriter.Flush() + + log.Info("Mock HTTP Server: writing response to %s", fixturePath) + _, err = io.Copy(fixture, response.Body) + assert.NoError(t, err, "writing the body of the HTTP response to %s failed", liveURL) + + err = fixture.Sync() + assert.NoError(t, err, "writing the body of the HTTP response to the fixture file failed") + } + + fixture, err := os.ReadFile(fixturePath) + assert.NoError(t, err, "missing mock HTTP response: "+fixturePath) + + w.WriteHeader(http.StatusOK) + + // replace any mention of the live HTTP service by the mocked host + stringFixture := strings.ReplaceAll(string(fixture), liveServerBaseURL, mockServerBaseURL) + // parse back the fixture file into a series of HTTP headers followed by response body + lines := strings.Split(stringFixture, "\n") + for idx, line := range lines { + colonIndex := strings.Index(line, ": ") + if colonIndex != -1 { + w.Header().Set(line[0:colonIndex], line[colonIndex+2:]) + } else { + // we reached the end of the headers (empty line), so what follows is the body + responseBody := strings.Join(lines[idx+1:], "\n") + _, err := w.Write([]byte(responseBody)) + assert.NoError(t, err, "writing the body of the HTTP response failed") + break + } + } + })) + mockServerBaseURL = server.URL + return server +} + +func NormalizedFullPath(url *url.URL) string { + // TODO normalize path (remove trailing slash?) + // TODO normalize RawQuery (order query parameters?) + if len(url.Query()) == 0 { + return url.EscapedPath() + } + return fmt.Sprintf("%s?%s", url.EscapedPath(), url.RawQuery) +} diff --git a/models/user/email_address.go b/models/user/email_address.go index 957e72fe89..cc62620e0f 100644 --- a/models/user/email_address.go +++ b/models/user/email_address.go @@ -231,6 +231,25 @@ func GetEmailAddresses(ctx context.Context, uid int64) ([]*EmailAddress, error) return emails, nil } +type ActivatedEmailAddress struct { + ID int64 + Email string +} + +func GetActivatedEmailAddresses(ctx context.Context, uid int64) ([]*ActivatedEmailAddress, error) { + emails := make([]*ActivatedEmailAddress, 0, 8) + if err := db.GetEngine(ctx). + Table("email_address"). + Select("id, email"). + Where("uid=?", uid). + And("is_activated=?", true). + Asc("id"). + Find(&emails); err != nil { + return nil, err + } + return emails, nil +} + // GetEmailAddressByID gets a user's email address by ID func GetEmailAddressByID(ctx context.Context, uid, id int64) (*EmailAddress, error) { // User ID is required for security reasons @@ -313,31 +332,7 @@ func updateActivation(ctx context.Context, email *EmailAddress, activate bool) e return UpdateUserCols(ctx, user, "rands") } -// MakeEmailPrimary sets primary email address of given user. -func MakeEmailPrimary(ctx context.Context, email *EmailAddress) error { - has, err := db.GetEngine(ctx).Get(email) - if err != nil { - return err - } else if !has { - return ErrEmailAddressNotExist{Email: email.Email} - } - - if !email.IsActivated { - return ErrEmailNotActivated - } - - user := &User{} - has, err = db.GetEngine(ctx).ID(email.UID).Get(user) - if err != nil { - return err - } else if !has { - return ErrUserNotExist{ - UID: email.UID, - Name: "", - KeyID: 0, - } - } - +func MakeEmailPrimaryWithUser(ctx context.Context, user *User, email *EmailAddress) error { ctx, committer, err := db.TxContext(ctx) if err != nil { return err @@ -367,6 +362,30 @@ func MakeEmailPrimary(ctx context.Context, email *EmailAddress) error { return committer.Commit() } +// MakeEmailPrimary sets primary email address of given user. +func MakeEmailPrimary(ctx context.Context, email *EmailAddress) error { + has, err := db.GetEngine(ctx).Get(email) + if err != nil { + return err + } else if !has { + return ErrEmailAddressNotExist{Email: email.Email} + } + + if !email.IsActivated { + return ErrEmailNotActivated + } + + user := &User{} + has, err = db.GetEngine(ctx).ID(email.UID).Get(user) + if err != nil { + return err + } else if !has { + return ErrUserNotExist{UID: email.UID} + } + + return MakeEmailPrimaryWithUser(ctx, user, email) +} + // VerifyActiveEmailCode verifies active email code when active account func VerifyActiveEmailCode(ctx context.Context, code, email string) *EmailAddress { minutes := setting.Service.ActiveCodeLives diff --git a/models/user/email_address_test.go b/models/user/email_address_test.go index 140443f82f..be1ccea544 100644 --- a/models/user/email_address_test.go +++ b/models/user/email_address_test.go @@ -4,6 +4,7 @@ package user_test import ( + "fmt" "testing" "code.gitea.io/gitea/models/db" @@ -219,3 +220,37 @@ func TestEmailAddressValidate(t *testing.T) { }) } } + +func TestGetActivatedEmailAddresses(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + testCases := []struct { + UID int64 + expected []*user_model.ActivatedEmailAddress + }{ + { + UID: 1, + expected: []*user_model.ActivatedEmailAddress{{ID: 9, Email: "user1@example.com"}, {ID: 33, Email: "user1-2@example.com"}, {ID: 34, Email: "user1-3@example.com"}}, + }, + { + UID: 2, + expected: []*user_model.ActivatedEmailAddress{{ID: 3, Email: "user2@example.com"}}, + }, + { + UID: 4, + expected: []*user_model.ActivatedEmailAddress{{ID: 11, Email: "user4@example.com"}}, + }, + { + UID: 11, + expected: []*user_model.ActivatedEmailAddress{}, + }, + } + + for _, testCase := range testCases { + t.Run(fmt.Sprintf("User %d", testCase.UID), func(t *testing.T) { + emails, err := user_model.GetActivatedEmailAddresses(db.DefaultContext, testCase.UID) + assert.NoError(t, err) + assert.Equal(t, testCase.expected, emails) + }) + } +} diff --git a/models/user/user.go b/models/user/user.go index c2a88de8d4..71f823dc31 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -216,6 +216,12 @@ func GetAllUsers(ctx context.Context) ([]*User, error) { return users, db.GetEngine(ctx).OrderBy("id").Where("type = ?", UserTypeIndividual).Find(&users) } +// GetAllAdmins returns a slice of all adminusers found in DB. +func GetAllAdmins(ctx context.Context) ([]*User, error) { + users := make([]*User, 0) + return users, db.GetEngine(ctx).OrderBy("id").Where("type = ?", UserTypeIndividual).And("is_admin = ?", true).Find(&users) +} + // IsLocal returns true if user login type is LoginPlain. func (u *User) IsLocal() bool { return u.LoginType <= auth.Plain diff --git a/models/user/user_test.go b/models/user/user_test.go index e5408bb94b..411d1ab830 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -484,6 +484,16 @@ func TestIsUserVisibleToViewer(t *testing.T) { test(user31, nil, false) } +func TestGetAllAdmins(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + admins, err := user_model.GetAllAdmins(db.DefaultContext) + assert.NoError(t, err) + + assert.Len(t, admins, 1) + assert.Equal(t, int64(1), admins[0].ID) +} + func Test_ValidateUser(t *testing.T) { oldSetting := setting.Service.AllowedUserVisibilityModesSlice defer func() { @@ -501,6 +511,11 @@ func Test_ValidateUser(t *testing.T) { } func Test_NormalizeUserFromEmail(t *testing.T) { + oldSetting := setting.Service.AllowDotsInUsernames + defer func() { + setting.Service.AllowDotsInUsernames = oldSetting + }() + setting.Service.AllowDotsInUsernames = true testCases := []struct { Input string Expected string diff --git a/modules/actions/github.go b/modules/actions/github.go index fafea4e11a..a988b2a124 100644 --- a/modules/actions/github.go +++ b/modules/actions/github.go @@ -35,6 +35,9 @@ func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEvent case GithubEventGollum: return triggedEvent == webhook_module.HookEventWiki + case GithubEventSchedule: + return triggedEvent == webhook_module.HookEventSchedule + case GithubEventIssues: switch triggedEvent { case webhook_module.HookEventIssues, @@ -70,9 +73,6 @@ func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEvent return false } - case GithubEventSchedule: - return triggedEvent == webhook_module.HookEventSchedule - default: return eventName == string(triggedEvent) } diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index 382338c957..00d83e06d7 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -22,7 +22,7 @@ import ( type DetectedWorkflow struct { EntryName string - TriggerEvent *jobparser.Event + TriggerEvent string Content []byte } @@ -103,7 +103,6 @@ func DetectWorkflows( commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader, - detectSchedule bool, ) ([]*DetectedWorkflow, []*DetectedWorkflow, error) { entries, err := ListWorkflows(commit) if err != nil { @@ -118,7 +117,6 @@ func DetectWorkflows( return nil, nil, err } - // one workflow may have multiple events events, err := GetEventsFromContent(content) if err != nil { log.Warn("ignore invalid workflow %q: %v", entry.Name(), err) @@ -127,18 +125,17 @@ func DetectWorkflows( for _, evt := range events { log.Trace("detect workflow %q for event %#v matching %q", entry.Name(), evt, triggedEvent) if evt.IsSchedule() { - if detectSchedule { - dwf := &DetectedWorkflow{ - EntryName: entry.Name(), - TriggerEvent: evt, - Content: content, - } - schedules = append(schedules, dwf) - } - } else if detectMatched(gitRepo, commit, triggedEvent, payload, evt) { dwf := &DetectedWorkflow{ EntryName: entry.Name(), - TriggerEvent: evt, + TriggerEvent: evt.Name, + Content: content, + } + schedules = append(schedules, dwf) + } + if detectMatched(gitRepo, commit, triggedEvent, payload, evt) { + dwf := &DetectedWorkflow{ + EntryName: entry.Name(), + TriggerEvent: evt.Name, Content: content, } workflows = append(workflows, dwf) @@ -149,41 +146,6 @@ func DetectWorkflows( return workflows, schedules, nil } -func DetectScheduledWorkflows(gitRepo *git.Repository, commit *git.Commit) ([]*DetectedWorkflow, error) { - entries, err := ListWorkflows(commit) - if err != nil { - return nil, err - } - - wfs := make([]*DetectedWorkflow, 0, len(entries)) - for _, entry := range entries { - content, err := GetContentFromEntry(entry) - if err != nil { - return nil, err - } - - // one workflow may have multiple events - events, err := GetEventsFromContent(content) - if err != nil { - log.Warn("ignore invalid workflow %q: %v", entry.Name(), err) - continue - } - for _, evt := range events { - if evt.IsSchedule() { - log.Trace("detect scheduled workflow: %q", entry.Name()) - dwf := &DetectedWorkflow{ - EntryName: entry.Name(), - TriggerEvent: evt, - Content: content, - } - wfs = append(wfs, dwf) - } - } - } - - return wfs, nil -} - func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader, evt *jobparser.Event) bool { if !canGithubEventMatch(evt.Name, triggedEvent) { return false @@ -191,11 +153,11 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web switch triggedEvent { case // events with no activity types + webhook_module.HookEventSchedule, webhook_module.HookEventCreate, webhook_module.HookEventDelete, webhook_module.HookEventFork, - webhook_module.HookEventWiki, - webhook_module.HookEventSchedule: + webhook_module.HookEventWiki: if len(evt.Acts()) != 0 { log.Warn("Ignore unsupported %s event arguments %v", triggedEvent, evt.Acts()) } diff --git a/modules/auth/password/hash/pbkdf2.go b/modules/auth/password/hash/pbkdf2.go index 9ff6d162fc..27382fedb8 100644 --- a/modules/auth/password/hash/pbkdf2.go +++ b/modules/auth/password/hash/pbkdf2.go @@ -4,12 +4,12 @@ package hash import ( + "crypto/sha256" "encoding/hex" "strings" "code.gitea.io/gitea/modules/log" - "github.com/minio/sha256-simd" "golang.org/x/crypto/pbkdf2" ) diff --git a/modules/avatar/hash.go b/modules/avatar/hash.go index 4fc28a7739..50db9c1943 100644 --- a/modules/avatar/hash.go +++ b/modules/avatar/hash.go @@ -4,10 +4,9 @@ package avatar import ( + "crypto/sha256" "encoding/hex" "strconv" - - "github.com/minio/sha256-simd" ) // HashAvatar will generate a unique string, which ensures that when there's a diff --git a/modules/avatar/identicon/identicon.go b/modules/avatar/identicon/identicon.go index 9b7a2faf05..63926d5f19 100644 --- a/modules/avatar/identicon/identicon.go +++ b/modules/avatar/identicon/identicon.go @@ -7,11 +7,10 @@ package identicon import ( + "crypto/sha256" "fmt" "image" "image/color" - - "github.com/minio/sha256-simd" ) const minImageSize = 16 diff --git a/modules/base/tool.go b/modules/base/tool.go index e9f4dfa279..b72f3a1930 100644 --- a/modules/base/tool.go +++ b/modules/base/tool.go @@ -5,6 +5,7 @@ package base import ( "crypto/sha1" + "crypto/sha256" "encoding/base64" "encoding/hex" "errors" @@ -22,7 +23,6 @@ import ( "code.gitea.io/gitea/modules/setting" "github.com/dustin/go-humanize" - "github.com/minio/sha256-simd" ) // EncodeSha1 string to sha1 hex value. diff --git a/modules/context/api.go b/modules/context/api.go index e226264a87..05b6a7a533 100644 --- a/modules/context/api.go +++ b/modules/context/api.go @@ -11,6 +11,7 @@ import ( "net/url" "strings" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" mc "code.gitea.io/gitea/modules/cache" @@ -38,6 +39,7 @@ type APIContext struct { ContextUser *user_model.User // the user which is being visited, in most cases it differs from Doer Repo *Repository + Comment *issues_model.Comment Org *APIOrganization Package *Package } diff --git a/modules/context/repo.go b/modules/context/repo.go index 75ebfec705..b48f6ded26 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -400,6 +400,7 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) { ctx.Data["PushMirrors"] = pushMirrors ctx.Data["RepoName"] = ctx.Repo.Repository.Name ctx.Data["IsEmptyRepo"] = ctx.Repo.Repository.IsEmpty + ctx.Data["DefaultWikiBranchName"] = setting.Repository.DefaultBranch } // RepoIDAssignment returns a handler which assigns the repo to the context. diff --git a/modules/generate/generate.go b/modules/generate/generate.go index ee3c76059b..df3e2474f9 100644 --- a/modules/generate/generate.go +++ b/modules/generate/generate.go @@ -38,22 +38,14 @@ func NewInternalToken() (string, error) { return internalToken, nil } -// NewJwtSecret generates a new value intended to be used for JWT secrets. -func NewJwtSecret() ([]byte, error) { +// NewJwtSecret generates a new base64 encoded value intended to be used for JWT secrets. +func NewJwtSecret() ([]byte, string, error) { bytes := make([]byte, 32) - _, err := io.ReadFull(rand.Reader, bytes) - if err != nil { - return nil, err - } - return bytes, nil -} - -// NewJwtSecretBase64 generates a new base64 encoded value intended to be used for JWT secrets. -func NewJwtSecretBase64() ([]byte, string, error) { - bytes, err := NewJwtSecret() + _, err := rand.Read(bytes) if err != nil { return nil, "", err } + return bytes, base64.RawURLEncoding.EncodeToString(bytes), nil } diff --git a/modules/git/commit.go b/modules/git/commit.go index 5d960e92f3..012ba975e8 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -515,6 +515,62 @@ func GetCommitFileStatus(ctx context.Context, repoPath, commitID string) (*Commi return fileStatus, nil } +func parseCommitRenames(renames *[][2]string, stdout io.Reader) { + rd := bufio.NewReader(stdout) + for { + // Skip (R || three digits || NULL byte) + _, err := rd.Discard(5) + if err != nil { + if err != io.EOF { + log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err) + } + return + } + oldFileName, err := rd.ReadString('\x00') + if err != nil { + if err != io.EOF { + log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err) + } + return + } + newFileName, err := rd.ReadString('\x00') + if err != nil { + if err != io.EOF { + log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err) + } + return + } + oldFileName = strings.TrimSuffix(oldFileName, "\x00") + newFileName = strings.TrimSuffix(newFileName, "\x00") + *renames = append(*renames, [2]string{oldFileName, newFileName}) + } +} + +// GetCommitFileRenames returns the renames that the commit contains. +func GetCommitFileRenames(ctx context.Context, repoPath, commitID string) ([][2]string, error) { + renames := [][2]string{} + stdout, w := io.Pipe() + done := make(chan struct{}) + go func() { + parseCommitRenames(&renames, stdout) + close(done) + }() + + stderr := new(bytes.Buffer) + err := NewCommand(ctx, "show", "--name-status", "--pretty=format:", "-z", "--diff-filter=R").AddDynamicArguments(commitID).Run(&RunOpts{ + Dir: repoPath, + Stdout: w, + Stderr: stderr, + }) + w.Close() // Close writer to exit parsing goroutine + if err != nil { + return nil, ConcatenateError(err, stderr.String()) + } + + <-done + return renames, nil +} + // GetFullCommitID returns full length (40) of commit ID by given short SHA in a repository. func GetFullCommitID(ctx context.Context, repoPath, shortID string) (string, error) { commitID, _, err := NewCommand(ctx, "rev-parse").AddDynamicArguments(shortID).RunStdString(&RunOpts{Dir: repoPath}) diff --git a/modules/git/commit_test.go b/modules/git/commit_test.go index e512eecc56..2de6feeb31 100644 --- a/modules/git/commit_test.go +++ b/modules/git/commit_test.go @@ -278,3 +278,30 @@ func TestGetCommitFileStatusMerges(t *testing.T) { assert.Equal(t, commitFileStatus.Removed, expected.Removed) assert.Equal(t, commitFileStatus.Modified, expected.Modified) } + +func TestParseCommitRenames(t *testing.T) { + testcases := []struct { + output string + renames [][2]string + }{ + { + output: "R090\x00renamed.txt\x00history.txt\x00", + renames: [][2]string{{"renamed.txt", "history.txt"}}, + }, + { + output: "R090\x00renamed.txt\x00history.txt\x00R000\x00corruptedstdouthere", + renames: [][2]string{{"renamed.txt", "history.txt"}}, + }, + { + output: "R100\x00renamed.txt\x00history.txt\x00R001\x00readme.md\x00README.md\x00", + renames: [][2]string{{"renamed.txt", "history.txt"}, {"readme.md", "README.md"}}, + }, + } + + for _, testcase := range testcases { + renames := [][2]string{} + parseCommitRenames(&renames, strings.NewReader(testcase.output)) + + assert.Equal(t, testcase.renames, renames) + } +} diff --git a/modules/git/last_commit_cache.go b/modules/git/last_commit_cache.go index 7c7baedd2f..5b62b90b27 100644 --- a/modules/git/last_commit_cache.go +++ b/modules/git/last_commit_cache.go @@ -4,12 +4,11 @@ package git import ( + "crypto/sha256" "fmt" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - - "github.com/minio/sha256-simd" ) // Cache represents a caching interface diff --git a/modules/git/repo_attribute.go b/modules/git/repo_attribute.go index 2b34f117f7..3c5a1429a9 100644 --- a/modules/git/repo_attribute.go +++ b/modules/git/repo_attribute.go @@ -291,7 +291,7 @@ func (repo *Repository) CheckAttributeReader(commitID string) (*CheckAttributeRe } checker := &CheckAttributeReader{ - Attributes: []string{"linguist-vendored", "linguist-generated", "linguist-language", "gitlab-language"}, + Attributes: []string{"linguist-vendored", "linguist-generated", "linguist-language", "gitlab-language", "linguist-documentation", "linguist-detectable"}, Repo: repo, IndexFile: indexFilename, WorkTree: worktree, diff --git a/modules/git/repo_language_stats.go b/modules/git/repo_language_stats.go index c40d6937b5..7ed2dc1587 100644 --- a/modules/git/repo_language_stats.go +++ b/modules/git/repo_language_stats.go @@ -13,6 +13,18 @@ const ( bigFileSize int64 = 1024 * 1024 // 1 MiB ) +type LinguistBoolAttrib struct { + Value string +} + +func (attrib *LinguistBoolAttrib) IsTrue() bool { + return attrib.Value == "set" || attrib.Value == "true" +} + +func (attrib *LinguistBoolAttrib) IsFalse() bool { + return attrib.Value == "unset" || attrib.Value == "false" +} + // mergeLanguageStats mergers language names with different cases. The name with most upper case letters is used. func mergeLanguageStats(stats map[string]int64) map[string]int64 { names := map[string]struct { diff --git a/modules/git/repo_language_stats_gogit.go b/modules/git/repo_language_stats_gogit.go index 4c6fbd6c7e..558a83af74 100644 --- a/modules/git/repo_language_stats_gogit.go +++ b/modules/git/repo_language_stats_gogit.go @@ -1,4 +1,5 @@ // Copyright 2020 The Gitea Authors. All rights reserved. +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. // SPDX-License-Identifier: MIT //go:build gogit @@ -57,23 +58,25 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err return nil } - notVendored := false - notGenerated := false + isVendored := LinguistBoolAttrib{} + isGenerated := LinguistBoolAttrib{} + isDocumentation := LinguistBoolAttrib{} + isDetectable := LinguistBoolAttrib{} if checker != nil { attrs, err := checker.CheckPath(f.Name) if err == nil { if vendored, has := attrs["linguist-vendored"]; has { - if vendored == "set" || vendored == "true" { - return nil - } - notVendored = vendored == "false" + isVendored = LinguistBoolAttrib{Value: vendored} } if generated, has := attrs["linguist-generated"]; has { - if generated == "set" || generated == "true" { - return nil - } - notGenerated = generated == "false" + isGenerated = LinguistBoolAttrib{Value: generated} + } + if documentation, has := attrs["linguist-documentation"]; has { + isDocumentation = LinguistBoolAttrib{Value: documentation} + } + if detectable, has := attrs["linguist-detectable"]; has { + isDetectable = LinguistBoolAttrib{Value: detectable} } if language, has := attrs["linguist-language"]; has && language != "unspecified" && language != "" { // group languages, such as Pug -> HTML; SCSS -> CSS @@ -105,8 +108,11 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err } } - if (!notVendored && analyze.IsVendor(f.Name)) || enry.IsDotFile(f.Name) || - enry.IsDocumentation(f.Name) || enry.IsConfiguration(f.Name) { + if isDetectable.IsFalse() || isVendored.IsTrue() || isDocumentation.IsTrue() || + (!isVendored.IsFalse() && analyze.IsVendor(f.Name)) || + enry.IsDotFile(f.Name) || + enry.IsConfiguration(f.Name) || + (!isDocumentation.IsFalse() && enry.IsDocumentation(f.Name)) { return nil } @@ -115,12 +121,11 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err if f.Size <= bigFileSize { content, _ = readFile(f, fileSizeLimit) } - if !notGenerated && enry.IsGenerated(f.Name, content) { + if !isGenerated.IsTrue() && enry.IsGenerated(f.Name, content) { return nil } // TODO: Use .gitattributes file for linguist overrides - language := analyze.GetCodeLanguage(f.Name, content) if language == enry.OtherLanguage || language == "" { return nil @@ -136,6 +141,13 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err if !checked { langtype := enry.GetLanguageType(language) included = langtype == enry.Programming || langtype == enry.Markup + if !included { + if isDetectable.IsTrue() { + included = true + } else { + return nil + } + } includedLanguage[language] = included } if included { diff --git a/modules/git/repo_language_stats_nogogit.go b/modules/git/repo_language_stats_nogogit.go index 1d94ad6c00..13876094cc 100644 --- a/modules/git/repo_language_stats_nogogit.go +++ b/modules/git/repo_language_stats_nogogit.go @@ -1,4 +1,5 @@ // Copyright 2020 The Gitea Authors. All rights reserved. +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. // SPDX-License-Identifier: MIT //go:build !gogit @@ -90,23 +91,25 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err continue } - notVendored := false - notGenerated := false + isVendored := LinguistBoolAttrib{} + isGenerated := LinguistBoolAttrib{} + isDocumentation := LinguistBoolAttrib{} + isDetectable := LinguistBoolAttrib{} if checker != nil { attrs, err := checker.CheckPath(f.Name()) if err == nil { if vendored, has := attrs["linguist-vendored"]; has { - if vendored == "set" || vendored == "true" { - continue - } - notVendored = vendored == "false" + isVendored = LinguistBoolAttrib{Value: vendored} } if generated, has := attrs["linguist-generated"]; has { - if generated == "set" || generated == "true" { - continue - } - notGenerated = generated == "false" + isGenerated = LinguistBoolAttrib{Value: generated} + } + if documentation, has := attrs["linguist-documentation"]; has { + isDocumentation = LinguistBoolAttrib{Value: documentation} + } + if detectable, has := attrs["linguist-detectable"]; has { + isDetectable = LinguistBoolAttrib{Value: detectable} } if language, has := attrs["linguist-language"]; has && language != "unspecified" && language != "" { // group languages, such as Pug -> HTML; SCSS -> CSS @@ -139,8 +142,11 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err } } - if (!notVendored && analyze.IsVendor(f.Name())) || enry.IsDotFile(f.Name()) || - enry.IsDocumentation(f.Name()) || enry.IsConfiguration(f.Name()) { + if isDetectable.IsFalse() || isVendored.IsTrue() || isDocumentation.IsTrue() || + (!isVendored.IsFalse() && analyze.IsVendor(f.Name())) || + enry.IsDotFile(f.Name()) || + enry.IsConfiguration(f.Name()) || + (!isDocumentation.IsFalse() && enry.IsDocumentation(f.Name())) { continue } @@ -173,7 +179,7 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err return nil, err } } - if !notGenerated && enry.IsGenerated(f.Name(), content) { + if !isGenerated.IsTrue() && enry.IsGenerated(f.Name(), content) { continue } @@ -194,6 +200,13 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err if !checked { langType := enry.GetLanguageType(language) included = langType == enry.Programming || langType == enry.Markup + if !included { + if isDetectable.IsTrue() { + included = true + } else { + continue + } + } includedLanguage[language] = included } if included { diff --git a/modules/git/tree_blob.go b/modules/git/tree_blob.go index 31d9f3d73b..e60c1f915b 100644 --- a/modules/git/tree_blob.go +++ b/modules/git/tree_blob.go @@ -1,9 +1,12 @@ // Copyright 2015 The Gogs Authors. All rights reserved. // Copyright 2019 The Gitea Authors. All rights reserved. +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. // SPDX-License-Identifier: MIT package git +import "strings" + // GetBlobByPath get the blob object according the path func (t *Tree) GetBlobByPath(relpath string) (*Blob, error) { entry, err := t.GetTreeEntryByPath(relpath) @@ -17,3 +20,21 @@ func (t *Tree) GetBlobByPath(relpath string) (*Blob, error) { return nil, ErrNotExist{"", relpath} } + +// GetBlobByFoldedPath returns the blob object at relpath, regardless of the +// case of relpath. If there are multiple files with the same case-insensitive +// name, the first one found will be returned. +func (t *Tree) GetBlobByFoldedPath(relpath string) (*Blob, error) { + entries, err := t.ListEntries() + if err != nil { + return nil, err + } + + for _, entry := range entries { + if strings.EqualFold(entry.Name(), relpath) { + return t.GetBlobByPath(entry.Name()) + } + } + + return nil, ErrNotExist{"", relpath} +} diff --git a/modules/lfs/content_store.go b/modules/lfs/content_store.go index daf8c6cfdd..0d9c0c98ac 100644 --- a/modules/lfs/content_store.go +++ b/modules/lfs/content_store.go @@ -4,6 +4,7 @@ package lfs import ( + "crypto/sha256" "encoding/hex" "errors" "hash" @@ -12,8 +13,6 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/storage" - - "github.com/minio/sha256-simd" ) var ( diff --git a/modules/lfs/pointer.go b/modules/lfs/pointer.go index 3e5bb8f91d..ebde20f826 100644 --- a/modules/lfs/pointer.go +++ b/modules/lfs/pointer.go @@ -4,6 +4,7 @@ package lfs import ( + "crypto/sha256" "encoding/hex" "errors" "fmt" @@ -12,8 +13,6 @@ import ( "regexp" "strconv" "strings" - - "github.com/minio/sha256-simd" ) const ( diff --git a/modules/markup/common/footnote.go b/modules/markup/common/footnote.go index 4406803694..0e75e2adfd 100644 --- a/modules/markup/common/footnote.go +++ b/modules/markup/common/footnote.go @@ -29,12 +29,17 @@ func CleanValue(value []byte) []byte { value = bytes.TrimSpace(value) rs := bytes.Runes(value) result := make([]rune, 0, len(rs)) + needsDash := false for _, r := range rs { - if unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_' || r == '-' { + switch { + case unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_': + if needsDash && len(result) > 0 { + result = append(result, '-') + } + needsDash = false result = append(result, unicode.ToLower(r)) - } - if unicode.IsSpace(r) { - result = append(result, '-') + default: + needsDash = true } } return []byte(string(result)) diff --git a/modules/markup/common/footnote_test.go b/modules/markup/common/footnote_test.go index 2327a7b14b..62763c5622 100644 --- a/modules/markup/common/footnote_test.go +++ b/modules/markup/common/footnote_test.go @@ -1,4 +1,5 @@ // Copyright 2023 The Gitea Authors. All rights reserved. +// Copyright 2023 The Forgejo Authors. All rights reserved. // SPDX-License-Identifier: MIT package common @@ -15,44 +16,45 @@ func TestCleanValue(t *testing.T) { }{ // Github behavior test cases {"", ""}, - {"test(0)", "test0"}, - {"test!1", "test1"}, - {"test:2", "test2"}, - {"test*3", "test3"}, - {"test!4", "test4"}, - {"test:5", "test5"}, - {"test*6", "test6"}, - {"test:6 a", "test6-a"}, - {"test:6 !b", "test6-b"}, - {"test:ad # df", "testad--df"}, - {"test:ad #23 df 2*/*", "testad-23-df-2"}, - {"test:ad 23 df 2*/*", "testad-23-df-2"}, - {"test:ad # 23 df 2*/*", "testad--23-df-2"}, + {"test.0.1", "test-0-1"}, + {"test(0)", "test-0"}, + {"test!1", "test-1"}, + {"test:2", "test-2"}, + {"test*3", "test-3"}, + {"test!4", "test-4"}, + {"test:5", "test-5"}, + {"test*6", "test-6"}, + {"test:6 a", "test-6-a"}, + {"test:6 !b", "test-6-b"}, + {"test:ad # df", "test-ad-df"}, + {"test:ad #23 df 2*/*", "test-ad-23-df-2"}, + {"test:ad 23 df 2*/*", "test-ad-23-df-2"}, + {"test:ad # 23 df 2*/*", "test-ad-23-df-2"}, {"Anchors in Markdown", "anchors-in-markdown"}, {"a_b_c", "a_b_c"}, {"a-b-c", "a-b-c"}, - {"a-b-c----", "a-b-c----"}, - {"test:6a", "test6a"}, - {"test:a6", "testa6"}, - {"tes a a a a", "tes-a-a---a--a"}, - {" tes a a a a ", "tes-a-a---a--a"}, + {"a-b-c----", "a-b-c"}, + {"test:6a", "test-6a"}, + {"test:a6", "test-a6"}, + {"tes a a a a", "tes-a-a-a-a"}, + {" tes a a a a ", "tes-a-a-a-a"}, {"Header with \"double quotes\"", "header-with-double-quotes"}, - {"Placeholder to force scrolling on link's click", "placeholder-to-force-scrolling-on-links-click"}, + {"Placeholder to force scrolling on link's click", "placeholder-to-force-scrolling-on-link-s-click"}, {"tes()", "tes"}, - {"tes(0)", "tes0"}, - {"tes{0}", "tes0"}, - {"tes[0]", "tes0"}, - {"test【0】", "test0"}, - {"tes…@a", "tesa"}, + {"tes(0)", "tes-0"}, + {"tes{0}", "tes-0"}, + {"tes[0]", "tes-0"}, + {"test【0】", "test-0"}, + {"tes…@a", "tes-a"}, {"tes¥& a", "tes-a"}, {"tes= a", "tes-a"}, - {"tes|a", "tesa"}, - {"tes\\a", "tesa"}, - {"tes/a", "tesa"}, + {"tes|a", "tes-a"}, + {"tes\\a", "tes-a"}, + {"tes/a", "tes-a"}, {"a啊啊b", "a啊啊b"}, - {"c🤔️🤔️d", "cd"}, - {"a⚡a", "aa"}, - {"e.~f", "ef"}, + {"c🤔️🤔️d", "c-d"}, + {"a⚡a", "a-a"}, + {"e.~f", "e-f"}, } for _, test := range tests { assert.Equal(t, []byte(test.expect), CleanValue([]byte(test.param)), test.param) diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go index bdf4011fa2..61167dd0b7 100644 --- a/modules/markup/markdown/markdown_test.go +++ b/modules/markup/markdown/markdown_test.go @@ -524,6 +524,18 @@ func TestMathBlock(t *testing.T) { "$$a$$", `
a
` + nl, }, + { + `\[a b\]`, + `
a b
` + nl, + }, + { + `\[a b]`, + `

[a b]

` + nl, + }, + { + `$$a`, + `

$$a

` + nl, + }, } for _, test := range testcases { @@ -534,6 +546,204 @@ func TestMathBlock(t *testing.T) { } } +func TestFootnote(t *testing.T) { + testcases := []struct { + testcase string + expected string + }{ + { + `Citation needed[^0]. +[^0]: Source`, + `

Citation needed1.

+
+
+
    +
  1. +

    Source ↩︎

    +
  2. +
+
+`, + }, + { + `Citation needed[^0]`, + `

Citation needed[^0]

+`, + }, + { + `Citation needed[^1], Citation needed twice[^3] +[^3]: Source`, + `

Citation needed[^1], Citation needed twice1

+
+
+
    +
  1. +

    Source ↩︎

    +
  2. +
+
+`, + }, + { + `Citation needed[^0] +[^1]: Source`, + `

Citation needed[^0]

+`, + }, + { + `Citation needed[^0] +[^0]: Source 1 +[^0]: Source 2`, + `

Citation needed1

+
+
+
    +
  1. +

    Source 1 ↩︎

    +
  2. +
+
+`, + }, + { + `Citation needed![^0] +[^0]: Source`, + `

Citation needed1

+
+
+
    +
  1. +

    Source ↩︎

    +
  2. +
+
+`, + }, + { + `Trigger [^`, + `

Trigger [^

+`, + }, + { + `Trigger 2 [^0`, + `

Trigger 2 [^0

+`, + }, + { + `Citation needed[^0] +[^0]: Source with citation needed[^1] +[^1]: Source`, + `

Citation needed1

+
+
+
    +
  1. +

    Source with citation needed2 ↩︎

    +
  2. +
  3. +

    Source ↩︎

    +
  4. +
+
+`, + }, + { + `Citation needed[^#] +[^#]: Source`, + `

Citation needed1

+
+
+
    +
  1. +

    Source ↩︎

    +
  2. +
+
+`, + }, + { + `Citation needed[^0] + [^0]: Source`, + `

Citation needed[^0]
+[^0]: Source

+`, + }, + { + `[^0]: Source + +Citation needed[^0].`, + `

Citation needed1.

+
+
+
    +
  1. +

    Source ↩︎

    +
  2. +
+
+`, + }, + { + `Citation needed[^] +[^]: Source`, + `

Citation needed[^]
+[^]: Source

+`, + }, + { + `Citation needed[^0] +[^0] Source`, + `

Citation needed[^0]
+[^0] Source

+`, + }, + { + `Citation needed[^0] +[^0 Source`, + `

Citation needed[^0]
+[^0 Source

+`, + }, + { + `Citation needed[^0] [^0]: Source`, + `

Citation needed[^0] [^0]: Source

+`, + }, + { + `Citation needed[^Source here 0 # 9-3] +[^Source here 0 # 9-3]: Source`, + `

Citation needed1

+
+
+
    +
  1. +

    Source ↩︎

    +
  2. +
+
+`, + }, + { + `Citation needed[^0] +[^0]:`, + `

Citation needed1

+
+
+
    +
  1. + ↩︎
  2. +
+
+`, + }, + } + for _, test := range testcases { + res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase) + assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase) + assert.Equal(t, test.expected, res, "Unexpected result in testcase %q", test.testcase) + } +} + func TestTaskList(t *testing.T) { testcases := []struct { testcase string diff --git a/modules/markup/markdown/math/block_parser.go b/modules/markup/markdown/math/block_parser.go index 7f714d7239..f3262c82c0 100644 --- a/modules/markup/markdown/math/block_parser.go +++ b/modules/markup/markdown/math/block_parser.go @@ -55,10 +55,7 @@ func (b *blockParser) Open(parent ast.Node, reader text.Reader, pc parser.Contex return node, parser.Close | parser.NoChildren } - reader.Advance(segment.Len() - 1) - segment.Start += 2 - node.Lines().Append(segment) - return node, parser.NoChildren + return nil, parser.NoChildren } // Continue parses the current line and returns a result of parsing. diff --git a/modules/markup/orgmode/orgmode.go b/modules/markup/orgmode/orgmode.go index abc641fbe2..e0c9b13a9e 100644 --- a/modules/markup/orgmode/orgmode.go +++ b/modules/markup/orgmode/orgmode.go @@ -135,7 +135,12 @@ type Writer struct { const mailto = "mailto:" -func (r *Writer) resolveLink(l org.RegularLink) string { +func (r *Writer) resolveLink(node org.Node) string { + l, ok := node.(org.RegularLink) + if !ok { + l = org.RegularLink{URL: strings.TrimPrefix(org.String(node), "file:")} + } + link := html.EscapeString(l.URL) if l.Protocol == "file" { link = link[len("file:"):] @@ -162,14 +167,14 @@ func (r *Writer) WriteRegularLink(l org.RegularLink) { if l.Description == nil { fmt.Fprintf(r, `%s`, link, link) } else { - imageSrc := r.resolveLink(l.Description[0].(org.RegularLink)) + imageSrc := r.resolveLink(l.Description[0]) fmt.Fprintf(r, `%s`, link, imageSrc, imageSrc) } case "video": if l.Description == nil { fmt.Fprintf(r, ``, link, link) } else { - videoSrc := r.resolveLink(l.Description[0].(org.RegularLink)) + videoSrc := r.resolveLink(l.Description[0]) fmt.Fprintf(r, ``, link, videoSrc, videoSrc) } default: diff --git a/modules/markup/orgmode/orgmode_test.go b/modules/markup/orgmode/orgmode_test.go index abf5ca8fcf..e963718883 100644 --- a/modules/markup/orgmode/orgmode_test.go +++ b/modules/markup/orgmode/orgmode_test.go @@ -80,6 +80,12 @@ func TestRender_Media(t *testing.T) { `

https://example.com/example.svg

`) test("[[https://example.com/example.mp4]]", `

`) + + // Text description. + test("[[file:./lem-post.png][file:./lem-post.png]]", + `

http://localhost:3000/gogits/gogs/lem-post.png

`) + test("[[file:./lem-post.mp4][file:./lem-post.mp4]]", + `

`) } func TestRender_Source(t *testing.T) { diff --git a/modules/repository/repo.go b/modules/repository/repo.go index fc3af04071..65b50b2e45 100644 --- a/modules/repository/repo.go +++ b/modules/repository/repo.go @@ -1,4 +1,5 @@ // Copyright 2019 The Gitea Authors. All rights reserved. +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. // SPDX-License-Identifier: MIT package repository @@ -99,7 +100,6 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User, Mirror: true, Quiet: true, Timeout: migrateTimeout, - Branch: "master", SkipTLSVerify: setting.Migrations.SkipTLSVerify, }); err != nil { log.Warn("Clone wiki: %v", err) @@ -107,6 +107,30 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User, return repo, fmt.Errorf("Failed to remove %s: %w", wikiPath, err) } } else { + // Figure out the branch of the wiki we just cloned. We assume + // that the default branch is to be used, and we'll use the same + // name as the source. + gitRepo, err := git.OpenRepository(ctx, wikiPath) + if err != nil { + log.Warn("Failed to open wiki repository during migration: %v", err) + if err := util.RemoveAll(wikiPath); err != nil { + return repo, fmt.Errorf("Failed to remove %s: %w", wikiPath, err) + } + return repo, err + } + defer gitRepo.Close() + + branch, err := gitRepo.GetDefaultBranch() + if err != nil { + log.Warn("Failed to get the default branch of a migrated wiki repo: %v", err) + if err := util.RemoveAll(wikiPath); err != nil { + return repo, fmt.Errorf("Failed to remove %s: %w", wikiPath, err) + } + + return repo, err + } + repo.WikiBranch = branch + if err := git.WriteCommitGraph(ctx, wikiPath); err != nil { return repo, err } diff --git a/modules/secret/secret.go b/modules/secret/secret.go index 9c2ecd181d..e70ae1839c 100644 --- a/modules/secret/secret.go +++ b/modules/secret/secret.go @@ -7,13 +7,12 @@ import ( "crypto/aes" "crypto/cipher" "crypto/rand" + "crypto/sha256" "encoding/base64" "encoding/hex" "errors" "fmt" "io" - - "github.com/minio/sha256-simd" ) // AesEncrypt encrypts text and given key with AES. diff --git a/modules/setting/admin.go b/modules/setting/admin.go index 2d2dd26de9..d7f0ee827d 100644 --- a/modules/setting/admin.go +++ b/modules/setting/admin.go @@ -5,8 +5,9 @@ package setting // Admin settings var Admin struct { - DisableRegularOrgCreation bool - DefaultEmailNotification string + DisableRegularOrgCreation bool + DefaultEmailNotification string + SendNotificationEmailOnNewUser bool } func loadAdminFrom(rootCfg ConfigProvider) { diff --git a/modules/setting/badges.go b/modules/setting/badges.go new file mode 100644 index 0000000000..e0c1cb55ec --- /dev/null +++ b/modules/setting/badges.go @@ -0,0 +1,24 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "text/template" +) + +// Badges settings +var Badges = struct { + Enabled bool `ini:"ENABLED"` + GeneratorURLTemplate string `ini:"GENERATOR_URL_TEMPLATE"` + GeneratorURLTemplateTemplate *template.Template `ini:"-"` +}{ + Enabled: true, + GeneratorURLTemplate: "https://img.shields.io/badge/{{.label}}-{{.text}}-{{.color}}", +} + +func loadBadgesFrom(rootCfg ConfigProvider) { + mustMapSetting(rootCfg, "badges", &Badges) + + Badges.GeneratorURLTemplateTemplate = template.Must(template.New("").Parse(Badges.GeneratorURLTemplate)) +} diff --git a/modules/setting/database.go b/modules/setting/database.go index b9d44aaa26..c7bc92e673 100644 --- a/modules/setting/database.go +++ b/modules/setting/database.go @@ -25,26 +25,27 @@ var ( // Database holds the database settings Database = struct { - Type DatabaseType - Host string - Name string - User string - Passwd string - Schema string - SSLMode string - Path string - LogSQL bool - MysqlCharset string - CharsetCollation string - Timeout int // seconds - SQLiteJournalMode string - DBConnectRetries int - DBConnectBackoff time.Duration - MaxIdleConns int - MaxOpenConns int - ConnMaxLifetime time.Duration - IterateBufferSize int - AutoMigration bool + Type DatabaseType + Host string + Name string + User string + Passwd string + Schema string + SSLMode string + Path string + LogSQL bool + MysqlCharset string + CharsetCollation string + Timeout int // seconds + SQLiteJournalMode string + DBConnectRetries int + DBConnectBackoff time.Duration + MaxIdleConns int + MaxOpenConns int + ConnMaxLifetime time.Duration + IterateBufferSize int + AutoMigration bool + SlowQueryThreshold time.Duration }{ Timeout: 500, IterateBufferSize: 50, @@ -87,6 +88,13 @@ func loadDBSetting(rootCfg ConfigProvider) { Database.DBConnectRetries = sec.Key("DB_RETRIES").MustInt(10) Database.DBConnectBackoff = sec.Key("DB_RETRY_BACKOFF").MustDuration(3 * time.Second) Database.AutoMigration = sec.Key("AUTO_MIGRATION").MustBool(true) + + deprecatedSetting(rootCfg, "database", "SLOW_QUERY_TRESHOLD", "database", "SLOW_QUERY_THRESHOLD", "1.23") + if sec.HasKey("SLOW_QUERY_TRESHOLD") && !sec.HasKey("SLOW_QUERY_THRESHOLD") { + Database.SlowQueryThreshold = sec.Key("SLOW_QUERY_TRESHOLD").MustDuration(5 * time.Second) + } else { + Database.SlowQueryThreshold = sec.Key("SLOW_QUERY_THRESHOLD").MustDuration(5 * time.Second) + } } // DBConnStr returns database connection string diff --git a/modules/setting/lfs.go b/modules/setting/lfs.go index a5ea537cef..7ab90669e7 100644 --- a/modules/setting/lfs.go +++ b/modules/setting/lfs.go @@ -64,7 +64,7 @@ func loadLFSFrom(rootCfg ConfigProvider) error { LFS.JWTSecretBase64 = loadSecret(rootCfg.Section("server"), "LFS_JWT_SECRET_URI", "LFS_JWT_SECRET") LFS.JWTSecretBytes, err = util.Base64FixedDecode(base64.RawURLEncoding, []byte(LFS.JWTSecretBase64), 32) if err != nil { - LFS.JWTSecretBytes, LFS.JWTSecretBase64, err = generate.NewJwtSecretBase64() + LFS.JWTSecretBytes, LFS.JWTSecretBase64, err = generate.NewJwtSecret() if err != nil { return fmt.Errorf("error generating JWT Secret for custom config: %v", err) } diff --git a/modules/setting/oauth2.go b/modules/setting/oauth2.go index 0d15e91ef0..e93ce188df 100644 --- a/modules/setting/oauth2.go +++ b/modules/setting/oauth2.go @@ -138,12 +138,11 @@ func loadOAuth2From(rootCfg ConfigProvider) { if InstallLock { if _, err := util.Base64FixedDecode(base64.RawURLEncoding, []byte(OAuth2.JWTSecretBase64), 32); err != nil { - key, err := generate.NewJwtSecret() + _, OAuth2.JWTSecretBase64, err = generate.NewJwtSecret() if err != nil { log.Fatal("error generating JWT secret: %v", err) } - OAuth2.JWTSecretBase64 = base64.RawURLEncoding.EncodeToString(key) saveCfg, err := rootCfg.PrepareSaving() if err != nil { log.Fatal("save oauth2.JWT_SECRET failed: %v", err) diff --git a/modules/setting/repository.go b/modules/setting/repository.go index 191ad04422..4ab566b7ff 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -7,6 +7,7 @@ import ( "os/exec" "path" "path/filepath" + "slices" "strings" "code.gitea.io/gitea/modules/log" @@ -19,6 +20,8 @@ const ( RepoCreatingPublic = "public" ) +var RecognisedRepositoryDownloadOrCloneMethods = []string{"download-zip", "download-targz", "download-bundle", "vscode-clone", "vscodium-clone", "cite"} + // ItemsPerPage maximum items per page in forks, watchers and stars of a repo const ItemsPerPage = 40 @@ -43,6 +46,7 @@ var ( DisabledRepoUnits []string DefaultRepoUnits []string DefaultForkRepoUnits []string + DownloadOrCloneMethods []string PrefixArchiveFiles bool DisableMigrations bool DisableStars bool `ini:"DISABLE_STARS"` @@ -109,6 +113,9 @@ var ( Wiki []string DefaultTrustModel string } `ini:"repository.signing"` + + SettableFlags []string + EnableFlags bool }{ DetectedCharsetsOrder: []string{ "UTF-8", @@ -151,7 +158,7 @@ var ( DefaultPrivate: RepoCreatingLastUserVisibility, DefaultPushCreatePrivate: true, MaxCreationLimit: -1, - PreferredLicenses: []string{"Apache License 2.0", "MIT License"}, + PreferredLicenses: []string{"Apache-2.0", "MIT"}, DisableHTTPGit: false, AccessControlAllowOrigin: "", UseCompatSSHURI: false, @@ -161,6 +168,7 @@ var ( DisabledRepoUnits: []string{}, DefaultRepoUnits: []string{}, DefaultForkRepoUnits: []string{}, + DownloadOrCloneMethods: []string{"download-zip", "download-targz", "download-bundle", "vscode-clone"}, PrefixArchiveFiles: true, DisableMigrations: false, DisableStars: false, @@ -265,6 +273,8 @@ var ( Wiki: []string{"never"}, DefaultTrustModel: "collaborator", }, + + EnableFlags: false, } RepoRootPath string ScriptType = "bash" @@ -361,4 +371,12 @@ func loadRepositoryFrom(rootCfg ConfigProvider) { if err := loadRepoArchiveFrom(rootCfg); err != nil { log.Fatal("loadRepoArchiveFrom: %v", err) } + + for _, method := range Repository.DownloadOrCloneMethods { + if !slices.Contains(RecognisedRepositoryDownloadOrCloneMethods, method) { + log.Error("Unrecognised repository download or clone method: %s", method) + } + } + + Repository.EnableFlags = sec.Key("ENABLE_FLAGS").MustBool() } diff --git a/modules/setting/service.go b/modules/setting/service.go index befb94b61b..afaee18101 100644 --- a/modules/setting/service.go +++ b/modules/setting/service.go @@ -68,6 +68,7 @@ var Service = struct { DefaultKeepEmailPrivate bool DefaultAllowCreateOrganization bool DefaultUserIsRestricted bool + AllowDotsInUsernames bool EnableTimetracking bool DefaultEnableTimetracking bool DefaultEnableDependencies bool @@ -180,6 +181,7 @@ func loadServiceFrom(rootCfg ConfigProvider) { Service.DefaultKeepEmailPrivate = sec.Key("DEFAULT_KEEP_EMAIL_PRIVATE").MustBool() Service.DefaultAllowCreateOrganization = sec.Key("DEFAULT_ALLOW_CREATE_ORGANIZATION").MustBool(true) Service.DefaultUserIsRestricted = sec.Key("DEFAULT_USER_IS_RESTRICTED").MustBool(false) + Service.AllowDotsInUsernames = sec.Key("ALLOW_DOTS_IN_USERNAMES").MustBool(true) Service.EnableTimetracking = sec.Key("ENABLE_TIMETRACKING").MustBool(true) if Service.EnableTimetracking { Service.DefaultEnableTimetracking = sec.Key("DEFAULT_ENABLE_TIMETRACKING").MustBool(true) diff --git a/modules/setting/setting.go b/modules/setting/setting.go index ebfd3b27be..c0d8d0ee23 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -147,6 +147,7 @@ func loadCommonSettingsFrom(cfg ConfigProvider) error { loadUIFrom(cfg) loadAdminFrom(cfg) loadAPIFrom(cfg) + loadBadgesFrom(cfg) loadMetricsFrom(cfg) loadCamoFrom(cfg) loadI18nFrom(cfg) diff --git a/modules/structs/hook.go b/modules/structs/hook.go index 9afcebdc59..9c2bbe8738 100644 --- a/modules/structs/hook.go +++ b/modules/structs/hook.go @@ -402,6 +402,16 @@ func (p *PullRequestPayload) JSONPayload() ([]byte, error) { return json.MarshalIndent(p, "", " ") } +type HookScheduleAction string + +const ( + HookScheduleCreated HookScheduleAction = "schedule" +) + +type SchedulePayload struct { + Action HookScheduleAction `json:"action"` +} + // ReviewPayload FIXME type ReviewPayload struct { Type string `json:"type"` diff --git a/modules/structs/pull_review.go b/modules/structs/pull_review.go index 810be8f521..c77ebea07d 100644 --- a/modules/structs/pull_review.go +++ b/modules/structs/pull_review.go @@ -89,6 +89,9 @@ type CreatePullReviewComment struct { NewLineNum int64 `json:"new_position"` } +// CreatePullReviewCommentOptions are options to create a pull review comment +type CreatePullReviewCommentOptions CreatePullReviewComment + // SubmitPullReviewOptions are options to submit a pending pull review type SubmitPullReviewOptions struct { Event ReviewStateType `json:"event"` diff --git a/modules/structs/repo.go b/modules/structs/repo.go index 3e0b5496d0..e20b6bc26e 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -88,6 +88,7 @@ type Repository struct { ExternalTracker *ExternalTracker `json:"external_tracker,omitempty"` HasWiki bool `json:"has_wiki"` ExternalWiki *ExternalWiki `json:"external_wiki,omitempty"` + WikiBranch string `json:"wiki_branch,omitempty"` HasPullRequests bool `json:"has_pull_requests"` HasProjects bool `json:"has_projects"` HasReleases bool `json:"has_releases"` @@ -175,6 +176,8 @@ type EditRepoOption struct { ExternalWiki *ExternalWiki `json:"external_wiki,omitempty"` // sets the default branch for this repository. DefaultBranch *string `json:"default_branch,omitempty"` + // sets the branch used for this repository's wiki. + WikiBranch *string `json:"wiki_branch,omitempty"` // either `true` to allow pull requests, or `false` to prevent pull request. HasPullRequests *bool `json:"has_pull_requests,omitempty"` // either `true` to enable project unit, or `false` to disable them. diff --git a/modules/structs/repo_flags.go b/modules/structs/repo_flags.go new file mode 100644 index 0000000000..5db714545c --- /dev/null +++ b/modules/structs/repo_flags.go @@ -0,0 +1,9 @@ +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. +// SPDX-License-Identifier: MIT + +package structs + +// ReplaceFlagsOption options when replacing the flags of a repository +type ReplaceFlagsOption struct { + Flags []string `json:"flags"` +} diff --git a/modules/templates/helper.go b/modules/templates/helper.go index 96cdd9ca46..bcb94bff25 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -96,6 +96,9 @@ func NewFuncMap() template.FuncMap { "AppDomain": func() string { // documented in mail-templates.md return setting.Domain }, + "RepoFlagsEnabled": func() bool { + return setting.Repository.EnableFlags + }, "AssetVersion": func() string { return setting.AssetVersion }, diff --git a/modules/test/logchecker.go b/modules/test/logchecker.go index 7bf234f560..0f12257f3e 100644 --- a/modules/test/logchecker.go +++ b/modules/test/logchecker.go @@ -55,13 +55,15 @@ func (lc *LogChecker) checkLogEvent(event *log.EventFormatted) { var checkerIndex int64 -func NewLogChecker(namePrefix string) (logChecker *LogChecker, cancel func()) { +func NewLogChecker(namePrefix string, level log.Level) (logChecker *LogChecker, cancel func()) { logger := log.GetManager().GetLogger(namePrefix) newCheckerIndex := atomic.AddInt64(&checkerIndex, 1) writerName := namePrefix + "-" + fmt.Sprint(newCheckerIndex) lc := &LogChecker{} - lc.EventWriterBaseImpl = log.NewEventWriterBase(writerName, "test-log-checker", log.WriterMode{}) + lc.EventWriterBaseImpl = log.NewEventWriterBase(writerName, "test-log-checker", log.WriterMode{ + Level: level, + }) logger.AddWriters(lc) return lc, func() { _ = logger.RemoveWriter(writerName) } } diff --git a/modules/test/logchecker_test.go b/modules/test/logchecker_test.go index 6b093ab1b3..0f410fed12 100644 --- a/modules/test/logchecker_test.go +++ b/modules/test/logchecker_test.go @@ -12,8 +12,8 @@ import ( "github.com/stretchr/testify/assert" ) -func TestLogChecker(t *testing.T) { - lc, cleanup := NewLogChecker(log.DEFAULT) +func TestLogCheckerInfo(t *testing.T) { + lc, cleanup := NewLogChecker(log.DEFAULT, log.INFO) defer cleanup() lc.Filter("First", "Third").StopMark("End") @@ -24,11 +24,13 @@ func TestLogChecker(t *testing.T) { assert.False(t, stopped) log.Info("First") + log.Debug("Third") filtered, stopped = lc.Check(100 * time.Millisecond) assert.ElementsMatch(t, []bool{true, false}, filtered) assert.False(t, stopped) log.Info("Second") + log.Debug("Third") filtered, stopped = lc.Check(100 * time.Millisecond) assert.ElementsMatch(t, []bool{true, false}, filtered) assert.False(t, stopped) @@ -43,3 +45,14 @@ func TestLogChecker(t *testing.T) { assert.ElementsMatch(t, []bool{true, true}, filtered) assert.True(t, stopped) } + +func TestLogCheckerDebug(t *testing.T) { + lc, cleanup := NewLogChecker(log.DEFAULT, log.DEBUG) + defer cleanup() + + lc.StopMark("End") + + log.Debug("End") + _, stopped := lc.Check(100 * time.Millisecond) + assert.True(t, stopped) +} diff --git a/modules/util/keypair.go b/modules/util/keypair.go index 97f2d9ebca..8b86c142af 100644 --- a/modules/util/keypair.go +++ b/modules/util/keypair.go @@ -7,10 +7,9 @@ import ( "crypto" "crypto/rand" "crypto/rsa" + "crypto/sha256" "crypto/x509" "encoding/pem" - - "github.com/minio/sha256-simd" ) // GenerateKeyPair generates a public and private keypair diff --git a/modules/util/keypair_test.go b/modules/util/keypair_test.go index c9925f7988..c6f68c845a 100644 --- a/modules/util/keypair_test.go +++ b/modules/util/keypair_test.go @@ -7,12 +7,12 @@ import ( "crypto" "crypto/rand" "crypto/rsa" + "crypto/sha256" "crypto/x509" "encoding/pem" "regexp" "testing" - "github.com/minio/sha256-simd" "github.com/stretchr/testify/assert" ) diff --git a/modules/validation/helpers.go b/modules/validation/helpers.go index f6e00f3887..567ad867fe 100644 --- a/modules/validation/helpers.go +++ b/modules/validation/helpers.go @@ -117,13 +117,20 @@ func IsValidExternalTrackerURLFormat(uri string) bool { } var ( - validUsernamePattern = regexp.MustCompile(`^[\da-zA-Z][-.\w]*$`) - invalidUsernamePattern = regexp.MustCompile(`[-._]{2,}|[-._]$`) // No consecutive or trailing non-alphanumeric chars + validUsernamePatternWithDots = regexp.MustCompile(`^[\da-zA-Z][-.\w]*$`) + validUsernamePatternWithoutDots = regexp.MustCompile(`^[\da-zA-Z][-\w]*$`) + + // No consecutive or trailing non-alphanumeric chars, catches both cases + invalidUsernamePattern = regexp.MustCompile(`[-._]{2,}|[-._]$`) ) // IsValidUsername checks if username is valid func IsValidUsername(name string) bool { // It is difficult to find a single pattern that is both readable and effective, // but it's easier to use positive and negative checks. - return validUsernamePattern.MatchString(name) && !invalidUsernamePattern.MatchString(name) + if setting.Service.AllowDotsInUsernames { + return validUsernamePatternWithDots.MatchString(name) && !invalidUsernamePattern.MatchString(name) + } + + return validUsernamePatternWithoutDots.MatchString(name) && !invalidUsernamePattern.MatchString(name) } diff --git a/modules/validation/helpers_test.go b/modules/validation/helpers_test.go index 52f383f698..a1bdf2a29c 100644 --- a/modules/validation/helpers_test.go +++ b/modules/validation/helpers_test.go @@ -155,7 +155,8 @@ func Test_IsValidExternalTrackerURLFormat(t *testing.T) { } } -func TestIsValidUsername(t *testing.T) { +func TestIsValidUsernameAllowDots(t *testing.T) { + setting.Service.AllowDotsInUsernames = true tests := []struct { arg string want bool @@ -185,3 +186,31 @@ func TestIsValidUsername(t *testing.T) { }) } } + +func TestIsValidUsernameBanDots(t *testing.T) { + setting.Service.AllowDotsInUsernames = false + defer func() { + setting.Service.AllowDotsInUsernames = true + }() + + tests := []struct { + arg string + want bool + }{ + {arg: "a", want: true}, + {arg: "abc", want: true}, + {arg: "0.b-c", want: false}, + {arg: "a.b-c_d", want: false}, + {arg: ".abc", want: false}, + {arg: "abc.", want: false}, + {arg: "a..bc", want: false}, + {arg: "a...bc", want: false}, + {arg: "a.-bc", want: false}, + {arg: "a._bc", want: false}, + } + for _, tt := range tests { + t.Run(tt.arg, func(t *testing.T) { + assert.Equalf(t, tt.want, IsValidUsername(tt.arg), "IsValidUsername[AllowDotsInUsernames=false](%v)", tt.arg) + }) + } +} diff --git a/modules/web/handler.go b/modules/web/handler.go index 26b7428016..728cc5a160 100644 --- a/modules/web/handler.go +++ b/modules/web/handler.go @@ -147,6 +147,16 @@ func toHandlerProvider(handler any) func(next http.Handler) http.Handler { } } + if hp, ok := handler.(func(next http.Handler) http.HandlerFunc); ok { + return func(next http.Handler) http.Handler { + h := hp(next) // this handle could be dynamically generated, so we can't use it for debug info + return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + routing.UpdateFuncInfo(req.Context(), funcInfo) + h.ServeHTTP(resp, req) + }) + } + } + provider := func(next http.Handler) http.Handler { return http.HandlerFunc(func(respOrig http.ResponseWriter, req *http.Request) { // wrap the response writer to check whether the response has been written diff --git a/modules/web/middleware/binding.go b/modules/web/middleware/binding.go index d9bcdf3b2a..4e7fca80e2 100644 --- a/modules/web/middleware/binding.go +++ b/modules/web/middleware/binding.go @@ -8,6 +8,7 @@ import ( "reflect" "strings" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/validation" @@ -135,7 +136,11 @@ func Validate(errs binding.Errors, data map[string]any, f Form, l translation.Lo case validation.ErrRegexPattern: data["ErrorMsg"] = trName + l.Tr("form.regex_pattern_error", errs[0].Message) case validation.ErrUsername: - data["ErrorMsg"] = trName + l.Tr("form.username_error") + if setting.Service.AllowDotsInUsernames { + data["ErrorMsg"] = trName + l.Tr("form.username_error") + } else { + data["ErrorMsg"] = trName + l.Tr("form.username_error_no_dots") + } case validation.ErrInvalidGroupTeamMap: data["ErrorMsg"] = trName + l.Tr("form.invalid_group_team_map_error", errs[0].Message) default: diff --git a/modules/web/middleware/data.go b/modules/web/middleware/data.go index 08d83f94be..c1d1af8528 100644 --- a/modules/web/middleware/data.go +++ b/modules/web/middleware/data.go @@ -53,6 +53,7 @@ func CommonTemplateContextData() ContextData { "ShowMilestonesDashboardPage": setting.Service.ShowMilestonesDashboardPage, "ShowFooterVersion": setting.Other.ShowFooterVersion, "DisableDownloadSourceArchives": setting.Repository.DisableDownloadSourceArchives, + "DownloadOrCloneMethods": setting.Repository.DownloadOrCloneMethods, "EnableSwagger": setting.API.EnableSwagger, "EnableOpenIDSignIn": setting.Service.EnableOpenIDSignIn, diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index f9fd9db5fd..14ae10c327 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1850,6 +1850,7 @@ pulls.outdated_with_base_branch = This branch is out-of-date with the base branc pulls.close = Close Pull Request pulls.closed_at = `closed this pull request %[2]s` pulls.reopened_at = `reopened this pull request %[2]s` +pulls.commit_ref_at = `referenced this pull request from a commit %[2]s` pulls.cmd_instruction_hint = `View command line instructions.` pulls.cmd_instruction_checkout_title = Checkout pulls.cmd_instruction_checkout_desc = From your project repository, check out a new branch and test the changes. @@ -2138,7 +2139,7 @@ settings.transfer_abort = Cancel transfer settings.transfer_abort_invalid = You cannot cancel a non existent repository transfer. settings.transfer_abort_success = The repository transfer to %s was successfully canceled. settings.transfer_desc = Transfer this repository to a user or to an organization for which you have administrator rights. -settings.transfer_form_title = Enter the repository name as confirmation: +settings.enter_repo_name = Enter the repository name as confirmation: settings.transfer_in_progress = There is currently an ongoing transfer. Please cancel it if you will like to transfer this repository to another user. settings.transfer_notices_1 = - You will lose access to the repository if you transfer it to an individual user. settings.transfer_notices_2 = - You will keep access to the repository if you transfer it to an organization that you (co-)own. @@ -2160,6 +2161,13 @@ settings.trust_model.committer.desc = Valid signatures will only be marked "trus settings.trust_model.collaboratorcommitter = Collaborator+Committer settings.trust_model.collaboratorcommitter.long = Collaborator+Committer: Trust signatures by collaborators which match the committer settings.trust_model.collaboratorcommitter.desc = Valid signatures by collaborators of this repository will be marked "trusted" if they match the committer. Otherwise, valid signatures will be marked "untrusted" if the signature matches the committer and "unmatched" otherwise. This will force Forgejo to be marked as the committer on signed commits with the actual committer marked as Co-Authored-By: and Co-Committed-By: trailer in the commit. The default Forgejo key must match a User in the database. +settings.wiki_rename_branch_main = Normalize the Wiki branch name +settings.wiki_rename_branch_main_desc = Rename the branch used internally by the Wiki to "%s". This is a permanent and cannot be undone. +settings.wiki_rename_branch_main_notices_1 = This operation CANNOT be undone. +settings.wiki_rename_branch_main_notices_2 = This will premanently rename the the internal branch of %s's repository wiki. Existing checkouts will need to be updated. +settings.wiki_branch_rename_success = The repository wiki's branch name has been successfully normalized. +settings.wiki_branch_rename_failure = Failed to normalize the repository wiki's branch name. +settings.confirm_wiki_branch_rename = Rename the wiki branch settings.wiki_delete = Delete Wiki Data settings.wiki_delete_desc = Deleting repository wiki data is permanent and cannot be undone. settings.wiki_delete_notices_1 = - This will permanently delete and disable the repository wiki for %s. diff --git a/routers/api/packages/chef/auth.go b/routers/api/packages/chef/auth.go index 3aef8281a4..a790e9a363 100644 --- a/routers/api/packages/chef/auth.go +++ b/routers/api/packages/chef/auth.go @@ -8,6 +8,7 @@ import ( "crypto" "crypto/rsa" "crypto/sha1" + "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/pem" @@ -26,8 +27,6 @@ import ( chef_module "code.gitea.io/gitea/modules/packages/chef" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/auth" - - "github.com/minio/sha256-simd" ) const ( diff --git a/routers/api/packages/maven/maven.go b/routers/api/packages/maven/maven.go index 0b93382b01..5106395eb1 100644 --- a/routers/api/packages/maven/maven.go +++ b/routers/api/packages/maven/maven.go @@ -6,6 +6,7 @@ package maven import ( "crypto/md5" "crypto/sha1" + "crypto/sha256" "crypto/sha512" "encoding/hex" "encoding/xml" @@ -26,8 +27,6 @@ import ( maven_module "code.gitea.io/gitea/modules/packages/maven" "code.gitea.io/gitea/routers/api/packages/helper" packages_service "code.gitea.io/gitea/services/packages" - - "github.com/minio/sha256-simd" ) const ( diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 9609a064e7..58814d3b2e 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -6,7 +6,7 @@ // // This documentation describes the Gitea API. // -// Schemes: http, https +// Schemes: https, http // BasePath: /api/v1 // Version: {{AppVer | JSEscape | Safe}} // License: MIT http://opensource.org/licenses/MIT @@ -73,6 +73,7 @@ import ( actions_model "code.gitea.io/gitea/models/actions" auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" @@ -230,6 +231,39 @@ func repoAssignment() func(ctx *context.APIContext) { } } +// must be used within a group with a call to repoAssignment() to set ctx.Repo +func commentAssignment(idParam string) func(ctx *context.APIContext) { + return func(ctx *context.APIContext) { + comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(idParam)) + if err != nil { + if issues_model.IsErrCommentNotExist(err) { + ctx.NotFound(err) + } else { + ctx.InternalServerError(err) + } + return + } + + if err = comment.LoadIssue(ctx); err != nil { + ctx.InternalServerError(err) + return + } + if comment.Issue == nil || comment.Issue.RepoID != ctx.Repo.Repository.ID { + ctx.NotFound() + return + } + + if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) { + ctx.NotFound() + return + } + + comment.Issue.Repo = ctx.Repo.Repository + + ctx.Comment = comment + } +} + func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.APIContext) { return func(ctx *context.APIContext) { if ctx.Package.AccessMode < accessMode && !ctx.IsUserSiteAdmin() { @@ -1104,6 +1138,18 @@ func Routes() *web.Route { m.Get("/permission", repo.GetRepoPermissions) }) }, reqToken()) + if setting.Repository.EnableFlags { + m.Group("/flags", func() { + m.Combo("").Get(repo.ListFlags). + Put(bind(api.ReplaceFlagsOption{}), repo.ReplaceAllFlags). + Delete(repo.DeleteAllFlags) + m.Group("/{flag}", func() { + m.Combo("").Get(repo.HasFlag). + Put(repo.AddFlag). + Delete(repo.DeleteFlag) + }) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryAdmin), reqToken(), reqSiteAdmin()) + } m.Get("/assignees", reqToken(), reqAnyRepoReader(), repo.GetAssignees) m.Get("/reviewers", reqToken(), reqAnyRepoReader(), repo.GetReviewers) m.Group("/teams", func() { @@ -1223,8 +1269,16 @@ func Routes() *web.Route { Get(repo.GetPullReview). Delete(reqToken(), repo.DeletePullReview). Post(reqToken(), bind(api.SubmitPullReviewOptions{}), repo.SubmitPullReview) - m.Combo("/comments"). - Get(repo.GetPullReviewComments) + m.Group("/comments", func() { + m.Combo(""). + Get(repo.GetPullReviewComments). + Post(reqToken(), bind(api.CreatePullReviewCommentOptions{}), repo.CreatePullReviewComment) + m.Group("/{comment}", func() { + m.Combo(""). + Get(repo.GetPullReviewComment). + Delete(reqToken(), repo.DeletePullReviewComment) + }, commentAssignment("comment")) + }) m.Post("/dismissals", reqToken(), bind(api.DismissPullReviewOptions{}), repo.DismissPullReview) m.Post("/undismissals", reqToken(), repo.UnDismissPullReview) }) @@ -1328,7 +1382,7 @@ func Routes() *web.Route { Patch(reqToken(), mustNotBeArchived, bind(api.EditAttachmentOptions{}), repo.EditIssueCommentAttachment). Delete(reqToken(), mustNotBeArchived, repo.DeleteIssueCommentAttachment) }, mustEnableAttachments) - }) + }, commentAssignment(":id")) }) m.Group("/{index}", func() { m.Combo("").Get(repo.GetIssue). diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index 2f678a4549..7988dec8d3 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -262,7 +262,9 @@ func GetArchive(ctx *context.APIContext) { // --- // summary: Get an archive of a repository // produces: - // - application/json + // - application/octet-stream + // - application/zip + // - application/gzip // parameters: // - name: owner // in: path @@ -342,7 +344,17 @@ func download(ctx *context.APIContext, archiveName string, archiver *repo_model. } defer fr.Close() + contentType := "" + switch archiver.Type { + case git.ZIP: + contentType = "application/zip" + case git.TARGZ: + // Per RFC6713. + contentType = "application/gzip" + } + ctx.ServeContent(fr, &context.ServeHeaderOptions{ + ContentType: contentType, Filename: downloadName, LastModified: archiver.CreatedUnix.AsLocalTime(), }) diff --git a/routers/api/v1/repo/flags.go b/routers/api/v1/repo/flags.go new file mode 100644 index 0000000000..cbb2c95914 --- /dev/null +++ b/routers/api/v1/repo/flags.go @@ -0,0 +1,245 @@ +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "net/http" + + "code.gitea.io/gitea/modules/context" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/web" +) + +func ListFlags(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/flags repository repoListFlags + // --- + // summary: List a repository's flags + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/StringSlice" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + + repoFlags, err := ctx.Repo.Repository.ListFlags(ctx) + if err != nil { + ctx.InternalServerError(err) + return + } + + flags := make([]string, len(repoFlags)) + for i := range repoFlags { + flags[i] = repoFlags[i].Name + } + + ctx.SetTotalCountHeader(int64(len(repoFlags))) + ctx.JSON(http.StatusOK, flags) +} + +func ReplaceAllFlags(ctx *context.APIContext) { + // swagger:operation PUT /repos/{owner}/{repo}/flags repository repoReplaceAllFlags + // --- + // summary: Replace all flags of a repository + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/ReplaceFlagsOption" + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + + flagsForm := web.GetForm(ctx).(*api.ReplaceFlagsOption) + + if err := ctx.Repo.Repository.ReplaceAllFlags(ctx, flagsForm.Flags); err != nil { + ctx.InternalServerError(err) + return + } + + ctx.Status(http.StatusNoContent) +} + +func DeleteAllFlags(ctx *context.APIContext) { + // swagger:operation DELETE /repos/{owner}/{repo}/flags repository repoDeleteAllFlags + // --- + // summary: Remove all flags from a repository + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + + if err := ctx.Repo.Repository.ReplaceAllFlags(ctx, nil); err != nil { + ctx.InternalServerError(err) + return + } + + ctx.Status(http.StatusNoContent) +} + +func HasFlag(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/flags/{flag} repository repoCheckFlag + // --- + // summary: Check if a repository has a given flag + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: flag + // in: path + // description: name of the flag + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + + hasFlag := ctx.Repo.Repository.HasFlag(ctx, ctx.Params(":flag")) + if hasFlag { + ctx.Status(http.StatusNoContent) + } else { + ctx.NotFound() + } +} + +func AddFlag(ctx *context.APIContext) { + // swagger:operation PUT /repos/{owner}/{repo}/flags/{flag} repository repoAddFlag + // --- + // summary: Add a flag to a repository + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: flag + // in: path + // description: name of the flag + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + + flag := ctx.Params(":flag") + + if ctx.Repo.Repository.HasFlag(ctx, flag) { + ctx.Status(http.StatusNoContent) + return + } + + if err := ctx.Repo.Repository.AddFlag(ctx, flag); err != nil { + ctx.InternalServerError(err) + return + } + ctx.Status(http.StatusNoContent) +} + +func DeleteFlag(ctx *context.APIContext) { + // swagger:operation DELETE /repos/{owner}/{repo}/flags/{flag} repository repoDeleteFlag + // --- + // summary: Remove a flag from a repository + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: flag + // in: path + // description: name of the flag + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + + flag := ctx.Params(":flag") + + if _, err := ctx.Repo.Repository.DeleteFlag(ctx, flag); err != nil { + ctx.InternalServerError(err) + return + } + ctx.Status(http.StatusNoContent) +} diff --git a/routers/api/v1/repo/issue_comment.go b/routers/api/v1/repo/issue_comment.go index 99cd93f4be..6f70e6bcec 100644 --- a/routers/api/v1/repo/issue_comment.go +++ b/routers/api/v1/repo/issue_comment.go @@ -454,29 +454,7 @@ func GetIssueComment(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id")) - if err != nil { - if issues_model.IsErrCommentNotExist(err) { - ctx.NotFound(err) - } else { - ctx.Error(http.StatusInternalServerError, "GetCommentByID", err) - } - return - } - - if err = comment.LoadIssue(ctx); err != nil { - ctx.InternalServerError(err) - return - } - if comment.Issue.RepoID != ctx.Repo.Repository.ID { - ctx.Status(http.StatusNotFound) - return - } - - if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) { - ctx.NotFound() - return - } + comment := ctx.Comment if comment.Type != issues_model.CommentTypeComment { ctx.Status(http.StatusNoContent) @@ -587,25 +565,7 @@ func EditIssueCommentDeprecated(ctx *context.APIContext) { } func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption) { - comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id")) - if err != nil { - if issues_model.IsErrCommentNotExist(err) { - ctx.NotFound(err) - } else { - ctx.Error(http.StatusInternalServerError, "GetCommentByID", err) - } - return - } - - if err := comment.LoadIssue(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadIssue", err) - return - } - - if comment.Issue.RepoID != ctx.Repo.Repository.ID { - ctx.Status(http.StatusNotFound) - return - } + comment := ctx.Comment if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) { ctx.Status(http.StatusForbidden) @@ -617,7 +577,7 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption) return } - err = comment.LoadIssue(ctx) + err := comment.LoadIssue(ctx) if err != nil { ctx.Error(http.StatusInternalServerError, "LoadIssue", err) return @@ -668,7 +628,7 @@ func DeleteIssueComment(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - deleteIssueComment(ctx) + deleteIssueComment(ctx, issues_model.CommentTypeComment) } // DeleteIssueCommentDeprecated delete a comment from an issue @@ -707,39 +667,21 @@ func DeleteIssueCommentDeprecated(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - deleteIssueComment(ctx) + deleteIssueComment(ctx, issues_model.CommentTypeComment) } -func deleteIssueComment(ctx *context.APIContext) { - comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id")) - if err != nil { - if issues_model.IsErrCommentNotExist(err) { - ctx.NotFound(err) - } else { - ctx.Error(http.StatusInternalServerError, "GetCommentByID", err) - } - return - } - - if err := comment.LoadIssue(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadIssue", err) - return - } - - if comment.Issue.RepoID != ctx.Repo.Repository.ID { - ctx.Status(http.StatusNotFound) - return - } +func deleteIssueComment(ctx *context.APIContext, commentType issues_model.CommentType) { + comment := ctx.Comment if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) { ctx.Status(http.StatusForbidden) return - } else if comment.Type != issues_model.CommentTypeComment { + } else if comment.Type != commentType { ctx.Status(http.StatusNoContent) return } - if err = issue_service.DeleteComment(ctx, ctx.Doer, comment); err != nil { + if err := issue_service.DeleteComment(ctx, ctx.Doer, comment); err != nil { ctx.Error(http.StatusInternalServerError, "DeleteCommentByID", err) return } diff --git a/routers/api/v1/repo/issue_comment_attachment.go b/routers/api/v1/repo/issue_comment_attachment.go index 4f4b88750e..5622a9292a 100644 --- a/routers/api/v1/repo/issue_comment_attachment.go +++ b/routers/api/v1/repo/issue_comment_attachment.go @@ -55,11 +55,8 @@ func GetIssueCommentAttachment(ctx *context.APIContext) { // "404": // "$ref": "#/responses/error" - comment := getIssueCommentSafe(ctx) - if comment == nil { - return - } - attachment := getIssueCommentAttachmentSafeRead(ctx, comment) + comment := ctx.Comment + attachment := getIssueCommentAttachmentSafeRead(ctx) if attachment == nil { return } @@ -101,10 +98,7 @@ func ListIssueCommentAttachments(ctx *context.APIContext) { // "$ref": "#/responses/AttachmentList" // "404": // "$ref": "#/responses/error" - comment := getIssueCommentSafe(ctx) - if comment == nil { - return - } + comment := ctx.Comment if err := comment.LoadAttachments(ctx); err != nil { ctx.Error(http.StatusInternalServerError, "LoadAttachments", err) @@ -166,14 +160,12 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) { // "$ref": "#/responses/repoArchivedError" // Check if comment exists and load comment - comment := getIssueCommentSafe(ctx) - if comment == nil { + + if !canUserWriteIssueCommentAttachment(ctx) { return } - if !canUserWriteIssueCommentAttachment(ctx, comment) { - return - } + comment := ctx.Comment updatedAt := ctx.Req.FormValue("updated_at") if len(updatedAt) != 0 { @@ -341,42 +333,17 @@ func DeleteIssueCommentAttachment(ctx *context.APIContext) { ctx.Status(http.StatusNoContent) } -func getIssueCommentSafe(ctx *context.APIContext) *issues_model.Comment { - comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64("id")) - if err != nil { - ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err) - return nil - } - if err := comment.LoadIssue(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "comment.LoadIssue", err) - return nil - } - if comment.Issue == nil || comment.Issue.RepoID != ctx.Repo.Repository.ID { - ctx.Error(http.StatusNotFound, "", "no matching issue comment found") - return nil - } - - if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) { - return nil - } - - comment.Issue.Repo = ctx.Repo.Repository - - return comment -} - func getIssueCommentAttachmentSafeWrite(ctx *context.APIContext) *repo_model.Attachment { - comment := getIssueCommentSafe(ctx) - if comment == nil { + if !canUserWriteIssueCommentAttachment(ctx) { return nil } - if !canUserWriteIssueCommentAttachment(ctx, comment) { - return nil - } - return getIssueCommentAttachmentSafeRead(ctx, comment) + return getIssueCommentAttachmentSafeRead(ctx) } -func canUserWriteIssueCommentAttachment(ctx *context.APIContext, comment *issues_model.Comment) bool { +func canUserWriteIssueCommentAttachment(ctx *context.APIContext) bool { + // ctx.Comment is assumed to be set in a safe way via a middleware + comment := ctx.Comment + canEditComment := ctx.IsSigned && (ctx.Doer.ID == comment.PosterID || ctx.IsUserRepoAdmin() || ctx.IsUserSiteAdmin()) && ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull) if !canEditComment { ctx.Error(http.StatusForbidden, "", "user should have permission to edit comment") @@ -386,7 +353,10 @@ func canUserWriteIssueCommentAttachment(ctx *context.APIContext, comment *issues return true } -func getIssueCommentAttachmentSafeRead(ctx *context.APIContext, comment *issues_model.Comment) *repo_model.Attachment { +func getIssueCommentAttachmentSafeRead(ctx *context.APIContext) *repo_model.Attachment { + // ctx.Comment is assumed to be set in a safe way via a middleware + comment := ctx.Comment + attachment, err := repo_model.GetAttachmentByID(ctx, ctx.ParamsInt64("attachment_id")) if err != nil { ctx.NotFoundOrServerError("GetAttachmentByID", repo_model.IsErrAttachmentNotExist, err) diff --git a/routers/api/v1/repo/issue_reaction.go b/routers/api/v1/repo/issue_reaction.go index b14dc146d9..27edc1abad 100644 --- a/routers/api/v1/repo/issue_reaction.go +++ b/routers/api/v1/repo/issue_reaction.go @@ -51,30 +51,7 @@ func GetIssueCommentReactions(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id")) - if err != nil { - if issues_model.IsErrCommentNotExist(err) { - ctx.NotFound(err) - } else { - ctx.Error(http.StatusInternalServerError, "GetCommentByID", err) - } - return - } - - if err := comment.LoadIssue(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "comment.LoadIssue", err) - return - } - - if comment.Issue.RepoID != ctx.Repo.Repository.ID { - ctx.NotFound() - return - } - - if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) { - ctx.Error(http.StatusForbidden, "GetIssueCommentReactions", errors.New("no permission to get reactions")) - return - } + comment := ctx.Comment reactions, _, err := issues_model.FindCommentReactions(ctx, comment.IssueID, comment.ID) if err != nil { @@ -188,30 +165,7 @@ func DeleteIssueCommentReaction(ctx *context.APIContext) { } func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOption, isCreateType bool) { - comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id")) - if err != nil { - if issues_model.IsErrCommentNotExist(err) { - ctx.NotFound(err) - } else { - ctx.Error(http.StatusInternalServerError, "GetCommentByID", err) - } - return - } - - if err = comment.LoadIssue(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "comment.LoadIssue() failed", err) - return - } - - if comment.Issue.RepoID != ctx.Repo.Repository.ID { - ctx.NotFound() - return - } - - if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) { - ctx.NotFound() - return - } + comment := ctx.Comment if comment.Issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull) { ctx.Error(http.StatusForbidden, "ChangeIssueCommentReaction", errors.New("no permission to change reaction")) @@ -243,7 +197,7 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp }) } else { // DeleteIssueCommentReaction part - err = issues_model.DeleteCommentReaction(ctx, ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Reaction) + err := issues_model.DeleteCommentReaction(ctx, ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Reaction) if err != nil { ctx.Error(http.StatusInternalServerError, "DeleteCommentReaction", err) return diff --git a/routers/api/v1/repo/pull_review.go b/routers/api/v1/repo/pull_review.go index 07d8f4877b..ab8deab362 100644 --- a/routers/api/v1/repo/pull_review.go +++ b/routers/api/v1/repo/pull_review.go @@ -208,6 +208,152 @@ func GetPullReviewComments(ctx *context.APIContext) { ctx.JSON(http.StatusOK, apiComments) } +// GetPullReviewComment get a pull review comment +func GetPullReviewComment(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments/{comment} repository repoGetPullReviewComment + // --- + // summary: Get a pull review comment + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: index + // in: path + // description: index of the pull request + // type: integer + // format: int64 + // required: true + // - name: id + // in: path + // description: id of the review + // type: integer + // format: int64 + // required: true + // - name: comment + // in: path + // description: id of the comment + // type: integer + // format: int64 + // required: true + // responses: + // "200": + // "$ref": "#/responses/PullReviewComment" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + + review, _, statusSet := prepareSingleReview(ctx) + if statusSet { + return + } + + if err := ctx.Comment.LoadPoster(ctx); err != nil { + ctx.InternalServerError(err) + return + } + + apiComment, err := convert.ToPullReviewComment(ctx, review, ctx.Comment, ctx.Doer) + if err != nil { + ctx.InternalServerError(err) + return + } + + ctx.JSON(http.StatusOK, apiComment) +} + +// CreatePullReviewComments add a new comment to a pull request review +func CreatePullReviewComment(ctx *context.APIContext) { + // swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments repository repoCreatePullReviewComment + // --- + // summary: Add a new comment to a pull request review + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: index + // in: path + // description: index of the pull request + // type: integer + // format: int64 + // required: true + // - name: id + // in: path + // description: id of the review + // type: integer + // format: int64 + // required: true + // - name: body + // in: body + // required: true + // schema: + // "$ref": "#/definitions/CreatePullReviewCommentOptions" + // responses: + // "200": + // "$ref": "#/responses/PullReviewComment" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" + + opts := web.GetForm(ctx).(*api.CreatePullReviewCommentOptions) + + review, pr, statusSet := prepareSingleReview(ctx) + if statusSet { + return + } + + if err := pr.Issue.LoadRepo(ctx); err != nil { + ctx.InternalServerError(err) + return + } + + line := opts.NewLineNum + if opts.OldLineNum > 0 { + line = opts.OldLineNum * -1 + } + + comment, err := pull_service.CreateCodeCommentKnownReviewID(ctx, + ctx.Doer, + pr.Issue.Repo, + pr.Issue, + opts.Body, + opts.Path, + line, + review.ID, + ) + if err != nil { + ctx.InternalServerError(err) + return + } + + apiComment, err := convert.ToPullReviewComment(ctx, review, comment, ctx.Doer) + if err != nil { + ctx.InternalServerError(err) + return + } + + ctx.JSON(http.StatusOK, apiComment) +} + // DeletePullReview delete a specific review from a pull request func DeletePullReview(ctx *context.APIContext) { // swagger:operation DELETE /repos/{owner}/{repo}/pulls/{index}/reviews/{id} repository repoDeletePullReview @@ -868,6 +1014,53 @@ func UnDismissPullReview(ctx *context.APIContext) { dismissReview(ctx, "", false, false) } +// DeletePullReviewComment delete a pull review comment +func DeletePullReviewComment(ctx *context.APIContext) { + // swagger:operation DELETE /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments/{comment} repository repoDeletePullReviewComment + // --- + // summary: Delete a pull review comment + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: index + // in: path + // description: index of the pull request + // type: integer + // format: int64 + // required: true + // - name: id + // in: path + // description: id of the review + // type: integer + // format: int64 + // required: true + // - name: comment + // in: path + // description: id of the comment + // type: integer + // format: int64 + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + + deleteIssueComment(ctx, issues_model.CommentTypeCode) +} + func dismissReview(ctx *context.APIContext, msg string, isDismiss, dismissPriors bool) { if !ctx.Repo.IsAdmin() { ctx.Error(http.StatusForbidden, "", "Must be repo admin") diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 2efdccb569..b7ac5b4543 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -34,6 +34,7 @@ import ( "code.gitea.io/gitea/services/convert" "code.gitea.io/gitea/services/issue" repo_service "code.gitea.io/gitea/services/repository" + wiki_service "code.gitea.io/gitea/services/wiki" ) // Search repositories via options @@ -740,6 +741,18 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err repo.DefaultBranch = *opts.DefaultBranch } + // Wiki branch is updated if changed + if opts.WikiBranch != nil && repo.WikiBranch != *opts.WikiBranch { + if err := wiki_service.NormalizeWikiBranch(ctx, repo, *opts.WikiBranch); err != nil { + ctx.Error(http.StatusInternalServerError, "NormalizeWikiBranch", err) + return err + } + // While NormalizeWikiBranch updates the db, we need to update *this* + // instance of `repo`, so that the `UpdateRepository` below will not + // reset the branch back. + repo.WikiBranch = *opts.WikiBranch + } + if err := repo_service.UpdateRepository(ctx, repo, visibilityChanged); err != nil { ctx.Error(http.StatusInternalServerError, "UpdateRepository", err) return err @@ -984,7 +997,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { } if len(units)+len(deleteUnitTypes) > 0 { - if err := repo_service.UpdateRepositoryUnits(ctx, repo, units, deleteUnitTypes); err != nil { + if err := repo_model.UpdateRepositoryUnits(ctx, repo, units, deleteUnitTypes); err != nil { ctx.Error(http.StatusInternalServerError, "UpdateRepositoryUnits", err) return err } diff --git a/routers/api/v1/repo/tag.go b/routers/api/v1/repo/tag.go index 2f19f95e66..ad812ace56 100644 --- a/routers/api/v1/repo/tag.go +++ b/routers/api/v1/repo/tag.go @@ -176,7 +176,7 @@ func CreateTag(ctx *context.APIContext) { // schema: // "$ref": "#/definitions/CreateTagOption" // responses: - // "200": + // "201": // "$ref": "#/responses/Tag" // "404": // "$ref": "#/responses/notFound" diff --git a/routers/api/v1/repo/wiki.go b/routers/api/v1/repo/wiki.go index 4f27500496..ba3e978a83 100644 --- a/routers/api/v1/repo/wiki.go +++ b/routers/api/v1/repo/wiki.go @@ -203,7 +203,7 @@ func getWikiPage(ctx *context.APIContext, wikiName wiki_service.WebPath) *api.Wi } return &api.WikiPage{ - WikiPageMetaData: wiki_service.ToWikiPageMetaData(wikiName, lastCommit, ctx.Repo.Repository), + WikiPageMetaData: convert.ToWikiPageMetaData(wikiName, lastCommit, ctx.Repo.Repository), ContentBase64: content, CommitCount: commitsCount, Sidebar: sidebarContent, @@ -333,7 +333,7 @@ func ListWikiPages(ctx *context.APIContext) { ctx.Error(http.StatusInternalServerError, "WikiFilenameToName", err) return } - pages = append(pages, wiki_service.ToWikiPageMetaData(wikiName, c, ctx.Repo.Repository)) + pages = append(pages, convert.ToWikiPageMetaData(wikiName, c, ctx.Repo.Repository)) } ctx.SetTotalCountHeader(int64(len(entries))) diff --git a/routers/api/v1/shared/runners.go b/routers/api/v1/shared/runners.go index a342bd4b63..fe1584d2e7 100644 --- a/routers/api/v1/shared/runners.go +++ b/routers/api/v1/shared/runners.go @@ -12,7 +12,7 @@ import ( "code.gitea.io/gitea/modules/util" ) -// RegistrationToken is response related to registeration token +// RegistrationToken is a string used to register a runner with a server // swagger:response RegistrationToken type RegistrationToken struct { Token string `json:"token"` diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index b5efbe916d..2886b865e8 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -17,6 +17,9 @@ type swaggerParameterBodies struct { // in:body AddCollaboratorOption api.AddCollaboratorOption + // in:body + ReplaceFlagsOption api.ReplaceFlagsOption + // in:body CreateEmailOption api.CreateEmailOption // in:body @@ -158,6 +161,9 @@ type swaggerParameterBodies struct { // in:body CreatePullReviewComment api.CreatePullReviewComment + // in:body + CreatePullReviewCommentOptions api.CreatePullReviewCommentOptions + // in:body SubmitPullReviewOptions api.SubmitPullReviewOptions diff --git a/routers/api/v1/user/app.go b/routers/api/v1/user/app.go index f045fb4d5d..eb35d8031a 100644 --- a/routers/api/v1/user/app.go +++ b/routers/api/v1/user/app.go @@ -244,7 +244,7 @@ func CreateOauth2Application(ctx *context.APIContext) { // ListOauth2Applications list all the Oauth2 application func ListOauth2Applications(ctx *context.APIContext) { - // swagger:operation GET /user/applications/oauth2 user userGetOauth2Application + // swagger:operation GET /user/applications/oauth2 user userGetOAuth2Applications // --- // summary: List the authenticated user's oauth2 applications // produces: diff --git a/routers/install/install.go b/routers/install/install.go index 765d5d001c..5c43bd486a 100644 --- a/routers/install/install.go +++ b/routers/install/install.go @@ -358,6 +358,12 @@ func SubmitInstall(ctx *context.Context) { ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplInstall, form) return } + if len(form.AdminPasswd) < setting.MinPasswordLength { + ctx.Data["Err_Admin"] = true + ctx.Data["Err_AdminPasswd"] = true + ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplInstall, form) + return + } } // Init the engine with migration @@ -407,7 +413,7 @@ func SubmitInstall(ctx *context.Context) { cfg.Section("server").Key("LFS_START_SERVER").SetValue("true") cfg.Section("lfs").Key("PATH").SetValue(form.LFSRootPath) var lfsJwtSecret string - if _, lfsJwtSecret, err = generate.NewJwtSecretBase64(); err != nil { + if _, lfsJwtSecret, err = generate.NewJwtSecret(); err != nil { ctx.RenderWithErr(ctx.Tr("install.lfs_jwt_secret_failed", err), tplInstall, &form) return } diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 0e2fd92a28..91f631acac 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -33,6 +33,7 @@ import ( "code.gitea.io/gitea/services/externalaccount" "code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/mailer" + notify_service "code.gitea.io/gitea/services/notify" user_service "code.gitea.io/gitea/services/user" "github.com/markbates/goth" @@ -606,6 +607,7 @@ func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth. } } + notify_service.NewUserSignUp(ctx, u) // update external user information if gothUser != nil { if err := externalaccount.UpdateExternalUser(ctx, u, *gothUser); err != nil { @@ -651,13 +653,22 @@ func Activate(ctx *context.Context) { } // Resend confirmation email. if setting.Service.RegisterEmailConfirm { - if ctx.Cache.IsExist("MailResendLimit_" + ctx.Doer.LowerName) { + var cacheKey string + if ctx.Cache.IsExist("MailChangedJustNow_" + ctx.Doer.LowerName) { + cacheKey = "MailChangedLimit_" + if err := ctx.Cache.Delete("MailChangedJustNow_" + ctx.Doer.LowerName); err != nil { + log.Error("Delete cache(MailChangedJustNow) fail: %v", err) + } + } else { + cacheKey = "MailResendLimit_" + } + if ctx.Cache.IsExist(cacheKey + ctx.Doer.LowerName) { ctx.Data["ResendLimited"] = true } else { ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale) mailer.SendActivateAccountMail(ctx.Locale, ctx.Doer) - if err := ctx.Cache.Put("MailResendLimit_"+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil { + if err := ctx.Cache.Put(cacheKey+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil { log.Error("Set cache(MailResendLimit) fail: %v", err) } } @@ -691,6 +702,43 @@ func Activate(ctx *context.Context) { func ActivatePost(ctx *context.Context) { code := ctx.FormString("code") if len(code) == 0 { + email := ctx.FormString("email") + if len(email) > 0 { + ctx.Data["IsActivatePage"] = true + if ctx.Doer == nil || ctx.Doer.IsActive { + ctx.NotFound("invalid user", nil) + return + } + // Change the primary email + if setting.Service.RegisterEmailConfirm { + if ctx.Cache.IsExist("MailChangeLimit_" + ctx.Doer.LowerName) { + ctx.Data["ResendLimited"] = true + } else { + ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale) + err := user_service.ReplaceInactivePrimaryEmail(ctx, ctx.Doer.Email, &user_model.EmailAddress{ + UID: ctx.Doer.ID, + Email: email, + }) + if err != nil { + ctx.Data["IsActivatePage"] = false + log.Error("Couldn't replace inactive primary email of user %d: %v", ctx.Doer.ID, err) + ctx.RenderWithErr(ctx.Tr("auth.change_unconfirmed_email_error", err), TplActivate, nil) + return + } + if err := ctx.Cache.Put("MailChangeLimit_"+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil { + log.Error("Set cache(MailChangeLimit) fail: %v", err) + } + if err := ctx.Cache.Put("MailChangedJustNow_"+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil { + log.Error("Set cache(MailChangedJustNow) fail: %v", err) + } + + // Confirmation mail will be re-sent after the redirect to `/user/activate` below. + } + } else { + ctx.Data["ServiceNotEnabled"] = true + } + } + ctx.Redirect(setting.AppSubURL + "/user/activate") return } diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index dc106a1cc0..2adcb3aea0 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -952,10 +952,16 @@ func SignInOAuthCallback(ctx *context.Context) { return } else if !setting.Service.AllowOnlyInternalRegistration && setting.OAuth2Client.EnableAutoRegistration { // create new user with details from oauth2 provider - var missingFields []string if gothUser.UserID == "" { - missingFields = append(missingFields, "sub") + log.Error("OAuth2 Provider %s returned empty or missing field: UserID", authSource.Name) + if authSource.IsOAuth2() && authSource.Cfg.(*oauth2.Source).Provider == "openidConnect" { + log.Error("You may need to change the 'OPENID_CONNECT_SCOPES' setting to request all required fields") + } + err = fmt.Errorf("OAuth2 Provider %s returned empty or missing field: UserID", authSource.Name) + ctx.ServerError("CreateUser", err) + return } + var missingFields []string if gothUser.Email == "" { missingFields = append(missingFields, "email") } @@ -963,12 +969,10 @@ func SignInOAuthCallback(ctx *context.Context) { missingFields = append(missingFields, "nickname") } if len(missingFields) > 0 { - log.Error("OAuth2 Provider %s returned empty or missing fields: %s", authSource.Name, missingFields) - if authSource.IsOAuth2() && authSource.Cfg.(*oauth2.Source).Provider == "openidConnect" { - log.Error("You may need to change the 'OPENID_CONNECT_SCOPES' setting to request all required fields") - } - err = fmt.Errorf("OAuth2 Provider %s returned empty or missing fields: %s", authSource.Name, missingFields) - ctx.ServerError("CreateUser", err) + // we don't have enough information to create an account automatically, + // so we prompt the user for the remaining bits + log.Trace("OAuth2 Provider %s returned empty or missing fields: %s, prompting the user for them", authSource.Name, missingFields) + showLinkingLogin(ctx, gothUser) return } uname, err := getUserName(&gothUser) diff --git a/routers/web/feed/convert.go b/routers/web/feed/convert.go index 66b01d3680..3cb3e62245 100644 --- a/routers/web/feed/convert.go +++ b/routers/web/feed/convert.go @@ -21,6 +21,7 @@ import ( "code.gitea.io/gitea/modules/util" "github.com/gorilla/feeds" + "github.com/jaytaylor/html2text" ) func toBranchLink(ctx *context.Context, act *activities_model.Action) string { @@ -240,8 +241,15 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio content = desc } + // It's a common practice for feed generators to use plain text titles. + // See https://codeberg.org/forgejo/forgejo/pulls/1595 + plainTitle, err := html2text.FromString(title, html2text.Options{OmitLinks: true}) + if err != nil { + return nil, err + } + items = append(items, &feeds.Item{ - Title: title, + Title: plainTitle, Link: link, Description: desc, IsPermaLink: "false", diff --git a/routers/web/feed/render.go b/routers/web/feed/render.go index 8931dae8cc..41f9af1c8c 100644 --- a/routers/web/feed/render.go +++ b/routers/web/feed/render.go @@ -8,11 +8,12 @@ import ( ) // RenderBranchFeed render format for branch or file -func RenderBranchFeed(ctx *context.Context) { - _, _, showFeedType := GetFeedType(ctx.Params(":reponame"), ctx.Req) - if ctx.Repo.TreePath == "" { - ShowBranchFeed(ctx, ctx.Repo.Repository, showFeedType) - } else { - ShowFileFeed(ctx, ctx.Repo.Repository, showFeedType) +func RenderBranchFeed(feedType string) func(ctx *context.Context) { + return func(ctx *context.Context) { + if ctx.Repo.TreePath == "" { + ShowBranchFeed(ctx, ctx.Repo.Repository, feedType) + } else { + ShowFileFeed(ctx, ctx.Repo.Repository, feedType) + } } } diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 9cda30d23d..63e3a352a5 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -46,6 +46,20 @@ func View(ctx *context_module.Context) { ctx.HTML(http.StatusOK, tplViewActions) } +func ViewLatest(ctx *context_module.Context) { + run, err := actions_model.GetLatestRun(ctx, ctx.Repo.Repository.ID) + if err != nil { + ctx.NotFound("GetLatestRun", err) + return + } + err = run.LoadAttributes(ctx) + if err != nil { + ctx.ServerError("LoadAttributes", err) + return + } + ctx.Redirect(run.HTMLURL(), http.StatusTemporaryRedirect) +} + type ViewRequest struct { LogCursors []struct { Step int `json:"step"` diff --git a/routers/web/repo/badges/badges.go b/routers/web/repo/badges/badges.go new file mode 100644 index 0000000000..8fe99c7fc1 --- /dev/null +++ b/routers/web/repo/badges/badges.go @@ -0,0 +1,165 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package badges + +import ( + "fmt" + "net/url" + "strings" + + actions_model "code.gitea.io/gitea/models/actions" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" + context_module "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/setting" +) + +func getBadgeURL(ctx *context_module.Context, label, text, color string) string { + sb := &strings.Builder{} + _ = setting.Badges.GeneratorURLTemplateTemplate.Execute(sb, map[string]string{ + "label": url.PathEscape(label), + "text": url.PathEscape(text), + "color": url.PathEscape(color), + }) + + badgeURL := sb.String() + q := ctx.Req.URL.Query() + // Remove any `branch` or `event` query parameters. They're used by the + // workflow badge route, and do not need forwarding to the badge generator. + delete(q, "branch") + delete(q, "event") + if len(q) > 0 { + return fmt.Sprintf("%s?%s", badgeURL, q.Encode()) + } + return badgeURL +} + +func redirectToBadge(ctx *context_module.Context, label, text, color string) { + ctx.Redirect(getBadgeURL(ctx, label, text, color)) +} + +func errorBadge(ctx *context_module.Context, label, text string) { + ctx.Redirect(getBadgeURL(ctx, label, text, "crimson")) +} + +func GetWorkflowBadge(ctx *context_module.Context) { + branch := ctx.Req.URL.Query().Get("branch") + if branch == "" { + branch = ctx.Repo.Repository.DefaultBranch + } + branch = fmt.Sprintf("refs/heads/%s", branch) + event := ctx.Req.URL.Query().Get("event") + + workflowFile := ctx.Params("workflow_name") + run, err := actions_model.GetLatestRunForBranchAndWorkflow(ctx, ctx.Repo.Repository.ID, branch, workflowFile, event) + if err != nil { + errorBadge(ctx, workflowFile, "Not found") + return + } + + var color string + switch run.Status { + case actions_model.StatusUnknown: + color = "lightgrey" + case actions_model.StatusWaiting: + color = "lightgrey" + case actions_model.StatusRunning: + color = "gold" + case actions_model.StatusSuccess: + color = "brightgreen" + case actions_model.StatusFailure: + color = "crimson" + case actions_model.StatusCancelled: + color = "orange" + case actions_model.StatusSkipped: + color = "blue" + case actions_model.StatusBlocked: + color = "yellow" + default: + color = "lightgrey" + } + + redirectToBadge(ctx, workflowFile, run.Status.String(), color) +} + +func getIssueOrPullBadge(ctx *context_module.Context, label, variant string, num int) { + var text string + if len(variant) > 0 { + text = fmt.Sprintf("%d %s", num, variant) + } else { + text = fmt.Sprintf("%d", num) + } + redirectToBadge(ctx, label, text, "blue") +} + +func getIssueBadge(ctx *context_module.Context, variant string, num int) { + if !ctx.Repo.CanRead(unit.TypeIssues) && + !ctx.Repo.CanRead(unit.TypeExternalTracker) { + errorBadge(ctx, "issues", "Not found") + return + } + + _, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalTracker) + if err == nil { + errorBadge(ctx, "issues", "Not found") + return + } + + getIssueOrPullBadge(ctx, "issues", variant, num) +} + +func getPullBadge(ctx *context_module.Context, variant string, num int) { + if !ctx.Repo.Repository.CanEnablePulls() || !ctx.Repo.CanRead(unit.TypePullRequests) { + errorBadge(ctx, "pulls", "Not found") + return + } + + getIssueOrPullBadge(ctx, "pulls", variant, num) +} + +func GetOpenIssuesBadge(ctx *context_module.Context) { + getIssueBadge(ctx, "open", ctx.Repo.Repository.NumOpenIssues) +} + +func GetClosedIssuesBadge(ctx *context_module.Context) { + getIssueBadge(ctx, "closed", ctx.Repo.Repository.NumClosedIssues) +} + +func GetTotalIssuesBadge(ctx *context_module.Context) { + getIssueBadge(ctx, "", ctx.Repo.Repository.NumIssues) +} + +func GetOpenPullsBadge(ctx *context_module.Context) { + getPullBadge(ctx, "open", ctx.Repo.Repository.NumOpenPulls) +} + +func GetClosedPullsBadge(ctx *context_module.Context) { + getPullBadge(ctx, "closed", ctx.Repo.Repository.NumClosedPulls) +} + +func GetTotalPullsBadge(ctx *context_module.Context) { + getPullBadge(ctx, "", ctx.Repo.Repository.NumPulls) +} + +func GetStarsBadge(ctx *context_module.Context) { + redirectToBadge(ctx, "stars", fmt.Sprintf("%d", ctx.Repo.Repository.NumStars), "blue") +} + +func GetLatestReleaseBadge(ctx *context_module.Context) { + release, err := repo_model.GetLatestReleaseByRepoID(ctx, ctx.Repo.Repository.ID) + if err != nil { + if repo_model.IsErrReleaseNotExist(err) { + errorBadge(ctx, "release", "Not found") + return + } + ctx.ServerError("GetLatestReleaseByRepoID", err) + } + + if err := release.LoadAttributes(ctx); err != nil { + ctx.ServerError("LoadAttributes", err) + return + } + + redirectToBadge(ctx, "release", release.TagName, "blue") +} diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go index d414779a14..fa9dd8a08c 100644 --- a/routers/web/repo/blame.go +++ b/routers/web/repo/blame.go @@ -8,7 +8,6 @@ import ( gotemplate "html/template" "net/http" "net/url" - "strconv" "strings" user_model "code.gitea.io/gitea/models/user" @@ -39,32 +38,15 @@ type blameRow struct { // RefBlame render blame page func RefBlame(ctx *context.Context) { - fileName := ctx.Repo.TreePath - if len(fileName) == 0 { - ctx.NotFound("Blame FileName", nil) + if ctx.Repo.TreePath == "" { + ctx.NotFound("No file specified", nil) return } - branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() - treeLink := branchLink - rawLink := ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL() - - if len(ctx.Repo.TreePath) > 0 { - treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath) - } - - var treeNames []string paths := make([]string, 0, 5) - if len(ctx.Repo.TreePath) > 0 { - treeNames = strings.Split(ctx.Repo.TreePath, "/") - for i := range treeNames { - paths = append(paths, strings.Join(treeNames[:i+1], "/")) - } - - ctx.Data["HasParentPath"] = true - if len(paths)-2 >= 0 { - ctx.Data["ParentPath"] = "/" + paths[len(paths)-1] - } + treeNames := strings.Split(ctx.Repo.TreePath, "/") + for i := range treeNames { + paths = append(paths, strings.Join(treeNames[:i+1], "/")) } // Get current entry user currently looking at. @@ -73,47 +55,35 @@ func RefBlame(ctx *context.Context) { HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err) return } - blob := entry.Blob() - ctx.Data["Paths"] = paths - ctx.Data["TreeLink"] = treeLink - ctx.Data["TreeNames"] = treeNames - ctx.Data["BranchLink"] = branchLink - - ctx.Data["RawFileLink"] = rawLink + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) ctx.Data["PageIsViewCode"] = true - ctx.Data["IsBlame"] = true + ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() + ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) + ctx.Data["Paths"] = paths + ctx.Data["TreeNames"] = treeNames + ctx.Data["FileSize"] = blob.Size() ctx.Data["FileName"] = blob.Name() - ctx.Data["NumLines"], err = blob.GetBlobLineCount() ctx.Data["NumLinesSet"] = true - + ctx.Data["NumLines"], err = blob.GetBlobLineCount() if err != nil { - ctx.NotFound("GetBlobLineCount", err) + ctx.ServerError("GetBlobLineCount", err) return } - bypassBlameIgnore, _ := strconv.ParseBool(ctx.FormString("bypass-blame-ignore")) - - result, err := performBlame(ctx, ctx.Repo.Repository.RepoPath(), ctx.Repo.Commit, fileName, bypassBlameIgnore) + result, err := performBlame(ctx, ctx.Repo.Commit, ctx.Repo.TreePath, ctx.FormBool("bypass-blame-ignore")) if err != nil { - ctx.NotFound("CreateBlameReader", err) + ctx.ServerError("performBlame", err) return } ctx.Data["UsesIgnoreRevs"] = result.UsesIgnoreRevs ctx.Data["FaultyIgnoreRevsFile"] = result.FaultyIgnoreRevsFile - // Get Topics of this repo - renderRepoTopics(ctx) - if ctx.Written() { - return - } - commitNames := processBlameParts(ctx, result.Parts) if ctx.Written() { return @@ -130,12 +100,13 @@ type blameResult struct { FaultyIgnoreRevsFile bool } -func performBlame(ctx *context.Context, repoPath string, commit *git.Commit, file string, bypassBlameIgnore bool) (*blameResult, error) { +func performBlame(ctx *context.Context, commit *git.Commit, file string, bypassBlameIgnore bool) (*blameResult, error) { + repoPath := ctx.Repo.Repository.RepoPath() objectFormat, err := ctx.Repo.GitRepo.GetObjectFormat() if err != nil { - ctx.NotFound("CreateBlameReader", err) return nil, err } + blameReader, err := git.CreateBlameReader(ctx, objectFormat, repoPath, commit, file, bypassBlameIgnore) if err != nil { return nil, err diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index 32fa973ef6..3b6e482281 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -244,6 +244,22 @@ func FileHistory(ctx *context.Context) { ctx.ServerError("CommitsByFileAndRange", err) return } + oldestCommit := commits[len(commits)-1] + + renamedFiles, err := git.GetCommitFileRenames(ctx, ctx.Repo.GitRepo.Path, oldestCommit.ID.String()) + if err != nil { + ctx.ServerError("GetCommitFileRenames", err) + return + } + + for _, renames := range renamedFiles { + if renames[1] == fileName { + ctx.Data["OldFilename"] = renames[0] + ctx.Data["OldFilenameHistory"] = fmt.Sprintf("%s/commits/commit/%s/%s", ctx.Repo.RepoLink, oldestCommit.ID.String(), renames[0]) + break + } + } + ctx.Data["Commits"] = git_model.ConvertFromGitCommit(ctx, commits, ctx.Repo.Repository) ctx.Data["Username"] = ctx.Repo.Owner.Name diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index 85d40e7820..39d9967d02 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -14,6 +14,7 @@ import ( git_model "code.gitea.io/gitea/models/git" 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/charset" "code.gitea.io/gitea/modules/context" @@ -99,6 +100,27 @@ func getParentTreeFields(treePath string) (treeNames, treePaths []string) { return treeNames, treePaths } +// getSelectableEmailAddresses returns which emails can be used by the user as +// email for a Git commiter. +func getSelectableEmailAddresses(ctx *context.Context) ([]*user_model.ActivatedEmailAddress, error) { + // Retrieve emails that the user could use for commiter identity. + commitEmails, err := user_model.GetActivatedEmailAddresses(ctx, ctx.Doer.ID) + if err != nil { + return nil, fmt.Errorf("GetActivatedEmailAddresses: %w", err) + } + + // Allow for the placeholder mail to be used. Use -1 as ID to identify + // this entry to be the placerholder mail of the user. + placeholderMail := &user_model.ActivatedEmailAddress{ID: -1, Email: ctx.Doer.GetPlaceholderEmail()} + if ctx.Doer.KeepEmailPrivate { + commitEmails = append([]*user_model.ActivatedEmailAddress{placeholderMail}, commitEmails...) + } else { + commitEmails = append(commitEmails, placeholderMail) + } + + return commitEmails, nil +} + func editFile(ctx *context.Context, isNewFile bool) { ctx.Data["PageIsEdit"] = true ctx.Data["IsNewFile"] = isNewFile @@ -177,6 +199,12 @@ func editFile(ctx *context.Context, isNewFile bool) { treeNames = append(treeNames, fileName) } + commitEmails, err := getSelectableEmailAddresses(ctx) + if err != nil { + ctx.ServerError("getSelectableEmailAddresses", err) + return + } + ctx.Data["TreeNames"] = treeNames ctx.Data["TreePaths"] = treePaths ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() @@ -192,6 +220,8 @@ func editFile(ctx *context.Context, isNewFile bool) { ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",") ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, treePath) + ctx.Data["CommitMails"] = commitEmails + ctx.Data["DefaultCommitMail"] = ctx.Doer.GetEmail() ctx.HTML(http.StatusOK, tplEditFile) } @@ -227,6 +257,12 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b branchName = form.NewBranchName } + commitEmails, err := getSelectableEmailAddresses(ctx) + if err != nil { + ctx.ServerError("getSelectableEmailAddresses", err) + return + } + ctx.Data["PageIsEdit"] = true ctx.Data["PageHasPosted"] = true ctx.Data["IsNewFile"] = isNewFile @@ -243,6 +279,8 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",") ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, form.TreePath) + ctx.Data["CommitMails"] = commitEmails + ctx.Data["DefaultCommitMail"] = ctx.Doer.GetEmail() if ctx.HasError() { ctx.HTML(http.StatusOK, tplEditFile) @@ -277,6 +315,30 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b operation = "create" } + gitIdentity := &files_service.IdentityOptions{ + Name: ctx.Doer.Name, + } + + // -1 is defined as placeholder email. + if form.CommitMailID == -1 { + gitIdentity.Email = ctx.Doer.GetPlaceholderEmail() + } else { + // Check if the given email is activated. + email, err := user_model.GetEmailAddressByID(ctx, ctx.Doer.ID, form.CommitMailID) + if err != nil { + ctx.ServerError("GetEmailAddressByID", err) + return + } + + if email == nil || !email.IsActivated { + ctx.Data["Err_CommitMailID"] = true + ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_mail"), tplEditFile, &form) + return + } + + gitIdentity.Email = email.Email + } + if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{ LastCommitID: form.LastCommit, OldBranch: ctx.Repo.BranchName, @@ -290,7 +352,9 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b ContentReader: strings.NewReader(strings.ReplaceAll(form.Content, "\r", "")), }, }, - Signoff: form.Signoff, + Signoff: form.Signoff, + Author: gitIdentity, + Committer: gitIdentity, }); err != nil { // This is where we handle all the errors thrown by files_service.ChangeRepoFiles if git.IsErrNotExist(err) { diff --git a/routers/web/repo/flags/manage.go b/routers/web/repo/flags/manage.go new file mode 100644 index 0000000000..840f6c3773 --- /dev/null +++ b/routers/web/repo/flags/manage.go @@ -0,0 +1,49 @@ +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. +// SPDX-License-Identifier: MIT + +package flags + +import ( + "net/http" + + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" +) + +const ( + tplRepoFlags base.TplName = "repo/flags" +) + +func Manage(ctx *context.Context) { + ctx.Data["IsRepoFlagsPage"] = true + ctx.Data["Title"] = ctx.Tr("repo.admin.manage_flags") + + flags := map[string]bool{} + for _, f := range setting.Repository.SettableFlags { + flags[f] = false + } + repoFlags, _ := ctx.Repo.Repository.ListFlags(ctx) + for _, f := range repoFlags { + flags[f.Name] = true + } + + ctx.Data["Flags"] = flags + + ctx.HTML(http.StatusOK, tplRepoFlags) +} + +func ManagePost(ctx *context.Context) { + newFlags := ctx.FormStrings("flags") + + err := ctx.Repo.Repository.ReplaceAllFlags(ctx, newFlags) + if err != nil { + ctx.Flash.Error(ctx.Tr("repo.admin.failed_to_replace_flags")) + log.Error("Error replacing repository flags for repo %d: %v", ctx.Repo.Repository.ID, err) + } else { + ctx.Flash.Success(ctx.Tr("repo.admin.flags_replaced")) + } + + ctx.Redirect(ctx.Repo.Repository.HTMLURL() + "/flags") +} diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index fd9b125db1..62ebb4f382 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -2504,7 +2504,8 @@ func UpdatePullReviewRequest(ctx *context.Context) { func SearchIssues(ctx *context.Context) { before, since, err := context.GetQueryBeforeSince(ctx.Base) if err != nil { - ctx.Error(http.StatusUnprocessableEntity, err.Error()) + log.Error("GetQueryBeforeSince: %v", err) + ctx.Error(http.StatusUnprocessableEntity, "invalid before or since") return } @@ -2541,10 +2542,11 @@ func SearchIssues(ctx *context.Context) { if ctx.FormString("owner") != "" { owner, err := user_model.GetUserByName(ctx, ctx.FormString("owner")) if err != nil { + log.Error("GetUserByName: %v", err) if user_model.IsErrUserNotExist(err) { ctx.Error(http.StatusBadRequest, "Owner not found", err.Error()) } else { - ctx.Error(http.StatusInternalServerError, "GetUserByName", err.Error()) + ctx.Error(http.StatusInternalServerError) } return } @@ -2555,15 +2557,16 @@ func SearchIssues(ctx *context.Context) { } if ctx.FormString("team") != "" { if ctx.FormString("owner") == "" { - ctx.Error(http.StatusBadRequest, "", "Owner organisation is required for filtering on team") + ctx.Error(http.StatusBadRequest, "Owner organisation is required for filtering on team") return } team, err := organization.GetTeam(ctx, opts.OwnerID, ctx.FormString("team")) if err != nil { + log.Error("GetTeam: %v", err) if organization.IsErrTeamNotExist(err) { - ctx.Error(http.StatusBadRequest, "Team not found", err.Error()) + ctx.Error(http.StatusBadRequest) } else { - ctx.Error(http.StatusInternalServerError, "GetUserByName", err.Error()) + ctx.Error(http.StatusInternalServerError) } return } @@ -2576,7 +2579,8 @@ func SearchIssues(ctx *context.Context) { } repoIDs, _, err = repo_model.SearchRepositoryIDs(ctx, opts) if err != nil { - ctx.Error(http.StatusInternalServerError, "SearchRepositoryIDs", err.Error()) + log.Error("SearchRepositoryIDs: %v", err) + ctx.Error(http.StatusInternalServerError) return } if len(repoIDs) == 0 { @@ -2610,7 +2614,8 @@ func SearchIssues(ctx *context.Context) { } includedAnyLabels, err = issues_model.GetLabelIDsByNames(ctx, includedLabelNames) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetLabelIDsByNames", err.Error()) + log.Error("GetLabelIDsByNames: %v", err) + ctx.Error(http.StatusInternalServerError) return } } @@ -2624,7 +2629,8 @@ func SearchIssues(ctx *context.Context) { } includedMilestones, err = issues_model.GetMilestoneIDsByNames(ctx, includedMilestoneNames) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetMilestoneIDsByNames", err.Error()) + log.Error("GetMilestoneIDsByNames: %v", err) + ctx.Error(http.StatusInternalServerError) return } } @@ -2691,12 +2697,14 @@ func SearchIssues(ctx *context.Context) { ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt) if err != nil { - ctx.Error(http.StatusInternalServerError, "SearchIssues", err.Error()) + log.Error("SearchIssues: %v", err) + ctx.Error(http.StatusInternalServerError) return } issues, err := issues_model.GetIssuesByIDs(ctx, ids, true) if err != nil { - ctx.Error(http.StatusInternalServerError, "FindIssuesByIDs", err.Error()) + log.Error("GetIssuesByIDs: %v", err) + ctx.Error(http.StatusInternalServerError) return } diff --git a/routers/web/repo/issue_content_history.go b/routers/web/repo/issue_content_history.go index 0f376db145..31e6ac608c 100644 --- a/routers/web/repo/issue_content_history.go +++ b/routers/web/repo/issue_content_history.go @@ -94,6 +94,8 @@ func canSoftDeleteContentHistory(ctx *context.Context, issue *issues_model.Issue // CanWrite means the doer can manage the issue/PR list if ctx.Repo.IsOwner() || ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) { canSoftDelete = true + } else if ctx.Doer == nil { + canSoftDelete = false } else { // for read-only users, they could still post issues or comments, // they should be able to delete the history related to their own issue/comment, a case is: diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 43784f0070..52c4cf8685 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -967,6 +967,18 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi return } + // determine if the user viewing the pull request can edit the head branch + if ctx.Doer != nil && pull.HeadRepo != nil && !pull.HasMerged { + headRepoPerm, err := access_model.GetUserRepoPermission(ctx, pull.HeadRepo, ctx.Doer) + if err != nil { + ctx.ServerError("GetUserRepoPermission", err) + return + } + ctx.Data["HeadBranchIsEditable"] = pull.HeadRepo.CanEnableEditor() && issues_model.CanMaintainerWriteToBranch(ctx, headRepoPerm, pull.HeadBranch, ctx.Doer) + ctx.Data["SourceRepoLink"] = pull.HeadRepo.Link() + ctx.Data["HeadBranch"] = pull.HeadBranch + } + if ctx.IsSigned && ctx.Doer != nil { if ctx.Data["CanMarkConversation"], err = issues_model.CanMarkConversation(ctx, issue, ctx.Doer); err != nil { ctx.ServerError("CanMarkConversation", err) diff --git a/routers/web/repo/pull_review.go b/routers/web/repo/pull_review.go index a6b3bd1c8d..e399176a4a 100644 --- a/routers/web/repo/pull_review.go +++ b/routers/web/repo/pull_review.go @@ -153,19 +153,12 @@ func UpdateResolveConversation(ctx *context.Context) { } func renderConversation(ctx *context.Context, comment *issues_model.Comment, origin string) { - ctx.Data["PageIsPullFiles"] = origin == "diff" - - comments, err := issues_model.FetchCodeCommentsByLine(ctx, comment.Issue, ctx.Doer, comment.TreePath, comment.Line, ctx.Data["ShowOutdatedComments"].(bool)) + comments, err := issues_model.FetchCodeCommentsByLine(ctx, comment.Issue, ctx.Doer, comment.TreePath, comment.Line, true) if err != nil { ctx.ServerError("FetchCodeCommentsByLine", err) return } - if len(comments) == 0 { - // if the comments are empty (deleted, outdated, etc), it doesn't need to render anything, just return an empty body to replace "conversation-holder" on the page - ctx.Resp.WriteHeader(http.StatusOK) - return - } - + ctx.Data["PageIsPullFiles"] = (origin == "diff") ctx.Data["comments"] = comments if ctx.Data["CanMarkConversation"], err = issues_model.CanMarkConversation(ctx, comment.Issue, ctx.Doer); err != nil { ctx.ServerError("CanMarkConversation", err) @@ -186,8 +179,6 @@ func renderConversation(ctx *context.Context, comment *issues_model.Comment, ori ctx.HTML(http.StatusOK, tplDiffConversation) } else if origin == "timeline" { ctx.HTML(http.StatusOK, tplTimelineConversation) - } else { - ctx.Error(http.StatusBadRequest, "Unknown origin: "+origin) } } diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go index fdb247d413..4d139f2b79 100644 --- a/routers/web/repo/release.go +++ b/routers/web/repo/release.go @@ -397,7 +397,11 @@ func NewReleasePost(ctx *context.Context) { return } - if !ctx.Repo.GitRepo.IsBranchExist(form.Target) { + objectFormat, _ := ctx.Repo.GitRepo.GetObjectFormat() + + // form.Target can be a branch name or a full commitID. + if !ctx.Repo.GitRepo.IsBranchExist(form.Target) && + len(form.Target) == objectFormat.FullLength() && !ctx.Repo.GitRepo.IsCommitExist(form.Target) { ctx.RenderWithErr(ctx.Tr("form.target_branch_not_exist"), tplReleaseNew, &form) return } diff --git a/routers/web/repo/setting/default_branch.go b/routers/web/repo/setting/default_branch.go index c8a576e576..9bf54e706a 100644 --- a/routers/web/repo/setting/default_branch.go +++ b/routers/web/repo/setting/default_branch.go @@ -6,12 +6,13 @@ package setting import ( "net/http" - git_model "code.gitea.io/gitea/models/git" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/routers/web/repo" - repo_service "code.gitea.io/gitea/services/repository" + notify_service "code.gitea.io/gitea/services/notify" ) // SetDefaultBranchPost set default branch @@ -34,14 +35,23 @@ func SetDefaultBranchPost(ctx *context.Context) { } branch := ctx.FormString("branch") - if err := repo_service.SetRepoDefaultBranch(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, branch); err != nil { - switch { - case git_model.IsErrBranchNotExist(err): - ctx.Status(http.StatusNotFound) - default: - ctx.ServerError("SetDefaultBranch", err) - } + if !ctx.Repo.GitRepo.IsBranchExist(branch) { + ctx.Status(http.StatusNotFound) return + } else if repo.DefaultBranch != branch { + repo.DefaultBranch = branch + if err := ctx.Repo.GitRepo.SetDefaultBranch(branch); err != nil { + if !git.IsErrUnsupportedVersion(err) { + ctx.ServerError("SetDefaultBranch", err) + return + } + } + if err := repo_model.UpdateDefaultBranch(ctx, repo); err != nil { + ctx.ServerError("SetDefaultBranch", err) + return + } + + notify_service.ChangeDefaultBranch(ctx, repo) } log.Trace("Repository basic settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name) diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index 6851d76df9..552507e57c 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -474,10 +474,17 @@ func SettingsPost(ctx *context.Context) { }) deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki) } else if form.EnableWiki && !form.EnableExternalWiki && !unit_model.TypeWiki.UnitGlobalDisabled() { + var wikiPermissions repo_model.UnitAccessMode + if form.GloballyWriteableWiki { + wikiPermissions = repo_model.UnitAccessModeWrite + } else { + wikiPermissions = repo_model.UnitAccessModeRead + } units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: unit_model.TypeWiki, - Config: new(repo_model.UnitConfig), + RepoID: repo.ID, + Type: unit_model.TypeWiki, + Config: new(repo_model.UnitConfig), + DefaultPermissions: wikiPermissions, }) deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki) } else { @@ -595,7 +602,7 @@ func SettingsPost(ctx *context.Context) { return } - if err := repo_service.UpdateRepositoryUnits(ctx, repo, units, deleteUnitTypes); err != nil { + if err := repo_model.UpdateRepositoryUnits(ctx, repo, units, deleteUnitTypes); err != nil { ctx.ServerError("UpdateRepositoryUnits", err) return } @@ -868,6 +875,27 @@ func SettingsPost(ctx *context.Context) { ctx.Flash.Success(ctx.Tr("repo.settings.wiki_deletion_success")) ctx.Redirect(ctx.Repo.RepoLink + "/settings") + case "rename-wiki-branch": + if !ctx.Repo.IsOwner() { + ctx.Error(http.StatusNotFound) + return + } + if repo.FullName() != form.RepoName { + ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) + return + } + + if err := wiki_service.NormalizeWikiBranch(ctx, repo, setting.Repository.DefaultBranch); err != nil { + log.Error("Normalize Wiki branch: %v", err.Error()) + ctx.Flash.Error(ctx.Tr("repo.settings.wiki_branch_rename_failure")) + ctx.Redirect(ctx.Repo.RepoLink + "/settings") + return + } + log.Trace("Repository wiki normalized: %s#%s", repo.FullName(), setting.Repository.DefaultBranch) + + ctx.Flash.Success(ctx.Tr("repo.settings.wiki_branch_rename_success")) + ctx.Redirect(ctx.Repo.RepoLink + "/settings") + case "archive": if !ctx.Repo.IsOwner() { ctx.Error(http.StatusForbidden) diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index af3021da11..1dda3a05bc 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -166,7 +166,7 @@ func renderDirectory(ctx *context.Context) { if ctx.Repo.TreePath != "" { ctx.Data["HideRepoInfo"] = true - ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefName) + ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+ctx.Repo.TreePath, ctx.Repo.RefName) } subfolder, readmeFile, err := findReadmeFileInEntries(ctx, entries, true) @@ -381,7 +381,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) { } defer dataRc.Close() - ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefName) + ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+ctx.Repo.TreePath, ctx.Repo.RefName) ctx.Data["FileIsSymlink"] = entry.IsLink() ctx.Data["FileName"] = blob.Name() ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) @@ -747,7 +747,7 @@ func checkHomeCodeViewable(ctx *context.Context) { } unit, ok := unit_model.Units[repoUnit.Type] - if ok && (firstUnit == nil || !firstUnit.IsLessThan(unit)) { + if ok && (firstUnit == nil || !firstUnit.IsLessThan(unit)) && repoUnit.Type.CanBeDefault() { firstUnit = &unit } } @@ -794,12 +794,19 @@ func Home(ctx *context.Context) { if setting.Other.EnableFeed { isFeed, _, showFeedType := feed.GetFeedType(ctx.Params(":reponame"), ctx.Req) if isFeed { - switch { - case ctx.Link == fmt.Sprintf("%s.%s", ctx.Repo.RepoLink, showFeedType): + if ctx.Link == fmt.Sprintf("%s.%s", ctx.Repo.RepoLink, showFeedType) { feed.ShowRepoFeed(ctx, ctx.Repo.Repository, showFeedType) - case ctx.Repo.TreePath == "": + return + } + + if ctx.Repo.Repository.IsEmpty { + ctx.NotFound("MustBeNotEmpty", nil) + return + } + + if ctx.Repo.TreePath == "" { feed.ShowBranchFeed(ctx, ctx.Repo.Repository, showFeedType) - case ctx.Repo.TreePath != "": + } else { feed.ShowFileFeed(ctx, ctx.Repo.Repository, showFeedType) } return @@ -1028,20 +1035,79 @@ func renderCode(ctx *context.Context) { return } - showRecentlyPushedNewBranches := true - if ctx.Repo.Repository.IsMirror || - !ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypePullRequests) { - showRecentlyPushedNewBranches = false + // If the repo is a mirror, don't display recently pushed branches. + if ctx.Repo.Repository.IsMirror { + goto PostRecentBranchCheck } - if showRecentlyPushedNewBranches { - ctx.Data["RecentlyPushedNewBranches"], err = git_model.FindRecentlyPushedNewBranches(ctx, ctx.Repo.Repository.ID, ctx.Doer.ID, ctx.Repo.Repository.DefaultBranch) - if err != nil { - ctx.ServerError("GetRecentlyPushedBranches", err) - return + + // If pull requests aren't enabled for either the current repo, or its + // base, don't display recently pushed branches. + if !(ctx.Repo.Repository.AllowsPulls(ctx) || + (ctx.Repo.Repository.BaseRepo != nil && ctx.Repo.Repository.BaseRepo.AllowsPulls(ctx))) { + goto PostRecentBranchCheck + } + + // Find recently pushed new branches to *this* repo. + branches, err := git_model.FindRecentlyPushedNewBranches(ctx, ctx.Repo.Repository.ID, ctx.Doer.ID, ctx.Repo.Repository.DefaultBranch) + if err != nil { + ctx.ServerError("FindRecentlyPushedBranches", err) + return + } + + // If this is not a fork, check if the signed in user has a fork, and + // check branches there. + if !ctx.Repo.Repository.IsFork { + repo := repo_model.GetForkedRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID) + if repo != nil { + baseBranches, err := git_model.FindRecentlyPushedNewBranches(ctx, repo.ID, ctx.Doer.ID, repo.DefaultBranch) + if err != nil { + ctx.ServerError("FindRecentlyPushedBranches", err) + return + } + branches = append(branches, baseBranches...) } } + + // Filter out branches that have no relation to the default branch of + // the repository. + var filteredBranches []*git_model.Branch + for _, branch := range branches { + repo, err := branch.GetRepo(ctx) + if err != nil { + continue + } + gitRepo, err := git.OpenRepository(ctx, repo.RepoPath()) + if err != nil { + continue + } + defer gitRepo.Close() + head, err := gitRepo.GetCommit(branch.CommitID) + if err != nil { + continue + } + defaultBranch, err := gitRepo.GetDefaultBranch() + if err != nil { + continue + } + defaultBranchHead, err := gitRepo.GetCommit(defaultBranch) + if err != nil { + continue + } + + hasMergeBase, err := head.HasPreviousCommit(defaultBranchHead.ID) + if err != nil { + continue + } + + if hasMergeBase { + filteredBranches = append(filteredBranches, branch) + } + } + + ctx.Data["RecentlyPushedNewBranches"] = filteredBranches } +PostRecentBranchCheck: var treeNames []string paths := make([]string, 0, 5) if len(ctx.Repo.TreePath) > 0 { diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index 5e7b971e67..2c445fe7fa 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -99,7 +99,7 @@ func findWikiRepoCommit(ctx *context.Context) (*git.Repository, *git.Commit, err return nil, nil, err } - commit, err := wikiRepo.GetBranchCommit(wiki_service.DefaultBranch) + commit, err := wikiRepo.GetBranchCommit(ctx.Repo.Repository.GetWikiBranchName()) if err != nil { return wikiRepo, nil, err } @@ -316,7 +316,7 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { } // get commit count - wiki revisions - commitsCount, _ := wikiRepo.FileCommitsCount(wiki_service.DefaultBranch, pageFilename) + commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.GetWikiBranchName(), pageFilename) ctx.Data["CommitCount"] = commitsCount return wikiRepo, entry @@ -368,7 +368,7 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) ctx.Data["footerContent"] = "" // get commit count - wiki revisions - commitsCount, _ := wikiRepo.FileCommitsCount(wiki_service.DefaultBranch, pageFilename) + commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.GetWikiBranchName(), pageFilename) ctx.Data["CommitCount"] = commitsCount // get page @@ -380,7 +380,7 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) // get Commit Count commitsHistory, err := wikiRepo.CommitsByFileAndRange( git.CommitsByFileAndRangeOptions{ - Revision: wiki_service.DefaultBranch, + Revision: ctx.Repo.Repository.GetWikiBranchName(), File: pageFilename, Page: page, }) diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go index 43827c0b30..203e5fa5a5 100644 --- a/routers/web/shared/user/header.go +++ b/routers/web/shared/user/header.go @@ -98,7 +98,7 @@ func FindUserProfileReadme(ctx *context.Context, doer *user_model.User) (profile if commit, err := profileGitRepo.GetBranchCommit(profileDbRepo.DefaultBranch); err != nil { log.Error("FindUserProfileReadme failed to GetBranchCommit: %v", err) } else { - profileReadmeBlob, _ = commit.GetBlobByPath("README.md") + profileReadmeBlob, _ = commit.GetBlobByFoldedPath("README.md") } } } diff --git a/routers/web/user/home.go b/routers/web/user/home.go index 44920817c9..79af74eb02 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -715,12 +715,15 @@ func UsernameSubRoute(ctx *context.Context) { reloadParam := func(suffix string) (success bool) { ctx.SetParams("username", strings.TrimSuffix(username, suffix)) context_service.UserAssignmentWeb()(ctx) + if ctx.Written() { + return false + } // check view permissions if !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) { ctx.NotFound("user", fmt.Errorf(ctx.ContextUser.Name)) return false } - return !ctx.Written() + return true } switch { case strings.HasSuffix(username, ".png"): diff --git a/routers/web/web.go b/routers/web/web.go index 6854c55210..1744ddb83a 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -37,6 +37,8 @@ import ( org_setting "code.gitea.io/gitea/routers/web/org/setting" "code.gitea.io/gitea/routers/web/repo" "code.gitea.io/gitea/routers/web/repo/actions" + "code.gitea.io/gitea/routers/web/repo/badges" + repo_flags "code.gitea.io/gitea/routers/web/repo/flags" repo_setting "code.gitea.io/gitea/routers/web/repo/setting" "code.gitea.io/gitea/routers/web/user" user_setting "code.gitea.io/gitea/routers/web/user/setting" @@ -49,17 +51,12 @@ import ( _ "code.gitea.io/gitea/modules/session" // to registers all internal adapters "gitea.com/go-chi/captcha" - "github.com/NYTimes/gziphandler" chi_middleware "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" + "github.com/klauspost/compress/gzhttp" "github.com/prometheus/client_golang/prometheus" ) -const ( - // GzipMinSize represents min size to compress for the body size of response - GzipMinSize = 1400 -) - // optionsCorsHandler return a http handler which sets CORS options if enabled by config, it blocks non-CORS OPTIONS requests. func optionsCorsHandler() func(next http.Handler) http.Handler { var corsHandler func(next http.Handler) http.Handler @@ -245,11 +242,11 @@ func Routes() *web.Route { var mid []any if setting.EnableGzip { - h, err := gziphandler.GzipHandlerWithOpts(gziphandler.MinSize(GzipMinSize)) + wrapper, err := gzhttp.NewWrapper(gzhttp.RandomJitter(32, 0, false)) if err != nil { - log.Fatal("GzipHandlerWithOpts failed: %v", err) + log.Fatal("gzhttp.NewWrapper failed: %v", err) } - mid = append(mid, h) + mid = append(mid, wrapper) } if setting.Service.EnableCaptcha { @@ -686,7 +683,9 @@ func registerRoutes(m *web.Route) { m.Get("", admin.Dashboard) m.Post("", web.Bind(forms.AdminDashboardForm{}), admin.DashboardPost) - m.Get("/self_check", admin.SelfCheck) + if setting.Database.Type.IsMySQL() || setting.Database.Type.IsMSSQL() { + m.Get("/self_check", admin.SelfCheck) + } m.Group("/config", func() { m.Get("", admin.Config) @@ -1249,7 +1248,7 @@ func registerRoutes(m *web.Route) { Post(web.Bind(forms.UploadRepoFileForm{}), repo.UploadFilePost) m.Combo("/_diffpatch/*").Get(repo.NewDiffPatch). Post(web.Bind(forms.EditRepoFileForm{}), repo.NewDiffPatchPost) - m.Combo("/_cherrypick/{sha:([a-f0-9]{7,64})}/*").Get(repo.CherryPick). + m.Combo("/_cherrypick/{sha:([a-f0-9]{4,64})}/*").Get(repo.CherryPick). Post(web.Bind(forms.CherryPickForm{}), repo.CherryPickPost) }, repo.MustBeEditable) m.Group("", func() { @@ -1334,6 +1333,24 @@ func registerRoutes(m *web.Route) { m.Get("/packages", repo.Packages) } + if setting.Badges.Enabled { + m.Group("/badges", func() { + m.Get("/workflows/{workflow_name}/badge.svg", badges.GetWorkflowBadge) + m.Group("/issues", func() { + m.Get(".svg", badges.GetTotalIssuesBadge) + m.Get("/open.svg", badges.GetOpenIssuesBadge) + m.Get("/closed.svg", badges.GetClosedIssuesBadge) + }) + m.Group("/pulls", func() { + m.Get(".svg", badges.GetTotalPullsBadge) + m.Get("/open.svg", badges.GetOpenPullsBadge) + m.Get("/closed.svg", badges.GetClosedPullsBadge) + }) + m.Get("/stars.svg", badges.GetStarsBadge) + m.Get("/release.svg", badges.GetLatestReleaseBadge) + }) + } + m.Group("/projects", func() { m.Get("", repo.Projects) m.Get("/{id}", repo.ViewProject) @@ -1365,23 +1382,28 @@ func registerRoutes(m *web.Route) { m.Post("/disable", reqRepoAdmin, actions.DisableWorkflowFile) m.Post("/enable", reqRepoAdmin, actions.EnableWorkflowFile) - m.Group("/runs/{run}", func() { - m.Combo(""). - Get(actions.View). - Post(web.Bind(actions.ViewRequest{}), actions.ViewPost) - m.Group("/jobs/{job}", func() { + m.Group("/runs", func() { + m.Get("/latest", actions.ViewLatest) + m.Group("/{run}", func() { m.Combo(""). Get(actions.View). Post(web.Bind(actions.ViewRequest{}), actions.ViewPost) + m.Group("/jobs/{job}", func() { + m.Combo(""). + Get(actions.View). + Post(web.Bind(actions.ViewRequest{}), actions.ViewPost) + m.Post("/rerun", reqRepoActionsWriter, actions.Rerun) + m.Get("/logs", actions.Logs) + }) + m.Post("/cancel", reqRepoActionsWriter, actions.Cancel) + m.Post("/approve", reqRepoActionsWriter, actions.Approve) + m.Post("/artifacts", actions.ArtifactsView) + m.Get("/artifacts/{artifact_name}", actions.ArtifactsDownloadView) m.Post("/rerun", reqRepoActionsWriter, actions.Rerun) - m.Get("/logs", actions.Logs) }) - m.Post("/cancel", reqRepoActionsWriter, actions.Cancel) - m.Post("/approve", reqRepoActionsWriter, actions.Approve) - m.Post("/artifacts", actions.ArtifactsView) - m.Get("/artifacts/{artifact_name}", actions.ArtifactsDownloadView) - m.Post("/rerun", reqRepoActionsWriter, actions.Rerun) }) + + m.Get("/workflows/{workflow_name}/badge.svg", badges.GetWorkflowBadge) }, reqRepoActionsReader, actions.MustEnableActions) m.Group("/wiki", func() { @@ -1391,8 +1413,8 @@ func registerRoutes(m *web.Route) { m.Combo("/*"). Get(repo.Wiki). Post(context.RepoMustNotBeArchived(), reqSignIn, reqRepoWikiWriter, web.Bind(forms.NewWikiForm{}), repo.WikiPost) - 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("/commit/{sha:[a-f0-9]{4,64}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff) + m.Get("/commit/{sha:[a-f0-9]{4,64}}.{ext:patch|diff}", repo.RawDiff) }, repo.MustEnableWiki, func(ctx *context.Context) { ctx.Data["PageIsWiki"] = true ctx.Data["CloneButtonOriginLink"] = ctx.Repo.Repository.WikiCloneLink() @@ -1452,7 +1474,7 @@ func registerRoutes(m *web.Route) { m.Group("/commits", func() { m.Get("", context.RepoRef(), repo.SetWhitespaceBehavior, repo.GetPullDiffStats, repo.ViewPullCommits) m.Get("/list", context.RepoRef(), repo.GetPullCommits) - m.Get("/{sha:[a-f0-9]{7,40}}", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForSingleCommit) + m.Get("/{sha:[a-f0-9]{4,40}}", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForSingleCommit) }) m.Post("/merge", context.RepoMustNotBeArchived(), web.Bind(forms.MergePullRequestForm{}), repo.MergePullRequest) m.Post("/cancel_auto_merge", context.RepoMustNotBeArchived(), repo.CancelAutoMergePullRequest) @@ -1461,8 +1483,8 @@ func registerRoutes(m *web.Route) { m.Post("/cleanup", context.RepoMustNotBeArchived(), context.RepoRef(), repo.CleanUpPullRequest) m.Group("/files", func() { m.Get("", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForAllCommitsOfPr) - m.Get("/{sha:[a-f0-9]{7,40}}", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesStartingFromCommit) - m.Get("/{shaFrom:[a-f0-9]{7,40}}..{shaTo:[a-f0-9]{7,40}}", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForRange) + m.Get("/{sha:[a-f0-9]{4,40}}", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesStartingFromCommit) + m.Get("/{shaFrom:[a-f0-9]{4,40}}..{shaTo:[a-f0-9]{4,40}}", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForRange) m.Group("/reviews", func() { m.Get("/new_comment", repo.RenderNewCodeCommentForm) m.Post("/comments", web.Bind(forms.CodeCommentForm{}), repo.SetShowOutdatedComments, repo.CreateCodeComment) @@ -1512,13 +1534,13 @@ func registerRoutes(m *web.Route) { 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) - m.Get("/commit/{sha:([a-f0-9]{7,64})$}/load-branches-and-tags", repo.LoadBranchesAndTags) - m.Get("/cherry-pick/{sha:([a-f0-9]{7,64})$}", repo.SetEditorconfigIfExists, repo.CherryPick) + m.Get("/commit/{sha:([a-f0-9]{4,64})$}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff) + m.Get("/commit/{sha:([a-f0-9]{4,64})$}/load-branches-and-tags", repo.LoadBranchesAndTags) + m.Get("/cherry-pick/{sha:([a-f0-9]{4,64})$}", repo.SetEditorconfigIfExists, repo.CherryPick) }, repo.MustBeNotEmpty, context.RepoRef(), reqRepoCodeReader) - m.Get("/rss/branch/*", context.RepoRefByType(context.RepoRefBranch), feedEnabled, feed.RenderBranchFeed) - m.Get("/atom/branch/*", context.RepoRefByType(context.RepoRefBranch), feedEnabled, feed.RenderBranchFeed) + m.Get("/rss/branch/*", repo.MustBeNotEmpty, context.RepoRefByType(context.RepoRefBranch), feedEnabled, feed.RenderBranchFeed("rss")) + m.Get("/atom/branch/*", repo.MustBeNotEmpty, context.RepoRefByType(context.RepoRefBranch), feedEnabled, feed.RenderBranchFeed("atom")) m.Group("/src", func() { m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.Home) @@ -1531,7 +1553,7 @@ func registerRoutes(m *web.Route) { m.Group("", func() { m.Get("/forks", repo.Forks) }, context.RepoRef(), reqRepoCodeReader) - m.Get("/commit/{sha:([a-f0-9]{7,64})}.{ext:patch|diff}", repo.MustBeNotEmpty, reqRepoCodeReader, repo.RawDiff) + m.Get("/commit/{sha:([a-f0-9]{4,64})}.{ext:patch|diff}", repo.MustBeNotEmpty, reqRepoCodeReader, repo.RawDiff) }, ignSignIn, context.RepoAssignment, context.UnitTypes()) m.Post("/{username}/{reponame}/lastcommit/*", ignSignInAndCsrf, context.RepoAssignment, context.UnitTypes(), context.RepoRefByType(context.RepoRefCommit), reqRepoCodeReader, repo.LastCommit) @@ -1568,6 +1590,13 @@ func registerRoutes(m *web.Route) { gitHTTPRouters(m) }) }) + + if setting.Repository.EnableFlags { + m.Group("/{username}/{reponame}/flags", func() { + m.Get("", repo_flags.Manage) + m.Post("", repo_flags.ManagePost) + }, adminReq, context.RepoAssignment, context.UnitTypes()) + } // ***** END: Repository ***** m.Group("/notifications", func() { diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index 77173e58a3..a5565495d9 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -118,9 +118,6 @@ func notify(ctx context.Context, input *notifyInput) error { return nil } if unit_model.TypeActions.UnitGlobalDisabled() { - if err := actions_model.CleanRepoScheduleTasks(ctx, input.Repo); err != nil { - log.Error("CleanRepoScheduleTasks: %v", err) - } return nil } if err := input.Repo.LoadUnits(ctx); err != nil { @@ -157,31 +154,23 @@ func notify(ctx context.Context, input *notifyInput) error { var detectedWorkflows []*actions_module.DetectedWorkflow actionsConfig := input.Repo.MustGetUnit(ctx, unit_model.TypeActions).ActionsConfig() - workflows, schedules, err := actions_module.DetectWorkflows(gitRepo, commit, - input.Event, - input.Payload, - input.Event == webhook_module.HookEventPush && git.RefName(input.Ref).BranchName() == input.Repo.DefaultBranch, - ) + workflows, schedules, err := actions_module.DetectWorkflows(gitRepo, commit, input.Event, input.Payload) if err != nil { return fmt.Errorf("DetectWorkflows: %w", err) } - log.Trace("repo %s with commit %s event %s find %d workflows and %d schedules", - input.Repo.RepoPath(), - commit.ID, - input.Event, - len(workflows), - len(schedules), - ) + if len(workflows) == 0 { + log.Trace("repo %s with commit %s couldn't find workflows", input.Repo.RepoPath(), commit.ID) + } else { + for _, wf := range workflows { + if actionsConfig.IsWorkflowDisabled(wf.EntryName) { + log.Trace("repo %s has disable workflows %s", input.Repo.RepoPath(), wf.EntryName) + continue + } - for _, wf := range workflows { - if actionsConfig.IsWorkflowDisabled(wf.EntryName) { - log.Trace("repo %s has disable workflows %s", input.Repo.RepoPath(), wf.EntryName) - continue - } - - if wf.TriggerEvent.Name != actions_module.GithubEventPullRequestTarget { - detectedWorkflows = append(detectedWorkflows, wf) + if wf.TriggerEvent != actions_module.GithubEventPullRequestTarget { + detectedWorkflows = append(detectedWorkflows, wf) + } } } @@ -192,7 +181,7 @@ func notify(ctx context.Context, input *notifyInput) error { if err != nil { return fmt.Errorf("gitRepo.GetCommit: %w", err) } - baseWorkflows, _, err := actions_module.DetectWorkflows(gitRepo, baseCommit, input.Event, input.Payload, false) + baseWorkflows, _, err := actions_module.DetectWorkflows(gitRepo, baseCommit, input.Event, input.Payload) if err != nil { return fmt.Errorf("DetectWorkflows: %w", err) } @@ -200,7 +189,7 @@ func notify(ctx context.Context, input *notifyInput) error { log.Trace("repo %s with commit %s couldn't find pull_request_target workflows", input.Repo.RepoPath(), baseCommit.ID) } else { for _, wf := range baseWorkflows { - if wf.TriggerEvent.Name == actions_module.GithubEventPullRequestTarget { + if wf.TriggerEvent == actions_module.GithubEventPullRequestTarget { detectedWorkflows = append(detectedWorkflows, wf) } } @@ -277,7 +266,7 @@ func handleWorkflows( IsForkPullRequest: isForkPullRequest, Event: input.Event, EventPayload: string(p), - TriggerEvent: dwf.TriggerEvent.Name, + TriggerEvent: dwf.TriggerEvent, Status: actions_model.StatusWaiting, } if need, err := ifNeedApproval(ctx, run, input.Repo, input.Doer); err != nil { @@ -301,7 +290,6 @@ func handleWorkflows( run.RepoID, run.Ref, run.WorkflowID, - run.Event, ); err != nil { log.Error("CancelRunningJobs: %v", err) } @@ -427,8 +415,8 @@ func handleSchedules( log.Error("CountSchedules: %v", err) return err } else if count > 0 { - if err := actions_model.CleanRepoScheduleTasks(ctx, input.Repo); err != nil { - log.Error("CleanRepoScheduleTasks: %v", err) + if err := actions_model.DeleteScheduleTaskByRepo(ctx, input.Repo.ID); err != nil { + log.Error("DeleteCronTaskByRepo: %v", err) } } @@ -437,7 +425,11 @@ func handleSchedules( return nil } - p, err := json.Marshal(input.Payload) + payload := &api.SchedulePayload{ + Action: api.HookScheduleCreated, + } + + p, err := json.Marshal(payload) if err != nil { return fmt.Errorf("json.Marshal: %w", err) } @@ -462,47 +454,28 @@ func handleSchedules( OwnerID: input.Repo.OwnerID, WorkflowID: dwf.EntryName, TriggerUserID: input.Doer.ID, - Ref: ref, + Ref: input.Repo.DefaultBranch, CommitSHA: commit.ID.String(), - Event: input.Event, + Event: webhook_module.HookEventType(api.HookScheduleCreated), EventPayload: string(p), Specs: schedules, Content: dwf.Content, } + + // cancel running jobs if the event is push + if run.Event == webhook_module.HookEventPush { + // cancel running jobs of the same workflow + if err := actions_model.CancelRunningJobs( + ctx, + run.RepoID, + run.Ref, + run.WorkflowID, + ); err != nil { + log.Error("CancelRunningJobs: %v", err) + } + } crons = append(crons, run) } return actions_model.CreateScheduleTask(ctx, crons) } - -// DetectAndHandleSchedules detects the schedule workflows on the default branch and create schedule tasks -func DetectAndHandleSchedules(ctx context.Context, repo *repo_model.Repository) error { - gitRepo, err := gitrepo.OpenRepository(context.Background(), repo) - if err != nil { - return fmt.Errorf("git.OpenRepository: %w", err) - } - defer gitRepo.Close() - - // Only detect schedule workflows on the default branch - commit, err := gitRepo.GetCommit(repo.DefaultBranch) - if err != nil { - return fmt.Errorf("gitRepo.GetCommit: %w", err) - } - scheduleWorkflows, err := actions_module.DetectScheduledWorkflows(gitRepo, commit) - if err != nil { - return fmt.Errorf("detect schedule workflows: %w", err) - } - if len(scheduleWorkflows) == 0 { - return nil - } - - // We need a notifyInput to call handleSchedules - // Here we use the commit author as the Doer of the notifyInput - commitUser, err := user_model.GetUserByEmail(ctx, commit.Author.Email) - if err != nil { - return fmt.Errorf("get user by email: %w", err) - } - notifyInput := newNotifyInput(repo, commitUser, webhook_module.HookEventSchedule) - - return handleSchedules(ctx, scheduleWorkflows, commit, notifyInput, repo.DefaultBranch) -} diff --git a/services/actions/schedule_tasks.go b/services/actions/schedule_tasks.go index 79dd84e0cc..dfde34d994 100644 --- a/services/actions/schedule_tasks.go +++ b/services/actions/schedule_tasks.go @@ -10,7 +10,6 @@ import ( actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" - repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/timeutil" @@ -60,21 +59,13 @@ func startTasks(ctx context.Context) error { row.RepoID, row.Schedule.Ref, row.Schedule.WorkflowID, - webhook_module.HookEventSchedule, ); err != nil { log.Error("CancelRunningJobs: %v", err) } } - cfg, err := row.Repo.GetUnit(ctx, unit.TypeActions) - if err != nil { - if repo_model.IsErrUnitTypeNotExist(err) { - // Skip the actions unit of this repo is disabled. - continue - } - return fmt.Errorf("GetUnit: %w", err) - } - if cfg.ActionsConfig().IsWorkflowDisabled(row.Schedule.WorkflowID) { + cfg := row.Repo.MustGetUnit(ctx, unit.TypeActions).ActionsConfig() + if cfg.IsWorkflowDisabled(row.Schedule.WorkflowID) { continue } @@ -121,8 +112,8 @@ func CreateScheduleTask(ctx context.Context, cron *actions_model.ActionSchedule) Ref: cron.Ref, CommitSHA: cron.CommitSHA, Event: cron.Event, - EventPayload: cron.EventPayload, TriggerEvent: string(webhook_module.HookEventSchedule), + EventPayload: cron.EventPayload, ScheduleID: cron.ID, Status: actions_model.StatusWaiting, } diff --git a/services/convert/pull_review.go b/services/convert/pull_review.go index aa7ad68a47..f7990e7a5c 100644 --- a/services/convert/pull_review.go +++ b/services/convert/pull_review.go @@ -66,7 +66,7 @@ func ToPullReviewList(ctx context.Context, rl []*issues_model.Review, doer *user result := make([]*api.PullReview, 0, len(rl)) for i := range rl { // show pending reviews only for the user who created them - if rl[i].Type == issues_model.ReviewTypePending && !(doer.IsAdmin || doer.ID == rl[i].ReviewerID) { + if rl[i].Type == issues_model.ReviewTypePending && (doer == nil || !(doer.IsAdmin || doer.ID == rl[i].ReviewerID)) { continue } r, err := ToPullReview(ctx, rl[i], doer) @@ -78,6 +78,33 @@ func ToPullReviewList(ctx context.Context, rl []*issues_model.Review, doer *user return result, nil } +// ToPullReviewCommentList convert the CodeComments of an review to it's api format +func ToPullReviewComment(ctx context.Context, review *issues_model.Review, comment *issues_model.Comment, doer *user_model.User) (*api.PullReviewComment, error) { + apiComment := &api.PullReviewComment{ + ID: comment.ID, + Body: comment.Content, + Poster: ToUser(ctx, comment.Poster, doer), + Resolver: ToUser(ctx, comment.ResolveDoer, doer), + ReviewID: review.ID, + Created: comment.CreatedUnix.AsTime(), + Updated: comment.UpdatedUnix.AsTime(), + Path: comment.TreePath, + CommitID: comment.CommitSHA, + OrigCommitID: comment.OldRef, + DiffHunk: patch2diff(comment.Patch), + HTMLURL: comment.HTMLURL(ctx), + HTMLPullURL: review.Issue.HTMLURL(), + } + + if comment.Line < 0 { + apiComment.OldLineNum = comment.UnsignedLine() + } else { + apiComment.LineNum = comment.UnsignedLine() + } + + return apiComment, nil +} + // ToPullReviewCommentList convert the CodeComments of an review to it's api format func ToPullReviewCommentList(ctx context.Context, review *issues_model.Review, doer *user_model.User) ([]*api.PullReviewComment, error) { if err := review.LoadAttributes(ctx); err != nil { @@ -92,26 +119,9 @@ func ToPullReviewCommentList(ctx context.Context, review *issues_model.Review, d for _, lines := range review.CodeComments { for _, comments := range lines { for _, comment := range comments { - apiComment := &api.PullReviewComment{ - ID: comment.ID, - Body: comment.Content, - Poster: ToUser(ctx, comment.Poster, doer), - Resolver: ToUser(ctx, comment.ResolveDoer, doer), - ReviewID: review.ID, - Created: comment.CreatedUnix.AsTime(), - Updated: comment.UpdatedUnix.AsTime(), - Path: comment.TreePath, - CommitID: comment.CommitSHA, - OrigCommitID: comment.OldRef, - DiffHunk: patch2diff(comment.Patch), - HTMLURL: comment.HTMLURL(ctx), - HTMLPullURL: review.Issue.HTMLURL(), - } - - if comment.Line < 0 { - apiComment.OldLineNum = comment.UnsignedLine() - } else { - apiComment.LineNum = comment.UnsignedLine() + apiComment, err := ToPullReviewComment(ctx, review, comment, doer) + if err != nil { + return nil, err } apiComments = append(apiComments, apiComment) } diff --git a/services/convert/pull_test.go b/services/convert/pull_test.go index e069fa4a68..66c7313f7d 100644 --- a/services/convert/pull_test.go +++ b/services/convert/pull_test.go @@ -12,6 +12,7 @@ import ( access_model "code.gitea.io/gitea/models/perm/access" 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/git" "code.gitea.io/gitea/modules/structs" @@ -47,3 +48,30 @@ func TestPullRequest_APIFormat(t *testing.T) { assert.Nil(t, apiPullRequest.Head.Repository) assert.EqualValues(t, -1, apiPullRequest.Head.RepoID) } + +func TestPullReviewList(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + t.Run("Pending review", func(t *testing.T) { + reviewer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + review := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 6, ReviewerID: reviewer.ID}) + rl := []*issues_model.Review{review} + + t.Run("Anonymous", func(t *testing.T) { + prList, err := ToPullReviewList(db.DefaultContext, rl, nil) + assert.NoError(t, err) + assert.Empty(t, prList) + }) + t.Run("Reviewer", func(t *testing.T) { + prList, err := ToPullReviewList(db.DefaultContext, rl, reviewer) + assert.NoError(t, err) + assert.Len(t, prList, 1) + }) + t.Run("Admin", func(t *testing.T) { + adminUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true}, unittest.Cond("id != ?", reviewer.ID)) + prList, err := ToPullReviewList(db.DefaultContext, rl, adminUser) + assert.NoError(t, err) + assert.Len(t, prList, 1) + }) + }) +} diff --git a/services/convert/repository.go b/services/convert/repository.go index c16180c0af..b032d97d73 100644 --- a/services/convert/repository.go +++ b/services/convert/repository.go @@ -208,6 +208,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR ExternalTracker: externalTracker, InternalTracker: internalTracker, HasWiki: hasWiki, + WikiBranch: repo.WikiBranch, HasProjects: hasProjects, HasReleases: hasReleases, HasPackages: hasPackages, diff --git a/services/convert/wiki.go b/services/convert/wiki.go index 767bfdb88d..1f04843483 100644 --- a/services/convert/wiki.go +++ b/services/convert/wiki.go @@ -6,8 +6,11 @@ package convert import ( "time" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/git" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" + wiki_service "code.gitea.io/gitea/services/wiki" ) // ToWikiCommit convert a git commit into a WikiCommit @@ -43,3 +46,15 @@ func ToWikiCommitList(commits []*git.Commit, total int64) *api.WikiCommitList { Count: total, } } + +// ToWikiPageMetaData converts meta information to a WikiPageMetaData +func ToWikiPageMetaData(wikiName wiki_service.WebPath, lastCommit *git.Commit, repo *repo_model.Repository) *api.WikiPageMetaData { + subURL := string(wikiName) + _, title := wiki_service.WebPathToUserTitle(wikiName) + return &api.WikiPageMetaData{ + Title: title, + HTMLURL: util.URLJoin(repo.HTMLURL(), "wiki", subURL), + SubURL: subURL, + LastCommit: ToWikiCommit(lastCommit), + } +} diff --git a/services/doctor/push_mirror_consistency.go b/services/doctor/push_mirror_consistency.go new file mode 100644 index 0000000000..68b96d6415 --- /dev/null +++ b/services/doctor/push_mirror_consistency.go @@ -0,0 +1,91 @@ +// Copyright 2023 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. +// SPDX-License-Identifier: MIT + +package doctor + +import ( + "context" + "strings" + + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/log" + + "xorm.io/builder" +) + +func FixPushMirrorsWithoutGitRemote(ctx context.Context, logger log.Logger, autofix bool) error { + var missingMirrors []*repo_model.PushMirror + + err := db.Iterate(ctx, builder.Gt{"id": 0}, func(ctx context.Context, repo *repo_model.Repository) error { + pushMirrors, _, err := repo_model.GetPushMirrorsByRepoID(ctx, repo.ID, db.ListOptions{}) + if err != nil { + return err + } + + for i := 0; i < len(pushMirrors); i++ { + _, err = repo_model.GetPushMirrorRemoteAddress(repo.OwnerName, repo.Name, pushMirrors[i].RemoteName) + if err != nil { + if strings.Contains(err.Error(), "No such remote") { + missingMirrors = append(missingMirrors, pushMirrors[i]) + } else if logger != nil { + logger.Warn("Unable to retrieve the remote address of a mirror: %s", err) + } + } + } + + return nil + }) + if err != nil { + if logger != nil { + logger.Critical("Unable to iterate across repounits to fix push mirrors without a git remote: Error %v", err) + } + return err + } + + count := len(missingMirrors) + if !autofix { + if logger != nil { + if count == 0 { + logger.Info("Found no push mirrors with missing git remotes") + } else { + logger.Warn("Found %d push mirrors with missing git remotes", count) + } + } + return nil + } + + for i := 0; i < len(missingMirrors); i++ { + if logger != nil { + logger.Info("Removing push mirror #%d (remote: %s), for repo: %s/%s", + missingMirrors[i].ID, + missingMirrors[i].RemoteName, + missingMirrors[i].GetRepository(ctx).OwnerName, + missingMirrors[i].GetRepository(ctx).Name) + } + + err = repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{ + ID: missingMirrors[i].ID, + RepoID: missingMirrors[i].RepoID, + RemoteName: missingMirrors[i].RemoteName, + }) + if err != nil { + if logger != nil { + logger.Critical("Error removing a push mirror (repo_id: %d, push_mirror: %d): %s", missingMirrors[i].Repo.ID, missingMirrors[i].ID, err) + } + return err + } + } + + return nil +} + +func init() { + Register(&Check{ + Title: "Check for push mirrors without a git remote configured", + Name: "fix-push-mirrors-without-git-remote", + IsDefault: false, + Run: FixPushMirrorsWithoutGitRemote, + Priority: 7, + }) +} diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 845eccf817..7cc07532ef 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -132,6 +132,7 @@ type RepoSettingForm struct { // Advanced settings EnableCode bool EnableWiki bool + GloballyWriteableWiki bool EnableExternalWiki bool ExternalWikiURL string EnableIssues bool @@ -760,6 +761,7 @@ type EditRepoFileForm struct { CommitChoice string `binding:"Required;MaxSize(50)"` NewBranchName string `binding:"GitRefName;MaxSize(100)"` LastCommit string + CommitMailID int64 `binding:"Required"` Signoff bool } diff --git a/services/lfs/server.go b/services/lfs/server.go index 62134b7d60..56714120ad 100644 --- a/services/lfs/server.go +++ b/services/lfs/server.go @@ -5,6 +5,7 @@ package lfs import ( stdCtx "context" + "crypto/sha256" "encoding/base64" "encoding/hex" "errors" @@ -33,7 +34,6 @@ import ( "code.gitea.io/gitea/modules/storage" "github.com/golang-jwt/jwt/v5" - "github.com/minio/sha256-simd" ) // requestContext contain variables from the HTTP request. diff --git a/services/mailer/mail_admin_new_user.go b/services/mailer/mail_admin_new_user.go new file mode 100644 index 0000000000..e9610e626a --- /dev/null +++ b/services/mailer/mail_admin_new_user.go @@ -0,0 +1,81 @@ +// Copyright 2023 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT +package mailer + +import ( + "bytes" + "context" + "strconv" + + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/translation" +) + +const ( + tplNewUserMail base.TplName = "notify/admin_new_user" +) + +var sa = SendAsync + +// MailNewUser sends notification emails on new user registrations to all admins +func MailNewUser(ctx context.Context, u *user_model.User) { + if !setting.Admin.SendNotificationEmailOnNewUser { + return + } + + if setting.MailService == nil { + // No mail service configured + return + } + + recipients, err := user_model.GetAllAdmins(ctx) + if err != nil { + log.Error("user_model.GetAllAdmins: %v", err) + return + } + + langMap := make(map[string][]string) + for _, r := range recipients { + langMap[r.Language] = append(langMap[r.Language], r.Email) + } + + for lang, tos := range langMap { + mailNewUser(ctx, u, lang, tos) + } +} + +func mailNewUser(ctx context.Context, u *user_model.User, lang string, tos []string) { + locale := translation.NewLocale(lang) + + manageUserURL := setting.AppURL + "admin/users/" + strconv.FormatInt(u.ID, 10) + subject := locale.Tr("mail.admin.new_user.subject", u.Name) + body := locale.Tr("mail.admin.new_user.text", manageUserURL) + mailMeta := map[string]any{ + "NewUser": u, + "NewUserUrl": u.HTMLURL(), + "Subject": subject, + "Body": body, + "Language": locale.Language(), + "Locale": locale, + "Str2html": templates.Str2html, + } + + var mailBody bytes.Buffer + + if err := bodyTemplates.ExecuteTemplate(&mailBody, string(tplNewUserMail), mailMeta); err != nil { + log.Error("ExecuteTemplate [%s]: %v", string(tplNewUserMail)+"/body", err) + return + } + + msgs := make([]*Message, 0, len(tos)) + for _, to := range tos { + msg := NewMessage(to, subject, mailBody.String()) + msg.Info = subject + msgs = append(msgs, msg) + } + sa(msgs...) +} diff --git a/services/mailer/mail_admin_new_user_test.go b/services/mailer/mail_admin_new_user_test.go new file mode 100644 index 0000000000..b89d888ee1 --- /dev/null +++ b/services/mailer/mail_admin_new_user_test.go @@ -0,0 +1,98 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package mailer + +import ( + "context" + "strconv" + "testing" + + "code.gitea.io/gitea/models/db" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/translation" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func getTestUsers(t *testing.T) []*user_model.User { + t.Helper() + admin := new(user_model.User) + admin.Name = "testadmin" + admin.IsAdmin = true + admin.Language = "en_US" + admin.Email = "admin@example.com" + require.NoError(t, user_model.CreateUser(db.DefaultContext, admin)) + + newUser := new(user_model.User) + newUser.Name = "new_user" + newUser.Language = "en_US" + newUser.IsAdmin = false + newUser.Email = "new_user@example.com" + newUser.LastLoginUnix = 1693648327 + newUser.CreatedUnix = 1693648027 + require.NoError(t, user_model.CreateUser(db.DefaultContext, newUser)) + + return []*user_model.User{admin, newUser} +} + +func cleanUpUsers(ctx context.Context, users []*user_model.User) { + for _, u := range users { + db.DeleteByID[user_model.User](ctx, u.ID) + } +} + +func TestAdminNotificationMail_test(t *testing.T) { + translation.InitLocales(context.Background()) + locale := translation.NewLocale("") + key := "mail.admin.new_user.user_info" + translatedKey := locale.Tr(key) + require.NotEqualValues(t, key, translatedKey) + + mailService := setting.Mailer{ + From: "test@example.com", + Protocol: "dummy", + } + + setting.MailService = &mailService + + // test with SEND_NOTIFICATION_EMAIL_ON_NEW_USER enabled + setting.Admin.SendNotificationEmailOnNewUser = true + + ctx := context.Background() + NewContext(ctx) + + users := getTestUsers(t) + oldSendAsync := sa + defer func() { + sa = oldSendAsync + cleanUpUsers(ctx, users) + }() + + called := false + sa = func(msgs ...*Message) { + assert.Equal(t, len(msgs), 1, "Test provides only one admin user, so only one email must be sent") + assert.Equal(t, msgs[0].To, users[0].Email, "checks if the recipient is the admin of the instance") + manageUserURL := setting.AppURL + "admin/users/" + strconv.FormatInt(users[1].ID, 10) + assert.Contains(t, msgs[0].Body, manageUserURL) + assert.Contains(t, msgs[0].Body, users[1].HTMLURL()) + assert.Contains(t, msgs[0].Body, translatedKey, "the .Locale translates to nothing") + assert.Contains(t, msgs[0].Body, users[1].Name, "user name of the newly created user") + for _, untranslated := range []string{"mail.admin", "admin.users"} { + assert.NotContains(t, msgs[0].Body, untranslated, "this is an untranslated placeholder prefix") + } + called = true + } + MailNewUser(ctx, users[1]) + assert.True(t, called) + + // test with SEND_NOTIFICATION_EMAIL_ON_NEW_USER disabled; emails shouldn't be sent + setting.Admin.SendNotificationEmailOnNewUser = false + sa = func(msgs ...*Message) { + assert.Equal(t, 1, 0, "this shouldn't execute. MailNewUser must exit early since SEND_NOTIFICATION_EMAIL_ON_NEW_USER is disabled") + } + + MailNewUser(ctx, users[1]) +} diff --git a/services/mailer/notify.go b/services/mailer/notify.go index e48b5d399d..54ab80aab9 100644 --- a/services/mailer/notify.go +++ b/services/mailer/notify.go @@ -202,3 +202,7 @@ func (m *mailNotifier) RepoPendingTransfer(ctx context.Context, doer, newOwner * log.Error("SendRepoTransferNotifyMail: %v", err) } } + +func (m *mailNotifier) NewUserSignUp(ctx context.Context, newUser *user_model.User) { + MailNewUser(ctx, newUser) +} diff --git a/services/mailer/token/token.go b/services/mailer/token/token.go index aa7b567188..8a5a762d6b 100644 --- a/services/mailer/token/token.go +++ b/services/mailer/token/token.go @@ -6,14 +6,13 @@ package token import ( "context" crypto_hmac "crypto/hmac" + "crypto/sha256" "encoding/base32" "fmt" "time" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/util" - - "github.com/minio/sha256-simd" ) // A token is a verifiable container describing an action. diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go index 7b21d9f4d2..0f0ca7e0f5 100644 --- a/services/migrations/gitea_uploader.go +++ b/services/migrations/gitea_uploader.go @@ -861,6 +861,11 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error { } for _, comment := range review.Comments { + // Skip code comment if it doesn't have a diff it is commeting on. + if comment.DiffHunk == "" { + continue + } + line := comment.Line if line != 0 { comment.Position = 1 diff --git a/services/migrations/gitea_uploader_test.go b/services/migrations/gitea_uploader_test.go index c8102c6b8b..33b7c6808f 100644 --- a/services/migrations/gitea_uploader_test.go +++ b/services/migrations/gitea_uploader_test.go @@ -496,7 +496,7 @@ func TestGiteaUploadUpdateGitForPullRequest(t *testing.T) { t.Run(testCase.name, func(t *testing.T) { stopMark := fmt.Sprintf(">>>>>>>>>>>>>STOP: %s<<<<<<<<<<<<<<<", testCase.name) - logChecker, cleanup := test.NewLogChecker(log.DEFAULT) + logChecker, cleanup := test.NewLogChecker(log.DEFAULT, log.INFO) logChecker.Filter(testCase.logFilter...).StopMark(stopMark) defer cleanup() diff --git a/services/migrations/gitlab_test.go b/services/migrations/gitlab_test.go index 1e0aa2b025..25324a087f 100644 --- a/services/migrations/gitlab_test.go +++ b/services/migrations/gitlab_test.go @@ -13,6 +13,7 @@ import ( "testing" "time" + "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/json" base "code.gitea.io/gitea/modules/migration" @@ -21,18 +22,15 @@ import ( ) func TestGitlabDownloadRepo(t *testing.T) { - // Skip tests if Gitlab token is not found + // If a GitLab access token is provided, this test will make HTTP requests to the live gitlab.com instance. + // When doing so, the responses from gitlab.com will be saved as test data files. + // If no access token is available, those cached responses will be used instead. gitlabPersonalAccessToken := os.Getenv("GITLAB_READ_TOKEN") - if gitlabPersonalAccessToken == "" { - t.Skip("skipped test because GITLAB_READ_TOKEN was not in the environment") - } + fixturePath := "./testdata/gitlab/full_download" + server := unittest.NewMockWebServer(t, "https://gitlab.com", fixturePath, gitlabPersonalAccessToken != "") + defer server.Close() - resp, err := http.Get("https://gitlab.com/gitea/test_repo") - if err != nil || resp.StatusCode != http.StatusOK { - t.Skipf("Can't access test repo, skipping %s", t.Name()) - } - - downloader, err := NewGitlabDownloader(context.Background(), "https://gitlab.com", "gitea/test_repo", "", "", gitlabPersonalAccessToken) + downloader, err := NewGitlabDownloader(context.Background(), server.URL, "gitea/test_repo", "", "", gitlabPersonalAccessToken) if err != nil { t.Fatalf("NewGitlabDownloader is nil: %v", err) } @@ -43,8 +41,8 @@ func TestGitlabDownloadRepo(t *testing.T) { Name: "test_repo", Owner: "", Description: "Test repository for testing migration from gitlab to gitea", - CloneURL: "https://gitlab.com/gitea/test_repo.git", - OriginalURL: "https://gitlab.com/gitea/test_repo", + CloneURL: server.URL + "/gitea/test_repo.git", + OriginalURL: server.URL + "/gitea/test_repo", DefaultBranch: "master", }, repo) @@ -281,17 +279,17 @@ func TestGitlabDownloadRepo(t *testing.T) { UserName: "real6543", Content: "tada", }}, - PatchURL: "https://gitlab.com/gitea/test_repo/-/merge_requests/2.patch", + PatchURL: server.URL + "/gitea/test_repo/-/merge_requests/2.patch", Head: base.PullRequestBranch{ Ref: "feat/test", - CloneURL: "https://gitlab.com/gitea/test_repo/-/merge_requests/2", + CloneURL: server.URL + "/gitea/test_repo/-/merge_requests/2", SHA: "9f733b96b98a4175276edf6a2e1231489c3bdd23", RepoName: "test_repo", OwnerName: "lafriks", }, Base: base.PullRequestBranch{ Ref: "master", - SHA: "", + SHA: "c59c9b451acca9d106cc19d61d87afe3fbbb8b83", OwnerName: "lafriks", RepoName: "test_repo", }, @@ -309,16 +307,16 @@ func TestGitlabDownloadRepo(t *testing.T) { assertReviewsEqual(t, []*base.Review{ { IssueIndex: 1, - ReviewerID: 4102996, - ReviewerName: "zeripath", - CreatedAt: time.Date(2019, 11, 28, 16, 2, 8, 377000000, time.UTC), + ReviewerID: 527793, + ReviewerName: "axifive", + CreatedAt: time.Date(2019, 11, 28, 8, 54, 41, 34000000, time.UTC), State: "APPROVED", }, { IssueIndex: 1, - ReviewerID: 527793, - ReviewerName: "axifive", - CreatedAt: time.Date(2019, 11, 28, 16, 2, 8, 377000000, time.UTC), + ReviewerID: 4102996, + ReviewerName: "zeripath", + CreatedAt: time.Date(2019, 11, 28, 8, 54, 41, 34000000, time.UTC), State: "APPROVED", }, }, rvs) @@ -330,12 +328,55 @@ func TestGitlabDownloadRepo(t *testing.T) { IssueIndex: 2, ReviewerID: 4575606, ReviewerName: "real6543", - CreatedAt: time.Date(2020, 4, 19, 19, 24, 21, 108000000, time.UTC), + CreatedAt: time.Date(2019, 11, 28, 15, 56, 54, 108000000, time.UTC), State: "APPROVED", }, }, rvs) } +func TestGitlabSkippedIssueNumber(t *testing.T) { + // If a GitLab access token is provided, this test will make HTTP requests to the live gitlab.com instance. + // When doing so, the responses from gitlab.com will be saved as test data files. + // If no access token is available, those cached responses will be used instead. + gitlabPersonalAccessToken := os.Getenv("GITLAB_READ_TOKEN") + fixturePath := "./testdata/gitlab/skipped_issue_number" + server := unittest.NewMockWebServer(t, "https://gitlab.com", fixturePath, gitlabPersonalAccessToken != "") + defer server.Close() + + downloader, err := NewGitlabDownloader(context.Background(), server.URL, "troyengel/archbuild", "", "", gitlabPersonalAccessToken) + if err != nil { + t.Fatalf("NewGitlabDownloader is nil: %v", err) + } + repo, err := downloader.GetRepoInfo() + assert.NoError(t, err) + assertRepositoryEqual(t, &base.Repository{ + Name: "archbuild", + Owner: "troyengel", + Description: "Arch packaging and build files", + CloneURL: server.URL + "/troyengel/archbuild.git", + OriginalURL: server.URL + "/troyengel/archbuild", + DefaultBranch: "master", + }, repo) + + issues, isEnd, err := downloader.GetIssues(1, 10) + assert.NoError(t, err) + assert.True(t, isEnd) + + // the only issue in this repository has number 2 + assert.EqualValues(t, 1, len(issues)) + assert.EqualValues(t, 2, issues[0].Number) + assert.EqualValues(t, "vpn unlimited errors", issues[0].Title) + + prs, _, err := downloader.GetPullRequests(1, 10) + assert.NoError(t, err) + // the only merge request in this repository has number 1, + // but we offset it by the maximum issue number so it becomes + // pull request 3 in Forgejo + assert.EqualValues(t, 1, len(prs)) + assert.EqualValues(t, 3, prs[0].Number) + assert.EqualValues(t, "Review", prs[0].Title) +} + func gitlabClientMockSetup(t *testing.T) (*http.ServeMux, *httptest.Server, *gitlab.Client) { // mux is the HTTP request multiplexer used with the test server. mux := http.NewServeMux() diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026 b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026 new file mode 100644 index 0000000000..4ecfe77e65 --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026 @@ -0,0 +1,22 @@ +X-Frame-Options: SAMEORIGIN +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:46 GMT +Cf-Cache-Status: MISS +Set-Cookie: _cfuvid=TkY5Br2q4C67LJ2jZWlgdQaosj3Z4aI81Qb27PNKXfo-1701333886606-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +Etag: W/"3cacfe29f44a69e84a577337eac55d89" +X-Content-Type-Options: nosniff +Ratelimit-Remaining: 1996 +Gitlab-Lb: haproxy-main-29-lb-gprd +Cache-Control: max-age=0, private, must-revalidate +Referrer-Policy: strict-origin-when-cross-origin +X-Gitlab-Meta: {"correlation_id":"291f87cd975a51a7b756806ce7b53e2e","version":"1"} +Ratelimit-Observed: 4 +Vary: Origin, Accept-Encoding +Gitlab-Sv: localhost +Content-Security-Policy: default-src 'none' +Ratelimit-Reset: 1701333946 +Content-Type: application/json +X-Runtime: 0.101081 +Strict-Transport-Security: max-age=31536000 +Ratelimit-Limit: 2000 + +{"id":15578026,"description":"Test repository for testing migration from gitlab to gitea","name":"test_repo","name_with_namespace":"gitea / test_repo","path":"test_repo","path_with_namespace":"gitea/test_repo","created_at":"2019-11-28T08:20:33.019Z","default_branch":"master","tag_list":["migration","test"],"topics":["migration","test"],"ssh_url_to_repo":"git@gitlab.com:gitea/test_repo.git","http_url_to_repo":"https://gitlab.com/gitea/test_repo.git","web_url":"https://gitlab.com/gitea/test_repo","readme_url":"https://gitlab.com/gitea/test_repo/-/blob/master/README.md","forks_count":1,"avatar_url":null,"star_count":0,"last_activity_at":"2020-04-19T19:46:04.527Z","namespace":{"id":3181312,"name":"gitea","path":"gitea","kind":"group","full_path":"gitea","parent_id":null,"avatar_url":"/uploads/-/system/group/avatar/3181312/gitea.png","web_url":"https://gitlab.com/groups/gitea"},"container_registry_image_prefix":"registry.gitlab.com/gitea/test_repo","_links":{"self":"https://gitlab.com/api/v4/projects/15578026","issues":"https://gitlab.com/api/v4/projects/15578026/issues","merge_requests":"https://gitlab.com/api/v4/projects/15578026/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/15578026/repository/branches","labels":"https://gitlab.com/api/v4/projects/15578026/labels","events":"https://gitlab.com/api/v4/projects/15578026/events","members":"https://gitlab.com/api/v4/projects/15578026/members","cluster_agents":"https://gitlab.com/api/v4/projects/15578026/cluster_agents"},"packages_enabled":true,"empty_repo":false,"archived":false,"visibility":"public","resolve_outdated_diff_discussions":false,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"container_registry_enabled":true,"service_desk_enabled":true,"can_create_merge_request_in":true,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","forking_access_level":"enabled","wiki_access_level":"enabled","builds_access_level":"enabled","snippets_access_level":"enabled","pages_access_level":"enabled","analytics_access_level":"enabled","container_registry_access_level":"enabled","security_and_compliance_access_level":"private","releases_access_level":"enabled","environments_access_level":"enabled","feature_flags_access_level":"enabled","infrastructure_access_level":"enabled","monitor_access_level":"enabled","model_experiments_access_level":"enabled","emails_disabled":false,"emails_enabled":true,"shared_runners_enabled":true,"lfs_enabled":true,"creator_id":1241334,"import_status":"none","open_issues_count":0,"description_html":"\u003cp data-sourcepos=\"1:1-1:58\" dir=\"auto\"\u003eTest repository for testing migration from gitlab to gitea\u003c/p\u003e","updated_at":"2022-08-26T19:41:46.691Z","ci_config_path":null,"public_jobs":true,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"allow_merge_on_skipped_pipeline":null,"request_access_enabled":true,"only_allow_merge_if_all_discussions_are_resolved":false,"remove_source_branch_after_merge":true,"printing_merge_request_link_enabled":true,"merge_method":"ff","squash_option":"default_off","enforce_auth_checks_on_uploads":true,"suggestion_commit_message":null,"merge_commit_template":null,"squash_commit_template":null,"issue_branch_template":null,"autoclose_referenced_issues":true,"external_authorization_classification_label":"","requirements_enabled":false,"requirements_access_level":"enabled","security_and_compliance_enabled":false,"compliance_frameworks":[],"permissions":{"project_access":null,"group_access":null}} \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues!page=1&per_page=2&sort=asc&state=all b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues!page=1&per_page=2&sort=asc&state=all new file mode 100644 index 0000000000..331a82720c --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues!page=1&per_page=2&sort=asc&state=all @@ -0,0 +1,29 @@ +Cache-Control: max-age=0, private, must-revalidate +Vary: Origin, Accept-Encoding +Gitlab-Lb: haproxy-main-24-lb-gprd +Content-Type: application/json +X-Per-Page: 2 +X-Prev-Page: +Set-Cookie: _cfuvid=N.nfy5eIdFH5lXhsnMyEbeBkoxabcl1SVeyyP0_NrdE-1701333887790-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +Content-Security-Policy: default-src 'none' +X-Content-Type-Options: nosniff +X-Next-Page: +X-Total-Pages: 1 +Etag: W/"4c0531a3595f741f229f5a105e013b95" +X-Gitlab-Meta: {"correlation_id":"b2eca136986f016d946685fb99287f1c","version":"1"} +Ratelimit-Observed: 8 +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:47 GMT +X-Frame-Options: SAMEORIGIN +Referrer-Policy: strict-origin-when-cross-origin +Ratelimit-Remaining: 1992 +Gitlab-Sv: localhost +Cf-Cache-Status: MISS +Strict-Transport-Security: max-age=31536000 +Ratelimit-Limit: 2000 +X-Total: 2 +X-Page: 1 +X-Runtime: 0.178587 +Ratelimit-Reset: 1701333947 +Link: ; rel="first", ; rel="last" + +[{"id":27687675,"iid":1,"project_id":15578026,"title":"Please add an animated gif icon to the merge button","description":"I just want the merge button to hurt my eyes a little. :stuck_out_tongue_closed_eyes:","state":"closed","created_at":"2019-11-28T08:43:35.459Z","updated_at":"2019-11-28T08:46:23.304Z","closed_at":"2019-11-28T08:46:23.275Z","closed_by":{"id":1241334,"username":"lafriks","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"labels":["bug","discussion"],"milestone":{"id":1082926,"iid":1,"project_id":15578026,"title":"1.0.0","description":"","state":"closed","created_at":"2019-11-28T08:42:30.301Z","updated_at":"2019-11-28T15:57:52.401Z","due_date":null,"start_date":null,"expired":false,"web_url":"https://gitlab.com/gitea/test_repo/-/milestones/1"},"assignees":[],"author":{"id":1241334,"username":"lafriks","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"type":"ISSUE","assignee":null,"user_notes_count":0,"merge_requests_count":0,"upvotes":1,"downvotes":0,"due_date":null,"confidential":false,"discussion_locked":null,"issue_type":"issue","web_url":"https://gitlab.com/gitea/test_repo/-/issues/1","time_stats":{"time_estimate":0,"total_time_spent":0,"human_time_estimate":null,"human_total_time_spent":null},"task_completion_status":{"count":0,"completed_count":0},"blocking_issues_count":0,"has_tasks":true,"task_status":"0 of 0 checklist items completed","_links":{"self":"https://gitlab.com/api/v4/projects/15578026/issues/1","notes":"https://gitlab.com/api/v4/projects/15578026/issues/1/notes","award_emoji":"https://gitlab.com/api/v4/projects/15578026/issues/1/award_emoji","project":"https://gitlab.com/api/v4/projects/15578026","closed_as_duplicate_of":null},"references":{"short":"#1","relative":"#1","full":"gitea/test_repo#1"},"severity":"UNKNOWN","moved_to_id":null,"service_desk_reply_to":null},{"id":27687706,"iid":2,"project_id":15578026,"title":"Test issue","description":"This is test issue 2, do not touch!","state":"closed","created_at":"2019-11-28T08:44:46.277Z","updated_at":"2019-11-28T08:45:44.987Z","closed_at":"2019-11-28T08:45:44.959Z","closed_by":{"id":1241334,"username":"lafriks","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"labels":["duplicate"],"milestone":{"id":1082927,"iid":2,"project_id":15578026,"title":"1.1.0","description":"","state":"active","created_at":"2019-11-28T08:42:44.575Z","updated_at":"2019-11-28T08:42:44.575Z","due_date":null,"start_date":null,"expired":false,"web_url":"https://gitlab.com/gitea/test_repo/-/milestones/2"},"assignees":[],"author":{"id":1241334,"username":"lafriks","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"type":"ISSUE","assignee":null,"user_notes_count":2,"merge_requests_count":0,"upvotes":1,"downvotes":1,"due_date":null,"confidential":false,"discussion_locked":null,"issue_type":"issue","web_url":"https://gitlab.com/gitea/test_repo/-/issues/2","time_stats":{"time_estimate":0,"total_time_spent":0,"human_time_estimate":null,"human_total_time_spent":null},"task_completion_status":{"count":0,"completed_count":0},"blocking_issues_count":0,"has_tasks":true,"task_status":"0 of 0 checklist items completed","_links":{"self":"https://gitlab.com/api/v4/projects/15578026/issues/2","notes":"https://gitlab.com/api/v4/projects/15578026/issues/2/notes","award_emoji":"https://gitlab.com/api/v4/projects/15578026/issues/2/award_emoji","project":"https://gitlab.com/api/v4/projects/15578026","closed_as_duplicate_of":null},"references":{"short":"#2","relative":"#2","full":"gitea/test_repo#2"},"severity":"UNKNOWN","moved_to_id":null,"service_desk_reply_to":null}] \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_1_award_emoji!page=1&per_page=2 b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_1_award_emoji!page=1&per_page=2 new file mode 100644 index 0000000000..d8ab294ee1 --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_1_award_emoji!page=1&per_page=2 @@ -0,0 +1,29 @@ +X-Total-Pages: 1 +Referrer-Policy: strict-origin-when-cross-origin +Cache-Control: max-age=0, private, must-revalidate +X-Frame-Options: SAMEORIGIN +X-Runtime: 0.052748 +X-Total: 2 +Gitlab-Sv: localhost +Link: ; rel="first", ; rel="last" +X-Content-Type-Options: nosniff +X-Gitlab-Meta: {"correlation_id":"22fc215ac386644c9cb8736b652cf702","version":"1"} +X-Per-Page: 2 +Strict-Transport-Security: max-age=31536000 +Ratelimit-Reset: 1701333947 +Content-Security-Policy: default-src 'none' +Ratelimit-Remaining: 1991 +Ratelimit-Observed: 9 +X-Next-Page: +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:47 GMT +Ratelimit-Limit: 2000 +Gitlab-Lb: haproxy-main-43-lb-gprd +Cf-Cache-Status: MISS +Etag: W/"69c922434ed11248c864d157eb8eabfc" +Content-Type: application/json +Vary: Origin, Accept-Encoding +X-Page: 1 +X-Prev-Page: +Set-Cookie: _cfuvid=POpffkskz4lvLcv2Fhjp7lF3MsmIOugWDzFGtb3ZUig-1701333887995-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None + +[{"id":3009580,"name":"thumbsup","user":{"id":1241334,"username":"lafriks","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"created_at":"2019-11-28T08:43:40.322Z","updated_at":"2019-11-28T08:43:40.322Z","awardable_id":27687675,"awardable_type":"Issue","url":null},{"id":3009585,"name":"open_mouth","user":{"id":1241334,"username":"lafriks","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"created_at":"2019-11-28T08:44:01.902Z","updated_at":"2019-11-28T08:44:01.902Z","awardable_id":27687675,"awardable_type":"Issue","url":null}] \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_1_award_emoji!page=2&per_page=2 b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_1_award_emoji!page=2&per_page=2 new file mode 100644 index 0000000000..248afeff8f --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_1_award_emoji!page=2&per_page=2 @@ -0,0 +1,31 @@ +Content-Type: application/json +Content-Length: 2 +X-Next-Page: +X-Gitlab-Meta: {"correlation_id":"6b9bc368e2cdc69a1b8e7d2dff7546c5","version":"1"} +Referrer-Policy: strict-origin-when-cross-origin +Ratelimit-Reset: 1701333948 +Strict-Transport-Security: max-age=31536000 +X-Per-Page: 2 +X-Total: 2 +Ratelimit-Limit: 2000 +Etag: W/"4f53cda18c2baa0c0354bb5f9a3ecbe5" +Link: ; rel="first", ; rel="last" +X-Runtime: 0.069944 +Ratelimit-Remaining: 1990 +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:48 GMT +Gitlab-Sv: localhost +X-Prev-Page: +X-Total-Pages: 1 +Ratelimit-Observed: 10 +Gitlab-Lb: haproxy-main-17-lb-gprd +Content-Security-Policy: default-src 'none' +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +Accept-Ranges: bytes +Cache-Control: max-age=0, private, must-revalidate +Vary: Origin, Accept-Encoding +X-Page: 2 +Cf-Cache-Status: MISS +Set-Cookie: _cfuvid=vcfsxezcg_2Kdh8xD5coOU_uxQIH1in.6BsRttrSIYg-1701333888217-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None + +[] \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_2_award_emoji!page=1&per_page=2 b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_2_award_emoji!page=1&per_page=2 new file mode 100644 index 0000000000..70c50c8fed --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_2_award_emoji!page=1&per_page=2 @@ -0,0 +1,29 @@ +Cf-Cache-Status: MISS +Etag: W/"5fdbcbf64f34ba0e74ce9dd8d6e0efe3" +X-Content-Type-Options: nosniff +X-Per-Page: 2 +Ratelimit-Limit: 2000 +Set-Cookie: _cfuvid=NLtUZdQlWvWiXr4L8Zfc555FowZOCxJlA0pAOAEkNvg-1701333888445-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +X-Runtime: 0.075612 +Ratelimit-Reset: 1701333948 +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:48 GMT +X-Page: 1 +X-Prev-Page: +X-Total-Pages: 3 +X-Frame-Options: SAMEORIGIN +Referrer-Policy: strict-origin-when-cross-origin +Content-Security-Policy: default-src 'none' +Ratelimit-Observed: 11 +Gitlab-Lb: haproxy-main-09-lb-gprd +Gitlab-Sv: localhost +Ratelimit-Remaining: 1989 +Cache-Control: max-age=0, private, must-revalidate +Link: ; rel="next", ; rel="first", ; rel="last" +X-Gitlab-Meta: {"correlation_id":"a69709cf552479bc5f5f3e4e1f808790","version":"1"} +X-Next-Page: 2 +Strict-Transport-Security: max-age=31536000 +Content-Type: application/json +Vary: Origin, Accept-Encoding +X-Total: 6 + +[{"id":3009627,"name":"thumbsup","user":{"id":1241334,"username":"lafriks","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"created_at":"2019-11-28T08:46:42.657Z","updated_at":"2019-11-28T08:46:42.657Z","awardable_id":27687706,"awardable_type":"Issue","url":null},{"id":3009628,"name":"thumbsdown","user":{"id":1241334,"username":"lafriks","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"created_at":"2019-11-28T08:46:43.471Z","updated_at":"2019-11-28T08:46:43.471Z","awardable_id":27687706,"awardable_type":"Issue","url":null}] \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_2_award_emoji!page=2&per_page=2 b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_2_award_emoji!page=2&per_page=2 new file mode 100644 index 0000000000..1014d9dbc5 --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_2_award_emoji!page=2&per_page=2 @@ -0,0 +1,29 @@ +Ratelimit-Limit: 2000 +X-Content-Type-Options: nosniff +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:48 GMT +X-Total: 6 +Set-Cookie: _cfuvid=E5GyZy0rB2zrRH0ewMyrJd1wBrt7A2sGNmOHTiWwbYk-1701333888703-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +X-Next-Page: 3 +X-Page: 2 +Link: ; rel="prev", ; rel="next", ; rel="first", ; rel="last" +X-Gitlab-Meta: {"correlation_id":"41e59fb2e78f5e68518b81bd4ff3bb1b","version":"1"} +X-Prev-Page: 1 +Cf-Cache-Status: MISS +Referrer-Policy: strict-origin-when-cross-origin +Gitlab-Lb: haproxy-main-26-lb-gprd +Cache-Control: max-age=0, private, must-revalidate +Etag: W/"d16c513b32212d9286fce6f53340c1cf" +Vary: Origin, Accept-Encoding +X-Per-Page: 2 +Strict-Transport-Security: max-age=31536000 +Ratelimit-Reset: 1701333948 +Content-Type: application/json +X-Runtime: 0.105758 +Ratelimit-Remaining: 1988 +Content-Security-Policy: default-src 'none' +X-Frame-Options: SAMEORIGIN +Gitlab-Sv: localhost +X-Total-Pages: 3 +Ratelimit-Observed: 12 + +[{"id":3009632,"name":"laughing","user":{"id":1241334,"username":"lafriks","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"created_at":"2019-11-28T08:47:14.381Z","updated_at":"2019-11-28T08:47:14.381Z","awardable_id":27687706,"awardable_type":"Issue","url":null},{"id":3009634,"name":"tada","user":{"id":1241334,"username":"lafriks","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"created_at":"2019-11-28T08:47:18.254Z","updated_at":"2019-11-28T08:47:18.254Z","awardable_id":27687706,"awardable_type":"Issue","url":null}] \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_2_award_emoji!page=3&per_page=2 b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_2_award_emoji!page=3&per_page=2 new file mode 100644 index 0000000000..9c42bfb9bf --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_2_award_emoji!page=3&per_page=2 @@ -0,0 +1,29 @@ +Content-Type: application/json +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +Referrer-Policy: strict-origin-when-cross-origin +Link: ; rel="prev", ; rel="first", ; rel="last" +Cf-Cache-Status: MISS +Etag: W/"165d37bf09a54bb31f4619cca8722cb4" +Ratelimit-Observed: 13 +Ratelimit-Limit: 2000 +Set-Cookie: _cfuvid=W6F2uJSFkB3Iyl27_xtklVvP4Z_nSsjPqytClHPW9H8-1701333888913-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +X-Page: 3 +X-Total-Pages: 3 +Strict-Transport-Security: max-age=31536000 +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:48 GMT +Cache-Control: max-age=0, private, must-revalidate +Vary: Origin, Accept-Encoding +X-Gitlab-Meta: {"correlation_id":"b805751aeb6f562dff684cec34dfdc0b","version":"1"} +X-Per-Page: 2 +X-Prev-Page: 2 +X-Runtime: 0.061943 +Gitlab-Lb: haproxy-main-11-lb-gprd +Gitlab-Sv: localhost +X-Total: 6 +Ratelimit-Remaining: 1987 +Content-Security-Policy: default-src 'none' +X-Next-Page: +Ratelimit-Reset: 1701333948 + +[{"id":3009636,"name":"confused","user":{"id":1241334,"username":"lafriks","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"created_at":"2019-11-28T08:47:27.248Z","updated_at":"2019-11-28T08:47:27.248Z","awardable_id":27687706,"awardable_type":"Issue","url":null},{"id":3009640,"name":"hearts","user":{"id":1241334,"username":"lafriks","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"created_at":"2019-11-28T08:47:33.059Z","updated_at":"2019-11-28T08:47:33.059Z","awardable_id":27687706,"awardable_type":"Issue","url":null}] \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_2_award_emoji!page=4&per_page=2 b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_2_award_emoji!page=4&per_page=2 new file mode 100644 index 0000000000..e550e4ad19 --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_2_award_emoji!page=4&per_page=2 @@ -0,0 +1,31 @@ +Ratelimit-Remaining: 1986 +Accept-Ranges: bytes +Set-Cookie: _cfuvid=DZQIMINjFohqKKjkWnojkq2xuUaqb42YEQg3BZXe68w-1701333889148-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +Strict-Transport-Security: max-age=31536000 +X-Prev-Page: +Ratelimit-Reset: 1701333949 +Ratelimit-Limit: 2000 +Etag: W/"4f53cda18c2baa0c0354bb5f9a3ecbe5" +Cache-Control: max-age=0, private, must-revalidate +X-Next-Page: +X-Page: 4 +Referrer-Policy: strict-origin-when-cross-origin +Gitlab-Sv: localhost +Content-Type: application/json +X-Content-Type-Options: nosniff +Link: ; rel="first", ; rel="last" +Vary: Origin, Accept-Encoding +Content-Security-Policy: default-src 'none' +X-Runtime: 0.081728 +X-Total: 6 +Content-Length: 2 +X-Frame-Options: SAMEORIGIN +X-Gitlab-Meta: {"correlation_id":"588184d2d9ad2c4a7e7c00a51575091c","version":"1"} +Cf-Cache-Status: MISS +X-Total-Pages: 3 +Ratelimit-Observed: 14 +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:49 GMT +Gitlab-Lb: haproxy-main-34-lb-gprd +X-Per-Page: 2 + +[] \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_2_discussions!page=1&per_page=100 b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_2_discussions!page=1&per_page=100 new file mode 100644 index 0000000000..d74fc325a3 --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_2_discussions!page=1&per_page=100 @@ -0,0 +1,29 @@ +X-Runtime: 0.173849 +Content-Security-Policy: default-src 'none' +X-Per-Page: 100 +X-Prev-Page: +X-Total: 4 +Ratelimit-Observed: 15 +X-Page: 1 +Ratelimit-Remaining: 1985 +Cache-Control: max-age=0, private, must-revalidate +X-Gitlab-Meta: {"correlation_id":"1d2b49b1b17e0c2d58c800a1b6c7eca3","version":"1"} +X-Next-Page: +Referrer-Policy: strict-origin-when-cross-origin +Cf-Cache-Status: MISS +Content-Type: application/json +Etag: W/"bcc91e8a7b2eac98b4d96ae791e0649d" +Vary: Origin, Accept-Encoding +X-Frame-Options: SAMEORIGIN +Strict-Transport-Security: max-age=31536000 +Ratelimit-Reset: 1701333949 +Gitlab-Lb: haproxy-main-08-lb-gprd +Gitlab-Sv: localhost +Link: ; rel="first", ; rel="last" +X-Content-Type-Options: nosniff +X-Total-Pages: 1 +Ratelimit-Limit: 2000 +Set-Cookie: _cfuvid=yj.PGr9ftsz1kNpgtsmQhAcGpdMnklLE.NQ9h71hm5Q-1701333889475-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:49 GMT + +[{"id":"617967369d98d8b73b6105a40318fe839f931a24","individual_note":true,"notes":[{"id":251637434,"type":null,"body":"This is a comment","attachment":null,"author":{"id":1241334,"username":"lafriks","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"created_at":"2019-11-28T08:44:52.501Z","updated_at":"2019-11-28T08:44:52.501Z","system":false,"noteable_id":27687706,"noteable_type":"Issue","project_id":15578026,"resolvable":false,"confidential":false,"internal":false,"noteable_iid":2,"commands_changes":{}}]},{"id":"b92d74daee411a17d844041bcd3c267ade58f680","individual_note":true,"notes":[{"id":251637528,"type":null,"body":"changed milestone to %2","attachment":null,"author":{"id":1241334,"username":"lafriks","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"created_at":"2019-11-28T08:45:02.329Z","updated_at":"2019-11-28T08:45:02.335Z","system":true,"noteable_id":27687706,"noteable_type":"Issue","project_id":15578026,"resolvable":false,"confidential":false,"internal":false,"noteable_iid":2,"commands_changes":{}}]},{"id":"6010f567d2b58758ef618070372c97891ac75349","individual_note":true,"notes":[{"id":251637892,"type":null,"body":"closed","attachment":null,"author":{"id":1241334,"username":"lafriks","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"created_at":"2019-11-28T08:45:45.007Z","updated_at":"2019-11-28T08:45:45.010Z","system":true,"noteable_id":27687706,"noteable_type":"Issue","project_id":15578026,"resolvable":false,"confidential":false,"internal":false,"noteable_iid":2,"commands_changes":{}}]},{"id":"632d0cbfd6a1a08f38aaf9ef7715116f4b188ebb","individual_note":true,"notes":[{"id":251637999,"type":null,"body":"A second comment","attachment":null,"author":{"id":1241334,"username":"lafriks","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"created_at":"2019-11-28T08:45:53.501Z","updated_at":"2019-11-28T08:45:53.501Z","system":false,"noteable_id":27687706,"noteable_type":"Issue","project_id":15578026,"resolvable":false,"confidential":false,"internal":false,"noteable_iid":2,"commands_changes":{}}]}] \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_labels!page=1&per_page=100 b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_labels!page=1&per_page=100 new file mode 100644 index 0000000000..c79889fbf4 --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_labels!page=1&per_page=100 @@ -0,0 +1,29 @@ +Cache-Control: max-age=0, private, must-revalidate +Strict-Transport-Security: max-age=31536000 +Gitlab-Lb: haproxy-main-28-lb-gprd +Link: ; rel="first", ; rel="last" +X-Per-Page: 100 +X-Runtime: 0.149464 +X-Total: 9 +Ratelimit-Observed: 6 +Gitlab-Sv: localhost +X-Frame-Options: SAMEORIGIN +X-Gitlab-Meta: {"correlation_id":"2ccddf20767704a98ed6b582db3b103e","version":"1"} +X-Next-Page: +X-Prev-Page: +X-Total-Pages: 1 +Ratelimit-Remaining: 1994 +Etag: W/"5a3fb9bc7b1018070943f4aa1353f8b6" +Ratelimit-Limit: 2000 +X-Page: 1 +Content-Type: application/json +Vary: Origin, Accept-Encoding +Set-Cookie: _cfuvid=geNpLvH8Cv5XeYfUVwtpaazw43v9lCcqHE.vyXGk3kU-1701333887126-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +X-Content-Type-Options: nosniff +Ratelimit-Reset: 1701333947 +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:47 GMT +Content-Security-Policy: default-src 'none' +Referrer-Policy: strict-origin-when-cross-origin +Cf-Cache-Status: MISS + +[{"id":12959095,"name":"bug","description":null,"description_html":"","text_color":"#FFFFFF","color":"#d9534f","subscribed":false,"priority":null,"is_project_label":true},{"id":12959097,"name":"confirmed","description":null,"description_html":"","text_color":"#FFFFFF","color":"#d9534f","subscribed":false,"priority":null,"is_project_label":true},{"id":12959096,"name":"critical","description":null,"description_html":"","text_color":"#FFFFFF","color":"#d9534f","subscribed":false,"priority":null,"is_project_label":true},{"id":12959100,"name":"discussion","description":null,"description_html":"","text_color":"#FFFFFF","color":"#428bca","subscribed":false,"priority":null,"is_project_label":true},{"id":12959098,"name":"documentation","description":null,"description_html":"","text_color":"#1F1E24","color":"#f0ad4e","subscribed":false,"priority":null,"is_project_label":true},{"id":12959554,"name":"duplicate","description":null,"description_html":"","text_color":"#FFFFFF","color":"#7F8C8D","subscribed":false,"priority":null,"is_project_label":true},{"id":12959102,"name":"enhancement","description":null,"description_html":"","text_color":"#FFFFFF","color":"#5cb85c","subscribed":false,"priority":null,"is_project_label":true},{"id":12959101,"name":"suggestion","description":null,"description_html":"","text_color":"#FFFFFF","color":"#428bca","subscribed":false,"priority":null,"is_project_label":true},{"id":12959099,"name":"support","description":null,"description_html":"","text_color":"#1F1E24","color":"#f0ad4e","subscribed":false,"priority":null,"is_project_label":true}] \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests!page=1&per_page=1&view=simple b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests!page=1&per_page=1&view=simple new file mode 100644 index 0000000000..7eec1ca917 --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests!page=1&per_page=1&view=simple @@ -0,0 +1,29 @@ +Content-Type: application/json +X-Page: 1 +Link: ; rel="next", ; rel="first", ; rel="last" +Vary: Origin, Accept-Encoding +X-Runtime: 0.139912 +X-Gitlab-Meta: {"correlation_id":"002c20b78ace441f5585931fed7093ed","version":"1"} +Gitlab-Sv: localhost +Cache-Control: max-age=0, private, must-revalidate +X-Total: 2 +X-Total-Pages: 2 +Ratelimit-Observed: 16 +Ratelimit-Limit: 2000 +X-Content-Type-Options: nosniff +Ratelimit-Remaining: 1984 +Set-Cookie: _cfuvid=6nsOEFMJm7NgrvYZAMGwiJBdm5A5CU71S33zOdN8Kyo-1701333889768-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +X-Per-Page: 1 +X-Prev-Page: +Strict-Transport-Security: max-age=31536000 +Ratelimit-Reset: 1701333949 +Gitlab-Lb: haproxy-main-09-lb-gprd +X-Next-Page: 2 +Cf-Cache-Status: MISS +Content-Security-Policy: default-src 'none' +Etag: W/"14f72c1f555b0e6348d338190e9e4839" +X-Frame-Options: SAMEORIGIN +Referrer-Policy: strict-origin-when-cross-origin +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:49 GMT + +[{"id":43524600,"iid":2,"project_id":15578026,"title":"Test branch","description":"do not merge this PR","state":"opened","created_at":"2019-11-28T15:56:54.104Z","updated_at":"2020-04-19T19:24:21.108Z","web_url":"https://gitlab.com/gitea/test_repo/-/merge_requests/2"}] \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_1_approvals b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_1_approvals new file mode 100644 index 0000000000..80df508bc3 --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_1_approvals @@ -0,0 +1,22 @@ +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:51 GMT +Ratelimit-Limit: 2000 +Content-Security-Policy: default-src 'none' +X-Gitlab-Meta: {"correlation_id":"0a1a7339f3527e175a537afe058a6ac7","version":"1"} +Ratelimit-Observed: 21 +Cache-Control: max-age=0, private, must-revalidate +Gitlab-Lb: haproxy-main-38-lb-gprd +X-Frame-Options: SAMEORIGIN +Strict-Transport-Security: max-age=31536000 +Cf-Cache-Status: MISS +Content-Type: application/json +Etag: W/"19aa54b7d4531bd5ab98282e0b772f20" +X-Content-Type-Options: nosniff +Set-Cookie: _cfuvid=J2edW64FLja6v0MZCh5tVbLYO42.VvIsTqQ.uj1Gr_k-1701333891171-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +Ratelimit-Reset: 1701333951 +Gitlab-Sv: localhost +Vary: Origin, Accept-Encoding +Ratelimit-Remaining: 1979 +X-Runtime: 0.155712 +Referrer-Policy: strict-origin-when-cross-origin + +{"id":43486906,"iid":1,"project_id":15578026,"title":"Update README.md","description":"add warning to readme","state":"merged","created_at":"2019-11-28T08:54:41.034Z","updated_at":"2019-11-28T16:02:08.377Z","merge_status":"can_be_merged","approved":true,"approvals_required":0,"approvals_left":0,"require_password_to_approve":false,"approved_by":[{"user":{"id":527793,"username":"axifive","name":"Alexey Terentyev","state":"active","locked":false,"avatar_url":"https://secure.gravatar.com/avatar/06683cd6b2e2c2ce0ab00fb80cc0729f?s=80\u0026d=identicon","web_url":"https://gitlab.com/axifive"}},{"user":{"id":4102996,"username":"zeripath","name":"zeripath","state":"active","locked":false,"avatar_url":"https://secure.gravatar.com/avatar/1ae18535c2b1aed798da090448997248?s=80\u0026d=identicon","web_url":"https://gitlab.com/zeripath"}}],"suggested_approvers":[],"approvers":[],"approver_groups":[],"user_has_approved":false,"user_can_approve":false,"approval_rules_left":[],"has_approval_rules":true,"merge_request_approvers_available":false,"multiple_approval_rules_available":false,"invalid_approvers_rules":[]} \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_2 b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_2 new file mode 100644 index 0000000000..7119342f6c --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_2 @@ -0,0 +1,22 @@ +Etag: W/"914149155d75f8d8f7ed2e5351f0fadb" +X-Runtime: 0.234286 +Ratelimit-Remaining: 1983 +Strict-Transport-Security: max-age=31536000 +Referrer-Policy: strict-origin-when-cross-origin +Gitlab-Sv: localhost +Set-Cookie: _cfuvid=aYLZ68TRL8gsnraUk.zZIxRvuv981nIhZNIO9vVpgbU-1701333890175-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +Ratelimit-Observed: 17 +Content-Type: application/json +Cache-Control: max-age=0, private, must-revalidate +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:50 GMT +Ratelimit-Limit: 2000 +Vary: Origin, Accept-Encoding +Content-Security-Policy: default-src 'none' +Gitlab-Lb: haproxy-main-33-lb-gprd +Cf-Cache-Status: MISS +Ratelimit-Reset: 1701333950 +X-Gitlab-Meta: {"correlation_id":"10d576ab8e82b745b7202a6daec3c5e1","version":"1"} + +{"id":43524600,"iid":2,"project_id":15578026,"title":"Test branch","description":"do not merge this PR","state":"opened","created_at":"2019-11-28T15:56:54.104Z","updated_at":"2020-04-19T19:24:21.108Z","merged_by":null,"merge_user":null,"merged_at":null,"closed_by":null,"closed_at":null,"target_branch":"master","source_branch":"feat/test","user_notes_count":0,"upvotes":1,"downvotes":0,"author":{"id":1241334,"username":"lafriks","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"assignees":[],"assignee":null,"reviewers":[],"source_project_id":15578026,"target_project_id":15578026,"labels":["bug"],"draft":false,"work_in_progress":false,"milestone":{"id":1082926,"iid":1,"project_id":15578026,"title":"1.0.0","description":"","state":"closed","created_at":"2019-11-28T08:42:30.301Z","updated_at":"2019-11-28T15:57:52.401Z","due_date":null,"start_date":null,"expired":false,"web_url":"https://gitlab.com/gitea/test_repo/-/milestones/1"},"merge_when_pipeline_succeeds":false,"merge_status":"can_be_merged","detailed_merge_status":"mergeable","sha":"9f733b96b98a4175276edf6a2e1231489c3bdd23","merge_commit_sha":null,"squash_commit_sha":null,"discussion_locked":null,"should_remove_source_branch":null,"force_remove_source_branch":true,"prepared_at":"2019-11-28T15:56:54.104Z","reference":"!2","references":{"short":"!2","relative":"!2","full":"gitea/test_repo!2"},"web_url":"https://gitlab.com/gitea/test_repo/-/merge_requests/2","time_stats":{"time_estimate":0,"total_time_spent":0,"human_time_estimate":null,"human_total_time_spent":null},"squash":true,"squash_on_merge":true,"task_completion_status":{"count":0,"completed_count":0},"has_conflicts":false,"blocking_discussions_resolved":true,"approvals_before_merge":null,"subscribed":false,"changes_count":"1","latest_build_started_at":null,"latest_build_finished_at":null,"first_deployed_to_production_at":null,"pipeline":null,"head_pipeline":null,"diff_refs":{"base_sha":"c59c9b451acca9d106cc19d61d87afe3fbbb8b83","head_sha":"9f733b96b98a4175276edf6a2e1231489c3bdd23","start_sha":"c59c9b451acca9d106cc19d61d87afe3fbbb8b83"},"merge_error":null,"first_contribution":false,"user":{"can_merge":false}} \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_2_approvals b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_2_approvals new file mode 100644 index 0000000000..02a1deae29 --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_2_approvals @@ -0,0 +1,22 @@ +Vary: Origin, Accept-Encoding +Ratelimit-Remaining: 1978 +Gitlab-Lb: haproxy-main-04-lb-gprd +Set-Cookie: _cfuvid=cKUtcpJODQwk9SDRn91k1DY8CY5Tg238DXGgT0a2go0-1701333891493-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +Content-Type: application/json +Cf-Cache-Status: MISS +Etag: W/"ce2f774c1b05c5c8a14ec9274444cba3" +Ratelimit-Limit: 2000 +X-Content-Type-Options: nosniff +Cache-Control: max-age=0, private, must-revalidate +X-Gitlab-Meta: {"correlation_id":"98ee1da556bdb3d764679ebab9ab5312","version":"1"} +Referrer-Policy: strict-origin-when-cross-origin +X-Frame-Options: SAMEORIGIN +Gitlab-Sv: localhost +Content-Security-Policy: default-src 'none' +X-Runtime: 0.165471 +Strict-Transport-Security: max-age=31536000 +Ratelimit-Observed: 22 +Ratelimit-Reset: 1701333951 +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:51 GMT + +{"id":43524600,"iid":2,"project_id":15578026,"title":"Test branch","description":"do not merge this PR","state":"opened","created_at":"2019-11-28T15:56:54.104Z","updated_at":"2020-04-19T19:24:21.108Z","merge_status":"can_be_merged","approved":true,"approvals_required":0,"approvals_left":0,"require_password_to_approve":false,"approved_by":[{"user":{"id":4575606,"username":"real6543","name":"6543","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/4575606/avatar.png","web_url":"https://gitlab.com/real6543"}}],"suggested_approvers":[],"approvers":[],"approver_groups":[{"group":{"id":3181312,"web_url":"https://gitlab.com/groups/gitea","name":"gitea","path":"gitea","description":"Mirror of Gitea source code repositories","visibility":"public","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"maintainer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":false,"emails_enabled":true,"mentions_disabled":null,"lfs_enabled":true,"default_branch_protection":2,"default_branch_protection_defaults":{"allowed_to_push":[{"access_level":30}],"allow_force_push":true,"allowed_to_merge":[{"access_level":30}]},"avatar_url":"https://gitlab.com/uploads/-/system/group/avatar/3181312/gitea.png","request_access_enabled":true,"full_name":"gitea","full_path":"gitea","created_at":"2018-07-04T16:32:10.176Z","parent_id":null,"shared_runners_setting":"enabled","ldap_cn":null,"ldap_access":null,"wiki_access_level":"enabled"}}],"user_has_approved":false,"user_can_approve":false,"approval_rules_left":[],"has_approval_rules":true,"merge_request_approvers_available":false,"multiple_approval_rules_available":false,"invalid_approvers_rules":[]} \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_2_award_emoji!page=1&per_page=1 b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_2_award_emoji!page=1&per_page=1 new file mode 100644 index 0000000000..aab7a74df8 --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_2_award_emoji!page=1&per_page=1 @@ -0,0 +1,29 @@ +X-Prev-Page: +X-Runtime: 0.066211 +Strict-Transport-Security: max-age=31536000 +Content-Security-Policy: default-src 'none' +X-Per-Page: 1 +X-Total: 2 +Ratelimit-Reset: 1701333950 +Ratelimit-Limit: 2000 +Cf-Cache-Status: MISS +X-Content-Type-Options: nosniff +X-Gitlab-Meta: {"correlation_id":"d0f8c843558e938161d7307b686f1cd9","version":"1"} +Gitlab-Sv: localhost +Set-Cookie: _cfuvid=WFMplweUX3zWl6uoteYRHeDcpElbTNYhWrIBbNEVC3A-1701333890399-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +Content-Type: application/json +X-Total-Pages: 2 +Etag: W/"798718b23a2ec66b16cce20cb7155116" +X-Next-Page: 2 +Link: ; rel="next", ; rel="first", ; rel="last" +Vary: Origin, Accept-Encoding +Ratelimit-Observed: 18 +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:50 GMT +X-Frame-Options: SAMEORIGIN +Referrer-Policy: strict-origin-when-cross-origin +Ratelimit-Remaining: 1982 +Gitlab-Lb: haproxy-main-31-lb-gprd +Cache-Control: max-age=0, private, must-revalidate +X-Page: 1 + +[{"id":5541414,"name":"thumbsup","user":{"id":4575606,"username":"real6543","name":"6543","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/4575606/avatar.png","web_url":"https://gitlab.com/real6543"},"created_at":"2020-09-02T23:42:34.310Z","updated_at":"2020-09-02T23:42:34.310Z","awardable_id":43524600,"awardable_type":"MergeRequest","url":null}] \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_2_award_emoji!page=2&per_page=1 b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_2_award_emoji!page=2&per_page=1 new file mode 100644 index 0000000000..5d85a567e8 --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_2_award_emoji!page=2&per_page=1 @@ -0,0 +1,29 @@ +Cf-Cache-Status: MISS +Etag: W/"e6776aaa57e6a81bf8a2d8823272cc70" +X-Frame-Options: SAMEORIGIN +X-Gitlab-Meta: {"correlation_id":"db01aae11b0cf7febcf051706159faae","version":"1"} +X-Total-Pages: 2 +Strict-Transport-Security: max-age=31536000 +Gitlab-Lb: haproxy-main-51-lb-gprd +X-Total: 2 +Referrer-Policy: strict-origin-when-cross-origin +Ratelimit-Limit: 2000 +Cache-Control: max-age=0, private, must-revalidate +X-Next-Page: +Set-Cookie: _cfuvid=X0n26HdiufQTXsshb4pvCyuf0jDSstPZ8GnIiyx57YU-1701333890628-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +Link: ; rel="prev", ; rel="first", ; rel="last" +Ratelimit-Observed: 19 +Ratelimit-Reset: 1701333950 +Content-Type: application/json +Vary: Origin, Accept-Encoding +X-Per-Page: 1 +X-Runtime: 0.067511 +Gitlab-Sv: localhost +X-Prev-Page: 1 +Ratelimit-Remaining: 1981 +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:50 GMT +Content-Security-Policy: default-src 'none' +X-Content-Type-Options: nosniff +X-Page: 2 + +[{"id":5541415,"name":"tada","user":{"id":4575606,"username":"real6543","name":"6543","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/4575606/avatar.png","web_url":"https://gitlab.com/real6543"},"created_at":"2020-09-02T23:42:59.060Z","updated_at":"2020-09-02T23:42:59.060Z","awardable_id":43524600,"awardable_type":"MergeRequest","url":null}] \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_2_award_emoji!page=3&per_page=1 b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_2_award_emoji!page=3&per_page=1 new file mode 100644 index 0000000000..aa5a50e45b --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_2_award_emoji!page=3&per_page=1 @@ -0,0 +1,31 @@ +X-Page: 3 +Strict-Transport-Security: max-age=31536000 +Accept-Ranges: bytes +Set-Cookie: _cfuvid=CBSpRhUuajZbJ9Mc_r7SkVmZawoSi5ofuts2TGyHgRk-1701333890842-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +Cache-Control: max-age=0, private, must-revalidate +X-Prev-Page: +X-Total-Pages: 2 +Ratelimit-Reset: 1701333950 +Gitlab-Sv: localhost +X-Content-Type-Options: nosniff +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:50 GMT +Gitlab-Lb: haproxy-main-05-lb-gprd +Etag: W/"4f53cda18c2baa0c0354bb5f9a3ecbe5" +Vary: Origin, Accept-Encoding +X-Frame-Options: SAMEORIGIN +X-Gitlab-Meta: {"correlation_id":"36817edd7cae21d5d6b875faf173ce80","version":"1"} +X-Per-Page: 1 +X-Total: 2 +Referrer-Policy: strict-origin-when-cross-origin +Content-Security-Policy: default-src 'none' +Link: ; rel="first", ; rel="last" +X-Next-Page: +X-Runtime: 0.061075 +Ratelimit-Limit: 2000 +Ratelimit-Observed: 20 +Cf-Cache-Status: MISS +Content-Type: application/json +Content-Length: 2 +Ratelimit-Remaining: 1980 + +[] \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_milestones!page=1&per_page=100&state=all b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_milestones!page=1&per_page=100&state=all new file mode 100644 index 0000000000..78310e123d --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_milestones!page=1&per_page=100&state=all @@ -0,0 +1,29 @@ +X-Content-Type-Options: nosniff +X-Next-Page: +Ratelimit-Observed: 5 +X-Frame-Options: SAMEORIGIN +X-Per-Page: 100 +Ratelimit-Limit: 2000 +Cf-Cache-Status: MISS +Strict-Transport-Security: max-age=31536000 +Referrer-Policy: strict-origin-when-cross-origin +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:46 GMT +Content-Security-Policy: default-src 'none' +Link: ; rel="first", ; rel="last" +X-Gitlab-Meta: {"correlation_id":"4f72373995f8681ce127c62d384745a3","version":"1"} +Gitlab-Sv: localhost +X-Runtime: 0.073691 +Ratelimit-Remaining: 1995 +Ratelimit-Reset: 1701333946 +Content-Type: application/json +Etag: W/"c8e2d3a5f05ee29c58b665c86684f9f9" +X-Page: 1 +Gitlab-Lb: haproxy-main-47-lb-gprd +Vary: Origin, Accept-Encoding +X-Prev-Page: +X-Total: 2 +Cache-Control: max-age=0, private, must-revalidate +X-Total-Pages: 1 +Set-Cookie: _cfuvid=ZfjvK5rh2nTUjEDt1Guwzd8zrl6uCDplfE8NBPbdJ7c-1701333886832-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None + +[{"id":1082927,"iid":2,"project_id":15578026,"title":"1.1.0","description":"","state":"active","created_at":"2019-11-28T08:42:44.575Z","updated_at":"2019-11-28T08:42:44.575Z","due_date":null,"start_date":null,"expired":false,"web_url":"https://gitlab.com/gitea/test_repo/-/milestones/2"},{"id":1082926,"iid":1,"project_id":15578026,"title":"1.0.0","description":"","state":"closed","created_at":"2019-11-28T08:42:30.301Z","updated_at":"2019-11-28T15:57:52.401Z","due_date":null,"start_date":null,"expired":false,"web_url":"https://gitlab.com/gitea/test_repo/-/milestones/1"}] \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_releases!page=1&per_page=100 b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_releases!page=1&per_page=100 new file mode 100644 index 0000000000..9be0b74729 --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_releases!page=1&per_page=100 @@ -0,0 +1,29 @@ +Strict-Transport-Security: max-age=31536000 +Referrer-Policy: strict-origin-when-cross-origin +Ratelimit-Reset: 1701333947 +X-Total: 1 +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:47 GMT +Ratelimit-Limit: 2000 +X-Runtime: 0.178411 +Gitlab-Lb: haproxy-main-15-lb-gprd +Vary: Origin, Accept-Encoding +X-Page: 1 +X-Frame-Options: SAMEORIGIN +Ratelimit-Observed: 7 +Cf-Cache-Status: MISS +Etag: W/"dccc7159dc4b46989d13128a7d6ee859" +X-Content-Type-Options: nosniff +Ratelimit-Remaining: 1993 +X-Gitlab-Meta: {"correlation_id":"0044a3de3ede2f913cabe6e464dd73c2","version":"1"} +X-Total-Pages: 1 +X-Next-Page: +X-Per-Page: 100 +Content-Type: application/json +Content-Security-Policy: default-src 'none' +Set-Cookie: _cfuvid=h1ayMNs6W_kFPoFe28IpiaFUz1ZAPvY6npUWxARRx4I-1701333887452-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +Link: ; rel="first", ; rel="last" +Gitlab-Sv: localhost +Cache-Control: max-age=0, private, must-revalidate +X-Prev-Page: + +[{"name":"First Release","tag_name":"v0.9.99","description":"A test release","created_at":"2019-11-28T09:09:48.840Z","released_at":"2019-11-28T09:09:48.836Z","upcoming_release":false,"author":{"id":1241334,"username":"lafriks","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"commit":{"id":"0720a3ec57c1f843568298117b874319e7deee75","short_id":"0720a3ec","created_at":"2019-11-28T08:49:16.000+00:00","parent_ids":["93ea21ce45d35690c35e80961d239645139e872c"],"title":"Add new file","message":"Add new file","author_name":"Lauris BH","author_email":"lauris@nix.lv","authored_date":"2019-11-28T08:49:16.000+00:00","committer_name":"Lauris BH","committer_email":"lauris@nix.lv","committed_date":"2019-11-28T08:49:16.000+00:00","trailers":{},"extended_trailers":{},"web_url":"https://gitlab.com/gitea/test_repo/-/commit/0720a3ec57c1f843568298117b874319e7deee75"},"commit_path":"/gitea/test_repo/-/commit/0720a3ec57c1f843568298117b874319e7deee75","tag_path":"/gitea/test_repo/-/tags/v0.9.99","assets":{"count":4,"sources":[{"format":"zip","url":"https://gitlab.com/gitea/test_repo/-/archive/v0.9.99/test_repo-v0.9.99.zip"},{"format":"tar.gz","url":"https://gitlab.com/gitea/test_repo/-/archive/v0.9.99/test_repo-v0.9.99.tar.gz"},{"format":"tar.bz2","url":"https://gitlab.com/gitea/test_repo/-/archive/v0.9.99/test_repo-v0.9.99.tar.bz2"},{"format":"tar","url":"https://gitlab.com/gitea/test_repo/-/archive/v0.9.99/test_repo-v0.9.99.tar"}],"links":[]},"evidences":[{"sha":"89f1223473ee01f192a83d0cb89f4d1eac1de74f01ad","filepath":"https://gitlab.com/gitea/test_repo/-/releases/v0.9.99/evidences/52147.json","collected_at":"2019-11-28T09:09:48.888Z"}],"_links":{"closed_issues_url":"https://gitlab.com/gitea/test_repo/-/issues?release_tag=v0.9.99\u0026scope=all\u0026state=closed","closed_merge_requests_url":"https://gitlab.com/gitea/test_repo/-/merge_requests?release_tag=v0.9.99\u0026scope=all\u0026state=closed","merged_merge_requests_url":"https://gitlab.com/gitea/test_repo/-/merge_requests?release_tag=v0.9.99\u0026scope=all\u0026state=merged","opened_issues_url":"https://gitlab.com/gitea/test_repo/-/issues?release_tag=v0.9.99\u0026scope=all\u0026state=opened","opened_merge_requests_url":"https://gitlab.com/gitea/test_repo/-/merge_requests?release_tag=v0.9.99\u0026scope=all\u0026state=opened","self":"https://gitlab.com/gitea/test_repo/-/releases/v0.9.99"}}] \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_gitea%2Ftest_repo b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_gitea%2Ftest_repo new file mode 100644 index 0000000000..07fed52a81 --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_gitea%2Ftest_repo @@ -0,0 +1,22 @@ +Content-Security-Policy: default-src 'none' +Vary: Origin, Accept-Encoding +Referrer-Policy: strict-origin-when-cross-origin +Gitlab-Sv: localhost +X-Content-Type-Options: nosniff +Ratelimit-Reset: 1701333946 +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:46 GMT +X-Runtime: 0.144599 +Content-Type: application/json +X-Gitlab-Meta: {"correlation_id":"919887b868b11ed34c5917e98b4be40d","version":"1"} +Ratelimit-Observed: 2 +Gitlab-Lb: haproxy-main-42-lb-gprd +Strict-Transport-Security: max-age=31536000 +Cf-Cache-Status: MISS +Ratelimit-Limit: 2000 +Set-Cookie: _cfuvid=BENIrMVlxs_tt.JplEkDKbUrMpOF_kjRRLJOifNTLqY-1701333886061-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +Etag: W/"3cacfe29f44a69e84a577337eac55d89" +X-Frame-Options: SAMEORIGIN +Cache-Control: max-age=0, private, must-revalidate +Ratelimit-Remaining: 1998 + +{"id":15578026,"description":"Test repository for testing migration from gitlab to gitea","name":"test_repo","name_with_namespace":"gitea / test_repo","path":"test_repo","path_with_namespace":"gitea/test_repo","created_at":"2019-11-28T08:20:33.019Z","default_branch":"master","tag_list":["migration","test"],"topics":["migration","test"],"ssh_url_to_repo":"git@gitlab.com:gitea/test_repo.git","http_url_to_repo":"https://gitlab.com/gitea/test_repo.git","web_url":"https://gitlab.com/gitea/test_repo","readme_url":"https://gitlab.com/gitea/test_repo/-/blob/master/README.md","forks_count":1,"avatar_url":null,"star_count":0,"last_activity_at":"2020-04-19T19:46:04.527Z","namespace":{"id":3181312,"name":"gitea","path":"gitea","kind":"group","full_path":"gitea","parent_id":null,"avatar_url":"/uploads/-/system/group/avatar/3181312/gitea.png","web_url":"https://gitlab.com/groups/gitea"},"container_registry_image_prefix":"registry.gitlab.com/gitea/test_repo","_links":{"self":"https://gitlab.com/api/v4/projects/15578026","issues":"https://gitlab.com/api/v4/projects/15578026/issues","merge_requests":"https://gitlab.com/api/v4/projects/15578026/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/15578026/repository/branches","labels":"https://gitlab.com/api/v4/projects/15578026/labels","events":"https://gitlab.com/api/v4/projects/15578026/events","members":"https://gitlab.com/api/v4/projects/15578026/members","cluster_agents":"https://gitlab.com/api/v4/projects/15578026/cluster_agents"},"packages_enabled":true,"empty_repo":false,"archived":false,"visibility":"public","resolve_outdated_diff_discussions":false,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"container_registry_enabled":true,"service_desk_enabled":true,"can_create_merge_request_in":true,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","forking_access_level":"enabled","wiki_access_level":"enabled","builds_access_level":"enabled","snippets_access_level":"enabled","pages_access_level":"enabled","analytics_access_level":"enabled","container_registry_access_level":"enabled","security_and_compliance_access_level":"private","releases_access_level":"enabled","environments_access_level":"enabled","feature_flags_access_level":"enabled","infrastructure_access_level":"enabled","monitor_access_level":"enabled","model_experiments_access_level":"enabled","emails_disabled":false,"emails_enabled":true,"shared_runners_enabled":true,"lfs_enabled":true,"creator_id":1241334,"import_status":"none","open_issues_count":0,"description_html":"\u003cp data-sourcepos=\"1:1-1:58\" dir=\"auto\"\u003eTest repository for testing migration from gitlab to gitea\u003c/p\u003e","updated_at":"2022-08-26T19:41:46.691Z","ci_config_path":null,"public_jobs":true,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"allow_merge_on_skipped_pipeline":null,"request_access_enabled":true,"only_allow_merge_if_all_discussions_are_resolved":false,"remove_source_branch_after_merge":true,"printing_merge_request_link_enabled":true,"merge_method":"ff","squash_option":"default_off","enforce_auth_checks_on_uploads":true,"suggestion_commit_message":null,"merge_commit_template":null,"squash_commit_template":null,"issue_branch_template":null,"autoclose_referenced_issues":true,"external_authorization_classification_label":"","requirements_enabled":false,"requirements_access_level":"enabled","security_and_compliance_enabled":false,"compliance_frameworks":[],"permissions":{"project_access":null,"group_access":null}} \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_version b/services/migrations/testdata/gitlab/full_download/_api_v4_version new file mode 100644 index 0000000000..7c2cd320ad --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_version @@ -0,0 +1,22 @@ +Etag: W/"4e5c0a031c3aacb6ba0a3c19e67d7592" +Ratelimit-Observed: 1 +Content-Type: application/json +Ratelimit-Remaining: 1999 +Set-Cookie: _cfuvid=qtYbzv8YeGg4q7XaV0aAE2.YqWIp_xrYPGilXrlecsk-1701333885742-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +X-Gitlab-Meta: {"correlation_id":"c7c75f0406e1b1b9705a6d7e9bdb06a5","version":"1"} +X-Runtime: 0.039264 +Cf-Cache-Status: MISS +Content-Security-Policy: default-src 'none' +Ratelimit-Reset: 1701333945 +Gitlab-Sv: localhost +Strict-Transport-Security: max-age=31536000 +Vary: Origin, Accept-Encoding +X-Content-Type-Options: nosniff +Referrer-Policy: strict-origin-when-cross-origin +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:45 GMT +Gitlab-Lb: haproxy-main-23-lb-gprd +Cache-Control: max-age=0, private, must-revalidate +X-Frame-Options: SAMEORIGIN +Ratelimit-Limit: 2000 + +{"version":"16.7.0-pre","revision":"acd848a9228","kas":{"enabled":true,"externalUrl":"wss://kas.gitlab.com","version":"v16.7.0-rc2"},"enterprise":true} \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996 b/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996 new file mode 100644 index 0000000000..db8d596173 --- /dev/null +++ b/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996 @@ -0,0 +1,22 @@ +X-Runtime: 0.088022 +Strict-Transport-Security: max-age=31536000 +Ratelimit-Observed: 3 +Cache-Control: max-age=0, private, must-revalidate +Etag: W/"03ce4f6ce1c1e8c5a31df8a44cf2fbdd" +Gitlab-Lb: haproxy-main-11-lb-gprd +Content-Security-Policy: default-src 'none' +Ratelimit-Limit: 2000 +X-Gitlab-Meta: {"correlation_id":"b57b226f741f9140a1fea54f65cb5cfd","version":"1"} +Referrer-Policy: strict-origin-when-cross-origin +Ratelimit-Remaining: 1997 +Ratelimit-Resettime: Thu, 30 Nov 2023 08:24:53 GMT +Set-Cookie: _cfuvid=V0ToiOTUW0XbtWq7BirwVNfL1_YP1POMrLBnDSEWS0M-1701332633965-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +Gitlab-Sv: localhost +Content-Type: application/json +Vary: Origin, Accept-Encoding +Ratelimit-Reset: 1701332693 +Cf-Cache-Status: MISS + +{"id":6590996,"description":"Arch packaging and build files","name":"archbuild","name_with_namespace":"Troy Engel / archbuild","path":"archbuild","path_with_namespace":"troyengel/archbuild","created_at":"2018-06-03T22:53:17.388Z","default_branch":"master","tag_list":[],"topics":[],"ssh_url_to_repo":"git@gitlab.com:troyengel/archbuild.git","http_url_to_repo":"https://gitlab.com/troyengel/archbuild.git","web_url":"https://gitlab.com/troyengel/archbuild","readme_url":"https://gitlab.com/troyengel/archbuild/-/blob/master/README.md","forks_count":0,"avatar_url":null,"star_count":0,"last_activity_at":"2020-12-13T18:09:32.071Z","namespace":{"id":1452515,"name":"Troy Engel","path":"troyengel","kind":"user","full_path":"troyengel","parent_id":null,"avatar_url":"https://secure.gravatar.com/avatar/b226c267929f1bcfcc446e75a025591c?s=80\u0026d=identicon","web_url":"https://gitlab.com/troyengel"},"container_registry_image_prefix":"registry.gitlab.com/troyengel/archbuild","_links":{"self":"https://gitlab.com/api/v4/projects/6590996","issues":"https://gitlab.com/api/v4/projects/6590996/issues","merge_requests":"https://gitlab.com/api/v4/projects/6590996/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/6590996/repository/branches","labels":"https://gitlab.com/api/v4/projects/6590996/labels","events":"https://gitlab.com/api/v4/projects/6590996/events","members":"https://gitlab.com/api/v4/projects/6590996/members","cluster_agents":"https://gitlab.com/api/v4/projects/6590996/cluster_agents"},"packages_enabled":null,"empty_repo":false,"archived":true,"visibility":"public","owner":{"id":1215848,"username":"troyengel","name":"Troy Engel","state":"active","locked":false,"avatar_url":"https://secure.gravatar.com/avatar/b226c267929f1bcfcc446e75a025591c?s=80\u0026d=identicon","web_url":"https://gitlab.com/troyengel"},"resolve_outdated_diff_discussions":false,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"container_registry_enabled":true,"service_desk_enabled":true,"can_create_merge_request_in":false,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","forking_access_level":"enabled","wiki_access_level":"enabled","builds_access_level":"enabled","snippets_access_level":"enabled","pages_access_level":"enabled","analytics_access_level":"enabled","container_registry_access_level":"enabled","security_and_compliance_access_level":"private","releases_access_level":"enabled","environments_access_level":"enabled","feature_flags_access_level":"enabled","infrastructure_access_level":"enabled","monitor_access_level":"enabled","model_experiments_access_level":"enabled","emails_disabled":false,"emails_enabled":true,"shared_runners_enabled":true,"lfs_enabled":false,"creator_id":1215848,"import_status":"finished","open_issues_count":0,"description_html":"\u003cp data-sourcepos=\"1:1-1:30\" dir=\"auto\"\u003eArch packaging and build files\u003c/p\u003e","updated_at":"2022-07-13T21:32:12.624Z","ci_config_path":null,"public_jobs":true,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"allow_merge_on_skipped_pipeline":null,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"remove_source_branch_after_merge":null,"printing_merge_request_link_enabled":true,"merge_method":"merge","squash_option":"default_off","enforce_auth_checks_on_uploads":true,"suggestion_commit_message":null,"merge_commit_template":null,"squash_commit_template":null,"issue_branch_template":null,"autoclose_referenced_issues":true,"external_authorization_classification_label":"","requirements_enabled":false,"requirements_access_level":"enabled","security_and_compliance_enabled":false,"compliance_frameworks":[],"permissions":{"project_access":null,"group_access":null}} \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996_issues!page=1&per_page=10&sort=asc&state=all b/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996_issues!page=1&per_page=10&sort=asc&state=all new file mode 100644 index 0000000000..99133d5f8d --- /dev/null +++ b/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996_issues!page=1&per_page=10&sort=asc&state=all @@ -0,0 +1,29 @@ +Link: ; rel="first", ; rel="last" +Ratelimit-Observed: 4 +Ratelimit-Remaining: 1996 +Gitlab-Lb: haproxy-main-04-lb-gprd +Vary: Origin, Accept-Encoding +Content-Security-Policy: default-src 'none' +X-Next-Page: +Ratelimit-Reset: 1701332694 +Etag: W/"f50a70d0fc1465a289d231f80806ced7" +X-Gitlab-Meta: {"correlation_id":"47afd74254dd7946d2b2bded87448c60","version":"1"} +X-Page: 1 +X-Prev-Page: +Referrer-Policy: strict-origin-when-cross-origin +Ratelimit-Resettime: Thu, 30 Nov 2023 08:24:54 GMT +Cf-Cache-Status: MISS +X-Total: 1 +X-Total-Pages: 1 +Strict-Transport-Security: max-age=31536000 +Content-Type: application/json +X-Frame-Options: SAMEORIGIN +Ratelimit-Limit: 2000 +Gitlab-Sv: localhost +Set-Cookie: _cfuvid=YDWTZ5VoSuLBDZgKsBnXMyYxz.0rHJ9TBYXv5zBj24Q-1701332634294-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +Cache-Control: max-age=0, private, must-revalidate +X-Content-Type-Options: nosniff +X-Per-Page: 10 +X-Runtime: 0.179458 + +[{"id":11201348,"iid":2,"project_id":6590996,"title":"vpn unlimited errors","description":"updated version to 2.8.0, build and tried running `vpnu-arch`:\n\n```\nvpn-unlimited: /usr/lib/libcurl.so.3: no version information available (required by /usr/lib/libvpnu_rpc.so.1)\nvpn-unlimited: /usr/lib/libssl.so.1.0.0: no version information available (required by /usr/lib/libvpnu_enc.so.1)\nvpn-unlimited: symbol lookup error: /usr/lib/libvpnu_rpc.so.1: undefined symbol: _ZNK4Json5Value8asStringEv\n```\n","state":"closed","created_at":"2016-03-26T16:41:12.000Z","updated_at":"2016-03-27T12:19:27.000Z","closed_at":null,"closed_by":null,"labels":[],"milestone":null,"assignees":[],"author":{"id":10273,"username":"brauliobo","name":"Bráulio Bhavamitra","state":"active","locked":false,"avatar_url":"https://secure.gravatar.com/avatar/cd3fcb7a417c8acb989fc320b604a2a8?s=80\u0026d=identicon","web_url":"https://gitlab.com/brauliobo"},"type":"ISSUE","assignee":null,"user_notes_count":1,"merge_requests_count":0,"upvotes":0,"downvotes":0,"due_date":null,"confidential":false,"discussion_locked":null,"issue_type":"issue","web_url":"https://gitlab.com/troyengel/archbuild/-/issues/2","time_stats":{"time_estimate":0,"total_time_spent":0,"human_time_estimate":null,"human_total_time_spent":null},"task_completion_status":{"count":0,"completed_count":0},"blocking_issues_count":0,"has_tasks":true,"task_status":"0 of 0 checklist items completed","_links":{"self":"https://gitlab.com/api/v4/projects/6590996/issues/2","notes":"https://gitlab.com/api/v4/projects/6590996/issues/2/notes","award_emoji":"https://gitlab.com/api/v4/projects/6590996/issues/2/award_emoji","project":"https://gitlab.com/api/v4/projects/6590996","closed_as_duplicate_of":null},"references":{"short":"#2","relative":"#2","full":"troyengel/archbuild#2"},"severity":"UNKNOWN","moved_to_id":null,"service_desk_reply_to":null}] \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996_issues_2_award_emoji!page=1&per_page=10 b/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996_issues_2_award_emoji!page=1&per_page=10 new file mode 100644 index 0000000000..8f829d08f0 --- /dev/null +++ b/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996_issues_2_award_emoji!page=1&per_page=10 @@ -0,0 +1,31 @@ +Gitlab-Sv: localhost +X-Content-Type-Options: nosniff +Gitlab-Lb: haproxy-main-25-lb-gprd +X-Total-Pages: 1 +Referrer-Policy: strict-origin-when-cross-origin +Ratelimit-Observed: 5 +Ratelimit-Remaining: 1995 +Content-Security-Policy: default-src 'none' +X-Gitlab-Meta: {"correlation_id":"eeab46d836341bd4cb18e3d2e82abf97","version":"1"} +Ratelimit-Limit: 2000 +Accept-Ranges: bytes +Content-Type: application/json +X-Page: 1 +X-Frame-Options: SAMEORIGIN +X-Prev-Page: +Cf-Cache-Status: MISS +X-Total: 0 +Ratelimit-Resettime: Thu, 30 Nov 2023 08:24:54 GMT +Link: ; rel="first", ; rel="last" +X-Per-Page: 10 +Set-Cookie: _cfuvid=c5HuTPxOuSXdHSuVrXQALS.uV7WvAYfc5Mc_143EAB8-1701332634513-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +Content-Length: 2 +Vary: Origin, Accept-Encoding +Cache-Control: max-age=0, private, must-revalidate +Etag: W/"4f53cda18c2baa0c0354bb5f9a3ecbe5" +X-Runtime: 0.069269 +Strict-Transport-Security: max-age=31536000 +Ratelimit-Reset: 1701332694 +X-Next-Page: + +[] \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996_merge_requests!page=1&per_page=10&view=simple b/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996_merge_requests!page=1&per_page=10&view=simple new file mode 100644 index 0000000000..5339392008 --- /dev/null +++ b/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996_merge_requests!page=1&per_page=10&view=simple @@ -0,0 +1,29 @@ +Vary: Origin, Accept-Encoding +Strict-Transport-Security: max-age=31536000 +Gitlab-Sv: localhost +X-Content-Type-Options: nosniff +X-Prev-Page: +Ratelimit-Reset: 1701332694 +Cache-Control: max-age=0, private, must-revalidate +Ratelimit-Limit: 2000 +Referrer-Policy: strict-origin-when-cross-origin +Ratelimit-Observed: 6 +Ratelimit-Resettime: Thu, 30 Nov 2023 08:24:54 GMT +Cf-Cache-Status: MISS +Content-Type: application/json +Content-Security-Policy: default-src 'none' +Etag: W/"1a50811aa3cccb2e6a404a976422a83a" +X-Total: 1 +Ratelimit-Remaining: 1994 +Set-Cookie: _cfuvid=u.zumTkG1ayCnh_OwrT9Q1Fl3MXV9Gh98W.ma4WN2Xs-1701332634745-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +Link: ; rel="first", ; rel="last" +X-Frame-Options: SAMEORIGIN +X-Page: 1 +X-Total-Pages: 1 +Gitlab-Lb: haproxy-main-05-lb-gprd +X-Gitlab-Meta: {"correlation_id":"907f9e1f94131ea7a6d1405100a8cc4b","version":"1"} +X-Next-Page: +X-Per-Page: 10 +X-Runtime: 0.078413 + +[{"id":10518914,"iid":1,"project_id":6590996,"title":"Review","description":"*Created by: cgtx*\n\n### remove patch from makedepends\n- patch is in base-devel\n- The group base-devel is assumed to be already installed when building with makepkg. Members of \"base-devel\" should not be included in makedepends arrays.\n- https://wiki.archlinux.org/index.php/Pkgbuild#makedepends\n### remove python2 from makedepends\n- python2 is a dependency of python2-setuptools. It is redundant to list it again.\n- You do not need to list packages that your software depends on if other packages your software depends on already have those packages listed in their dependency.\n- https://wiki.archlinux.org/index.php/Pkgbuild#depends\n### more simple find/delete command\n- just because\n","state":"merged","created_at":"2014-12-12T15:01:32.000Z","updated_at":"2014-12-12T15:28:38.000Z","web_url":"https://gitlab.com/troyengel/archbuild/-/merge_requests/1"}] \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996_merge_requests_1 b/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996_merge_requests_1 new file mode 100644 index 0000000000..18e8a8583f --- /dev/null +++ b/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996_merge_requests_1 @@ -0,0 +1,22 @@ +Ratelimit-Observed: 7 +Set-Cookie: _cfuvid=_b9GQEo3CBPMs9QmGE89dBdOmbSTfnYjZlzValULQPs-1701332635000-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +Strict-Transport-Security: max-age=31536000 +Ratelimit-Resettime: Thu, 30 Nov 2023 08:24:54 GMT +Gitlab-Lb: haproxy-main-50-lb-gprd +Gitlab-Sv: localhost +X-Gitlab-Meta: {"correlation_id":"da44cd0303a4e62cc52ed8de3b2adf14","version":"1"} +Referrer-Policy: strict-origin-when-cross-origin +Ratelimit-Remaining: 1993 +Etag: W/"f6299e7e884cb8df8109256c086eb4e7" +X-Runtime: 0.107573 +Content-Type: application/json +Ratelimit-Reset: 1701332694 +X-Frame-Options: SAMEORIGIN +Cache-Control: max-age=0, private, must-revalidate +X-Content-Type-Options: nosniff +Ratelimit-Limit: 2000 +Cf-Cache-Status: MISS +Content-Security-Policy: default-src 'none' +Vary: Origin, Accept-Encoding + +{"id":10518914,"iid":1,"project_id":6590996,"title":"Review","description":"*Created by: cgtx*\n\n### remove patch from makedepends\n- patch is in base-devel\n- The group base-devel is assumed to be already installed when building with makepkg. Members of \"base-devel\" should not be included in makedepends arrays.\n- https://wiki.archlinux.org/index.php/Pkgbuild#makedepends\n### remove python2 from makedepends\n- python2 is a dependency of python2-setuptools. It is redundant to list it again.\n- You do not need to list packages that your software depends on if other packages your software depends on already have those packages listed in their dependency.\n- https://wiki.archlinux.org/index.php/Pkgbuild#depends\n### more simple find/delete command\n- just because\n","state":"merged","created_at":"2014-12-12T15:01:32.000Z","updated_at":"2014-12-12T15:28:38.000Z","merged_by":null,"merge_user":null,"merged_at":null,"closed_by":null,"closed_at":null,"target_branch":"master","source_branch":"cgtx:review","user_notes_count":1,"upvotes":0,"downvotes":0,"author":{"id":1215848,"username":"troyengel","name":"Troy Engel","state":"active","locked":false,"avatar_url":"https://secure.gravatar.com/avatar/b226c267929f1bcfcc446e75a025591c?s=80\u0026d=identicon","web_url":"https://gitlab.com/troyengel"},"assignees":[],"assignee":null,"reviewers":[],"source_project_id":6590996,"target_project_id":6590996,"labels":[],"draft":false,"work_in_progress":false,"milestone":null,"merge_when_pipeline_succeeds":false,"merge_status":"cannot_be_merged","detailed_merge_status":"not_open","sha":"9006fee398299beed8f5d5086f8e6008ffc02280","merge_commit_sha":null,"squash_commit_sha":null,"discussion_locked":null,"should_remove_source_branch":null,"force_remove_source_branch":null,"prepared_at":"2014-12-12T15:01:32.000Z","reference":"!1","references":{"short":"!1","relative":"!1","full":"troyengel/archbuild!1"},"web_url":"https://gitlab.com/troyengel/archbuild/-/merge_requests/1","time_stats":{"time_estimate":0,"total_time_spent":0,"human_time_estimate":null,"human_total_time_spent":null},"squash":false,"squash_on_merge":false,"task_completion_status":{"count":0,"completed_count":0},"has_conflicts":true,"blocking_discussions_resolved":true,"approvals_before_merge":null,"subscribed":false,"changes_count":"1","latest_build_started_at":null,"latest_build_finished_at":null,"first_deployed_to_production_at":null,"pipeline":null,"head_pipeline":null,"diff_refs":{"base_sha":"6edcf8fc09f6c44213c892f5108d34a5255a47e1","head_sha":"9006fee398299beed8f5d5086f8e6008ffc02280","start_sha":"6edcf8fc09f6c44213c892f5108d34a5255a47e1"},"merge_error":null,"first_contribution":false,"user":{"can_merge":false}} \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996_merge_requests_1_award_emoji!page=1&per_page=10 b/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996_merge_requests_1_award_emoji!page=1&per_page=10 new file mode 100644 index 0000000000..d6f8dd4941 --- /dev/null +++ b/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996_merge_requests_1_award_emoji!page=1&per_page=10 @@ -0,0 +1,31 @@ +Link: ; rel="first", ; rel="last" +Set-Cookie: _cfuvid=qK29tijoyp0AdVoHf9Lqjc8Y28h4jplJDW9hOFLfq28-1701332635229-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +Cache-Control: max-age=0, private, must-revalidate +Etag: W/"4f53cda18c2baa0c0354bb5f9a3ecbe5" +Ratelimit-Observed: 8 +Gitlab-Sv: localhost +Content-Length: 2 +Gitlab-Lb: haproxy-main-16-lb-gprd +X-Total: 0 +Ratelimit-Remaining: 1992 +Ratelimit-Reset: 1701332695 +Ratelimit-Limit: 2000 +Vary: Origin, Accept-Encoding +X-Frame-Options: SAMEORIGIN +Content-Type: application/json +X-Content-Type-Options: nosniff +X-Next-Page: +X-Page: 1 +Strict-Transport-Security: max-age=31536000 +Accept-Ranges: bytes +Content-Security-Policy: default-src 'none' +X-Per-Page: 10 +X-Total-Pages: 1 +Referrer-Policy: strict-origin-when-cross-origin +Ratelimit-Resettime: Thu, 30 Nov 2023 08:24:55 GMT +Cf-Cache-Status: MISS +X-Gitlab-Meta: {"correlation_id":"eb59d63fed23cdbec69308570cc49c3e","version":"1"} +X-Runtime: 0.065972 +X-Prev-Page: + +[] \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_troyengel%2Farchbuild b/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_troyengel%2Farchbuild new file mode 100644 index 0000000000..a8c2882c26 --- /dev/null +++ b/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_troyengel%2Farchbuild @@ -0,0 +1,22 @@ +Ratelimit-Resettime: Thu, 30 Nov 2023 08:24:53 GMT +Gitlab-Lb: haproxy-main-41-lb-gprd +Cache-Control: max-age=0, private, must-revalidate +Referrer-Policy: strict-origin-when-cross-origin +Cf-Cache-Status: MISS +X-Content-Type-Options: nosniff +Set-Cookie: _cfuvid=r78xThY2IPR6QvHnea1t_L7DbvuQp4.HWOiG1cKTWUg-1701332633720-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +Ratelimit-Limit: 2000 +Strict-Transport-Security: max-age=31536000 +Vary: Origin, Accept-Encoding +X-Gitlab-Meta: {"correlation_id":"4c3e0f8b5858454b6e138ecae9902a8d","version":"1"} +X-Runtime: 0.097047 +Ratelimit-Observed: 2 +Ratelimit-Remaining: 1998 +X-Frame-Options: SAMEORIGIN +Content-Security-Policy: default-src 'none' +Etag: W/"03ce4f6ce1c1e8c5a31df8a44cf2fbdd" +Content-Type: application/json +Gitlab-Sv: localhost +Ratelimit-Reset: 1701332693 + +{"id":6590996,"description":"Arch packaging and build files","name":"archbuild","name_with_namespace":"Troy Engel / archbuild","path":"archbuild","path_with_namespace":"troyengel/archbuild","created_at":"2018-06-03T22:53:17.388Z","default_branch":"master","tag_list":[],"topics":[],"ssh_url_to_repo":"git@gitlab.com:troyengel/archbuild.git","http_url_to_repo":"https://gitlab.com/troyengel/archbuild.git","web_url":"https://gitlab.com/troyengel/archbuild","readme_url":"https://gitlab.com/troyengel/archbuild/-/blob/master/README.md","forks_count":0,"avatar_url":null,"star_count":0,"last_activity_at":"2020-12-13T18:09:32.071Z","namespace":{"id":1452515,"name":"Troy Engel","path":"troyengel","kind":"user","full_path":"troyengel","parent_id":null,"avatar_url":"https://secure.gravatar.com/avatar/b226c267929f1bcfcc446e75a025591c?s=80\u0026d=identicon","web_url":"https://gitlab.com/troyengel"},"container_registry_image_prefix":"registry.gitlab.com/troyengel/archbuild","_links":{"self":"https://gitlab.com/api/v4/projects/6590996","issues":"https://gitlab.com/api/v4/projects/6590996/issues","merge_requests":"https://gitlab.com/api/v4/projects/6590996/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/6590996/repository/branches","labels":"https://gitlab.com/api/v4/projects/6590996/labels","events":"https://gitlab.com/api/v4/projects/6590996/events","members":"https://gitlab.com/api/v4/projects/6590996/members","cluster_agents":"https://gitlab.com/api/v4/projects/6590996/cluster_agents"},"packages_enabled":null,"empty_repo":false,"archived":true,"visibility":"public","owner":{"id":1215848,"username":"troyengel","name":"Troy Engel","state":"active","locked":false,"avatar_url":"https://secure.gravatar.com/avatar/b226c267929f1bcfcc446e75a025591c?s=80\u0026d=identicon","web_url":"https://gitlab.com/troyengel"},"resolve_outdated_diff_discussions":false,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"container_registry_enabled":true,"service_desk_enabled":true,"can_create_merge_request_in":false,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","forking_access_level":"enabled","wiki_access_level":"enabled","builds_access_level":"enabled","snippets_access_level":"enabled","pages_access_level":"enabled","analytics_access_level":"enabled","container_registry_access_level":"enabled","security_and_compliance_access_level":"private","releases_access_level":"enabled","environments_access_level":"enabled","feature_flags_access_level":"enabled","infrastructure_access_level":"enabled","monitor_access_level":"enabled","model_experiments_access_level":"enabled","emails_disabled":false,"emails_enabled":true,"shared_runners_enabled":true,"lfs_enabled":false,"creator_id":1215848,"import_status":"finished","open_issues_count":0,"description_html":"\u003cp data-sourcepos=\"1:1-1:30\" dir=\"auto\"\u003eArch packaging and build files\u003c/p\u003e","updated_at":"2022-07-13T21:32:12.624Z","ci_config_path":null,"public_jobs":true,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"allow_merge_on_skipped_pipeline":null,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"remove_source_branch_after_merge":null,"printing_merge_request_link_enabled":true,"merge_method":"merge","squash_option":"default_off","enforce_auth_checks_on_uploads":true,"suggestion_commit_message":null,"merge_commit_template":null,"squash_commit_template":null,"issue_branch_template":null,"autoclose_referenced_issues":true,"external_authorization_classification_label":"","requirements_enabled":false,"requirements_access_level":"enabled","security_and_compliance_enabled":false,"compliance_frameworks":[],"permissions":{"project_access":null,"group_access":null}} \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_version b/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_version new file mode 100644 index 0000000000..eb6df2ffb1 --- /dev/null +++ b/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_version @@ -0,0 +1,22 @@ +Ratelimit-Observed: 1 +X-Gitlab-Meta: {"correlation_id":"aa75720bd9c597c7f2f886a4042d1f80","version":"1"} +Etag: W/"4e5c0a031c3aacb6ba0a3c19e67d7592" +X-Content-Type-Options: nosniff +Ratelimit-Limit: 2000 +Ratelimit-Resettime: Thu, 30 Nov 2023 08:24:53 GMT +X-Runtime: 0.039899 +Ratelimit-Remaining: 1999 +Set-Cookie: _cfuvid=7OAEitQ3J0BOxrXk2pMBApFg1KFnz5aBVqOY7mHwLRk-1701332633452-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +Content-Security-Policy: default-src 'none' +Gitlab-Sv: localhost +Cf-Cache-Status: MISS +Vary: Origin, Accept-Encoding +X-Frame-Options: SAMEORIGIN +Cache-Control: max-age=0, private, must-revalidate +Strict-Transport-Security: max-age=31536000 +Referrer-Policy: strict-origin-when-cross-origin +Ratelimit-Reset: 1701332693 +Gitlab-Lb: haproxy-main-39-lb-gprd +Content-Type: application/json + +{"version":"16.7.0-pre","revision":"acd848a9228","kas":{"enabled":true,"externalUrl":"wss://kas.gitlab.com","version":"v16.7.0-rc2"},"enterprise":true} \ No newline at end of file diff --git a/services/notify/notifier.go b/services/notify/notifier.go index ed053a812a..3230a5e5f5 100644 --- a/services/notify/notifier.go +++ b/services/notify/notifier.go @@ -59,6 +59,8 @@ type Notifier interface { EditWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, page, comment string) DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, page string) + NewUserSignUp(ctx context.Context, newUser *user_model.User) + NewRelease(ctx context.Context, rel *repo_model.Release) UpdateRelease(ctx context.Context, doer *user_model.User, rel *repo_model.Release) DeleteRelease(ctx context.Context, doer *user_model.User, rel *repo_model.Release) diff --git a/services/notify/notify.go b/services/notify/notify.go index 16fbb6325d..9cb329d302 100644 --- a/services/notify/notify.go +++ b/services/notify/notify.go @@ -347,6 +347,13 @@ func RepoPendingTransfer(ctx context.Context, doer, newOwner *user_model.User, r } } +// NewUserSignUp notifies about a newly signed up user to notifiers +func NewUserSignUp(ctx context.Context, newUser *user_model.User) { + for _, notifier := range notifiers { + notifier.NewUserSignUp(ctx, newUser) + } +} + // PackageCreate notifies creation of a package to notifiers func PackageCreate(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor) { for _, notifier := range notifiers { diff --git a/services/notify/null.go b/services/notify/null.go index dddd421bef..894d118eac 100644 --- a/services/notify/null.go +++ b/services/notify/null.go @@ -197,6 +197,9 @@ func (*NullNotifier) SyncDeleteRef(ctx context.Context, doer *user_model.User, r func (*NullNotifier) RepoPendingTransfer(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository) { } +func (*NullNotifier) NewUserSignUp(ctx context.Context, newUser *user_model.User) { +} + // PackageCreate places a place holder function func (*NullNotifier) PackageCreate(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor) { } diff --git a/services/pull/commit_status.go b/services/pull/commit_status.go index b73816c7eb..06e66fad77 100644 --- a/services/pull/commit_status.go +++ b/services/pull/commit_status.go @@ -6,6 +6,8 @@ package pull import ( "context" + "errors" + "fmt" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" @@ -16,7 +18,6 @@ import ( "code.gitea.io/gitea/modules/structs" "github.com/gobwas/glob" - "github.com/pkg/errors" ) // MergeRequiredContextsCommitStatus returns a commit status state for given required contexts @@ -96,7 +97,7 @@ func IsCommitStatusContextSuccess(commitStatuses []*git_model.CommitStatus, requ func IsPullCommitStatusPass(ctx context.Context, pr *issues_model.PullRequest) (bool, error) { pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch) if err != nil { - return false, errors.Wrap(err, "GetLatestCommitStatus") + return false, fmt.Errorf("GetFirstMatchProtectedBranchRule: %w", err) } if pb == nil || !pb.EnableStatusCheck { return true, nil @@ -113,21 +114,21 @@ func IsPullCommitStatusPass(ctx context.Context, pr *issues_model.PullRequest) ( func GetPullRequestCommitStatusState(ctx context.Context, pr *issues_model.PullRequest) (structs.CommitStatusState, error) { // Ensure HeadRepo is loaded if err := pr.LoadHeadRepo(ctx); err != nil { - return "", errors.Wrap(err, "LoadHeadRepo") + return "", fmt.Errorf("LoadHeadRepo: %w", err) } // check if all required status checks are successful headGitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pr.HeadRepo) if err != nil { - return "", errors.Wrap(err, "OpenRepository") + return "", fmt.Errorf("RepositoryFromContextOrOpen: %w", err) } defer closer.Close() if pr.Flow == issues_model.PullRequestFlowGithub && !headGitRepo.IsBranchExist(pr.HeadBranch) { - return "", errors.New("Head branch does not exist, can not merge") + return "", errors.New("head branch does not exist, can not merge") } if pr.Flow == issues_model.PullRequestFlowAGit && !git.IsReferenceExist(ctx, headGitRepo.Path, pr.GetGitRefName()) { - return "", errors.New("Head branch does not exist, can not merge") + return "", errors.New("head branch does not exist, can not merge") } var sha string @@ -141,17 +142,17 @@ func GetPullRequestCommitStatusState(ctx context.Context, pr *issues_model.PullR } if err := pr.LoadBaseRepo(ctx); err != nil { - return "", errors.Wrap(err, "LoadBaseRepo") + return "", fmt.Errorf("LoadBaseRepo: %w", err) } commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, pr.BaseRepo.ID, sha, db.ListOptions{ListAll: true}) if err != nil { - return "", errors.Wrap(err, "GetLatestCommitStatus") + return "", fmt.Errorf("GetLatestCommitStatus: %w", err) } pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch) if err != nil { - return "", errors.Wrap(err, "LoadProtectedBranch") + return "", fmt.Errorf("GetFirstMatchProtectedBranchRule: %w", err) } var requiredContexts []string if pb != nil { diff --git a/services/pull/merge.go b/services/pull/merge.go index 63f0268beb..91b110351a 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -181,7 +181,7 @@ func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.U } defer func() { - go AddTestPullRequestTask(doer, pr.BaseRepo.ID, pr.BaseBranch, false, "", "") + AddTestPullRequestTask(ctx, doer, pr.BaseRepo.ID, pr.BaseBranch, false, "", "") }() pr.MergedCommitID, err = doMergeAndPush(ctx, pr, doer, mergeStyle, expectedHeadCommitID, message) diff --git a/services/pull/pull.go b/services/pull/pull.go index 7700b70682..ef6c589a2f 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -292,113 +292,129 @@ func checkForInvalidation(ctx context.Context, requests issues_model.PullRequest // AddTestPullRequestTask adds new test tasks by given head/base repository and head/base branch, // and generate new patch for testing as needed. -func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string, isSync bool, oldCommitID, newCommitID string) { - log.Trace("AddTestPullRequestTask [head_repo_id: %d, head_branch: %s]: finding pull requests", repoID, branch) - graceful.GetManager().RunWithShutdownContext(func(ctx context.Context) { +func AddTestPullRequestTask(ctx context.Context, doer *user_model.User, repoID int64, branch string, isSync bool, oldCommitID, newCommitID string) { + // When TestPullRequest runs it must ignore any PR with an index > maxPR because they + // would have been created after the goroutine started. They are in the future. + // This guards the following race: + // * commit A is pushed + // * goroutine starts but does not run TestPullRequest yet + // * a pull request with commit A as the head is created + // * goroutine continues and runs TestPullRequest + maxPR, err := issues_model.GetMaxIssueIndexForRepo(ctx, repoID) + if err != nil { + log.Error("AddTestPullRequestTask GetMaxIssueIndexForRepo(%d): %v", repoID, err) + return + } + log.Trace("AddTestPullRequestTask [head_repo_id: %d, head_branch: %s]: only pull requests with index <= %d will be considered", repoID, branch, maxPR) + go graceful.GetManager().RunWithShutdownContext(func(ctx context.Context) { // There is no sensible way to shut this down ":-(" // If you don't let it run all the way then you will lose data - // TODO: graceful: AddTestPullRequestTask needs to become a queue! + // TODO: graceful: TestPullRequest needs to become a queue! - // GetUnmergedPullRequestsByHeadInfo() only return open and unmerged PR. - prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(ctx, repoID, branch) - if err != nil { - log.Error("Find pull requests [head_repo_id: %d, head_branch: %s]: %v", repoID, branch, err) - return - } + TestPullRequest(ctx, doer, repoID, maxPR, branch, isSync, oldCommitID, newCommitID) + }) +} - for _, pr := range prs { - log.Trace("Updating PR[%d]: composing new test task", pr.ID) - if pr.Flow == issues_model.PullRequestFlowGithub { - if err := PushToBaseRepo(ctx, pr); err != nil { - log.Error("PushToBaseRepo: %v", err) - continue - } - } else { +func TestPullRequest(ctx context.Context, doer *user_model.User, repoID, maxPR int64, branch string, isSync bool, oldCommitID, newCommitID string) { + // GetUnmergedPullRequestsByHeadInfo() only return open and unmerged PR. + prs, err := issues_model.GetUnmergedPullRequestsByHeadInfoMax(ctx, repoID, maxPR, branch) + if err != nil { + log.Error("Find pull requests [head_repo_id: %d, head_branch: %s]: %v", repoID, branch, err) + return + } + + for _, pr := range prs { + log.Trace("Updating PR[id=%d,index=%d]: composing new test task", pr.ID, pr.Index) + if pr.Flow == issues_model.PullRequestFlowGithub { + if err := PushToBaseRepo(ctx, pr); err != nil { + log.Error("PushToBaseRepo: %v", err) continue } - - AddToTaskQueue(ctx, pr) - comment, err := CreatePushPullComment(ctx, doer, pr, oldCommitID, newCommitID) - if err == nil && comment != nil { - notify_service.PullRequestPushCommits(ctx, doer, pr, comment) - } + } else { + continue } - if isSync { - requests := issues_model.PullRequestList(prs) - if err = requests.LoadAttributes(ctx); err != nil { - log.Error("PullRequestList.LoadAttributes: %v", err) - } - if invalidationErr := checkForInvalidation(ctx, requests, repoID, doer, branch); invalidationErr != nil { - log.Error("checkForInvalidation: %v", invalidationErr) - } - if err == nil { - for _, pr := range prs { - objectFormat, _ := git.GetObjectFormatOfRepo(ctx, pr.BaseRepo.RepoPath()) - if newCommitID != "" && newCommitID != objectFormat.EmptyObjectID().String() { - changed, err := checkIfPRContentChanged(ctx, pr, oldCommitID, newCommitID) - if err != nil { - log.Error("checkIfPRContentChanged: %v", err) - } - if changed { - // Mark old reviews as stale if diff to mergebase has changed - if err := issues_model.MarkReviewsAsStale(ctx, pr.IssueID); err != nil { - log.Error("MarkReviewsAsStale: %v", err) - } + AddToTaskQueue(ctx, pr) + comment, err := CreatePushPullComment(ctx, doer, pr, oldCommitID, newCommitID) + if err == nil && comment != nil { + notify_service.PullRequestPushCommits(ctx, doer, pr, comment) + } + } - // dismiss all approval reviews if protected branch rule item enabled. - pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch) - if err != nil { - log.Error("GetFirstMatchProtectedBranchRule: %v", err) - } - if pb != nil && pb.DismissStaleApprovals { - if err := DismissApprovalReviews(ctx, doer, pr); err != nil { - log.Error("DismissApprovalReviews: %v", err) - } - } + if isSync { + requests := issues_model.PullRequestList(prs) + if err = requests.LoadAttributes(ctx); err != nil { + log.Error("PullRequestList.LoadAttributes: %v", err) + } + if invalidationErr := checkForInvalidation(ctx, requests, repoID, doer, branch); invalidationErr != nil { + log.Error("checkForInvalidation: %v", invalidationErr) + } + if err == nil { + for _, pr := range prs { + objectFormat, _ := git.GetObjectFormatOfRepo(ctx, pr.BaseRepo.RepoPath()) + if newCommitID != "" && newCommitID != objectFormat.EmptyObjectID().String() { + changed, err := checkIfPRContentChanged(ctx, pr, oldCommitID, newCommitID) + if err != nil { + log.Error("checkIfPRContentChanged: %v", err) + } + if changed { + // Mark old reviews as stale if diff to mergebase has changed + if err := issues_model.MarkReviewsAsStale(ctx, pr.IssueID); err != nil { + log.Error("MarkReviewsAsStale: %v", err) } - if err := issues_model.MarkReviewsAsNotStale(ctx, pr.IssueID, newCommitID); err != nil { - log.Error("MarkReviewsAsNotStale: %v", err) - } - divergence, err := GetDiverging(ctx, pr) + + // dismiss all approval reviews if protected branch rule item enabled. + pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch) if err != nil { - log.Error("GetDiverging: %v", err) - } else { - err = pr.UpdateCommitDivergence(ctx, divergence.Ahead, divergence.Behind) - if err != nil { - log.Error("UpdateCommitDivergence: %v", err) + log.Error("GetFirstMatchProtectedBranchRule: %v", err) + } + if pb != nil && pb.DismissStaleApprovals { + if err := DismissApprovalReviews(ctx, doer, pr); err != nil { + log.Error("DismissApprovalReviews: %v", err) } } } - - notify_service.PullRequestSynchronized(ctx, doer, pr) + if err := issues_model.MarkReviewsAsNotStale(ctx, pr.IssueID, newCommitID); err != nil { + log.Error("MarkReviewsAsNotStale: %v", err) + } + divergence, err := GetDiverging(ctx, pr) + if err != nil { + log.Error("GetDiverging: %v", err) + } else { + err = pr.UpdateCommitDivergence(ctx, divergence.Ahead, divergence.Behind) + if err != nil { + log.Error("UpdateCommitDivergence: %v", err) + } + } } + + notify_service.PullRequestSynchronized(ctx, doer, pr) } } + } - log.Trace("AddTestPullRequestTask [base_repo_id: %d, base_branch: %s]: finding pull requests", repoID, branch) - prs, err = issues_model.GetUnmergedPullRequestsByBaseInfo(ctx, repoID, branch) + log.Trace("TestPullRequest [base_repo_id: %d, base_branch: %s]: finding pull requests", repoID, branch) + prs, err = issues_model.GetUnmergedPullRequestsByBaseInfo(ctx, repoID, branch) + if err != nil { + log.Error("Find pull requests [base_repo_id: %d, base_branch: %s]: %v", repoID, branch, err) + return + } + for _, pr := range prs { + divergence, err := GetDiverging(ctx, pr) if err != nil { - log.Error("Find pull requests [base_repo_id: %d, base_branch: %s]: %v", repoID, branch, err) - return - } - for _, pr := range prs { - divergence, err := GetDiverging(ctx, pr) - if err != nil { - if git_model.IsErrBranchNotExist(err) && !git.IsBranchExist(ctx, pr.HeadRepo.RepoPath(), pr.HeadBranch) { - log.Warn("Cannot test PR %s/%d: head_branch %s no longer exists", pr.BaseRepo.Name, pr.IssueID, pr.HeadBranch) - } else { - log.Error("GetDiverging: %v", err) - } + if git_model.IsErrBranchNotExist(err) && !git.IsBranchExist(ctx, pr.HeadRepo.RepoPath(), pr.HeadBranch) { + log.Warn("Cannot test PR %s/%d: head_branch %s no longer exists", pr.BaseRepo.Name, pr.IssueID, pr.HeadBranch) } else { - err = pr.UpdateCommitDivergence(ctx, divergence.Ahead, divergence.Behind) - if err != nil { - log.Error("UpdateCommitDivergence: %v", err) - } + log.Error("GetDiverging: %v", err) + } + } else { + err = pr.UpdateCommitDivergence(ctx, divergence.Ahead, divergence.Behind) + if err != nil { + log.Error("UpdateCommitDivergence: %v", err) } - AddToTaskQueue(ctx, pr) } - }) + AddToTaskQueue(ctx, pr) + } } // checkIfPRContentChanged checks if diff to target branch has changed by push diff --git a/services/pull/review.go b/services/pull/review.go index d4ea975612..7e9e7d40a6 100644 --- a/services/pull/review.go +++ b/services/pull/review.go @@ -96,7 +96,7 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git. return nil, err } - comment, err := createCodeComment(ctx, + comment, err := CreateCodeCommentKnownReviewID(ctx, doer, issue.Repo, issue, @@ -136,7 +136,7 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git. } } - comment, err := createCodeComment(ctx, + comment, err := CreateCodeCommentKnownReviewID(ctx, doer, issue.Repo, issue, @@ -162,7 +162,7 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git. } // createCodeComment creates a plain code comment at the specified line / path -func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, content, treePath string, line, reviewID int64) (*issues_model.Comment, error) { +func CreateCodeCommentKnownReviewID(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, content, treePath string, line, reviewID int64) (*issues_model.Comment, error) { var commitID, patch string if err := issue.LoadPullRequest(ctx); err != nil { return nil, fmt.Errorf("LoadPullRequest: %w", err) diff --git a/services/pull/update.go b/services/pull/update.go index bc8c4a25e5..1b133857f9 100644 --- a/services/pull/update.go +++ b/services/pull/update.go @@ -36,7 +36,7 @@ func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model. if rebase { defer func() { - go AddTestPullRequestTask(doer, pr.BaseRepo.ID, pr.BaseBranch, false, "", "") + AddTestPullRequestTask(ctx, doer, pr.BaseRepo.ID, pr.BaseBranch, false, "", "") }() return updateHeadByRebaseOnToBase(ctx, pr, doer, message) @@ -75,7 +75,7 @@ func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model. _, err = doMergeAndPush(ctx, reversePR, doer, repo_model.MergeStyleMerge, "", message) defer func() { - go AddTestPullRequestTask(doer, reversePR.HeadRepo.ID, reversePR.HeadBranch, false, "", "") + AddTestPullRequestTask(ctx, doer, reversePR.HeadRepo.ID, reversePR.HeadBranch, false, "", "") }() return err diff --git a/services/repository/branch.go b/services/repository/branch.go index e2e50297af..d7ac25b7c9 100644 --- a/services/repository/branch.go +++ b/services/repository/branch.go @@ -10,7 +10,6 @@ import ( "strings" "code.gitea.io/gitea/models" - actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" @@ -24,7 +23,6 @@ import ( repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" - webhook_module "code.gitea.io/gitea/modules/webhook" notify_service "code.gitea.io/gitea/services/notify" files_service "code.gitea.io/gitea/services/repository/files" @@ -97,7 +95,13 @@ func LoadBranches(ctx context.Context, repo *repo_model.Repository, gitRepo *git for i := range dbBranches { branch, err := loadOneBranch(ctx, repo, dbBranches[i], &rules, repoIDToRepo, repoIDToGitRepo) if err != nil { - return nil, nil, 0, fmt.Errorf("loadOneBranch: %v", err) + log.Error("loadOneBranch() on repo #%d, branch '%s' failed: %v", repo.ID, dbBranches[i].Name, err) + + // TODO: Ideally, we would only do this if the branch doesn't exist + // anymore. That is not practical to check here currently, so we do + // this for all kinds of errors. + totalNumOfBranches-- + continue } branches = append(branches, branch) @@ -133,7 +137,7 @@ func loadOneBranch(ctx context.Context, repo *repo_model.Repository, dbBranch *g var err error divergence, err = files_service.CountDivergingCommits(ctx, repo, git.BranchPrefix+branchName) if err != nil { - log.Error("CountDivergingCommits: %v", err) + return nil, fmt.Errorf("CountDivergingCommits: %v", err) } } @@ -306,28 +310,13 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, doer *user_m return "from_not_exist", nil } - if err := git_model.RenameBranch(ctx, repo, from, to, func(ctx context.Context, isDefault bool) error { + if err := git_model.RenameBranch(ctx, repo, from, to, func(isDefault bool) error { err2 := gitRepo.RenameBranch(from, to) if err2 != nil { return err2 } if isDefault { - // if default branch changed, we need to delete all schedules and cron jobs - if err := actions_model.DeleteScheduleTaskByRepo(ctx, repo.ID); err != nil { - log.Error("DeleteCronTaskByRepo: %v", err) - } - // cancel running cron jobs of this repository and delete old schedules - if err := actions_model.CancelRunningJobs( - ctx, - repo.ID, - from, - "", - webhook_module.HookEventSchedule, - ); err != nil { - log.Error("CancelRunningJobs: %v", err) - } - err2 = gitRepo.SetDefaultBranch(to) if err2 != nil { return err2 @@ -376,7 +365,7 @@ func DeleteBranch(ctx context.Context, doer *user_model.User, repo *repo_model.R rawBranch, err := git_model.GetBranch(ctx, repo.ID, branchName) if err != nil { - return fmt.Errorf("GetBranch: %vc", err) + return fmt.Errorf("GetBranch: %v", err) } objectFormat, err := gitRepo.GetObjectFormat() @@ -463,50 +452,3 @@ func AddAllRepoBranchesToSyncQueue(ctx context.Context, doerID int64) error { } return nil } - -func SetRepoDefaultBranch(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, newBranchName string) error { - if repo.DefaultBranch == newBranchName { - return nil - } - - if !gitRepo.IsBranchExist(newBranchName) { - return git_model.ErrBranchNotExist{ - BranchName: newBranchName, - } - } - - oldDefaultBranchName := repo.DefaultBranch - repo.DefaultBranch = newBranchName - if err := db.WithTx(ctx, func(ctx context.Context) error { - if err := repo_model.UpdateDefaultBranch(ctx, repo); err != nil { - return err - } - - if err := actions_model.DeleteScheduleTaskByRepo(ctx, repo.ID); err != nil { - log.Error("DeleteCronTaskByRepo: %v", err) - } - // cancel running cron jobs of this repository and delete old schedules - if err := actions_model.CancelRunningJobs( - ctx, - repo.ID, - oldDefaultBranchName, - "", - webhook_module.HookEventSchedule, - ); err != nil { - log.Error("CancelRunningJobs: %v", err) - } - - if err := gitRepo.SetDefaultBranch(newBranchName); err != nil { - if !git.IsErrUnsupportedVersion(err) { - return err - } - } - return nil - }); err != nil { - return err - } - - notify_service.ChangeDefaultBranch(ctx, repo) - - return nil -} diff --git a/services/repository/create.go b/services/repository/create.go index a648c0d816..c3b50ae747 100644 --- a/services/repository/create.go +++ b/services/repository/create.go @@ -173,6 +173,7 @@ func initRepository(ctx context.Context, repoPath string, u *user_model.User, re } repo.DefaultBranch = setting.Repository.DefaultBranch + repo.WikiBranch = setting.Repository.DefaultBranch if len(opts.DefaultBranch) > 0 { repo.DefaultBranch = opts.DefaultBranch @@ -240,6 +241,7 @@ func CreateRepositoryDirectly(ctx context.Context, doer, u *user_model.User, opt TrustModel: opts.TrustModel, IsMirror: opts.IsMirror, DefaultBranch: opts.DefaultBranch, + WikiBranch: setting.Repository.DefaultBranch, ObjectFormatName: opts.ObjectFormatName, } diff --git a/services/repository/files/content.go b/services/repository/files/content.go index c278d7f835..8adec4aff8 100644 --- a/services/repository/files/content.go +++ b/services/repository/files/content.go @@ -220,7 +220,7 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, treePath, ref } } // Handle links - if entry.IsRegular() || entry.IsLink() { + if entry.IsRegular() || entry.IsLink() || entry.IsExecutable() { downloadURL, err := url.Parse(repo.HTMLURL() + "/raw/" + url.PathEscape(string(refType)) + "/" + util.PathEscapeSegments(ref) + "/" + util.PathEscapeSegments(treePath)) if err != nil { return nil, err diff --git a/services/repository/files/file.go b/services/repository/files/file.go index 16783f5b5f..852cca0371 100644 --- a/services/repository/files/file.go +++ b/services/repository/files/file.go @@ -123,6 +123,8 @@ func GetAuthorAndCommitterUsers(author, committer *IdentityOptions, doer *user_m if committer.Name != "" { committerUser.FullName = committer.Name } + // Use the provided email and not revert to placeholder mail. + committerUser.KeepEmailPrivate = false } else { committerUser = &user_model.User{ FullName: committer.Name, @@ -136,6 +138,8 @@ func GetAuthorAndCommitterUsers(author, committer *IdentityOptions, doer *user_m if authorUser.Name != "" { authorUser.FullName = author.Name } + // Use the provided email and not revert to placeholder mail. + authorUser.KeepEmailPrivate = false } else { authorUser = &user_model.User{ FullName: author.Name, diff --git a/services/repository/push.go b/services/repository/push.go index bedcf6f252..2ef8cac95e 100644 --- a/services/repository/push.go +++ b/services/repository/push.go @@ -171,7 +171,7 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { branch := opts.RefFullName.BranchName() if !opts.IsDelRef() { log.Trace("TriggerTask '%s/%s' by %s", repo.Name, branch, pusher.Name) - go pull_service.AddTestPullRequestTask(pusher, repo.ID, branch, true, opts.OldCommitID, opts.NewCommitID) + pull_service.AddTestPullRequestTask(ctx, pusher, repo.ID, branch, true, opts.OldCommitID, opts.NewCommitID) newCommit, err := gitRepo.GetCommit(opts.NewCommitID) if err != nil { diff --git a/services/repository/setting.go b/services/repository/setting.go index b82f24271e..7dded5d6be 100644 --- a/services/repository/setting.go +++ b/services/repository/setting.go @@ -5,14 +5,10 @@ package repository import ( "context" - "slices" - actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" - "code.gitea.io/gitea/modules/log" - actions_service "code.gitea.io/gitea/services/actions" ) // UpdateRepositoryUnits updates a repository's units @@ -28,21 +24,6 @@ func UpdateRepositoryUnits(ctx context.Context, repo *repo_model.Repository, uni deleteUnitTypes = append(deleteUnitTypes, u.Type) } - if slices.Contains(deleteUnitTypes, unit.TypeActions) { - if err := actions_model.CleanRepoScheduleTasks(ctx, repo); err != nil { - log.Error("CleanRepoScheduleTasks: %v", err) - } - } - - for _, u := range units { - if u.Type == unit.TypeActions { - if err := actions_service.DetectAndHandleSchedules(ctx, repo); err != nil { - log.Error("DetectAndHandleSchedules: %v", err) - } - break - } - } - if _, err = db.GetEngine(ctx).Where("repo_id = ?", repo.ID).In("type", deleteUnitTypes).Delete(new(repo_model.RepoUnit)); err != nil { return err } diff --git a/services/user/email.go b/services/user/email.go index 07e19bc688..12ac1f6ca4 100644 --- a/services/user/email.go +++ b/services/user/email.go @@ -145,6 +145,33 @@ func AddEmailAddresses(ctx context.Context, u *user_model.User, emails []string) return nil } +// ReplaceInactivePrimaryEmail replaces the primary email of a given user, even if the primary is not yet activated. +func ReplaceInactivePrimaryEmail(ctx context.Context, oldEmail string, email *user_model.EmailAddress) error { + user := &user_model.User{} + has, err := db.GetEngine(ctx).ID(email.UID).Get(user) + if err != nil { + return err + } else if !has { + return user_model.ErrUserNotExist{ + UID: email.UID, + Name: "", + KeyID: 0, + } + } + + err = AddEmailAddresses(ctx, user, []string{email.Email}) + if err != nil { + return err + } + + err = user_model.MakeEmailPrimaryWithUser(ctx, user, email) + if err != nil { + return err + } + + return DeleteEmailAddresses(ctx, user, []string{oldEmail}) +} + func DeleteEmailAddresses(ctx context.Context, u *user_model.User, emails []string) error { for _, emailStr := range emails { // Check if address exists diff --git a/services/user/email_test.go b/services/user/email_test.go index 8f419b69f9..66d4821346 100644 --- a/services/user/email_test.go +++ b/services/user/email_test.go @@ -107,6 +107,28 @@ func TestAddEmailAddresses(t *testing.T) { assert.True(t, user_model.IsErrEmailAlreadyUsed(err)) } +func TestReplaceInactivePrimaryEmail(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + email := &user_model.EmailAddress{ + Email: "user9999999@example.com", + UID: 9999999, + } + err := ReplaceInactivePrimaryEmail(db.DefaultContext, "user10@example.com", email) + assert.Error(t, err) + assert.True(t, user_model.IsErrUserNotExist(err)) + + email = &user_model.EmailAddress{ + Email: "user201@example.com", + UID: 10, + } + err = ReplaceInactivePrimaryEmail(db.DefaultContext, "user10@example.com", email) + assert.NoError(t, err) + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 10}) + assert.Equal(t, "user201@example.com", user.Email) +} + func TestDeleteEmailAddresses(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) diff --git a/services/webhook/deliver.go b/services/webhook/deliver.go index c86b037ad5..16819f846a 100644 --- a/services/webhook/deliver.go +++ b/services/webhook/deliver.go @@ -7,6 +7,7 @@ import ( "context" "crypto/hmac" "crypto/sha1" + "crypto/sha256" "crypto/tls" "encoding/hex" "fmt" @@ -29,7 +30,6 @@ import ( webhook_module "code.gitea.io/gitea/modules/webhook" "github.com/gobwas/glob" - "github.com/minio/sha256-simd" ) // Deliver deliver hook task diff --git a/services/webhook/deliver_test.go b/services/webhook/deliver_test.go index 72aa00478a..eca2ba244b 100644 --- a/services/webhook/deliver_test.go +++ b/services/webhook/deliver_test.go @@ -8,6 +8,7 @@ import ( "net/http" "net/http/httptest" "net/url" + "os" "testing" "time" @@ -25,9 +26,15 @@ import ( func TestWebhookProxy(t *testing.T) { oldWebhook := setting.Webhook + oldHTTPProxy := os.Getenv("http_proxy") + oldHTTPSProxy := os.Getenv("https_proxy") t.Cleanup(func() { setting.Webhook = oldWebhook + os.Setenv("http_proxy", oldHTTPProxy) + os.Setenv("https_proxy", oldHTTPSProxy) }) + os.Unsetenv("http_proxy") + os.Unsetenv("https_proxy") setting.Webhook.ProxyURL = "http://localhost:8080" setting.Webhook.ProxyURLFixed, _ = url.Parse(setting.Webhook.ProxyURL) diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go index 50d52d3140..81e0b84ea8 100644 --- a/services/wiki/wiki.go +++ b/services/wiki/wiki.go @@ -1,5 +1,6 @@ // Copyright 2015 The Gogs Authors. All rights reserved. // Copyright 2019 The Gitea Authors. All rights reserved. +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. // SPDX-License-Identifier: MIT package wiki @@ -19,7 +20,6 @@ import ( repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/sync" asymkey_service "code.gitea.io/gitea/services/asymkey" - repo_service "code.gitea.io/gitea/services/repository" ) // TODO: use clustered lock (unique queue? or *abuse* cache) @@ -27,7 +27,6 @@ var wikiWorkingPool = sync.NewExclusivePool() const ( DefaultRemote = "origin" - DefaultBranch = "master" ) // InitWiki initializes a wiki for repository, @@ -37,26 +36,74 @@ func InitWiki(ctx context.Context, repo *repo_model.Repository) error { return nil } + branch := repo.GetWikiBranchName() + if err := git.InitRepository(ctx, repo.WikiPath(), true, repo.ObjectFormatName); err != nil { return fmt.Errorf("InitRepository: %w", err) } else if err = repo_module.CreateDelegateHooks(repo.WikiPath()); err != nil { return fmt.Errorf("createDelegateHooks: %w", err) - } else if _, _, err = git.NewCommand(ctx, "symbolic-ref", "HEAD", git.BranchPrefix+DefaultBranch).RunStdString(&git.RunOpts{Dir: repo.WikiPath()}); err != nil { - return fmt.Errorf("unable to set default wiki branch to master: %w", err) + } else if _, _, err = git.NewCommand(ctx, "symbolic-ref", "HEAD").AddDynamicArguments(git.BranchPrefix + branch).RunStdString(&git.RunOpts{Dir: repo.WikiPath()}); err != nil { + return fmt.Errorf("unable to set default wiki branch to %s: %w", branch, err) } return nil } +// NormalizeWikiBranch renames a repository wiki's branch to `setting.Repository.DefaultBranch` +func NormalizeWikiBranch(ctx context.Context, repo *repo_model.Repository, to string) error { + from := repo.GetWikiBranchName() + + if err := repo.MustNotBeArchived(); err != nil { + return err + } + + updateDB := func() error { + repo.WikiBranch = to + return repo_model.UpdateRepositoryCols(ctx, repo, "wiki_branch") + } + + if !repo.HasWiki() { + return updateDB() + } + + if from == to { + return nil + } + + gitRepo, err := git.OpenRepository(ctx, repo.WikiPath()) + if err != nil { + return err + } + defer gitRepo.Close() + + if gitRepo.IsBranchExist(to) { + return nil + } + + if !gitRepo.IsBranchExist(from) { + return nil + } + + if err := gitRepo.RenameBranch(from, to); err != nil { + return err + } + + if err := gitRepo.SetDefaultBranch(to); err != nil { + return err + } + + return updateDB() +} + // prepareGitPath try to find a suitable file path with file name by the given raw wiki name. // return: existence, prepared file path with name, error -func prepareGitPath(gitRepo *git.Repository, wikiPath WebPath) (bool, string, error) { +func prepareGitPath(gitRepo *git.Repository, branch string, wikiPath WebPath) (bool, string, error) { unescaped := string(wikiPath) + ".md" gitPath := WebPathToGitPath(wikiPath) // Look for both files - filesInIndex, err := gitRepo.LsTree(DefaultBranch, unescaped, gitPath) + filesInIndex, err := gitRepo.LsTree(branch, unescaped, gitPath) if err != nil { - if strings.Contains(err.Error(), "Not a valid object name master") { + if strings.Contains(err.Error(), "Not a valid object name "+branch) { return false, gitPath, nil } log.Error("%v", err) @@ -95,7 +142,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model return fmt.Errorf("InitWiki: %w", err) } - hasMasterBranch := git.IsBranchExist(ctx, repo.WikiPath(), DefaultBranch) + hasMasterBranch := git.IsBranchExist(ctx, repo.WikiPath(), repo.GetWikiBranchName()) basePath, err := repo_module.CreateTemporaryPath("update-wiki") if err != nil { @@ -113,7 +160,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model } if hasMasterBranch { - cloneOpts.Branch = DefaultBranch + cloneOpts.Branch = repo.GetWikiBranchName() } if err := git.Clone(ctx, repo.WikiPath(), basePath, cloneOpts); err != nil { @@ -135,7 +182,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model } } - isWikiExist, newWikiPath, err := prepareGitPath(gitRepo, newWikiName) + isWikiExist, newWikiPath, err := prepareGitPath(gitRepo, repo.GetWikiBranchName(), newWikiName) if err != nil { return err } @@ -151,7 +198,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model isOldWikiExist := true oldWikiPath := newWikiPath if oldWikiName != newWikiName { - isOldWikiExist, oldWikiPath, err = prepareGitPath(gitRepo, oldWikiName) + isOldWikiExist, oldWikiPath, err = prepareGitPath(gitRepo, repo.GetWikiBranchName(), oldWikiName) if err != nil { return err } @@ -212,7 +259,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model if err := git.Push(gitRepo.Ctx, basePath, git.PushOptions{ Remote: DefaultRemote, - Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, DefaultBranch), + Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, repo.GetWikiBranchName()), Env: repo_module.FullPushingEnvironment( doer, doer, @@ -269,7 +316,7 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model if err := git.Clone(ctx, repo.WikiPath(), basePath, git.CloneRepoOptions{ Bare: true, Shared: true, - Branch: DefaultBranch, + Branch: repo.GetWikiBranchName(), }); err != nil { log.Error("Failed to clone repository: %s (%v)", repo.FullName(), err) return fmt.Errorf("failed to clone repository: %s (%w)", repo.FullName(), err) @@ -287,7 +334,7 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model return fmt.Errorf("unable to read HEAD tree to index in: %s %w", basePath, err) } - found, wikiPath, err := prepareGitPath(gitRepo, wikiName) + found, wikiPath, err := prepareGitPath(gitRepo, repo.GetWikiBranchName(), wikiName) if err != nil { return err } @@ -331,7 +378,7 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model if err := git.Push(gitRepo.Ctx, basePath, git.PushOptions{ Remote: DefaultRemote, - Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, DefaultBranch), + Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, repo.GetWikiBranchName()), Env: repo_module.FullPushingEnvironment( doer, doer, @@ -351,7 +398,7 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model // DeleteWiki removes the actual and local copy of repository wiki. func DeleteWiki(ctx context.Context, repo *repo_model.Repository) error { - if err := repo_service.UpdateRepositoryUnits(ctx, repo, nil, []unit.Type{unit.TypeWiki}); err != nil { + if err := repo_model.UpdateRepositoryUnits(ctx, repo, nil, []unit.Type{unit.TypeWiki}); err != nil { return err } diff --git a/services/wiki/wiki_path.go b/services/wiki/wiki_path.go index 74c7064043..e51d6c630c 100644 --- a/services/wiki/wiki_path.go +++ b/services/wiki/wiki_path.go @@ -9,10 +9,7 @@ import ( "strings" repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/modules/git" - api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" - "code.gitea.io/gitea/services/convert" ) // To define the wiki related concepts: @@ -158,15 +155,3 @@ func UserTitleToWebPath(base, title string) WebPath { } return WebPath(title) } - -// ToWikiPageMetaData converts meta information to a WikiPageMetaData -func ToWikiPageMetaData(wikiName WebPath, lastCommit *git.Commit, repo *repo_model.Repository) *api.WikiPageMetaData { - subURL := string(wikiName) - _, title := WebPathToUserTitle(wikiName) - return &api.WikiPageMetaData{ - Title: title, - HTMLURL: util.URLJoin(repo.HTMLURL(), "wiki", subURL), - SubURL: subURL, - LastCommit: convert.ToWikiCommit(lastCommit), - } -} diff --git a/services/wiki/wiki_test.go b/services/wiki/wiki_test.go index 59c77060f2..ef0c3a0a3a 100644 --- a/services/wiki/wiki_test.go +++ b/services/wiki/wiki_test.go @@ -170,7 +170,7 @@ func TestRepository_AddWikiPage(t *testing.T) { return } defer gitRepo.Close() - masterTree, err := gitRepo.GetTree(DefaultBranch) + masterTree, err := gitRepo.GetTree("master") assert.NoError(t, err) gitPath := WebPathToGitPath(webPath) entry, err := masterTree.GetTreeEntryByPath(gitPath) @@ -215,7 +215,7 @@ func TestRepository_EditWikiPage(t *testing.T) { // Now need to show that the page has been added: gitRepo, err := gitrepo.OpenWikiRepository(git.DefaultContext, repo) assert.NoError(t, err) - masterTree, err := gitRepo.GetTree(DefaultBranch) + masterTree, err := gitRepo.GetTree("master") assert.NoError(t, err) gitPath := WebPathToGitPath(webPath) entry, err := masterTree.GetTreeEntryByPath(gitPath) @@ -242,7 +242,7 @@ func TestRepository_DeleteWikiPage(t *testing.T) { return } defer gitRepo.Close() - masterTree, err := gitRepo.GetTree(DefaultBranch) + masterTree, err := gitRepo.GetTree("master") assert.NoError(t, err) gitPath := WebPathToGitPath("Home") _, err = masterTree.GetTreeEntryByPath(gitPath) @@ -280,7 +280,7 @@ func TestPrepareWikiFileName(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { webPath := UserTitleToWebPath("", tt.arg) - existence, newWikiPath, err := prepareGitPath(gitRepo, webPath) + existence, newWikiPath, err := prepareGitPath(gitRepo, "master", webPath) if (err != nil) != tt.wantErr { assert.NoError(t, err) return @@ -312,7 +312,7 @@ func TestPrepareWikiFileName_FirstPage(t *testing.T) { } defer gitRepo.Close() - existence, newWikiPath, err := prepareGitPath(gitRepo, "Home") + existence, newWikiPath, err := prepareGitPath(gitRepo, "master", "Home") assert.False(t, existence) assert.NoError(t, err) assert.EqualValues(t, "Home.md", newWikiPath) diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl index 1cc4b7bb09..ce6edf8a97 100644 --- a/templates/admin/config.tmpl +++ b/templates/admin/config.tmpl @@ -157,6 +157,8 @@
{{if .Service.DefaultKeepEmailPrivate}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
{{ctx.Locale.Tr "admin.config.default_allow_create_organization"}}
{{if .Service.DefaultAllowCreateOrganization}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
+
{{ctx.Locale.Tr "admin.config.allow_dots_in_usernames"}}
+
{{if .Service.AllowDotsInUsernames}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
{{ctx.Locale.Tr "admin.config.enable_timetracking"}}
{{if .Service.EnableTimetracking}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
{{if .Service.EnableTimetracking}} diff --git a/templates/admin/navbar.tmpl b/templates/admin/navbar.tmpl index fa79f0f759..f23bdee124 100644 --- a/templates/admin/navbar.tmpl +++ b/templates/admin/navbar.tmpl @@ -4,9 +4,11 @@ {{ctx.Locale.Tr "admin.dashboard"}} + {{if or .DatabaseType.IsMySQL .DatabaseType.IsMSSQL}} {{ctx.Locale.Tr "admin.self_check"}} + {{end}}
{{ctx.Locale.Tr "admin.identity_access"}} {{template "repo/commits_table" .}} + {{if .OldFilename}} +
+ {{ctx.Locale.Tr "repo.commits.renamed_from" .OldFilename}} ({{ctx.Locale.Tr "repo.commits.browse_further"}}) +
+ {{end}} {{template "base/footer" .}} diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl index be7c7e80f2..05559fc9a7 100644 --- a/templates/repo/diff/box.tmpl +++ b/templates/repo/diff/box.tmpl @@ -166,6 +166,9 @@ {{ctx.Locale.Tr "repo.diff.view_file"}} {{else}} {{ctx.Locale.Tr "repo.diff.view_file"}} + {{if and $.HeadBranchIsEditable (not $file.IsLFSFile)}} + {{ctx.Locale.Tr "repo.editor.edit_this_file"}} + {{end}} {{end}} {{end}} {{if $isReviewFile}} diff --git a/templates/repo/diff/compare.tmpl b/templates/repo/diff/compare.tmpl index 15574ad988..c2a114ba4e 100644 --- a/templates/repo/diff/compare.tmpl +++ b/templates/repo/diff/compare.tmpl @@ -67,12 +67,12 @@ {{range .Branches}}
{{$BaseCompareName}}:{{.}}
{{end}} - {{if not .PullRequestCtx.SameRepo}} + {{if and (not .PullRequestCtx.SameRepo) ($.HeadRepo.AllowsPulls ctx)}} {{range .HeadBranches}}
{{$HeadCompareName}}:{{.}}
{{end}} {{end}} - {{if .OwnForkRepo}} + {{if and .OwnForkRepo (.OwnForkRepo.AllowsPulls ctx)}} {{range .OwnForkRepoBranches}}
{{$OwnForkCompareName}}:{{.}}
{{end}} @@ -87,17 +87,17 @@ {{range .Tags}}
{{$BaseCompareName}}:{{.}}
{{end}} - {{if not .PullRequestCtx.SameRepo}} + {{if and (not .PullRequestCtx.SameRepo) ($.HeadRepo.AllowsPulls ctx)}} {{range .HeadTags}}
{{$HeadCompareName}}:{{.}}
{{end}} {{end}} - {{if .OwnForkRepo}} + {{if and .OwnForkRepo (.OwnForkRepo.AllowsPulls ctx)}} {{range .OwnForkRepoTags}}
{{$OwnForkCompareName}}:{{.}}
{{end}} {{end}} - {{if .RootRepo}} + {{if and .RootRepo (.RootRepo.AllowsPulls ctx)}} {{range .RootRepoTags}}
{{$RootRepoCompareName}}:{{.}}
{{end}} diff --git a/templates/repo/editor/commit_form.tmpl b/templates/repo/editor/commit_form.tmpl index 34dde576a1..6fd240da62 100644 --- a/templates/repo/editor/commit_form.tmpl +++ b/templates/repo/editor/commit_form.tmpl @@ -66,6 +66,14 @@ {{end}} +
+ + +
+ + + + + + +{{template "base/footer" .}} diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl index 93102467cc..ef3e40eea8 100644 --- a/templates/repo/header.tmpl +++ b/templates/repo/header.tmpl @@ -212,6 +212,12 @@ {{template "custom/extra_tabs" .}} + {{if and RepoFlagsEnabled .SignedUser.IsAdmin}} + + {{svg "octicon-milestone"}} {{ctx.Locale.Tr "repo.admin.manage_flags"}} + + {{end}} + {{if .Permission.IsAdmin}} {{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}} diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl index d91dc4394e..5e27d9160c 100644 --- a/templates/repo/home.tmpl +++ b/templates/repo/home.tmpl @@ -52,6 +52,14 @@ {{end}} + + {{if RepoFlagsEnabled}} + {{template "custom/repo_flag_banners" .}} + {{if .SignedUser.IsAdmin}} + {{template "repo/admin_flags" .}} + {{end}} + {{end}} + {{if .Repository.IsArchived}}
{{if .Repository.ArchivedUnix.IsZero}} @@ -131,15 +139,32 @@ {{template "repo/clone_script" .}}{{/* the script will update `.js-clone-url` and related elements */}} diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index ade0ea34cf..53c20a68bb 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -152,7 +152,11 @@ {{template "shared/user/avatarlink" dict "user" .Poster}} {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.commit_ref_at" .EventTag $createdStr | Safe}} + {{if .Issue.IsPull}} + {{ctx.Locale.Tr "repo.pulls.commit_ref_at" .EventTag $createdStr | Safe}} + {{else}} + {{ctx.Locale.Tr "repo.issues.commit_ref_at" .EventTag $createdStr | Safe}} + {{end}}
{{svg "octicon-git-commit"}} diff --git a/templates/repo/issue/view_content/conversation.tmpl b/templates/repo/issue/view_content/conversation.tmpl index c9e5ee6275..0e978dafcd 100644 --- a/templates/repo/issue/view_content/conversation.tmpl +++ b/templates/repo/issue/view_content/conversation.tmpl @@ -1,6 +1,7 @@ {{$invalid := (index .comments 0).Invalidated}} {{$resolved := (index .comments 0).IsResolved}} {{$resolveDoer := (index .comments 0).ResolveDoer}} +{{$hideByDefault := or $resolved (and $invalid (not .ShowOutdatedComments))}} {{$isNotPending := (not (eq (index .comments 0).Review.Type 0))}}
@@ -14,7 +15,7 @@
{{if or $invalid $resolved}} - -