diff --git a/assets/go-licenses.json b/assets/go-licenses.json index fcfde088004..64c3b8b51c7 100644 --- a/assets/go-licenses.json +++ b/assets/go-licenses.json @@ -1124,6 +1124,16 @@ "path": "github.com/valyala/fastjson/LICENSE", "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2018 Aliaksandr Valialkin\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n" }, + { + "name": "github.com/wneessen/go-mail", + "path": "github.com/wneessen/go-mail/LICENSE", + "licenseText": "MIT License\n\nCopyright (c) 2022-2023 The go-mail Authors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" + }, + { + "name": "github.com/wneessen/go-mail/smtp", + "path": "github.com/wneessen/go-mail/smtp/LICENSE", + "licenseText": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." + }, { "name": "github.com/x448/float16", "path": "github.com/x448/float16/LICENSE", @@ -1259,11 +1269,6 @@ "path": "google.golang.org/protobuf/LICENSE", "licenseText": "Copyright (c) 2018 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" }, - { - "name": "gopkg.in/gomail.v2", - "path": "gopkg.in/gomail.v2/LICENSE", - "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2014 Alexandre Cesaro\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n" - }, { "name": "gopkg.in/ini.v1", "path": "gopkg.in/ini.v1/LICENSE", diff --git a/contrib/fixtures/fixture_generation.go b/contrib/fixtures/fixture_generation.go deleted file mode 100644 index 31797cc800f..00000000000 --- a/contrib/fixtures/fixture_generation.go +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2020 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -//nolint:forbidigo -package main - -import ( - "context" - "fmt" - "os" - "path/filepath" - - "code.gitea.io/gitea/models" - "code.gitea.io/gitea/models/unittest" -) - -// To generate derivative fixtures, execute the following from Gitea's repository base dir: -// go run -tags 'sqlite sqlite_unlock_notify' contrib/fixtures/fixture_generation.go [fixture...] - -var ( - generators = []struct { - gen func(ctx context.Context) (string, error) - name string - }{ - { - models.GetYamlFixturesAccess, "access", - }, - } - fixturesDir string -) - -func main() { - pathToGiteaRoot := "." - fixturesDir = filepath.Join(pathToGiteaRoot, "models", "fixtures") - if err := unittest.CreateTestEngine(unittest.FixturesOptions{ - Dir: fixturesDir, - }); err != nil { - fmt.Printf("CreateTestEngine: %+v", err) - os.Exit(1) - } - if err := unittest.PrepareTestDatabase(); err != nil { - fmt.Printf("PrepareTestDatabase: %+v\n", err) - os.Exit(1) - } - ctx := context.Background() - if len(os.Args) == 0 { - for _, r := range os.Args { - if err := generate(ctx, r); err != nil { - fmt.Printf("generate '%s': %+v\n", r, err) - os.Exit(1) - } - } - } else { - for _, g := range generators { - if err := generate(ctx, g.name); err != nil { - fmt.Printf("generate '%s': %+v\n", g.name, err) - os.Exit(1) - } - } - } -} - -func generate(ctx context.Context, name string) error { - for _, g := range generators { - if g.name == name { - data, err := g.gen(ctx) - if err != nil { - return err - } - path := filepath.Join(fixturesDir, name+".yml") - if err := os.WriteFile(path, []byte(data), 0o644); err != nil { - return fmt.Errorf("%s: %+v", path, err) - } - fmt.Printf("%s created.\n", path) - return nil - } - } - - return fmt.Errorf("generator not found") -} diff --git a/go.mod b/go.mod index bbd81868684..80b62ce83f8 100644 --- a/go.mod +++ b/go.mod @@ -114,6 +114,7 @@ require ( github.com/tstranex/u2f v1.0.0 github.com/ulikunitz/xz v0.5.12 github.com/urfave/cli/v2 v2.27.5 + github.com/wneessen/go-mail v0.5.2 github.com/xanzy/go-gitlab v0.112.0 github.com/xeipuuv/gojsonschema v1.2.0 github.com/yohcop/openid-go v1.0.1 @@ -130,7 +131,6 @@ require ( golang.org/x/tools v0.26.0 google.golang.org/grpc v1.67.1 google.golang.org/protobuf v1.35.1 - gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/ini.v1 v1.67.0 gopkg.in/yaml.v3 v3.0.1 mvdan.cc/xurls/v2 v2.5.0 @@ -319,7 +319,6 @@ require ( golang.org/x/mod v0.21.0 // indirect golang.org/x/time v0.7.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect - gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index df3b7d899cf..d1b7890fb68 100644 --- a/go.sum +++ b/go.sum @@ -815,6 +815,8 @@ github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5 github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +github.com/wneessen/go-mail v0.5.2 h1:MZKwgHJoRboLJ+EHMLuHpZc95wo+u1xViL/4XSswDT8= +github.com/wneessen/go-mail v0.5.2/go.mod h1:kRroJvEq2hOSEPFRiKjN7Csrz0G1w+RpiGR3b6yo+Ck= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xanzy/go-gitlab v0.112.0 h1:6Z0cqEooCvBMfBIHw+CgO4AKGRV8na/9781xOb0+DKw= @@ -887,8 +889,10 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= @@ -901,6 +905,9 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -920,8 +927,10 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= @@ -934,6 +943,9 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -966,10 +978,13 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -977,8 +992,10 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -989,7 +1006,9 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= @@ -1004,6 +1023,8 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1024,8 +1045,6 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= -gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1033,8 +1052,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= -gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= diff --git a/models/fixture_generation.go b/models/fixture_generation.go deleted file mode 100644 index 6234caefada..00000000000 --- a/models/fixture_generation.go +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2020 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package models - -import ( - "context" - "fmt" - "strings" - - "code.gitea.io/gitea/models/db" - access_model "code.gitea.io/gitea/models/perm/access" - repo_model "code.gitea.io/gitea/models/repo" -) - -// GetYamlFixturesAccess returns a string containing the contents -// for the access table, as recalculated using repo.RecalculateAccesses() -func GetYamlFixturesAccess(ctx context.Context) (string, error) { - repos := make([]*repo_model.Repository, 0, 50) - if err := db.GetEngine(ctx).Find(&repos); err != nil { - return "", err - } - - for _, repo := range repos { - repo.MustOwner(ctx) - if err := access_model.RecalculateAccesses(ctx, repo); err != nil { - return "", err - } - } - - var b strings.Builder - - accesses := make([]*access_model.Access, 0, 200) - if err := db.GetEngine(ctx).OrderBy("user_id, repo_id").Find(&accesses); err != nil { - return "", err - } - - for i, a := range accesses { - fmt.Fprintf(&b, "-\n") - fmt.Fprintf(&b, " id: %d\n", i+1) - fmt.Fprintf(&b, " user_id: %d\n", a.UserID) - fmt.Fprintf(&b, " repo_id: %d\n", a.RepoID) - fmt.Fprintf(&b, " mode: %d\n", a.Mode) - if i < len(accesses)-1 { - fmt.Fprintf(&b, "\n") - } - } - - return b.String(), nil -} diff --git a/models/fixture_test.go b/models/fixture_test.go deleted file mode 100644 index de5f412388d..00000000000 --- a/models/fixture_test.go +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2020 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package models - -import ( - "context" - "os" - "path/filepath" - "testing" - - "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/unittest" - "code.gitea.io/gitea/modules/util" - - "github.com/stretchr/testify/assert" -) - -func TestFixtureGeneration(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - test := func(ctx context.Context, gen func(ctx context.Context) (string, error), name string) { - expected, err := gen(ctx) - if !assert.NoError(t, err) { - return - } - p := filepath.Join(unittest.FixturesDir(), name+".yml") - bytes, err := os.ReadFile(p) - if !assert.NoError(t, err) { - return - } - data := string(util.NormalizeEOL(bytes)) - assert.EqualValues(t, expected, data, "Differences detected for %s", p) - } - - test(db.DefaultContext, GetYamlFixturesAccess, "access") -} diff --git a/models/packages/arch/search.go b/models/packages/arch/search.go new file mode 100644 index 00000000000..f35c037b23a --- /dev/null +++ b/models/packages/arch/search.go @@ -0,0 +1,38 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package arch + +import ( + "context" + + packages_model "code.gitea.io/gitea/models/packages" + arch_module "code.gitea.io/gitea/modules/packages/arch" +) + +// GetRepositories gets all available repositories +func GetRepositories(ctx context.Context, ownerID int64) ([]string, error) { + return packages_model.GetDistinctPropertyValues( + ctx, + packages_model.TypeArch, + ownerID, + packages_model.PropertyTypeFile, + arch_module.PropertyRepository, + nil, + ) +} + +// GetArchitectures gets all available architectures for the given repository +func GetArchitectures(ctx context.Context, ownerID int64, repository string) ([]string, error) { + return packages_model.GetDistinctPropertyValues( + ctx, + packages_model.TypeArch, + ownerID, + packages_model.PropertyTypeFile, + arch_module.PropertyArchitecture, + &packages_model.DistinctPropertyDependency{ + Name: arch_module.PropertyRepository, + Value: repository, + }, + ) +} diff --git a/models/packages/descriptor.go b/models/packages/descriptor.go index b8ef698d382..803b73c9689 100644 --- a/models/packages/descriptor.go +++ b/models/packages/descriptor.go @@ -13,6 +13,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/packages/alpine" + "code.gitea.io/gitea/modules/packages/arch" "code.gitea.io/gitea/modules/packages/cargo" "code.gitea.io/gitea/modules/packages/chef" "code.gitea.io/gitea/modules/packages/composer" @@ -150,6 +151,8 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc switch p.Type { case TypeAlpine: metadata = &alpine.VersionMetadata{} + case TypeArch: + metadata = &arch.VersionMetadata{} case TypeCargo: metadata = &cargo.Metadata{} case TypeChef: diff --git a/models/packages/package.go b/models/packages/package.go index 65a25741509..417d62d1993 100644 --- a/models/packages/package.go +++ b/models/packages/package.go @@ -31,6 +31,7 @@ type Type string // List of supported packages const ( TypeAlpine Type = "alpine" + TypeArch Type = "arch" TypeCargo Type = "cargo" TypeChef Type = "chef" TypeComposer Type = "composer" @@ -55,6 +56,7 @@ const ( var TypeList = []Type{ TypeAlpine, + TypeArch, TypeCargo, TypeChef, TypeComposer, @@ -82,6 +84,8 @@ func (pt Type) Name() string { switch pt { case TypeAlpine: return "Alpine" + case TypeArch: + return "Arch" case TypeCargo: return "Cargo" case TypeChef: @@ -131,6 +135,8 @@ func (pt Type) SVGName() string { switch pt { case TypeAlpine: return "gitea-alpine" + case TypeArch: + return "gitea-arch" case TypeCargo: return "gitea-cargo" case TypeChef: diff --git a/models/packages/package_file.go b/models/packages/package_file.go index 1bb6b57a34e..270cb32fdf6 100644 --- a/models/packages/package_file.go +++ b/models/packages/package_file.go @@ -221,6 +221,11 @@ func SearchFiles(ctx context.Context, opts *PackageFileSearchOptions) ([]*Packag return pfs, count, err } +// HasFiles tests if there are files of packages matching the search options +func HasFiles(ctx context.Context, opts *PackageFileSearchOptions) (bool, error) { + return db.Exist[PackageFile](ctx, opts.toConds()) +} + // CalculateFileSize sums up all blob sizes matching the search options. // It does NOT respect the deduplication of blobs. func CalculateFileSize(ctx context.Context, opts *PackageFileSearchOptions) (int64, error) { diff --git a/modules/git/repo_commit_gogit.go b/modules/git/repo_commit_gogit.go index 84580be9a5b..993013eef7b 100644 --- a/modules/git/repo_commit_gogit.go +++ b/modules/git/repo_commit_gogit.go @@ -14,9 +14,16 @@ import ( "github.com/go-git/go-git/v5/plumbing/object" ) -// GetRefCommitID returns the last commit ID string of given reference (branch or tag). +// GetRefCommitID returns the last commit ID string of given reference. func (repo *Repository) GetRefCommitID(name string) (string, error) { - ref, err := repo.gogitRepo.Reference(plumbing.ReferenceName(name), true) + if plumbing.IsHash(name) { + return name, nil + } + refName := plumbing.ReferenceName(name) + if err := refName.Validate(); err != nil { + return "", err + } + ref, err := repo.gogitRepo.Reference(refName, true) if err != nil { if err == plumbing.ErrReferenceNotFound { return "", ErrNotExist{ diff --git a/modules/git/repo_commit_test.go b/modules/git/repo_commit_test.go index 19983b47b19..4c26fa2a486 100644 --- a/modules/git/repo_commit_test.go +++ b/modules/git/repo_commit_test.go @@ -101,3 +101,28 @@ func TestRepository_CommitsBetweenIDs(t *testing.T) { assert.Len(t, commits, c.ExpectedCommits, "case %d", i) } } + +func TestGetRefCommitID(t *testing.T) { + bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") + bareRepo1, err := openRepositoryWithDefaultContext(bareRepo1Path) + assert.NoError(t, err) + defer bareRepo1.Close() + + // these test case are specific to the repo1_bare test repo + testCases := []struct { + Ref string + ExpectedCommitID string + }{ + {RefNameFromBranch("master").String(), "ce064814f4a0d337b333e646ece456cd39fab612"}, + {RefNameFromBranch("branch1").String(), "2839944139e0de9737a044f78b0e4b40d989a9e3"}, + {RefNameFromTag("test").String(), "3ad28a9149a2864384548f3d17ed7f38014c9e8a"}, + {"ce064814f4a0d337b333e646ece456cd39fab612", "ce064814f4a0d337b333e646ece456cd39fab612"}, + } + + for _, testCase := range testCases { + commitID, err := bareRepo1.GetRefCommitID(testCase.Ref) + if assert.NoError(t, err) { + assert.Equal(t, testCase.ExpectedCommitID, commitID) + } + } +} diff --git a/modules/packages/arch/metadata.go b/modules/packages/arch/metadata.go new file mode 100644 index 00000000000..e1e79c60e0d --- /dev/null +++ b/modules/packages/arch/metadata.go @@ -0,0 +1,249 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package arch + +import ( + "archive/tar" + "bufio" + "bytes" + "compress/gzip" + "io" + "regexp" + "strconv" + "strings" + + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/validation" + + "github.com/klauspost/compress/zstd" + "github.com/ulikunitz/xz" +) + +const ( + PropertyRepository = "arch.repository" + PropertyArchitecture = "arch.architecture" + PropertySignature = "arch.signature" + PropertyMetadata = "arch.metadata" + + SettingKeyPrivate = "arch.key.private" + SettingKeyPublic = "arch.key.public" + + RepositoryPackage = "_arch" + RepositoryVersion = "_repository" + + AnyArch = "any" +) + +var ( + ErrMissingPKGINFOFile = util.NewInvalidArgumentErrorf(".PKGINFO file is missing") + ErrUnsupportedFormat = util.NewInvalidArgumentErrorf("unsupported package container format") + ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid") + ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid") + ErrInvalidArchitecture = util.NewInvalidArgumentErrorf("package architecture is invalid") + + // https://man.archlinux.org/man/PKGBUILD.5 + namePattern = regexp.MustCompile(`\A[a-zA-Z0-9@._+-]+\z`) + versionPattern = regexp.MustCompile(`\A(?:[0-9]:)?[a-zA-Z0-9.+~]+(?:-[a-zA-Z0-9.+-~]+)?\z`) +) + +type Package struct { + Name string + Version string + VersionMetadata VersionMetadata + FileMetadata FileMetadata + FileCompressionExtension string +} + +type VersionMetadata struct { + Description string `json:"description,omitempty"` + ProjectURL string `json:"project_url,omitempty"` + Licenses []string `json:"licenses,omitempty"` +} + +type FileMetadata struct { + Architecture string `json:"architecture"` + Base string `json:"base,omitempty"` + InstalledSize int64 `json:"installed_size,omitempty"` + BuildDate int64 `json:"build_date,omitempty"` + Packager string `json:"packager,omitempty"` + Groups []string `json:"groups,omitempty"` + Provides []string `json:"provides,omitempty"` + Depends []string `json:"depends,omitempty"` + OptDepends []string `json:"opt_depends,omitempty"` + MakeDepends []string `json:"make_depends,omitempty"` + CheckDepends []string `json:"check_depends,omitempty"` + XData []string `json:"xdata,omitempty"` + Backup []string `json:"backup,omitempty"` + Files []string `json:"files,omitempty"` +} + +// ParsePackage parses an Arch package file +func ParsePackage(r io.Reader) (*Package, error) { + header := make([]byte, 10) + n, err := util.ReadAtMost(r, header) + if err != nil { + return nil, err + } + + r = io.MultiReader(bytes.NewReader(header[:n]), r) + + var inner io.Reader + var compressionType string + if bytes.HasPrefix(header, []byte{0x28, 0xB5, 0x2F, 0xFD}) { // zst + zr, err := zstd.NewReader(r) + if err != nil { + return nil, err + } + defer zr.Close() + + inner = zr + compressionType = "zst" + } else if bytes.HasPrefix(header, []byte{0xFD, 0x37, 0x7A, 0x58, 0x5A}) { // xz + xzr, err := xz.NewReader(r) + if err != nil { + return nil, err + } + + inner = xzr + compressionType = "xz" + } else if bytes.HasPrefix(header, []byte{0x1F, 0x8B}) { // gz + gzr, err := gzip.NewReader(r) + if err != nil { + return nil, err + } + defer gzr.Close() + + inner = gzr + compressionType = "gz" + } else { + return nil, ErrUnsupportedFormat + } + + var p *Package + files := make([]string, 0, 10) + + tr := tar.NewReader(inner) + for { + hd, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + + if hd.Typeflag != tar.TypeReg { + continue + } + + filename := hd.FileInfo().Name() + if filename == ".PKGINFO" { + p, err = ParsePackageInfo(tr) + if err != nil { + return nil, err + } + } else if !strings.HasPrefix(filename, ".") { + files = append(files, hd.Name) + } + } + + if p == nil { + return nil, ErrMissingPKGINFOFile + } + + p.FileMetadata.Files = files + p.FileCompressionExtension = compressionType + + return p, nil +} + +// ParsePackageInfo parses a .PKGINFO file to retrieve the metadata +// https://man.archlinux.org/man/PKGBUILD.5 +// https://gitlab.archlinux.org/pacman/pacman/-/blob/master/lib/libalpm/be_package.c#L161 +func ParsePackageInfo(r io.Reader) (*Package, error) { + p := &Package{} + + s := bufio.NewScanner(r) + for s.Scan() { + line := s.Text() + + if strings.HasPrefix(line, "#") { + continue + } + + i := strings.IndexRune(line, '=') + if i == -1 { + continue + } + + key := strings.TrimSpace(line[:i]) + value := strings.TrimSpace(line[i+1:]) + + switch key { + case "pkgname": + p.Name = value + case "pkgbase": + p.FileMetadata.Base = value + case "pkgver": + p.Version = value + case "pkgdesc": + p.VersionMetadata.Description = value + case "url": + p.VersionMetadata.ProjectURL = value + case "packager": + p.FileMetadata.Packager = value + case "arch": + p.FileMetadata.Architecture = value + case "license": + p.VersionMetadata.Licenses = append(p.VersionMetadata.Licenses, value) + case "provides": + p.FileMetadata.Provides = append(p.FileMetadata.Provides, value) + case "depend": + p.FileMetadata.Depends = append(p.FileMetadata.Depends, value) + case "optdepend": + p.FileMetadata.OptDepends = append(p.FileMetadata.OptDepends, value) + case "makedepend": + p.FileMetadata.MakeDepends = append(p.FileMetadata.MakeDepends, value) + case "checkdepend": + p.FileMetadata.CheckDepends = append(p.FileMetadata.CheckDepends, value) + case "backup": + p.FileMetadata.Backup = append(p.FileMetadata.Backup, value) + case "group": + p.FileMetadata.Groups = append(p.FileMetadata.Groups, value) + case "builddate": + date, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return nil, err + } + p.FileMetadata.BuildDate = date + case "size": + size, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return nil, err + } + p.FileMetadata.InstalledSize = size + case "xdata": + p.FileMetadata.XData = append(p.FileMetadata.XData, value) + } + } + if err := s.Err(); err != nil { + return nil, err + } + + if !namePattern.MatchString(p.Name) { + return nil, ErrInvalidName + } + if !versionPattern.MatchString(p.Version) { + return nil, ErrInvalidVersion + } + if p.FileMetadata.Architecture == "" { + return nil, ErrInvalidArchitecture + } + + if !validation.IsValidURL(p.VersionMetadata.ProjectURL) { + p.VersionMetadata.ProjectURL = "" + } + + return p, nil +} diff --git a/modules/packages/arch/metadata_test.go b/modules/packages/arch/metadata_test.go new file mode 100644 index 00000000000..f611ef5e845 --- /dev/null +++ b/modules/packages/arch/metadata_test.go @@ -0,0 +1,157 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package arch + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "io" + "testing" + + "github.com/klauspost/compress/zstd" + "github.com/stretchr/testify/assert" + "github.com/ulikunitz/xz" +) + +const ( + packageName = "gitea" + packageVersion = "1.0.1" + packageDescription = "Package Description" + packageProjectURL = "https://gitea.com" + packagePackager = "KN4CK3R " +) + +func createPKGINFOContent(name, version string) []byte { + return []byte(`pkgname = ` + name + ` +pkgbase = ` + name + ` +pkgver = ` + version + ` +pkgdesc = ` + packageDescription + ` +url = ` + packageProjectURL + ` +# comment +group=group +builddate = 1678834800 +size = 123456 +arch = x86_64 +license = MIT +packager = ` + packagePackager + ` +depend = common +xdata = value +depend = gitea +provides = common +provides = gitea +optdepend = hex +checkdepend = common +makedepend = cmake +backup = usr/bin/paket1`) +} + +func TestParsePackage(t *testing.T) { + createPackage := func(compression string, files map[string][]byte) io.Reader { + var buf bytes.Buffer + var cw io.WriteCloser + switch compression { + case "zst": + cw, _ = zstd.NewWriter(&buf) + case "xz": + cw, _ = xz.NewWriter(&buf) + case "gz": + cw = gzip.NewWriter(&buf) + } + tw := tar.NewWriter(cw) + + for name, content := range files { + hdr := &tar.Header{ + Name: name, + Mode: 0o600, + Size: int64(len(content)), + } + tw.WriteHeader(hdr) + tw.Write(content) + } + + tw.Close() + cw.Close() + + return &buf + } + + for _, c := range []string{"gz", "xz", "zst"} { + t.Run(c, func(t *testing.T) { + t.Run("MissingPKGINFOFile", func(t *testing.T) { + data := createPackage(c, map[string][]byte{"dummy.txt": {}}) + + pp, err := ParsePackage(data) + assert.Nil(t, pp) + assert.ErrorIs(t, err, ErrMissingPKGINFOFile) + }) + + t.Run("InvalidPKGINFOFile", func(t *testing.T) { + data := createPackage(c, map[string][]byte{".PKGINFO": {}}) + + pp, err := ParsePackage(data) + assert.Nil(t, pp) + assert.ErrorIs(t, err, ErrInvalidName) + }) + + t.Run("Valid", func(t *testing.T) { + data := createPackage(c, map[string][]byte{ + ".PKGINFO": createPKGINFOContent(packageName, packageVersion), + "/test/dummy.txt": {}, + }) + + p, err := ParsePackage(data) + assert.NoError(t, err) + assert.NotNil(t, p) + + assert.ElementsMatch(t, []string{"/test/dummy.txt"}, p.FileMetadata.Files) + }) + }) + } +} + +func TestParsePackageInfo(t *testing.T) { + t.Run("InvalidName", func(t *testing.T) { + data := createPKGINFOContent("", packageVersion) + + p, err := ParsePackageInfo(bytes.NewReader(data)) + assert.Nil(t, p) + assert.ErrorIs(t, err, ErrInvalidName) + }) + + t.Run("InvalidVersion", func(t *testing.T) { + data := createPKGINFOContent(packageName, "") + + p, err := ParsePackageInfo(bytes.NewReader(data)) + assert.Nil(t, p) + assert.ErrorIs(t, err, ErrInvalidVersion) + }) + + t.Run("Valid", func(t *testing.T) { + data := createPKGINFOContent(packageName, packageVersion) + + p, err := ParsePackageInfo(bytes.NewReader(data)) + assert.NoError(t, err) + assert.NotNil(t, p) + + assert.Equal(t, packageName, p.Name) + assert.Equal(t, packageName, p.FileMetadata.Base) + assert.Equal(t, packageVersion, p.Version) + assert.Equal(t, packageDescription, p.VersionMetadata.Description) + assert.Equal(t, packagePackager, p.FileMetadata.Packager) + assert.Equal(t, packageProjectURL, p.VersionMetadata.ProjectURL) + assert.ElementsMatch(t, []string{"MIT"}, p.VersionMetadata.Licenses) + assert.EqualValues(t, 1678834800, p.FileMetadata.BuildDate) + assert.EqualValues(t, 123456, p.FileMetadata.InstalledSize) + assert.Equal(t, "x86_64", p.FileMetadata.Architecture) + assert.ElementsMatch(t, []string{"value"}, p.FileMetadata.XData) + assert.ElementsMatch(t, []string{"group"}, p.FileMetadata.Groups) + assert.ElementsMatch(t, []string{"common", "gitea"}, p.FileMetadata.Provides) + assert.ElementsMatch(t, []string{"common", "gitea"}, p.FileMetadata.Depends) + assert.ElementsMatch(t, []string{"hex"}, p.FileMetadata.OptDepends) + assert.ElementsMatch(t, []string{"common"}, p.FileMetadata.CheckDepends) + assert.ElementsMatch(t, []string{"cmake"}, p.FileMetadata.MakeDepends) + assert.ElementsMatch(t, []string{"usr/bin/paket1"}, p.FileMetadata.Backup) + }) +} diff --git a/modules/setting/packages.go b/modules/setting/packages.go index bc093e7ea6c..3f618cfd641 100644 --- a/modules/setting/packages.go +++ b/modules/setting/packages.go @@ -22,6 +22,7 @@ var ( LimitTotalOwnerCount int64 LimitTotalOwnerSize int64 LimitSizeAlpine int64 + LimitSizeArch int64 LimitSizeCargo int64 LimitSizeChef int64 LimitSizeComposer int64 @@ -79,6 +80,7 @@ func loadPackagesFrom(rootCfg ConfigProvider) (err error) { Packages.LimitTotalOwnerSize = mustBytes(sec, "LIMIT_TOTAL_OWNER_SIZE") Packages.LimitSizeAlpine = mustBytes(sec, "LIMIT_SIZE_ALPINE") + Packages.LimitSizeArch = mustBytes(sec, "LIMIT_SIZE_ARCH") Packages.LimitSizeCargo = mustBytes(sec, "LIMIT_SIZE_CARGO") Packages.LimitSizeChef = mustBytes(sec, "LIMIT_SIZE_CHEF") Packages.LimitSizeComposer = mustBytes(sec, "LIMIT_SIZE_COMPOSER") diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 182a9ec7576..e42d5113ac1 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3536,6 +3536,11 @@ alpine.repository = Repository Info alpine.repository.branches = Branches alpine.repository.repositories = Repositories alpine.repository.architectures = Architectures +arch.registry = Add server with related repository and architecture to /etc/pacman.conf: +arch.install = Sync package with pacman: +arch.repository = Repository Info +arch.repository.repositories = Repositories +arch.repository.architectures = Architectures cargo.registry = Setup this registry in the Cargo configuration file (for example ~/.cargo/config.toml): cargo.install = To install the package using Cargo, run the following command: chef.registry = Setup this registry in your ~/.chef/config.rb file: diff --git a/public/assets/img/svg/gitea-arch.svg b/public/assets/img/svg/gitea-arch.svg new file mode 100644 index 00000000000..943a92c5794 --- /dev/null +++ b/public/assets/img/svg/gitea-arch.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index c3da5a7513b..4e194f65fa1 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/packages/alpine" + "code.gitea.io/gitea/routers/api/packages/arch" "code.gitea.io/gitea/routers/api/packages/cargo" "code.gitea.io/gitea/routers/api/packages/chef" "code.gitea.io/gitea/routers/api/packages/composer" @@ -135,6 +136,49 @@ func CommonRoutes() *web.Router { }) }) }, reqPackageAccess(perm.AccessModeRead)) + r.Group("/arch", func() { + r.Methods("HEAD,GET", "/repository.key", arch.GetRepositoryKey) + + r.Methods("HEAD,GET,PUT,DELETE", "*", func(ctx *context.Context) { + path := strings.Trim(ctx.PathParam("*"), "/") + + if ctx.Req.Method == "PUT" { + reqPackageAccess(perm.AccessModeWrite)(ctx) + if ctx.Written() { + return + } + ctx.SetPathParam("repository", path) + arch.UploadPackageFile(ctx) + return + } + + pathFields := strings.Split(path, "/") + pathFieldsLen := len(pathFields) + + if (ctx.Req.Method == "HEAD" || ctx.Req.Method == "GET") && pathFieldsLen >= 2 { + ctx.SetPathParam("repository", strings.Join(pathFields[:pathFieldsLen-2], "/")) + ctx.SetPathParam("architecture", pathFields[pathFieldsLen-2]) + ctx.SetPathParam("filename", pathFields[pathFieldsLen-1]) + arch.GetPackageOrRepositoryFile(ctx) + return + } + + if ctx.Req.Method == "DELETE" && pathFieldsLen >= 3 { + reqPackageAccess(perm.AccessModeWrite)(ctx) + if ctx.Written() { + return + } + ctx.SetPathParam("repository", strings.Join(pathFields[:pathFieldsLen-3], "/")) + ctx.SetPathParam("name", pathFields[pathFieldsLen-3]) + ctx.SetPathParam("version", pathFields[pathFieldsLen-2]) + ctx.SetPathParam("architecture", pathFields[pathFieldsLen-1]) + arch.DeletePackageVersion(ctx) + return + } + + ctx.Status(http.StatusNotFound) + }) + }, reqPackageAccess(perm.AccessModeRead)) r.Group("/cargo", func() { r.Group("/api/v1/crates", func() { r.Get("", cargo.SearchPackages) diff --git a/routers/api/packages/arch/arch.go b/routers/api/packages/arch/arch.go new file mode 100644 index 00000000000..573e93cfb01 --- /dev/null +++ b/routers/api/packages/arch/arch.go @@ -0,0 +1,306 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package arch + +import ( + "bytes" + "encoding/base64" + "errors" + "fmt" + "io" + "net/http" + "strings" + + packages_model "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/modules/json" + packages_module "code.gitea.io/gitea/modules/packages" + arch_module "code.gitea.io/gitea/modules/packages/arch" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/routers/api/packages/helper" + "code.gitea.io/gitea/services/context" + packages_service "code.gitea.io/gitea/services/packages" + arch_service "code.gitea.io/gitea/services/packages/arch" +) + +func apiError(ctx *context.Context, status int, obj any) { + helper.LogAndProcessError(ctx, status, obj, func(message string) { + ctx.PlainText(status, message) + }) +} + +func GetRepositoryKey(ctx *context.Context) { + _, pub, err := arch_service.GetOrCreateKeyPair(ctx, ctx.Package.Owner.ID) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + ctx.ServeContent(strings.NewReader(pub), &context.ServeHeaderOptions{ + ContentType: "application/pgp-keys", + }) +} + +func UploadPackageFile(ctx *context.Context) { + repository := strings.TrimSpace(ctx.PathParam("repository")) + + upload, needToClose, err := ctx.UploadStream() + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + if needToClose { + defer upload.Close() + } + + buf, err := packages_module.CreateHashedBufferFromReader(upload) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + defer buf.Close() + + pck, err := arch_module.ParsePackage(buf) + if err != nil { + if errors.Is(err, util.ErrInvalidArgument) || err == io.EOF { + apiError(ctx, http.StatusBadRequest, err) + } else { + apiError(ctx, http.StatusInternalServerError, err) + } + return + } + + if _, err := buf.Seek(0, io.SeekStart); err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + fileMetadataRaw, err := json.Marshal(pck.FileMetadata) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + signature, err := arch_service.SignData(ctx, ctx.Package.Owner.ID, buf) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + if _, err := buf.Seek(0, io.SeekStart); err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + release, err := arch_service.AquireRegistryLock(ctx, ctx.Package.Owner.ID) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + defer release() + + // Search for duplicates with different file compression + has, err := packages_model.HasFiles(ctx, &packages_model.PackageFileSearchOptions{ + OwnerID: ctx.Package.Owner.ID, + PackageType: packages_model.TypeArch, + Query: fmt.Sprintf("%s-%s-%s.pkg.tar.%%", pck.Name, pck.Version, pck.FileMetadata.Architecture), + Properties: map[string]string{ + arch_module.PropertyRepository: repository, + arch_module.PropertyArchitecture: pck.FileMetadata.Architecture, + }, + }) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + if has { + apiError(ctx, http.StatusConflict, packages_model.ErrDuplicatePackageFile) + return + } + + _, _, err = packages_service.CreatePackageOrAddFileToExisting( + ctx, + &packages_service.PackageCreationInfo{ + PackageInfo: packages_service.PackageInfo{ + Owner: ctx.Package.Owner, + PackageType: packages_model.TypeArch, + Name: pck.Name, + Version: pck.Version, + }, + Creator: ctx.Doer, + Metadata: pck.VersionMetadata, + }, + &packages_service.PackageFileCreationInfo{ + PackageFileInfo: packages_service.PackageFileInfo{ + Filename: fmt.Sprintf("%s-%s-%s.pkg.tar.%s", pck.Name, pck.Version, pck.FileMetadata.Architecture, pck.FileCompressionExtension), + CompositeKey: fmt.Sprintf("%s|%s", repository, pck.FileMetadata.Architecture), + }, + Creator: ctx.Doer, + Data: buf, + IsLead: true, + Properties: map[string]string{ + arch_module.PropertyRepository: repository, + arch_module.PropertyArchitecture: pck.FileMetadata.Architecture, + arch_module.PropertyMetadata: string(fileMetadataRaw), + arch_module.PropertySignature: base64.StdEncoding.EncodeToString(signature), + }, + }, + ) + if err != nil { + switch err { + case packages_model.ErrDuplicatePackageVersion, packages_model.ErrDuplicatePackageFile: + apiError(ctx, http.StatusConflict, err) + case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize: + apiError(ctx, http.StatusForbidden, err) + default: + apiError(ctx, http.StatusInternalServerError, err) + } + return + } + + if err := arch_service.BuildSpecificRepositoryFiles(ctx, ctx.Package.Owner.ID, repository, pck.FileMetadata.Architecture); err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + ctx.Status(http.StatusCreated) +} + +func GetPackageOrRepositoryFile(ctx *context.Context) { + repository := ctx.PathParam("repository") + architecture := ctx.PathParam("architecture") + filename := ctx.PathParam("filename") + filenameOrig := filename + + isSignature := strings.HasSuffix(filename, ".sig") + if isSignature { + filename = filename[:len(filename)-len(".sig")] + } + + opts := &packages_model.PackageFileSearchOptions{ + OwnerID: ctx.Package.Owner.ID, + PackageType: packages_model.TypeArch, + Query: filename, + CompositeKey: fmt.Sprintf("%s|%s", repository, architecture), + } + + if strings.HasSuffix(filename, ".db.tar.gz") || strings.HasSuffix(filename, ".files.tar.gz") || strings.HasSuffix(filename, ".files") || strings.HasSuffix(filename, ".db") { + // The requested filename is based on the user-defined repository name. + // Normalize everything to "packages.db". + opts.Query = arch_service.IndexArchiveFilename + + pv, err := arch_service.GetOrCreateRepositoryVersion(ctx, ctx.Package.Owner.ID) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + opts.VersionID = pv.ID + } + + pfs, _, err := packages_model.SearchFiles(ctx, opts) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + if len(pfs) == 0 { + // Try again with architecture 'any' + if architecture == arch_module.AnyArch { + apiError(ctx, http.StatusNotFound, nil) + return + } + + opts.CompositeKey = fmt.Sprintf("%s|%s", repository, arch_module.AnyArch) + if pfs, _, err = packages_model.SearchFiles(ctx, opts); err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + } + if len(pfs) != 1 { + apiError(ctx, http.StatusNotFound, nil) + return + } + + if isSignature { + pfps, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, pfs[0].ID, arch_module.PropertySignature) + if err != nil || len(pfps) == 0 { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + data, err := base64.StdEncoding.DecodeString(pfps[0].Value) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + ctx.ServeContent(bytes.NewReader(data), &context.ServeHeaderOptions{ + Filename: filenameOrig, + }) + return + } + + s, u, pf, err := packages_service.GetPackageFileStream(ctx, pfs[0]) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + apiError(ctx, http.StatusNotFound, err) + } else { + apiError(ctx, http.StatusInternalServerError, err) + } + return + } + + helper.ServePackageFile(ctx, s, u, pf) +} + +func DeletePackageVersion(ctx *context.Context) { + repository := ctx.PathParam("repository") + architecture := ctx.PathParam("architecture") + name := ctx.PathParam("name") + version := ctx.PathParam("version") + + release, err := arch_service.AquireRegistryLock(ctx, ctx.Package.Owner.ID) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + defer release() + + pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeArch, name, version) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + apiError(ctx, http.StatusNotFound, err) + } else { + apiError(ctx, http.StatusInternalServerError, err) + } + return + } + + pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{ + VersionID: pv.ID, + CompositeKey: fmt.Sprintf("%s|%s", repository, architecture), + }) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + if len(pfs) != 1 { + apiError(ctx, http.StatusNotFound, nil) + return + } + + if err := packages_service.RemovePackageFileAndVersionIfUnreferenced(ctx, ctx.Doer, pfs[0]); err != nil { + if errors.Is(err, util.ErrNotExist) { + apiError(ctx, http.StatusNotFound, err) + } else { + apiError(ctx, http.StatusInternalServerError, err) + } + return + } + + if err := arch_service.BuildSpecificRepositoryFiles(ctx, ctx.Package.Owner.ID, repository, architecture); err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + ctx.Status(http.StatusNoContent) +} diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 415f34d1fbe..833f59981b4 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -637,8 +637,12 @@ func attachmentsHTML(ctx *context.Context, attachments []*repo_model.Attachment, return attachHTML } -// get all teams that current user can mention -func handleTeamMentions(ctx *context.Context) { +// handleMentionableAssigneesAndTeams gets all teams that current user can mention, and fills the assignee users to the context data +func handleMentionableAssigneesAndTeams(ctx *context.Context, assignees []*user_model.User) { + // TODO: need to figure out how many places this is really used, and rename it to "MentionableAssignees" + // at the moment it is used on the issue list page, for the markdown editor mention + ctx.Data["Assignees"] = assignees + if ctx.Doer == nil || !ctx.Repo.Owner.IsOrganization() { return } diff --git a/routers/web/repo/issue_list.go b/routers/web/repo/issue_list.go index ee2fc080f50..50bb6684330 100644 --- a/routers/web/repo/issue_list.go +++ b/routers/web/repo/issue_list.go @@ -715,9 +715,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt ctx.ServerError("GetRepoAssignees", err) return } - ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers) - - handleTeamMentions(ctx) + handleMentionableAssigneesAndTeams(ctx, shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)) if ctx.Written() { return } diff --git a/routers/web/repo/issue_page_meta.go b/routers/web/repo/issue_page_meta.go index e04d76b287d..7eda6e3c736 100644 --- a/routers/web/repo/issue_page_meta.go +++ b/routers/web/repo/issue_page_meta.go @@ -148,7 +148,7 @@ func (d *IssuePageMetaData) retrieveAssigneesDataForIssueWriter(ctx *context.Con d.AssigneesData.SelectedAssigneeIDs = strings.Join(ids, ",") } // FIXME: this is a tricky part which writes ctx.Data["Mentionable*"] - handleTeamMentions(ctx) + handleMentionableAssigneesAndTeams(ctx, d.AssigneesData.CandidateAssignees) } func (d *IssuePageMetaData) retrieveProjectsDataForIssueWriter(ctx *context.Context) { diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index e3b329d01d8..0325585e531 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -840,9 +840,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi ctx.ServerError("GetRepoAssignees", err) return } - ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers) - - handleTeamMentions(ctx) + handleMentionableAssigneesAndTeams(ctx, shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)) if ctx.Written() { return } diff --git a/routers/web/shared/user/helper.go b/routers/web/shared/user/helper.go index 6186b9b9ffb..dfd65420c1c 100644 --- a/routers/web/shared/user/helper.go +++ b/routers/web/shared/user/helper.go @@ -4,19 +4,23 @@ package user import ( - "sort" + "slices" "code.gitea.io/gitea/models/user" ) func MakeSelfOnTop(doer *user.User, users []*user.User) []*user.User { if doer != nil { - sort.Slice(users, func(i, j int) bool { - if users[i].ID == users[j].ID { - return false - } - return users[i].ID == doer.ID // if users[i] is self, put it before others, so less=true + idx := slices.IndexFunc(users, func(u *user.User) bool { + return u.ID == doer.ID }) + if idx > 0 { + newUsers := make([]*user.User, len(users)) + newUsers[0] = users[idx] + copy(newUsers[1:], users[:idx]) + copy(newUsers[idx+1:], users[idx+1:]) + return newUsers + } } return users } diff --git a/routers/web/user/package.go b/routers/web/user/package.go index 4e39eabc0ff..c6f85ac734e 100644 --- a/routers/web/user/package.go +++ b/routers/web/user/package.go @@ -20,6 +20,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" alpine_module "code.gitea.io/gitea/modules/packages/alpine" + arch_module "code.gitea.io/gitea/modules/packages/arch" debian_module "code.gitea.io/gitea/modules/packages/debian" rpm_module "code.gitea.io/gitea/modules/packages/rpm" "code.gitea.io/gitea/modules/setting" @@ -178,13 +179,13 @@ func ViewPackageVersion(ctx *context.Context) { ctx.Data["IsPackagesPage"] = true ctx.Data["PackageDescriptor"] = pd + registryHostURL, err := url.Parse(httplib.GuessCurrentHostURL(ctx)) + if err != nil { + registryHostURL, _ = url.Parse(setting.AppURL) + } + ctx.Data["PackageRegistryHost"] = registryHostURL.Host + switch pd.Package.Type { - case packages_model.TypeContainer: - registryAppURL, err := url.Parse(httplib.GuessCurrentAppURL(ctx)) - if err != nil { - registryAppURL, _ = url.Parse(setting.AppURL) - } - ctx.Data["RegistryHost"] = registryAppURL.Host case packages_model.TypeAlpine: branches := make(container.Set[string]) repositories := make(container.Set[string]) @@ -204,6 +205,23 @@ func ViewPackageVersion(ctx *context.Context) { } ctx.Data["Branches"] = util.Sorted(branches.Values()) + ctx.Data["Repositories"] = util.Sorted(repositories.Values()) + ctx.Data["Architectures"] = util.Sorted(architectures.Values()) + case packages_model.TypeArch: + repositories := make(container.Set[string]) + architectures := make(container.Set[string]) + + for _, f := range pd.Files { + for _, pp := range f.Properties { + switch pp.Name { + case arch_module.PropertyRepository: + repositories.Add(pp.Value) + case arch_module.PropertyArchitecture: + architectures.Add(pp.Value) + } + } + } + ctx.Data["Repositories"] = util.Sorted(repositories.Values()) ctx.Data["Architectures"] = util.Sorted(architectures.Values()) case packages_model.TypeDebian: @@ -249,7 +267,6 @@ func ViewPackageVersion(ctx *context.Context) { var ( total int64 pvs []*packages_model.PackageVersion - err error ) switch pd.Package.Type { case packages_model.TypeContainer: diff --git a/services/forms/package_form.go b/services/forms/package_form.go index cc940d42d34..9b6f9071647 100644 --- a/services/forms/package_form.go +++ b/services/forms/package_form.go @@ -15,7 +15,7 @@ import ( type PackageCleanupRuleForm struct { ID int64 Enabled bool - Type string `binding:"Required;In(alpine,cargo,chef,composer,conan,conda,container,cran,debian,generic,go,helm,maven,npm,nuget,pub,pypi,rpm,rubygems,swift,vagrant)"` + Type string `binding:"Required;In(alpine,arch,cargo,chef,composer,conan,conda,container,cran,debian,generic,go,helm,maven,npm,nuget,pub,pypi,rpm,rubygems,swift,vagrant)"` KeepCount int `binding:"In(0,1,5,10,25,50,100)"` KeepPattern string `binding:"RegexPattern"` RemoveDays int `binding:"In(0,7,14,30,60,90,180)"` diff --git a/services/mailer/mail_test.go b/services/mailer/mail_test.go index 42de7599ebd..185b72f0699 100644 --- a/services/mailer/mail_test.go +++ b/services/mailer/mail_test.go @@ -93,20 +93,20 @@ func TestComposeIssueCommentMessage(t *testing.T) { assert.NoError(t, err) assert.Len(t, msgs, 2) gomailMsg := msgs[0].ToMessage() - replyTo := gomailMsg.GetHeader("Reply-To")[0] - subject := gomailMsg.GetHeader("Subject")[0] + replyTo := gomailMsg.GetGenHeader("Reply-To")[0] + subject := gomailMsg.GetGenHeader("Subject")[0] - assert.Len(t, gomailMsg.GetHeader("To"), 1, "exactly one recipient is expected in the To field") + assert.Len(t, gomailMsg.GetAddrHeader("To"), 1, "exactly one recipient is expected in the To field") tokenRegex := regexp.MustCompile(`\Aincoming\+(.+)@localhost\z`) assert.Regexp(t, tokenRegex, replyTo) token := tokenRegex.FindAllStringSubmatch(replyTo, 1)[0][1] assert.Equal(t, "Re: ", subject[:4], "Comment reply subject should contain Re:") assert.Equal(t, "Re: [user2/repo1] @user2 #1 - issue1", subject) - assert.Equal(t, "", gomailMsg.GetHeader("In-Reply-To")[0], "In-Reply-To header doesn't match") - assert.ElementsMatch(t, []string{"", ""}, gomailMsg.GetHeader("References"), "References header doesn't match") - assert.Equal(t, "", gomailMsg.GetHeader("Message-ID")[0], "Message-ID header doesn't match") - assert.Equal(t, "", gomailMsg.GetHeader("List-Post")[0]) - assert.Len(t, gomailMsg.GetHeader("List-Unsubscribe"), 2) // url + mailto + assert.Equal(t, "", gomailMsg.GetGenHeader("In-Reply-To")[0], "In-Reply-To header doesn't match") + assert.ElementsMatch(t, []string{"", ""}, gomailMsg.GetGenHeader("References"), "References header doesn't match") + assert.Equal(t, "", gomailMsg.GetGenHeader("Message-ID")[0], "Message-ID header doesn't match") + assert.Equal(t, "", gomailMsg.GetGenHeader("List-Post")[0]) + assert.Len(t, gomailMsg.GetGenHeader("List-Unsubscribe"), 2) // url + mailto var buf bytes.Buffer gomailMsg.WriteTo(&buf) @@ -139,19 +139,19 @@ func TestComposeIssueMessage(t *testing.T) { assert.Len(t, msgs, 2) gomailMsg := msgs[0].ToMessage() - mailto := gomailMsg.GetHeader("To") - subject := gomailMsg.GetHeader("Subject") - messageID := gomailMsg.GetHeader("Message-ID") - inReplyTo := gomailMsg.GetHeader("In-Reply-To") - references := gomailMsg.GetHeader("References") + mailto := gomailMsg.GetAddrHeader("To") + subject := gomailMsg.GetGenHeader("Subject") + messageID := gomailMsg.GetGenHeader("Message-ID") + inReplyTo := gomailMsg.GetGenHeader("In-Reply-To") + references := gomailMsg.GetGenHeader("References") assert.Len(t, mailto, 1, "exactly one recipient is expected in the To field") assert.Equal(t, "[user2/repo1] @user2 #1 - issue1", subject[0]) assert.Equal(t, "", inReplyTo[0], "In-Reply-To header doesn't match") assert.Equal(t, "", references[0], "References header doesn't match") assert.Equal(t, "", messageID[0], "Message-ID header doesn't match") - assert.Empty(t, gomailMsg.GetHeader("List-Post")) // incoming mail feature disabled - assert.Len(t, gomailMsg.GetHeader("List-Unsubscribe"), 1) // url without mailto + assert.Empty(t, gomailMsg.GetGenHeader("List-Post")) // incoming mail feature disabled + assert.Len(t, gomailMsg.GetGenHeader("List-Unsubscribe"), 1) // url without mailto } func TestTemplateSelection(t *testing.T) { @@ -169,7 +169,7 @@ func TestTemplateSelection(t *testing.T) { template.Must(bodyTemplates.New("issue/close").Parse("issue/close/body")) expect := func(t *testing.T, msg *sender_service.Message, expSubject, expBody string) { - subject := msg.ToMessage().GetHeader("Subject") + subject := msg.ToMessage().GetGenHeader("Subject") msgbuf := new(bytes.Buffer) _, _ = msg.ToMessage().WriteTo(msgbuf) wholemsg := msgbuf.String() @@ -225,7 +225,7 @@ func TestTemplateServices(t *testing.T) { Content: "test body", Comment: comment, }, recipients, fromMention, "TestTemplateServices") - subject := msg.ToMessage().GetHeader("Subject") + subject := msg.ToMessage().GetGenHeader("Subject") msgbuf := new(bytes.Buffer) _, _ = msg.ToMessage().WriteTo(msgbuf) wholemsg := msgbuf.String() diff --git a/services/mailer/mailer.go b/services/mailer/mailer.go index bf4b5a43cb1..bcd4facca92 100644 --- a/services/mailer/mailer.go +++ b/services/mailer/mailer.go @@ -48,11 +48,11 @@ func NewContext(ctx context.Context) { mailQueue = queue.CreateSimpleQueue(graceful.GetManager().ShutdownContext(), "mail", func(items ...*sender_service.Message) []*sender_service.Message { for _, msg := range items { gomailMsg := msg.ToMessage() - log.Trace("New e-mail sending request %s: %s", gomailMsg.GetHeader("To"), msg.Info) + log.Trace("New e-mail sending request %s: %s", gomailMsg.GetGenHeader("To"), msg.Info) if err := sender_service.Send(sender, msg); err != nil { - log.Error("Failed to send emails %s: %s - %v", gomailMsg.GetHeader("To"), msg.Info, err) + log.Error("Failed to send emails %s: %s - %v", gomailMsg.GetGenHeader("To"), msg.Info, err) } else { - log.Trace("E-mails sent %s: %s", gomailMsg.GetHeader("To"), msg.Info) + log.Trace("E-mails sent %s: %s", gomailMsg.GetGenHeader("To"), msg.Info) } } return nil diff --git a/services/mailer/sender/main_test.go b/services/mailer/sender/main_test.go new file mode 100644 index 00000000000..c67057964fa --- /dev/null +++ b/services/mailer/sender/main_test.go @@ -0,0 +1,14 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package sender + +import ( + "testing" + + "code.gitea.io/gitea/models/unittest" +) + +func TestMain(m *testing.M) { + unittest.MainTest(m) +} diff --git a/services/mailer/sender/message.go b/services/mailer/sender/message.go index a3255692f07..db20675572d 100644 --- a/services/mailer/sender/message.go +++ b/services/mailer/sender/message.go @@ -6,6 +6,7 @@ package sender import ( "fmt" "hash/fnv" + "net/mail" "strings" "time" @@ -14,7 +15,7 @@ import ( "code.gitea.io/gitea/modules/setting" "github.com/jaytaylor/html2text" - "gopkg.in/gomail.v2" + gomail "github.com/wneessen/go-mail" ) // Message mail body and log info @@ -31,45 +32,46 @@ type Message struct { } // ToMessage converts a Message to gomail.Message -func (m *Message) ToMessage() *gomail.Message { - msg := gomail.NewMessage() - msg.SetAddressHeader("From", m.FromAddress, m.FromDisplayName) - msg.SetHeader("To", m.To) +func (m *Message) ToMessage() *gomail.Msg { + msg := gomail.NewMsg() + addr := mail.Address{Name: m.FromDisplayName, Address: m.FromAddress} + _ = msg.SetAddrHeader("From", addr.String()) + _ = msg.SetAddrHeader("To", m.To) if m.ReplyTo != "" { - msg.SetHeader("Reply-To", m.ReplyTo) + msg.SetGenHeader("Reply-To", m.ReplyTo) } for header := range m.Headers { - msg.SetHeader(header, m.Headers[header]...) + msg.SetGenHeader(gomail.Header(header), m.Headers[header]...) } if setting.MailService.SubjectPrefix != "" { - msg.SetHeader("Subject", setting.MailService.SubjectPrefix+" "+m.Subject) + msg.SetGenHeader("Subject", setting.MailService.SubjectPrefix+" "+m.Subject) } else { - msg.SetHeader("Subject", m.Subject) + msg.SetGenHeader("Subject", m.Subject) } - msg.SetDateHeader("Date", m.Date) - msg.SetHeader("X-Auto-Response-Suppress", "All") + msg.SetDateWithValue(m.Date) + msg.SetGenHeader("X-Auto-Response-Suppress", "All") plainBody, err := html2text.FromString(m.Body) if err != nil || setting.MailService.SendAsPlainText { if strings.Contains(base.TruncateString(m.Body, 100), "") { log.Warn("Mail contains HTML but configured to send as plain text.") } - msg.SetBody("text/plain", plainBody) + msg.SetBodyString("text/plain", plainBody) } else { - msg.SetBody("text/plain", plainBody) - msg.AddAlternative("text/html", m.Body) + msg.SetBodyString("text/plain", plainBody) + msg.AddAlternativeString("text/html", m.Body) } - if len(msg.GetHeader("Message-ID")) == 0 { - msg.SetHeader("Message-ID", m.generateAutoMessageID()) + if len(msg.GetGenHeader("Message-ID")) == 0 { + msg.SetGenHeader("Message-ID", m.generateAutoMessageID()) } for k, v := range setting.MailService.OverrideHeader { - if len(msg.GetHeader(k)) != 0 { + if len(msg.GetGenHeader(gomail.Header(k))) != 0 { log.Debug("Mailer override header '%s' as per config", k) } - msg.SetHeader(k, v...) + msg.SetGenHeader(gomail.Header(k), v...) } return msg diff --git a/services/mailer/sender/message_test.go b/services/mailer/sender/message_test.go index d47052685ef..63d0bc349a9 100644 --- a/services/mailer/sender/message_test.go +++ b/services/mailer/sender/message_test.go @@ -25,25 +25,27 @@ func TestGenerateMessageID(t *testing.T) { m := NewMessageFrom("", "display-name", "from-address", "subject", "body") m.Date = date gm := m.ToMessage() - assert.Equal(t, "", gm.GetHeader("Message-ID")[0]) + assert.Equal(t, "", gm.GetGenHeader("Message-ID")[0]) m = NewMessageFrom("a@b.com", "display-name", "from-address", "subject", "body") m.Date = date gm = m.ToMessage() - assert.Equal(t, "", gm.GetHeader("Message-ID")[0]) + assert.Equal(t, "", gm.GetGenHeader("Message-ID")[0]) m = NewMessageFrom("a@b.com", "display-name", "from-address", "subject", "body") m.SetHeader("Message-ID", "") gm = m.ToMessage() - assert.Equal(t, "", gm.GetHeader("Message-ID")[0]) + assert.Equal(t, "", gm.GetGenHeader("Message-ID")[0]) } func TestToMessage(t *testing.T) { - oldConf := *setting.MailService + oldConf := setting.MailService defer func() { - setting.MailService = &oldConf + setting.MailService = oldConf }() - setting.MailService.From = "test@gitea.com" + setting.MailService = &setting.Mailer{ + From: "test@gitea.com", + } m1 := Message{ Info: "info", @@ -54,18 +56,24 @@ func TestToMessage(t *testing.T) { Body: "Some Issue got closed by Y-Man", } + assertHeaders := func(t *testing.T, expected, header map[string]string) { + for k, v := range expected { + assert.Equal(t, v, header[k], "Header %s should be %s but got %s", k, v, header[k]) + } + } + buf := &strings.Builder{} _, err := m1.ToMessage().WriteTo(buf) assert.NoError(t, err) header, _ := extractMailHeaderAndContent(t, buf.String()) - assert.EqualValues(t, map[string]string{ + assertHeaders(t, map[string]string{ "Content-Type": "multipart/alternative;", "Date": "Mon, 01 Jan 0001 00:00:00 +0000", "From": "\"Test Gitea\" ", "Message-ID": "", - "Mime-Version": "1.0", + "MIME-Version": "1.0", "Subject": "Issue X Closed", - "To": "a@b.com", + "To": "", "X-Auto-Response-Suppress": "All", }, header) @@ -78,14 +86,14 @@ func TestToMessage(t *testing.T) { _, err = m1.ToMessage().WriteTo(buf) assert.NoError(t, err) header, _ = extractMailHeaderAndContent(t, buf.String()) - assert.EqualValues(t, map[string]string{ + assertHeaders(t, map[string]string{ "Content-Type": "multipart/alternative;", "Date": "Mon, 01 Jan 0001 00:00:00 +0000", "From": "\"Test Gitea\" ", "Message-ID": "", - "Mime-Version": "1.0", + "MIME-Version": "1.0", "Subject": "Issue X Closed", - "To": "a@b.com", + "To": "", "X-Auto-Response-Suppress": "All", "Auto-Submitted": "auto-generated", }, header) diff --git a/services/mailer/sender/sender.go b/services/mailer/sender/sender.go index bf317aa8462..e470c2f2b3d 100644 --- a/services/mailer/sender/sender.go +++ b/services/mailer/sender/sender.go @@ -4,13 +4,15 @@ package sender import ( + "io" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - - "gopkg.in/gomail.v2" ) -type Sender gomail.Sender +type Sender interface { + Send(from string, to []string, msg io.WriterTo) error +} var Send = send @@ -19,9 +21,18 @@ func send(sender Sender, msgs ...*Message) error { log.Error("Mailer: Send is being invoked but mail service hasn't been initialized") return nil } - goMsgs := []*gomail.Message{} for _, msg := range msgs { - goMsgs = append(goMsgs, msg.ToMessage()) + m := msg.ToMessage() + froms := m.GetFrom() + to, err := m.GetRecipients() + if err != nil { + return err + } + + // TODO: implement sending from multiple addresses + if err := sender.Send(froms[0].Address, to, m); err != nil { + return err + } } - return gomail.Send(sender, goMsgs...) + return nil } diff --git a/services/mailer/sender/smtp.go b/services/mailer/sender/smtp.go index ab49b7e5f83..c53c3da9975 100644 --- a/services/mailer/sender/smtp.go +++ b/services/mailer/sender/smtp.go @@ -8,12 +8,13 @@ import ( "fmt" "io" "net" - "net/smtp" "os" "strings" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + + "github.com/wneessen/go-mail/smtp" ) // SMTPSender Sender SMTP mail sender @@ -106,7 +107,7 @@ func (s *SMTPSender) Send(from string, to []string, msg io.WriterTo) error { if strings.Contains(options, "CRAM-MD5") { auth = smtp.CRAMMD5Auth(opts.User, opts.Passwd) } else if strings.Contains(options, "PLAIN") { - auth = smtp.PlainAuth("", opts.User, opts.Passwd, host) + auth = smtp.PlainAuth("", opts.User, opts.Passwd, host, false) } else if strings.Contains(options, "LOGIN") { // Patch for AUTH LOGIN auth = LoginAuth(opts.User, opts.Passwd) @@ -146,5 +147,10 @@ func (s *SMTPSender) Send(from string, to []string, msg io.WriterTo) error { return fmt.Errorf("SMTP close failed: %w", err) } - return client.Quit() + err = client.Quit() + if err != nil { + log.Error("Quit client failed: %v", err) + } + + return nil } diff --git a/services/mailer/sender/smtp_auth.go b/services/mailer/sender/smtp_auth.go index df65498a5a7..260b12437b7 100644 --- a/services/mailer/sender/smtp_auth.go +++ b/services/mailer/sender/smtp_auth.go @@ -5,9 +5,9 @@ package sender import ( "fmt" - "net/smtp" "github.com/Azure/go-ntlmssp" + "github.com/wneessen/go-mail/smtp" ) type loginAuth struct { diff --git a/services/packages/alpine/repository.go b/services/packages/alpine/repository.go index 664ab345598..27e63919803 100644 --- a/services/packages/alpine/repository.go +++ b/services/packages/alpine/repository.go @@ -72,7 +72,7 @@ func GetOrCreateKeyPair(ctx context.Context, ownerID int64) (string, string, err return priv, pub, nil } -// BuildAllRepositoryFiles (re)builds all repository files for every available distributions, components and architectures +// BuildAllRepositoryFiles (re)builds all repository files for every available branches, repositories and architectures func BuildAllRepositoryFiles(ctx context.Context, ownerID int64) error { pv, err := GetOrCreateRepositoryVersion(ctx, ownerID) if err != nil { diff --git a/services/packages/arch/repository.go b/services/packages/arch/repository.go new file mode 100644 index 00000000000..ab1b85ae958 --- /dev/null +++ b/services/packages/arch/repository.go @@ -0,0 +1,401 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package arch + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "encoding/base64" + "errors" + "fmt" + "io" + "os" + "strings" + + packages_model "code.gitea.io/gitea/models/packages" + arch_model "code.gitea.io/gitea/models/packages/arch" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/globallock" + "code.gitea.io/gitea/modules/json" + packages_module "code.gitea.io/gitea/modules/packages" + arch_module "code.gitea.io/gitea/modules/packages/arch" + "code.gitea.io/gitea/modules/util" + packages_service "code.gitea.io/gitea/services/packages" + + "github.com/keybase/go-crypto/openpgp" + "github.com/keybase/go-crypto/openpgp/armor" + "github.com/keybase/go-crypto/openpgp/packet" +) + +const ( + IndexArchiveFilename = "packages.db" +) + +func AquireRegistryLock(ctx context.Context, ownerID int64) (globallock.ReleaseFunc, error) { + return globallock.Lock(ctx, fmt.Sprintf("packages_arch_%d", ownerID)) +} + +// GetOrCreateRepositoryVersion gets or creates the internal repository package +// The Arch registry needs multiple index files which are stored in this package. +func GetOrCreateRepositoryVersion(ctx context.Context, ownerID int64) (*packages_model.PackageVersion, error) { + return packages_service.GetOrCreateInternalPackageVersion(ctx, ownerID, packages_model.TypeArch, arch_module.RepositoryPackage, arch_module.RepositoryVersion) +} + +// GetOrCreateKeyPair gets or creates the PGP keys used to sign repository files +func GetOrCreateKeyPair(ctx context.Context, ownerID int64) (string, string, error) { + priv, err := user_model.GetSetting(ctx, ownerID, arch_module.SettingKeyPrivate) + if err != nil && !errors.Is(err, util.ErrNotExist) { + return "", "", err + } + + pub, err := user_model.GetSetting(ctx, ownerID, arch_module.SettingKeyPublic) + if err != nil && !errors.Is(err, util.ErrNotExist) { + return "", "", err + } + + if priv == "" || pub == "" { + priv, pub, err = generateKeypair() + if err != nil { + return "", "", err + } + + if err := user_model.SetUserSetting(ctx, ownerID, arch_module.SettingKeyPrivate, priv); err != nil { + return "", "", err + } + + if err := user_model.SetUserSetting(ctx, ownerID, arch_module.SettingKeyPublic, pub); err != nil { + return "", "", err + } + } + + return priv, pub, nil +} + +func generateKeypair() (string, string, error) { + e, err := openpgp.NewEntity("", "Arch Registry", "", nil) + if err != nil { + return "", "", err + } + + var priv strings.Builder + var pub strings.Builder + + w, err := armor.Encode(&priv, openpgp.PrivateKeyType, nil) + if err != nil { + return "", "", err + } + if err := e.SerializePrivate(w, nil); err != nil { + return "", "", err + } + w.Close() + + w, err = armor.Encode(&pub, openpgp.PublicKeyType, nil) + if err != nil { + return "", "", err + } + if err := e.Serialize(w); err != nil { + return "", "", err + } + w.Close() + + return priv.String(), pub.String(), nil +} + +func SignData(ctx context.Context, ownerID int64, r io.Reader) ([]byte, error) { + priv, _, err := GetOrCreateKeyPair(ctx, ownerID) + if err != nil { + return nil, err + } + + block, err := armor.Decode(strings.NewReader(priv)) + if err != nil { + return nil, err + } + + e, err := openpgp.ReadEntity(packet.NewReader(block.Body)) + if err != nil { + return nil, err + } + + buf := &bytes.Buffer{} + if err := openpgp.DetachSign(buf, e, r, nil); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +// BuildAllRepositoryFiles (re)builds all repository files for every available repositories and architectures +func BuildAllRepositoryFiles(ctx context.Context, ownerID int64) error { + pv, err := GetOrCreateRepositoryVersion(ctx, ownerID) + if err != nil { + return err + } + + // 1. Delete all existing repository files + pfs, err := packages_model.GetFilesByVersionID(ctx, pv.ID) + if err != nil { + return err + } + + for _, pf := range pfs { + if err := packages_service.DeletePackageFile(ctx, pf); err != nil { + return err + } + } + + // 2. (Re)Build repository files for existing packages + repositories, err := arch_model.GetRepositories(ctx, ownerID) + if err != nil { + return err + } + for _, repository := range repositories { + architectures, err := arch_model.GetArchitectures(ctx, ownerID, repository) + if err != nil { + return err + } + for _, architecture := range architectures { + if err := buildPackagesIndex(ctx, ownerID, pv, repository, architecture); err != nil { + return fmt.Errorf("failed to build repository files [%s/%s]: %w", repository, architecture, err) + } + } + } + + return nil +} + +// BuildSpecificRepositoryFiles builds index files for the repository +func BuildSpecificRepositoryFiles(ctx context.Context, ownerID int64, repository, architecture string) error { + pv, err := GetOrCreateRepositoryVersion(ctx, ownerID) + if err != nil { + return err + } + + architectures := container.SetOf(architecture) + if architecture == arch_module.AnyArch { + // Update all other architectures too when updating the any index + additionalArchitectures, err := arch_model.GetArchitectures(ctx, ownerID, repository) + if err != nil { + return err + } + architectures.AddMultiple(additionalArchitectures...) + } + + for architecture := range architectures { + if err := buildPackagesIndex(ctx, ownerID, pv, repository, architecture); err != nil { + return err + } + } + return nil +} + +func searchPackageFiles(ctx context.Context, ownerID int64, repository, architecture string) ([]*packages_model.PackageFile, error) { + pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{ + OwnerID: ownerID, + PackageType: packages_model.TypeArch, + Query: "%.pkg.tar.%", + Properties: map[string]string{ + arch_module.PropertyRepository: repository, + arch_module.PropertyArchitecture: architecture, + }, + }) + if err != nil { + return nil, err + } + return pfs, nil +} + +func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *packages_model.PackageVersion, repository, architecture string) error { + pfs, err := searchPackageFiles(ctx, ownerID, repository, architecture) + if err != nil { + return err + } + if architecture != arch_module.AnyArch { + // Add all any packages too + anyarchFiles, err := searchPackageFiles(ctx, ownerID, repository, arch_module.AnyArch) + if err != nil { + return err + } + pfs = append(pfs, anyarchFiles...) + } + + // Delete the package indices if there are no packages + if len(pfs) == 0 { + pf, err := packages_model.GetFileForVersionByName(ctx, repoVersion.ID, IndexArchiveFilename, fmt.Sprintf("%s|%s", repository, architecture)) + if err != nil && !errors.Is(err, util.ErrNotExist) { + return err + } else if pf == nil { + return nil + } + + return packages_service.DeletePackageFile(ctx, pf) + } + + indexContent, _ := packages_module.NewHashedBuffer() + defer indexContent.Close() + + gw := gzip.NewWriter(indexContent) + tw := tar.NewWriter(gw) + + cache := make(map[int64]*packages_model.Package) + + for _, pf := range pfs { + opts := &entryOptions{ + File: pf, + } + + opts.Version, err = packages_model.GetVersionByID(ctx, pf.VersionID) + if err != nil { + return err + } + if err := json.Unmarshal([]byte(opts.Version.MetadataJSON), &opts.VersionMetadata); err != nil { + return err + } + opts.Package = cache[opts.Version.PackageID] + if opts.Package == nil { + opts.Package, err = packages_model.GetPackageByID(ctx, opts.Version.PackageID) + if err != nil { + return err + } + cache[opts.Package.ID] = opts.Package + } + opts.Blob, err = packages_model.GetBlobByID(ctx, pf.BlobID) + if err != nil { + return err + } + + sig, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, pf.ID, arch_module.PropertySignature) + if err != nil { + return err + } + if len(sig) == 0 { + return util.ErrNotExist + } + opts.Signature = sig[0].Value + + meta, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, pf.ID, arch_module.PropertyMetadata) + if err != nil { + return err + } + if len(meta) == 0 { + return util.ErrNotExist + } + if err := json.Unmarshal([]byte(meta[0].Value), &opts.FileMetadata); err != nil { + return err + } + + if err := writeFiles(tw, opts); err != nil { + return err + } + if err := writeDescription(tw, opts); err != nil { + return err + } + } + + tw.Close() + gw.Close() + + signature, err := SignData(ctx, ownerID, indexContent) + if err != nil { + return err + } + + if _, err := indexContent.Seek(0, io.SeekStart); err != nil { + return err + } + + _, err = packages_service.AddFileToPackageVersionInternal( + ctx, + repoVersion, + &packages_service.PackageFileCreationInfo{ + PackageFileInfo: packages_service.PackageFileInfo{ + Filename: IndexArchiveFilename, + CompositeKey: fmt.Sprintf("%s|%s", repository, architecture), + }, + Creator: user_model.NewGhostUser(), + Data: indexContent, + IsLead: false, + OverwriteExisting: true, + Properties: map[string]string{ + arch_module.PropertyRepository: repository, + arch_module.PropertyArchitecture: architecture, + arch_module.PropertySignature: base64.StdEncoding.EncodeToString(signature), + }, + }, + ) + return err +} + +type entryOptions struct { + Package *packages_model.Package + Version *packages_model.PackageVersion + VersionMetadata *arch_module.VersionMetadata + File *packages_model.PackageFile + FileMetadata *arch_module.FileMetadata + Blob *packages_model.PackageBlob + Signature string +} + +type keyValue struct { + Key string + Value string +} + +func writeFiles(tw *tar.Writer, opts *entryOptions) error { + return writeFields(tw, fmt.Sprintf("%s-%s/files", opts.Package.Name, opts.Version.Version), []keyValue{ + {"FILES", strings.Join(opts.FileMetadata.Files, "\n")}, + }) +} + +// https://gitlab.archlinux.org/pacman/pacman/-/blob/master/lib/libalpm/be_sync.c#L562 +func writeDescription(tw *tar.Writer, opts *entryOptions) error { + return writeFields(tw, fmt.Sprintf("%s-%s/desc", opts.Package.Name, opts.Version.Version), []keyValue{ + {"FILENAME", opts.File.Name}, + {"MD5SUM", opts.Blob.HashMD5}, + {"SHA256SUM", opts.Blob.HashSHA256}, + {"PGPSIG", opts.Signature}, + {"CSIZE", fmt.Sprintf("%d", opts.Blob.Size)}, + {"ISIZE", fmt.Sprintf("%d", opts.FileMetadata.InstalledSize)}, + {"NAME", opts.Package.Name}, + {"BASE", opts.FileMetadata.Base}, + {"ARCH", opts.FileMetadata.Architecture}, + {"VERSION", opts.Version.Version}, + {"DESC", opts.VersionMetadata.Description}, + {"URL", opts.VersionMetadata.ProjectURL}, + {"LICENSE", strings.Join(opts.VersionMetadata.Licenses, "\n")}, + {"GROUPS", strings.Join(opts.FileMetadata.Groups, "\n")}, + {"BUILDDATE", fmt.Sprintf("%d", opts.FileMetadata.BuildDate)}, + {"PACKAGER", opts.FileMetadata.Packager}, + {"PROVIDES", strings.Join(opts.FileMetadata.Provides, "\n")}, + {"DEPENDS", strings.Join(opts.FileMetadata.Depends, "\n")}, + {"OPTDEPENDS", strings.Join(opts.FileMetadata.OptDepends, "\n")}, + {"MAKEDEPENDS", strings.Join(opts.FileMetadata.MakeDepends, "\n")}, + {"CHECKDEPENDS", strings.Join(opts.FileMetadata.CheckDepends, "\n")}, + {"XDATA", strings.Join(opts.FileMetadata.XData, "\n")}, + }) +} + +func writeFields(tw *tar.Writer, filename string, fields []keyValue) error { + buf := &bytes.Buffer{} + for _, kv := range fields { + if kv.Value == "" { + continue + } + fmt.Fprintf(buf, "%%%s%%\n%s\n\n", kv.Key, kv.Value) + } + + if err := tw.WriteHeader(&tar.Header{ + Name: filename, + Size: int64(buf.Len()), + Mode: int64(os.ModePerm), + }); err != nil { + return err + } + + _, err := io.Copy(tw, buf) + return err +} diff --git a/services/packages/cleanup/cleanup.go b/services/packages/cleanup/cleanup.go index d7c9355da5e..b7ba2b6ac4a 100644 --- a/services/packages/cleanup/cleanup.go +++ b/services/packages/cleanup/cleanup.go @@ -16,6 +16,7 @@ import ( packages_module "code.gitea.io/gitea/modules/packages" packages_service "code.gitea.io/gitea/services/packages" alpine_service "code.gitea.io/gitea/services/packages/alpine" + arch_service "code.gitea.io/gitea/services/packages/arch" cargo_service "code.gitea.io/gitea/services/packages/cargo" container_service "code.gitea.io/gitea/services/packages/container" debian_service "code.gitea.io/gitea/services/packages/debian" @@ -120,18 +121,29 @@ func ExecuteCleanupRules(outerCtx context.Context) error { } if anyVersionDeleted { - if pcr.Type == packages_model.TypeDebian { + switch pcr.Type { + case packages_model.TypeDebian: if err := debian_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil { return fmt.Errorf("CleanupRule [%d]: debian.BuildAllRepositoryFiles failed: %w", pcr.ID, err) } - } else if pcr.Type == packages_model.TypeAlpine { + case packages_model.TypeAlpine: if err := alpine_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil { return fmt.Errorf("CleanupRule [%d]: alpine.BuildAllRepositoryFiles failed: %w", pcr.ID, err) } - } else if pcr.Type == packages_model.TypeRpm { + case packages_model.TypeRpm: if err := rpm_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil { return fmt.Errorf("CleanupRule [%d]: rpm.BuildAllRepositoryFiles failed: %w", pcr.ID, err) } + case packages_model.TypeArch: + release, err := arch_service.AquireRegistryLock(ctx, pcr.OwnerID) + if err != nil { + return err + } + defer release() + + if err := arch_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil { + return fmt.Errorf("CleanupRule [%d]: arch.BuildAllRepositoryFiles failed: %w", pcr.ID, err) + } } } return nil diff --git a/services/packages/packages.go b/services/packages/packages.go index 95579be34be..55351afce29 100644 --- a/services/packages/packages.go +++ b/services/packages/packages.go @@ -355,6 +355,8 @@ func CheckSizeQuotaExceeded(ctx context.Context, doer, owner *user_model.User, p switch packageType { case packages_model.TypeAlpine: typeSpecificSize = setting.Packages.LimitSizeAlpine + case packages_model.TypeArch: + typeSpecificSize = setting.Packages.LimitSizeArch case packages_model.TypeCargo: typeSpecificSize = setting.Packages.LimitSizeCargo case packages_model.TypeChef: diff --git a/templates/package/content/arch.tmpl b/templates/package/content/arch.tmpl new file mode 100644 index 00000000000..1c568cbb783 --- /dev/null +++ b/templates/package/content/arch.tmpl @@ -0,0 +1,41 @@ +{{if eq .PackageDescriptor.Package.Type "arch"}} +

{{ctx.Locale.Tr "packages.installation"}}

+
+
+
+ +
[{{.PackageDescriptor.Owner.LowerName}}.{{.PackageRegistryHost}}]
+SigLevel = Optional TrustAll
+Server = 
+
+
+ +
pacman -Sy {{.PackageDescriptor.Package.LowerName}}
+
+
+ +
+
+
+ +

{{ctx.Locale.Tr "packages.arch.repository"}}

+
+ + + + + + + + + + + +
{{ctx.Locale.Tr "packages.arch.repository.repositories"}}
{{StringUtils.Join .Repositories ", "}}
{{ctx.Locale.Tr "packages.arch.repository.architectures"}}
{{StringUtils.Join .Architectures ", "}}
+
+ + {{if .PackageDescriptor.Metadata.Description}} +

{{ctx.Locale.Tr "packages.about"}}

+
{{.PackageDescriptor.Metadata.Description}}
+ {{end}} +{{end}} diff --git a/templates/package/content/container.tmpl b/templates/package/content/container.tmpl index 138fedecb3f..aaed25bfbd6 100644 --- a/templates/package/content/container.tmpl +++ b/templates/package/content/container.tmpl @@ -5,13 +5,13 @@
{{if eq .PackageDescriptor.Metadata.Type "helm"}} -
helm pull oci://{{.RegistryHost}}/{{.PackageDescriptor.Owner.LowerName}}/{{.PackageDescriptor.Package.LowerName}} --version {{.PackageDescriptor.Version.LowerVersion}}
+
helm pull oci://{{.PackageRegistryHost}}/{{.PackageDescriptor.Owner.LowerName}}/{{.PackageDescriptor.Package.LowerName}} --version {{.PackageDescriptor.Version.LowerVersion}}
{{else}} {{$separator := ":"}} {{if not .PackageDescriptor.Metadata.IsTagged}} {{$separator = "@"}} {{end}} -
docker pull {{.RegistryHost}}/{{.PackageDescriptor.Owner.LowerName}}/{{.PackageDescriptor.Package.LowerName}}{{$separator}}{{.PackageDescriptor.Version.LowerVersion}}
+
docker pull {{.PackageRegistryHost}}/{{.PackageDescriptor.Owner.LowerName}}/{{.PackageDescriptor.Package.LowerName}}{{$separator}}{{.PackageDescriptor.Version.LowerVersion}}
{{end}}
diff --git a/templates/package/metadata/alpine.tmpl b/templates/package/metadata/alpine.tmpl index 3e7f10f66a6..c9174948b1e 100644 --- a/templates/package/metadata/alpine.tmpl +++ b/templates/package/metadata/alpine.tmpl @@ -1,5 +1,5 @@ {{if eq .PackageDescriptor.Package.Type "alpine"}} - {{if .PackageDescriptor.Metadata.Maintainer}}
{{svg "octicon-person" 16 "mr-3"}} {{.PackageDescriptor.Metadata.Maintainer}}
{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "mr-3"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}
{{end}} + {{if .PackageDescriptor.Metadata.Maintainer}}
{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Maintainer}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} + {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}
{{end}} {{end}} diff --git a/templates/package/metadata/arch.tmpl b/templates/package/metadata/arch.tmpl new file mode 100644 index 00000000000..2aea036ec2d --- /dev/null +++ b/templates/package/metadata/arch.tmpl @@ -0,0 +1,4 @@ +{{if eq .PackageDescriptor.Package.Type "arch"}} + {{range .PackageDescriptor.Metadata.Licenses}}
{{svg "octicon-law"}} {{.}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} +{{end}} diff --git a/templates/package/metadata/cargo.tmpl b/templates/package/metadata/cargo.tmpl index 5ad3c20a932..f7dd887a24d 100644 --- a/templates/package/metadata/cargo.tmpl +++ b/templates/package/metadata/cargo.tmpl @@ -1,7 +1,7 @@ {{if eq .PackageDescriptor.Package.Type "cargo"}} - {{range .PackageDescriptor.Metadata.Authors}}
{{svg "octicon-person" 16 "tw-mr-2"}} {{.}}
{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.RepositoryURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.repository_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.DocumentationURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.documentation_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}
{{end}} + {{range .PackageDescriptor.Metadata.Authors}}
{{svg "octicon-person"}} {{.}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}{{end}} + {{if .PackageDescriptor.Metadata.DocumentationURL}}{{end}} + {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}
{{end}} {{end}} diff --git a/templates/package/metadata/chef.tmpl b/templates/package/metadata/chef.tmpl index 23a9ce3ec0b..6bf606ca487 100644 --- a/templates/package/metadata/chef.tmpl +++ b/templates/package/metadata/chef.tmpl @@ -1,5 +1,5 @@ {{if eq .PackageDescriptor.Package.Type "chef"}} - {{if .PackageDescriptor.Metadata.Author}}
{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} - {{if .PackageDescriptor.Metadata.RepositoryURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.repository_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}
{{end}} + {{if .PackageDescriptor.Metadata.Author}}
{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}{{end}} + {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}
{{end}} {{end}} diff --git a/templates/package/metadata/composer.tmpl b/templates/package/metadata/composer.tmpl index 0f6ff9d6f2d..e69e91745fc 100644 --- a/templates/package/metadata/composer.tmpl +++ b/templates/package/metadata/composer.tmpl @@ -1,5 +1,5 @@ {{if eq .PackageDescriptor.Package.Type "composer"}} - {{range .PackageDescriptor.Metadata.Authors}}
{{svg "octicon-person" 16 "tw-mr-2"}} {{.Name}}
{{end}} - {{if .PackageDescriptor.Metadata.Homepage}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{range .PackageDescriptor.Metadata.License}}
{{svg "octicon-law" 16 "tw-mr-2"}} {{.}}
{{end}} + {{range .PackageDescriptor.Metadata.Authors}}
{{svg "octicon-person"}} {{.Name}}
{{end}} + {{if .PackageDescriptor.Metadata.Homepage}}{{end}} + {{range .PackageDescriptor.Metadata.License}}
{{svg "octicon-law"}} {{.}}
{{end}} {{end}} diff --git a/templates/package/metadata/conan.tmpl b/templates/package/metadata/conan.tmpl index 4e05ec2587c..8b153755539 100644 --- a/templates/package/metadata/conan.tmpl +++ b/templates/package/metadata/conan.tmpl @@ -1,6 +1,6 @@ {{if eq .PackageDescriptor.Package.Type "conan"}} - {{if .PackageDescriptor.Metadata.Author}}
{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}
{{end}} - {{if .PackageDescriptor.Metadata.RepositoryURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.repository_site"}}
{{end}} + {{if .PackageDescriptor.Metadata.Author}}
{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} + {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}
{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}{{end}} {{end}} diff --git a/templates/package/metadata/conda.tmpl b/templates/package/metadata/conda.tmpl index 3628686e13a..4add9453fa7 100644 --- a/templates/package/metadata/conda.tmpl +++ b/templates/package/metadata/conda.tmpl @@ -1,6 +1,6 @@ {{if eq .PackageDescriptor.Package.Type "conda"}} - {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}
{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.RepositoryURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.repository_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.DocumentationURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.documentation_site"}}
{{end}} + {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}{{end}} + {{if .PackageDescriptor.Metadata.DocumentationURL}}{{end}} {{end}} diff --git a/templates/package/metadata/container.tmpl b/templates/package/metadata/container.tmpl index f5abb7ef6e5..ecc17964d78 100644 --- a/templates/package/metadata/container.tmpl +++ b/templates/package/metadata/container.tmpl @@ -1,9 +1,9 @@ {{if eq .PackageDescriptor.Package.Type "container"}} -
{{svg "octicon-package" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Type.Name}}
- {{if .PackageDescriptor.Metadata.Platform}}
{{svg "octicon-cpu" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Platform}}
{{end}} - {{range .PackageDescriptor.Metadata.Authors}}
{{svg "octicon-person" 16 "tw-mr-2"}} {{.}}
{{end}} - {{if .PackageDescriptor.Metadata.Licenses}}
{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Licenses}}
{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.RepositoryURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.repository_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.DocumentationURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.documentation_site"}}
{{end}} +
{{svg "octicon-package"}} {{.PackageDescriptor.Metadata.Type.Name}}
+ {{if .PackageDescriptor.Metadata.Platform}}
{{svg "octicon-cpu"}} {{.PackageDescriptor.Metadata.Platform}}
{{end}} + {{range .PackageDescriptor.Metadata.Authors}}
{{svg "octicon-person"}} {{.}}
{{end}} + {{if .PackageDescriptor.Metadata.Licenses}}
{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.Licenses}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}{{end}} + {{if .PackageDescriptor.Metadata.DocumentationURL}}{{end}} {{end}} diff --git a/templates/package/metadata/cran.tmpl b/templates/package/metadata/cran.tmpl index 1d5a11e1967..3ada7ac743a 100644 --- a/templates/package/metadata/cran.tmpl +++ b/templates/package/metadata/cran.tmpl @@ -1,5 +1,5 @@ {{if eq .PackageDescriptor.Package.Type "cran"}} - {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law" 16 "mr-3"}} {{.PackageDescriptor.Metadata.License}}
{{end}} - {{range .PackageDescriptor.Metadata.Authors}}
{{svg "octicon-person" 16 "mr-3"}} {{.}}
{{end}} - {{range .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "mr-3"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} + {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}
{{end}} + {{range .PackageDescriptor.Metadata.Authors}}
{{svg "octicon-person"}} {{.}}
{{end}} + {{range .PackageDescriptor.Metadata.ProjectURL}}{{end}} {{end}} diff --git a/templates/package/metadata/debian.tmpl b/templates/package/metadata/debian.tmpl index 3cd845c9fe9..d35e8b00daa 100644 --- a/templates/package/metadata/debian.tmpl +++ b/templates/package/metadata/debian.tmpl @@ -1,4 +1,4 @@ {{if eq .PackageDescriptor.Package.Type "debian"}} - {{if .PackageDescriptor.Metadata.Maintainer}}
{{svg "octicon-person" 16 "mr-3"}} {{.PackageDescriptor.Metadata.Maintainer}}
{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "mr-3"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} + {{if .PackageDescriptor.Metadata.Maintainer}}
{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Maintainer}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} {{end}} diff --git a/templates/package/metadata/helm.tmpl b/templates/package/metadata/helm.tmpl index 50ea4849991..b3b3f348cf1 100644 --- a/templates/package/metadata/helm.tmpl +++ b/templates/package/metadata/helm.tmpl @@ -1,4 +1,4 @@ {{if eq .PackageDescriptor.Package.Type "helm"}} - {{range .PackageDescriptor.Metadata.Maintainers}}
{{svg "octicon-person" 16 "tw-mr-2"}} {{.Name}}
{{end}} - {{if .PackageDescriptor.Metadata.Home}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} + {{range .PackageDescriptor.Metadata.Maintainers}}
{{svg "octicon-person"}} {{.Name}}
{{end}} + {{if .PackageDescriptor.Metadata.Home}}{{end}} {{end}} diff --git a/templates/package/metadata/maven.tmpl b/templates/package/metadata/maven.tmpl index 36412723d24..33662be7caf 100644 --- a/templates/package/metadata/maven.tmpl +++ b/templates/package/metadata/maven.tmpl @@ -1,8 +1,8 @@ {{if and (eq .PackageDescriptor.Package.Type "maven") (not .PackageDescriptor.Metadata)}} -
{{svg "octicon-note" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.no_metadata"}}
+
{{svg "octicon-note"}} {{ctx.Locale.Tr "packages.no_metadata"}}
{{end}} {{if and (eq .PackageDescriptor.Package.Type "maven") .PackageDescriptor.Metadata}} - {{if .PackageDescriptor.Metadata.Name}}
{{svg "octicon-note" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Name}}
{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{range .PackageDescriptor.Metadata.Licenses}}
{{svg "octicon-law" 16 "tw-mr-2"}} {{.}}
{{end}} + {{if .PackageDescriptor.Metadata.Name}}
{{svg "octicon-note"}} {{.PackageDescriptor.Metadata.Name}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} + {{range .PackageDescriptor.Metadata.Licenses}}
{{svg "octicon-law"}} {{.}}
{{end}} {{end}} diff --git a/templates/package/metadata/npm.tmpl b/templates/package/metadata/npm.tmpl index df37504e374..ff245f2b03e 100644 --- a/templates/package/metadata/npm.tmpl +++ b/templates/package/metadata/npm.tmpl @@ -1,8 +1,8 @@ {{if eq .PackageDescriptor.Package.Type "npm"}} - {{if .PackageDescriptor.Metadata.Author}}
{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}
{{end}} + {{if .PackageDescriptor.Metadata.Author}}
{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} + {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}
{{end}} {{range .PackageDescriptor.VersionProperties}} - {{if eq .Name "npm.tag"}}
{{svg "octicon-versions" 16 "tw-mr-2"}} {{.Value}}
{{end}} + {{if eq .Name "npm.tag"}}
{{svg "octicon-versions"}} {{.Value}}
{{end}} {{end}} {{end}} diff --git a/templates/package/metadata/nuget.tmpl b/templates/package/metadata/nuget.tmpl index 5534577bd26..2d18528f857 100644 --- a/templates/package/metadata/nuget.tmpl +++ b/templates/package/metadata/nuget.tmpl @@ -1,5 +1,5 @@ {{if eq .PackageDescriptor.Package.Type "nuget"}} - {{if .PackageDescriptor.Metadata.Authors}}
{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Authors}}
{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.RepositoryURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.repository_site"}}
{{end}} + {{if .PackageDescriptor.Metadata.Authors}}
{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Authors}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}{{end}} {{end}} diff --git a/templates/package/metadata/pub.tmpl b/templates/package/metadata/pub.tmpl index 16f7cec3704..e54207c4c60 100644 --- a/templates/package/metadata/pub.tmpl +++ b/templates/package/metadata/pub.tmpl @@ -1,5 +1,5 @@ {{if eq .PackageDescriptor.Package.Type "pub"}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.RepositoryURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.repository_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.DocumentationURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.documentation_site"}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}{{end}} + {{if .PackageDescriptor.Metadata.DocumentationURL}}{{end}} {{end}} diff --git a/templates/package/metadata/pypi.tmpl b/templates/package/metadata/pypi.tmpl index 3d9b213907f..9dfac07cbfe 100644 --- a/templates/package/metadata/pypi.tmpl +++ b/templates/package/metadata/pypi.tmpl @@ -1,5 +1,5 @@ {{if eq .PackageDescriptor.Package.Type "pypi"}} - {{if .PackageDescriptor.Metadata.Author}}
{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}
{{end}} + {{if .PackageDescriptor.Metadata.Author}}
{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} + {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}
{{end}} {{end}} diff --git a/templates/package/metadata/rpm.tmpl b/templates/package/metadata/rpm.tmpl index eda8a489f3c..65093933a92 100644 --- a/templates/package/metadata/rpm.tmpl +++ b/templates/package/metadata/rpm.tmpl @@ -1,4 +1,4 @@ {{if eq .PackageDescriptor.Package.Type "rpm"}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} + {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}
{{end}} {{end}} diff --git a/templates/package/metadata/rubygems.tmpl b/templates/package/metadata/rubygems.tmpl index 9b11287691c..04fc3695abe 100644 --- a/templates/package/metadata/rubygems.tmpl +++ b/templates/package/metadata/rubygems.tmpl @@ -1,5 +1,5 @@ {{if eq .PackageDescriptor.Package.Type "rubygems"}} - {{range .PackageDescriptor.Metadata.Authors}}
{{svg "octicon-person" 16 "tw-mr-2"}} {{.}}
{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{range .PackageDescriptor.Metadata.Licenses}}
{{svg "octicon-law" 16 "tw-mr-2"}} {{.}}
{{end}} + {{range .PackageDescriptor.Metadata.Authors}}
{{svg "octicon-person"}} {{.}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}} {{end}} + {{range .PackageDescriptor.Metadata.Licenses}}
{{svg "octicon-law"}} {{.}}
{{end}} {{end}} diff --git a/templates/package/metadata/swift.tmpl b/templates/package/metadata/swift.tmpl index fdffb6dedec..fe28759de3f 100644 --- a/templates/package/metadata/swift.tmpl +++ b/templates/package/metadata/swift.tmpl @@ -1,4 +1,4 @@ {{if eq .PackageDescriptor.Package.Type "swift"}} - {{if .PackageDescriptor.Metadata.Author.String}}
{{svg "octicon-person" 16 "mr-3"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} - {{if .PackageDescriptor.Metadata.RepositoryURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.repository_site"}}
{{end}} + {{if .PackageDescriptor.Metadata.Author.String}}
{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}{{end}} {{end}} diff --git a/templates/package/metadata/vagrant.tmpl b/templates/package/metadata/vagrant.tmpl index 4628a2dcbb0..795ab33da93 100644 --- a/templates/package/metadata/vagrant.tmpl +++ b/templates/package/metadata/vagrant.tmpl @@ -1,5 +1,5 @@ {{if eq .PackageDescriptor.Package.Type "vagrant"}} - {{if .PackageDescriptor.Metadata.Author}}
{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.RepositoryURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.repository_site"}}
{{end}} + {{if .PackageDescriptor.Metadata.Author}}
{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}{{end}} {{end}} diff --git a/templates/package/view.tmpl b/templates/package/view.tmpl index 0f06d7afbd3..9e92207466d 100644 --- a/templates/package/view.tmpl +++ b/templates/package/view.tmpl @@ -1,12 +1,10 @@ {{template "base/head" .}} -
+
{{template "shared/user/org_profile_avatar" .}}
{{template "user/overview/header" .}}
-
-

{{.PackageDescriptor.Package.Name}} ({{.PackageDescriptor.Version.Version}})

-
+

{{.PackageDescriptor.Package.Name}} ({{.PackageDescriptor.Version.Version}})

{{$timeStr := DateUtils.TimeSince .PackageDescriptor.Version.CreatedUnix}} {{if .HasRepositoryAccess}} @@ -19,6 +17,7 @@
{{template "package/content/alpine" .}} + {{template "package/content/arch" .}} {{template "package/content/cargo" .}} {{template "package/content/chef" .}} {{template "package/content/composer" .}} @@ -42,14 +41,15 @@
{{ctx.Locale.Tr "packages.details"}} -
-
{{svg .PackageDescriptor.Package.Type.SVGName 16 "tw-mr-2"}} {{.PackageDescriptor.Package.Type.Name}}
+
+
{{svg .PackageDescriptor.Package.Type.SVGName}} {{.PackageDescriptor.Package.Type.Name}}
{{if .HasRepositoryAccess}} -
{{svg "octicon-repo" 16 "tw-mr-2"}} {{.PackageDescriptor.Repository.FullName}}
+ {{end}} -
{{svg "octicon-calendar" 16 "tw-mr-2"}} {{DateUtils.TimeSince .PackageDescriptor.Version.CreatedUnix}}
-
{{svg "octicon-download" 16 "tw-mr-2"}} {{.PackageDescriptor.Version.DownloadCount}}
+
{{svg "octicon-calendar"}} {{DateUtils.TimeSince .PackageDescriptor.Version.CreatedUnix}}
+
{{svg "octicon-download"}} {{.PackageDescriptor.Version.DownloadCount}}
{{template "package/metadata/alpine" .}} + {{template "package/metadata/arch" .}} {{template "package/metadata/cargo" .}} {{template "package/metadata/chef" .}} {{template "package/metadata/composer" .}} @@ -70,7 +70,7 @@ {{template "package/metadata/swift" .}} {{template "package/metadata/vagrant" .}} {{if not (and (eq .PackageDescriptor.Package.Type "container") .PackageDescriptor.Metadata.Manifests)}} -
{{svg "octicon-database" 16 "tw-mr-2"}} {{FileSize .PackageDescriptor.CalculateBlobSize}}
+
{{svg "octicon-database"}} {{FileSize .PackageDescriptor.CalculateBlobSize}}
{{end}}
{{if not (eq .PackageDescriptor.Package.Type "container")}} @@ -98,12 +98,12 @@
{{if or .CanWritePackages .HasRepositoryAccess}}
-
+
{{if .HasRepositoryAccess}} -
{{svg "octicon-issue-opened" 16 "tw-mr-2"}} {{ctx.Locale.Tr "repo.issues"}}
+
{{svg "octicon-issue-opened"}} {{ctx.Locale.Tr "repo.issues"}}
{{end}} {{if .CanWritePackages}} -
{{svg "octicon-tools" 16 "tw-mr-2"}} {{ctx.Locale.Tr "repo.settings"}}
+
{{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}}
{{end}}
{{end}} diff --git a/templates/repo/editor/edit.tmpl b/templates/repo/editor/edit.tmpl index c5568268276..204a4269707 100644 --- a/templates/repo/editor/edit.tmpl +++ b/templates/repo/editor/edit.tmpl @@ -3,7 +3,10 @@ {{template "repo/header" .}}
{{template "base/alert" .}} -
+ {{.CsrfTokenHtml}} @@ -29,7 +32,7 @@
{{template "base/footer" .}} diff --git a/tests/integration/api_packages_arch_test.go b/tests/integration/api_packages_arch_test.go new file mode 100644 index 00000000000..9c7a9dd19de --- /dev/null +++ b/tests/integration/api_packages_arch_test.go @@ -0,0 +1,302 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "fmt" + "io" + "net/http" + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + arch_module "code.gitea.io/gitea/modules/packages/arch" + arch_service "code.gitea.io/gitea/services/packages/arch" + "code.gitea.io/gitea/tests" + + "github.com/klauspost/compress/zstd" + "github.com/stretchr/testify/assert" + "github.com/ulikunitz/xz" +) + +func TestPackageArch(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + packageName := "gitea-test" + packageVersion := "1.4.1-r3" + + createPackage := func(compression, name, version, architecture string) []byte { + var buf bytes.Buffer + var cw io.WriteCloser + switch compression { + case "zst": + cw, _ = zstd.NewWriter(&buf) + case "xz": + cw, _ = xz.NewWriter(&buf) + case "gz": + cw = gzip.NewWriter(&buf) + } + tw := tar.NewWriter(cw) + + info := []byte(`pkgname = ` + name + ` +pkgbase = ` + name + ` +pkgver = ` + version + ` +pkgdesc = Description +# comment +builddate = 1678834800 +size = 8 +arch = ` + architecture + ` +license = MIT`) + + hdr := &tar.Header{ + Name: ".PKGINFO", + Mode: 0o600, + Size: int64(len(info)), + } + tw.WriteHeader(hdr) + tw.Write(info) + + for _, file := range []string{"etc/dummy", "opt/file/bin"} { + hdr := &tar.Header{ + Name: file, + Mode: 0o600, + Size: 4, + } + tw.WriteHeader(hdr) + tw.Write([]byte("test")) + } + + tw.Close() + cw.Close() + + return buf.Bytes() + } + + compressions := []string{"gz", "xz", "zst"} + repositories := []string{"main", "testing", "with/slash", ""} + + rootURL := fmt.Sprintf("/api/packages/%s/arch", user.Name) + + t.Run("RepositoryKey", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", rootURL+"/repository.key") + resp := MakeRequest(t, req, http.StatusOK) + + assert.Equal(t, "application/pgp-keys", resp.Header().Get("Content-Type")) + assert.Contains(t, resp.Body.String(), "-----BEGIN PGP PUBLIC KEY BLOCK-----") + }) + + contentAarch64Gz := createPackage("gz", packageName, packageVersion, "aarch64") + for _, compression := range compressions { + contentAarch64 := createPackage(compression, packageName, packageVersion, "aarch64") + contentAny := createPackage(compression, packageName+"_"+arch_module.AnyArch, packageVersion, arch_module.AnyArch) + + for _, repository := range repositories { + t.Run(fmt.Sprintf("[%s,%s]", repository, compression), func(t *testing.T) { + t.Run("Upload", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + uploadURL := fmt.Sprintf("%s/%s", rootURL, repository) + + req := NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader([]byte{})) + MakeRequest(t, req, http.StatusUnauthorized) + + req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader([]byte{})). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusBadRequest) + + req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(contentAarch64)). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusCreated) + + pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeArch) + assert.NoError(t, err) + assert.Len(t, pvs, 1) + + pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0]) + assert.NoError(t, err) + assert.Nil(t, pd.SemVer) + assert.IsType(t, &arch_module.VersionMetadata{}, pd.Metadata) + assert.Equal(t, packageName, pd.Package.Name) + assert.Equal(t, packageVersion, pd.Version.Version) + + pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID) + assert.NoError(t, err) + assert.NotEmpty(t, pfs) + assert.Condition(t, func() bool { + seen := false + expectedFilename := fmt.Sprintf("%s-%s-aarch64.pkg.tar.%s", packageName, packageVersion, compression) + expectedCompositeKey := fmt.Sprintf("%s|aarch64", repository) + for _, pf := range pfs { + if pf.Name == expectedFilename && pf.CompositeKey == expectedCompositeKey { + if seen { + return false + } + seen = true + + assert.True(t, pf.IsLead) + + pfps, err := packages.GetProperties(db.DefaultContext, packages.PropertyTypeFile, pf.ID) + assert.NoError(t, err) + + for _, pfp := range pfps { + switch pfp.Name { + case arch_module.PropertyRepository: + assert.Equal(t, repository, pfp.Value) + case arch_module.PropertyArchitecture: + assert.Equal(t, "aarch64", pfp.Value) + } + } + } + } + return seen + }) + + req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(contentAarch64)). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusConflict) + + // Add same package with different compression leads to conflict + req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(contentAarch64Gz)). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusConflict) + }) + + readIndexContent := func(r io.Reader) (map[string]string, error) { + gzr, err := gzip.NewReader(r) + if err != nil { + return nil, err + } + + content := make(map[string]string) + + tr := tar.NewReader(gzr) + for { + hd, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + + buf, err := io.ReadAll(tr) + if err != nil { + return nil, err + } + + content[hd.Name] = string(buf) + } + + return content, nil + } + + t.Run("Index", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s", rootURL, repository, arch_service.IndexArchiveFilename)) + resp := MakeRequest(t, req, http.StatusOK) + + content, err := readIndexContent(resp.Body) + assert.NoError(t, err) + + desc, has := content[fmt.Sprintf("%s-%s/desc", packageName, packageVersion)] + assert.True(t, has) + assert.Contains(t, desc, "%FILENAME%\n"+fmt.Sprintf("%s-%s-aarch64.pkg.tar.%s", packageName, packageVersion, compression)+"\n\n") + assert.Contains(t, desc, "%NAME%\n"+packageName+"\n\n") + assert.Contains(t, desc, "%VERSION%\n"+packageVersion+"\n\n") + assert.Contains(t, desc, "%ARCH%\naarch64\n") + assert.NotContains(t, desc, "%ARCH%\n"+arch_module.AnyArch+"\n") + assert.Contains(t, desc, "%LICENSE%\nMIT\n") + + files, has := content[fmt.Sprintf("%s-%s/files", packageName, packageVersion)] + assert.True(t, has) + assert.Contains(t, files, "%FILES%\netc/dummy\nopt/file/bin\n\n") + + for _, indexFile := range []string{ + arch_service.IndexArchiveFilename, + arch_service.IndexArchiveFilename + ".tar.gz", + "index.db", + "index.db.tar.gz", + "index.files", + "index.files.tar.gz", + } { + req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s", rootURL, repository, indexFile)) + MakeRequest(t, req, http.StatusOK) + + req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s.sig", rootURL, repository, indexFile)) + MakeRequest(t, req, http.StatusOK) + } + }) + + t.Run("Download", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s-%s-aarch64.pkg.tar.%s", rootURL, repository, packageName, packageVersion, compression)) + MakeRequest(t, req, http.StatusOK) + + req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s-%s-aarch64.pkg.tar.%s.sig", rootURL, repository, packageName, packageVersion, compression)) + MakeRequest(t, req, http.StatusOK) + }) + + t.Run("Any", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/%s", rootURL, repository), bytes.NewReader(contentAny)). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusCreated) + + req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s", rootURL, repository, arch_service.IndexArchiveFilename)) + resp := MakeRequest(t, req, http.StatusOK) + + content, err := readIndexContent(resp.Body) + assert.NoError(t, err) + + desc, has := content[fmt.Sprintf("%s-%s/desc", packageName, packageVersion)] + assert.True(t, has) + assert.Contains(t, desc, "%NAME%\n"+packageName+"\n\n") + assert.Contains(t, desc, "%ARCH%\naarch64\n") + + desc, has = content[fmt.Sprintf("%s-%s/desc", packageName+"_"+arch_module.AnyArch, packageVersion)] + assert.True(t, has) + assert.Contains(t, desc, "%NAME%\n"+packageName+"_any\n\n") + assert.Contains(t, desc, "%ARCH%\n"+arch_module.AnyArch+"\n") + + // "any" architecture package should be available with every architecture requested + for _, arch := range []string{arch_module.AnyArch, "aarch64", "myarch"} { + req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/%s-%s-any.pkg.tar.%s", rootURL, repository, arch, packageName+"_"+arch_module.AnyArch, packageVersion, compression)) + MakeRequest(t, req, http.StatusOK) + } + + req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s/%s/any", rootURL, repository, packageName+"_"+arch_module.AnyArch, packageVersion)). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusNoContent) + }) + + t.Run("Delete", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s/%s/aarch64", rootURL, repository, packageName, packageVersion)) + MakeRequest(t, req, http.StatusUnauthorized) + + req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s/%s/aarch64", rootURL, repository, packageName, packageVersion)). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusNoContent) + + // Deleting the last file of an architecture should remove that index + req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s", rootURL, repository, arch_service.IndexArchiveFilename)) + MakeRequest(t, req, http.StatusNotFound) + }) + }) + } + } +} diff --git a/web_src/css/base.css b/web_src/css/base.css index babbf4c89dc..8f5ef51c4aa 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -1386,7 +1386,7 @@ table th[data-sortt-desc] .svg { .flex-text-block { display: flex; align-items: center; - gap: .25rem; + gap: .5rem; min-width: 0; } diff --git a/web_src/js/features/codeeditor.ts b/web_src/js/features/codeeditor.ts index ee515e25e24..93b2042fa92 100644 --- a/web_src/js/features/codeeditor.ts +++ b/web_src/js/features/codeeditor.ts @@ -134,19 +134,17 @@ function getFileBasedOptions(filename: string, lineWrapExts: string[]) { } function togglePreviewDisplay(previewable: boolean) { - const previewTab = document.querySelector('a[data-tab="preview"]'); + const previewTab = document.querySelector('a[data-tab="preview"]'); if (!previewTab) return; if (previewable) { - const newUrl = (previewTab.getAttribute('data-url') || '').replace(/(.*)\/.*/, `$1/markup`); - previewTab.setAttribute('data-url', newUrl); previewTab.style.display = ''; } else { previewTab.style.display = 'none'; // If the "preview" tab was active, user changes the filename to a non-previewable one, // then the "preview" tab becomes inactive (hidden), so the "write" tab should become active if (previewTab.classList.contains('active')) { - const writeTab = document.querySelector('a[data-tab="write"]'); + const writeTab = document.querySelector('a[data-tab="write"]'); writeTab.click(); } } diff --git a/web_src/js/features/comp/ConfirmModal.ts b/web_src/js/features/comp/ConfirmModal.ts index bf645cdbdb4..1ce490ec2e2 100644 --- a/web_src/js/features/comp/ConfirmModal.ts +++ b/web_src/js/features/comp/ConfirmModal.ts @@ -5,7 +5,7 @@ import {fomanticQuery} from '../../modules/fomantic/base.ts'; const {i18n} = window.config; -export function confirmModal({header = '', content = '', confirmButtonColor = 'primary'} = {}) { +export function confirmModal({header = '', content = '', confirmButtonColor = 'primary'} = {}): Promise { return new Promise((resolve) => { const headerHtml = header ? `
${htmlEscape(header)}
` : ''; const modal = createElementFromHTML(` diff --git a/web_src/js/features/repo-editor.ts b/web_src/js/features/repo-editor.ts index adae55f25c9..96b08250fb1 100644 --- a/web_src/js/features/repo-editor.ts +++ b/web_src/js/features/repo-editor.ts @@ -1,4 +1,3 @@ -import $ from 'jquery'; import {htmlEscape} from 'escape-goat'; import {createCodeEditor} from './codeeditor.ts'; import {hideElem, queryElems, showElem, createElementFromHTML} from '../utils/dom.ts'; @@ -6,39 +5,33 @@ import {initMarkupContent} from '../markup/content.ts'; import {attachRefIssueContextPopup} from './contextpopup.ts'; import {POST} from '../modules/fetch.ts'; import {initDropzone} from './dropzone.ts'; +import {confirmModal} from './comp/ConfirmModal.ts'; +import {applyAreYouSure} from '../vendor/jquery.are-you-sure.ts'; +import {fomanticQuery} from '../modules/fomantic/base.ts'; -function initEditPreviewTab($form) { - const $tabMenu = $form.find('.repo-editor-menu'); - $tabMenu.find('.item').tab(); - const $previewTab = $tabMenu.find('a[data-tab="preview"]'); - if ($previewTab.length) { - $previewTab.on('click', async function () { - const $this = $(this); - let context = `${$this.data('context')}/`; - const mode = $this.data('markup-mode') || 'comment'; - const $treePathEl = $form.find('input#tree_path'); - if ($treePathEl.length > 0) { - context += $treePathEl.val(); - } - context = context.substring(0, context.lastIndexOf('/')); +function initEditPreviewTab(elForm: HTMLFormElement) { + const elTabMenu = elForm.querySelector('.repo-editor-menu'); + fomanticQuery(elTabMenu.querySelectorAll('.item')).tab(); - const formData = new FormData(); - formData.append('mode', mode); - formData.append('context', context); - formData.append('text', $form.find('.tab[data-tab="write"] textarea').val()); - formData.append('file_path', $treePathEl.val()); - try { - const response = await POST($this.data('url'), {data: formData}); - const data = await response.text(); - const $previewPanel = $form.find('.tab[data-tab="preview"]'); - if ($previewPanel.length) { - renderPreviewPanelContent($previewPanel, data); - } - } catch (error) { - console.error('Error:', error); - } - }); - } + const elPreviewTab = elTabMenu.querySelector('a[data-tab="preview"]'); + const elPreviewPanel = elForm.querySelector('.tab[data-tab="preview"]'); + if (!elPreviewTab || !elPreviewPanel) return; + + elPreviewTab.addEventListener('click', async () => { + const elTreePath = elForm.querySelector('input#tree_path'); + const previewUrl = elPreviewTab.getAttribute('data-preview-url'); + const previewContextRef = elPreviewTab.getAttribute('data-preview-context-ref'); + let previewContext = `${previewContextRef}/${elTreePath.value}`; + previewContext = previewContext.substring(0, previewContext.lastIndexOf('/')); + const formData = new FormData(); + formData.append('mode', 'file'); + formData.append('context', previewContext); + formData.append('text', elForm.querySelector('.tab[data-tab="write"] textarea').value); + formData.append('file_path', elTreePath.value); + const response = await POST(previewUrl, {data: formData}); + const data = await response.text(); + renderPreviewPanelContent(elPreviewPanel, data); + }); } export function initRepoEditor() { @@ -151,8 +144,8 @@ export function initRepoEditor() { } }); - const $form = $('.repository.editor .edit.form'); - initEditPreviewTab($form); + const elForm = document.querySelector('.repository.editor .edit.form'); + initEditPreviewTab(elForm); (async () => { const editor = await createCodeEditor(editArea, filenameInput); @@ -160,16 +153,16 @@ export function initRepoEditor() { // Using events from https://github.com/codedance/jquery.AreYouSure#advanced-usage // to enable or disable the commit button const commitButton = document.querySelector('#commit-button'); - const $editForm = $('.ui.edit.form'); const dirtyFileClass = 'dirty-file'; // Disabling the button at the start - if ($('input[name="page_has_posted"]').val() !== 'true') { + if (document.querySelector('input[name="page_has_posted"]').value !== 'true') { commitButton.disabled = true; } // Registering a custom listener for the file path and the file content - $editForm.areYouSure({ + // FIXME: it is not quite right here (old bug), it causes double-init, the global areYouSure "dirty" class will also be added + applyAreYouSure(elForm, { silent: true, dirtyClass: dirtyFileClass, fieldSelector: ':input:not(.commit-form-wrapper :input)', @@ -187,15 +180,17 @@ export function initRepoEditor() { editor.setValue(value); } - commitButton?.addEventListener('click', (e) => { + commitButton?.addEventListener('click', async (e) => { // A modal which asks if an empty file should be committed if (!editArea.value) { - $('#edit-empty-content-modal').modal({ - onApprove() { - $('.edit.form').trigger('submit'); - }, - }).modal('show'); e.preventDefault(); + if (await confirmModal({ + header: elForm.getAttribute('data-text-empty-confirm-header'), + content: elForm.getAttribute('data-text-empty-confirm-content'), + })) { + elForm.classList.remove('dirty'); + elForm.submit(); + } } }); })(); diff --git a/web_src/js/vendor/jquery.are-you-sure.ts b/web_src/js/vendor/jquery.are-you-sure.ts index bd621a145e4..9efe783c547 100644 --- a/web_src/js/vendor/jquery.are-you-sure.ts +++ b/web_src/js/vendor/jquery.are-you-sure.ts @@ -196,6 +196,6 @@ export function initAreYouSure($) { }; } -export function applyAreYouSure(selector: string) { - $(selector).areYouSure(); +export function applyAreYouSure(selectorOrEl: string|Element|$, opts = {}) { + $(selectorOrEl).areYouSure(opts); } diff --git a/web_src/svg/gitea-arch.svg b/web_src/svg/gitea-arch.svg new file mode 100644 index 00000000000..ba8254d8049 --- /dev/null +++ b/web_src/svg/gitea-arch.svg @@ -0,0 +1 @@ + \ No newline at end of file