From 0a6f6354bbc03d4a3ad382a432113301e9ea9d86 Mon Sep 17 00:00:00 2001 From: John Olheiser Date: Mon, 13 Mar 2023 14:41:38 -0500 Subject: [PATCH 01/11] Purge API comment (#23451) This PR just adds the `purge` query parameter to the swagger docs for admin user delete. Signed-off-by: jolheiser --- routers/api/v1/admin/user.go | 4 ++++ templates/swagger/v1_json.tmpl | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go index 1fbdab3e5598..4192d8654d7c 100644 --- a/routers/api/v1/admin/user.go +++ b/routers/api/v1/admin/user.go @@ -305,6 +305,10 @@ func DeleteUser(ctx *context.APIContext) { // description: username of user to delete // type: string // required: true + // - name: purge + // in: query + // description: purge the user from the system completely + // type: boolean // responses: // "204": // "$ref": "#/responses/empty" diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index cb88e175ea5f..b64f6dcd87dc 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -493,6 +493,12 @@ "name": "username", "in": "path", "required": true + }, + { + "type": "boolean", + "description": "purge the user from the system completely", + "name": "purge", + "in": "query" } ], "responses": { From c709fa17a77eae391cafbe72d6b2594f74d86a60 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Mon, 13 Mar 2023 21:28:39 +0100 Subject: [PATCH 02/11] Add Swift package registry (#22404) This PR adds a [Swift](https://www.swift.org/) package registry. ![grafik](https://user-images.githubusercontent.com/1666336/211842523-07521cbd-8fb6-400f-820c-ee8048b05ae8.png) --- custom/conf/app.example.ini | 2 + .../doc/advanced/config-cheat-sheet.en-us.md | 1 + docs/content/doc/packages/overview.en-us.md | 1 + docs/content/doc/packages/swift.en-us.md | 93 ++++ models/packages/descriptor.go | 3 + models/packages/package.go | 6 + modules/packages/swift/metadata.go | 214 ++++++++ modules/packages/swift/metadata_test.go | 144 ++++++ modules/setting/packages.go | 2 + options/locale/locale_en-US.ini | 4 + public/img/svg/gitea-swift.svg | 1 + routers/api/packages/api.go | 36 ++ routers/api/packages/swift/swift.go | 464 ++++++++++++++++++ routers/api/v1/packages/package.go | 2 +- services/forms/package_form.go | 2 +- services/packages/packages.go | 2 + templates/package/content/swift.tmpl | 40 ++ templates/package/metadata/swift.tmpl | 4 + templates/package/view.tmpl | 2 + templates/swagger/v1_json.tmpl | 1 + tests/integration/api_packages_swift_test.go | 326 ++++++++++++ web_src/svg/gitea-swift.svg | 5 + 22 files changed, 1353 insertions(+), 2 deletions(-) create mode 100644 docs/content/doc/packages/swift.en-us.md create mode 100644 modules/packages/swift/metadata.go create mode 100644 modules/packages/swift/metadata_test.go create mode 100644 public/img/svg/gitea-swift.svg create mode 100644 routers/api/packages/swift/swift.go create mode 100644 templates/package/content/swift.tmpl create mode 100644 templates/package/metadata/swift.tmpl create mode 100644 tests/integration/api_packages_swift_test.go create mode 100644 web_src/svg/gitea-swift.svg diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index b2b5af0af8e0..e53ed7ad9fd8 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -2516,6 +2516,8 @@ ROUTER = console ;LIMIT_SIZE_PYPI = -1 ;; Maximum size of a RubyGems upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) ;LIMIT_SIZE_RUBYGEMS = -1 +;; Maximum size of a Swift upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) +;LIMIT_SIZE_SWIFT = -1 ;; Maximum size of a Vagrant upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) ;LIMIT_SIZE_VAGRANT = -1 diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index b67d6cdf5f68..4b9c519cd80d 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -1254,6 +1254,7 @@ Task queue configuration has been moved to `queue.task`. However, the below conf - `LIMIT_SIZE_PUB`: **-1**: Maximum size of a Pub upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) - `LIMIT_SIZE_PYPI`: **-1**: Maximum size of a PyPI upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) - `LIMIT_SIZE_RUBYGEMS`: **-1**: Maximum size of a RubyGems upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) +- `LIMIT_SIZE_SWIFT`: **-1**: Maximum size of a Swift upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) - `LIMIT_SIZE_VAGRANT`: **-1**: Maximum size of a Vagrant upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) ## Mirror (`mirror`) diff --git a/docs/content/doc/packages/overview.en-us.md b/docs/content/doc/packages/overview.en-us.md index f93fec639349..08da8ced4844 100644 --- a/docs/content/doc/packages/overview.en-us.md +++ b/docs/content/doc/packages/overview.en-us.md @@ -40,6 +40,7 @@ The following package managers are currently supported: | [Pub]({{< relref "doc/packages/pub.en-us.md" >}}) | Dart | `dart`, `flutter` | | [PyPI]({{< relref "doc/packages/pypi.en-us.md" >}}) | Python | `pip`, `twine` | | [RubyGems]({{< relref "doc/packages/rubygems.en-us.md" >}}) | Ruby | `gem`, `Bundler` | +| [Swift]({{< relref "doc/packages/rubygems.en-us.md" >}}) | Swift | `swift` | | [Vagrant]({{< relref "doc/packages/vagrant.en-us.md" >}}) | - | `vagrant` | **The following paragraphs only apply if Packages are not globally disabled!** diff --git a/docs/content/doc/packages/swift.en-us.md b/docs/content/doc/packages/swift.en-us.md new file mode 100644 index 000000000000..61a4c9a55d42 --- /dev/null +++ b/docs/content/doc/packages/swift.en-us.md @@ -0,0 +1,93 @@ +--- +date: "2023-01-10T00:00:00+00:00" +title: "Swift Packages Repository" +slug: "packages/swift" +draft: false +toc: false +menu: + sidebar: + parent: "packages" + name: "Swift" + weight: 95 + identifier: "swift" +--- + +# Swift Packages Repository + +Publish [Swift](hhttps://www.swift.org/) packages for your user or organization. + +**Table of Contents** + +{{< toc >}} + +## Requirements + +To work with the Swift package registry, you need to use [swift](https://www.swift.org/getting-started/) to consume and a HTTP client (like `curl`) to publish packages. + +## Configuring the package registry + +To register the package registry and provide credentials, execute: + +```shell +swift package-registry set https://gitea.example.com/api/packages/{owner}/swift -login {username} -password {password} +``` + +| Placeholder | Description | +| ------------ | ----------- | +| `owner` | The owner of the package. | +| `username` | Your Gitea username. | +| `password` | Your Gitea password. If you are using 2FA or OAuth use a [personal access token]({{< relref "doc/developers/api-usage.en-us.md#authentication" >}}) instead of the password. | + +The login is optional and only needed if the package registry is private. + +## Publish a package + +First you have to pack the contents of your package: + +```shell +swift package archive-source +``` + +To publish the package perform a HTTP PUT request with the package content in the request body. + +```shell --user your_username:your_password_or_token \ +curl -X PUT --user {username}:{password} \ + -H "Accept: application/vnd.swift.registry.v1+json" \ + -F source-archive=@/path/to/package.zip \ + -F metadata={metadata} \ + https://gitea.example.com/api/packages/{owner}/swift/{scope}/{name}/{version} +``` + +| Placeholder | Description | +| ----------- | ----------- | +| `username` | Your Gitea username. | +| `password` | Your Gitea password. If you are using 2FA or OAuth use a [personal access token]({{< relref "doc/developers/api-usage.en-us.md#authentication" >}}) instead of the password. | +| `owner` | The owner of the package. | +| `scope` | The package scope. | +| `name` | The package name. | +| `version` | The package version. | +| `metadata` | (Optional) The metadata of the package. JSON encoded subset of https://schema.org/SoftwareSourceCode | + +You cannot publish a package if a package of the same name and version already exists. You must delete the existing package first. + +## Install a package + +To install a Swift package from the package registry, add it in the `Package.swift` file dependencies list: + +``` +dependencies: [ + .package(id: "{scope}.{name}", from:"{version}") +] +``` + +| Parameter | Description | +| ----------- | ----------- | +| `scope` | The package scope. | +| `name` | The package name. | +| `version` | The package version. | + +Afterwards execute the following command to install it: + +```shell +swift package resolve +``` diff --git a/models/packages/descriptor.go b/models/packages/descriptor.go index f4be21e74e20..06699b5d572b 100644 --- a/models/packages/descriptor.go +++ b/models/packages/descriptor.go @@ -24,6 +24,7 @@ import ( "code.gitea.io/gitea/modules/packages/pub" "code.gitea.io/gitea/modules/packages/pypi" "code.gitea.io/gitea/modules/packages/rubygems" + "code.gitea.io/gitea/modules/packages/swift" "code.gitea.io/gitea/modules/packages/vagrant" "github.com/hashicorp/go-version" @@ -159,6 +160,8 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc metadata = &pypi.Metadata{} case TypeRubyGems: metadata = &rubygems.Metadata{} + case TypeSwift: + metadata = &swift.Metadata{} case TypeVagrant: metadata = &vagrant.Metadata{} default: diff --git a/models/packages/package.go b/models/packages/package.go index 32f30fab9b40..ccc9257c3123 100644 --- a/models/packages/package.go +++ b/models/packages/package.go @@ -44,6 +44,7 @@ const ( TypePub Type = "pub" TypePyPI Type = "pypi" TypeRubyGems Type = "rubygems" + TypeSwift Type = "swift" TypeVagrant Type = "vagrant" ) @@ -62,6 +63,7 @@ var TypeList = []Type{ TypePub, TypePyPI, TypeRubyGems, + TypeSwift, TypeVagrant, } @@ -96,6 +98,8 @@ func (pt Type) Name() string { return "PyPI" case TypeRubyGems: return "RubyGems" + case TypeSwift: + return "Swift" case TypeVagrant: return "Vagrant" } @@ -133,6 +137,8 @@ func (pt Type) SVGName() string { return "gitea-python" case TypeRubyGems: return "gitea-rubygems" + case TypeSwift: + return "gitea-swift" case TypeVagrant: return "gitea-vagrant" } diff --git a/modules/packages/swift/metadata.go b/modules/packages/swift/metadata.go new file mode 100644 index 000000000000..24c4262ab724 --- /dev/null +++ b/modules/packages/swift/metadata.go @@ -0,0 +1,214 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package swift + +import ( + "archive/zip" + "fmt" + "io" + "path" + "regexp" + "strings" + + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/validation" + + "github.com/hashicorp/go-version" +) + +var ( + ErrMissingManifestFile = util.NewInvalidArgumentErrorf("Package.swift file is missing") + ErrManifestFileTooLarge = util.NewInvalidArgumentErrorf("Package.swift file is too large") + ErrInvalidManifestVersion = util.NewInvalidArgumentErrorf("manifest version is invalid") + + manifestPattern = regexp.MustCompile(`\APackage(?:@swift-(\d+(?:\.\d+)?(?:\.\d+)?))?\.swift\z`) + toolsVersionPattern = regexp.MustCompile(`\A// swift-tools-version:(\d+(?:\.\d+)?(?:\.\d+)?)`) +) + +const ( + maxManifestFileSize = 128 * 1024 + + PropertyScope = "swift.scope" + PropertyName = "swift.name" + PropertyRepositoryURL = "swift.repository_url" +) + +// Package represents a Swift package +type Package struct { + RepositoryURLs []string + Metadata *Metadata +} + +// Metadata represents the metadata of a Swift package +type Metadata struct { + Description string `json:"description,omitempty"` + Keywords []string `json:"keywords,omitempty"` + RepositoryURL string `json:"repository_url,omitempty"` + License string `json:"license,omitempty"` + Author Person `json:"author,omitempty"` + Manifests map[string]*Manifest `json:"manifests,omitempty"` +} + +// Manifest represents a Package.swift file +type Manifest struct { + Content string `json:"content"` + ToolsVersion string `json:"tools_version,omitempty"` +} + +// https://schema.org/SoftwareSourceCode +type SoftwareSourceCode struct { + Context []string `json:"@context"` + Type string `json:"@type"` + Name string `json:"name"` + Version string `json:"version"` + Description string `json:"description,omitempty"` + Keywords []string `json:"keywords,omitempty"` + CodeRepository string `json:"codeRepository,omitempty"` + License string `json:"license,omitempty"` + Author Person `json:"author"` + ProgrammingLanguage ProgrammingLanguage `json:"programmingLanguage"` + RepositoryURLs []string `json:"repositoryURLs,omitempty"` +} + +// https://schema.org/ProgrammingLanguage +type ProgrammingLanguage struct { + Type string `json:"@type"` + Name string `json:"name"` + URL string `json:"url"` +} + +// https://schema.org/Person +type Person struct { + Type string `json:"@type,omitempty"` + GivenName string `json:"givenName,omitempty"` + MiddleName string `json:"middleName,omitempty"` + FamilyName string `json:"familyName,omitempty"` +} + +func (p Person) String() string { + var sb strings.Builder + if p.GivenName != "" { + sb.WriteString(p.GivenName) + } + if p.MiddleName != "" { + if sb.Len() > 0 { + sb.WriteRune(' ') + } + sb.WriteString(p.MiddleName) + } + if p.FamilyName != "" { + if sb.Len() > 0 { + sb.WriteRune(' ') + } + sb.WriteString(p.FamilyName) + } + return sb.String() +} + +// ParsePackage parses the Swift package upload +func ParsePackage(sr io.ReaderAt, size int64, mr io.Reader) (*Package, error) { + zr, err := zip.NewReader(sr, size) + if err != nil { + return nil, err + } + + p := &Package{ + Metadata: &Metadata{ + Manifests: make(map[string]*Manifest), + }, + } + + for _, file := range zr.File { + manifestMatch := manifestPattern.FindStringSubmatch(path.Base(file.Name)) + if len(manifestMatch) == 0 { + continue + } + + if file.UncompressedSize64 > maxManifestFileSize { + return nil, ErrManifestFileTooLarge + } + + f, err := zr.Open(file.Name) + if err != nil { + return nil, err + } + + content, err := io.ReadAll(f) + + if err := f.Close(); err != nil { + return nil, err + } + + if err != nil { + return nil, err + } + + swiftVersion := "" + if len(manifestMatch) == 2 && manifestMatch[1] != "" { + v, err := version.NewSemver(manifestMatch[1]) + if err != nil { + return nil, ErrInvalidManifestVersion + } + swiftVersion = TrimmedVersionString(v) + } + + manifest := &Manifest{ + Content: string(content), + } + + toolsMatch := toolsVersionPattern.FindStringSubmatch(manifest.Content) + if len(toolsMatch) == 2 { + v, err := version.NewSemver(toolsMatch[1]) + if err != nil { + return nil, ErrInvalidManifestVersion + } + + manifest.ToolsVersion = TrimmedVersionString(v) + } + + p.Metadata.Manifests[swiftVersion] = manifest + } + + if _, found := p.Metadata.Manifests[""]; !found { + return nil, ErrMissingManifestFile + } + + if mr != nil { + var ssc *SoftwareSourceCode + if err := json.NewDecoder(mr).Decode(&ssc); err != nil { + return nil, err + } + + p.Metadata.Description = ssc.Description + p.Metadata.Keywords = ssc.Keywords + p.Metadata.License = ssc.License + p.Metadata.Author = Person{ + GivenName: ssc.Author.GivenName, + MiddleName: ssc.Author.MiddleName, + FamilyName: ssc.Author.FamilyName, + } + + p.Metadata.RepositoryURL = ssc.CodeRepository + if !validation.IsValidURL(p.Metadata.RepositoryURL) { + p.Metadata.RepositoryURL = "" + } + + p.RepositoryURLs = ssc.RepositoryURLs + } + + return p, nil +} + +// TrimmedVersionString returns the version string without the patch segment if it is zero +func TrimmedVersionString(v *version.Version) string { + segments := v.Segments64() + + var b strings.Builder + fmt.Fprintf(&b, "%d.%d", segments[0], segments[1]) + if segments[2] != 0 { + fmt.Fprintf(&b, ".%d", segments[2]) + } + return b.String() +} diff --git a/modules/packages/swift/metadata_test.go b/modules/packages/swift/metadata_test.go new file mode 100644 index 000000000000..3913c2355ba2 --- /dev/null +++ b/modules/packages/swift/metadata_test.go @@ -0,0 +1,144 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package swift + +import ( + "archive/zip" + "bytes" + "strings" + "testing" + + "github.com/hashicorp/go-version" + "github.com/stretchr/testify/assert" +) + +const ( + packageName = "gitea" + packageVersion = "1.0.1" + packageDescription = "Package Description" + packageRepositoryURL = "https://gitea.io/gitea/gitea" + packageAuthor = "KN4CK3R" + packageLicense = "MIT" +) + +func TestParsePackage(t *testing.T) { + createArchive := func(files map[string][]byte) *bytes.Reader { + var buf bytes.Buffer + zw := zip.NewWriter(&buf) + for filename, content := range files { + w, _ := zw.Create(filename) + w.Write(content) + } + zw.Close() + return bytes.NewReader(buf.Bytes()) + } + + t.Run("MissingManifestFile", func(t *testing.T) { + data := createArchive(map[string][]byte{"dummy.txt": {}}) + + p, err := ParsePackage(data, data.Size(), nil) + assert.Nil(t, p) + assert.ErrorIs(t, err, ErrMissingManifestFile) + }) + + t.Run("ManifestFileTooLarge", func(t *testing.T) { + data := createArchive(map[string][]byte{ + "Package.swift": make([]byte, maxManifestFileSize+1), + }) + + p, err := ParsePackage(data, data.Size(), nil) + assert.Nil(t, p) + assert.ErrorIs(t, err, ErrManifestFileTooLarge) + }) + + t.Run("WithoutMetadata", func(t *testing.T) { + content1 := "// swift-tools-version:5.7\n//\n// Package.swift" + content2 := "// swift-tools-version:5.6\n//\n// Package@swift-5.6.swift" + + data := createArchive(map[string][]byte{ + "Package.swift": []byte(content1), + "Package@swift-5.5.swift": []byte(content2), + }) + + p, err := ParsePackage(data, data.Size(), nil) + assert.NotNil(t, p) + assert.NoError(t, err) + + assert.NotNil(t, p.Metadata) + assert.Empty(t, p.RepositoryURLs) + assert.Len(t, p.Metadata.Manifests, 2) + m := p.Metadata.Manifests[""] + assert.Equal(t, "5.7", m.ToolsVersion) + assert.Equal(t, content1, m.Content) + m = p.Metadata.Manifests["5.5"] + assert.Equal(t, "5.6", m.ToolsVersion) + assert.Equal(t, content2, m.Content) + }) + + t.Run("WithMetadata", func(t *testing.T) { + data := createArchive(map[string][]byte{ + "Package.swift": []byte("// swift-tools-version:5.7\n//\n// Package.swift"), + }) + + p, err := ParsePackage( + data, + data.Size(), + strings.NewReader(`{"name":"`+packageName+`","version":"`+packageVersion+`","description":"`+packageDescription+`","keywords":["swift","package"],"license":"`+packageLicense+`","codeRepository":"`+packageRepositoryURL+`","author":{"givenName":"`+packageAuthor+`"},"repositoryURLs":["`+packageRepositoryURL+`"]}`), + ) + assert.NotNil(t, p) + assert.NoError(t, err) + + assert.NotNil(t, p.Metadata) + assert.Len(t, p.Metadata.Manifests, 1) + m := p.Metadata.Manifests[""] + assert.Equal(t, "5.7", m.ToolsVersion) + + assert.Equal(t, packageDescription, p.Metadata.Description) + assert.ElementsMatch(t, []string{"swift", "package"}, p.Metadata.Keywords) + assert.Equal(t, packageLicense, p.Metadata.License) + assert.Equal(t, packageAuthor, p.Metadata.Author.GivenName) + assert.Equal(t, packageRepositoryURL, p.Metadata.RepositoryURL) + assert.ElementsMatch(t, []string{packageRepositoryURL}, p.RepositoryURLs) + }) +} + +func TestTrimmedVersionString(t *testing.T) { + cases := []struct { + Version *version.Version + Expected string + }{ + { + Version: version.Must(version.NewVersion("1")), + Expected: "1.0", + }, + { + Version: version.Must(version.NewVersion("1.0")), + Expected: "1.0", + }, + { + Version: version.Must(version.NewVersion("1.0.0")), + Expected: "1.0", + }, + { + Version: version.Must(version.NewVersion("1.0.1")), + Expected: "1.0.1", + }, + { + Version: version.Must(version.NewVersion("1.0+meta")), + Expected: "1.0", + }, + { + Version: version.Must(version.NewVersion("1.0.0+meta")), + Expected: "1.0", + }, + { + Version: version.Must(version.NewVersion("1.0.1+meta")), + Expected: "1.0.1", + }, + } + + for _, c := range cases { + assert.Equal(t, c.Expected, TrimmedVersionString(c.Version)) + } +} diff --git a/modules/setting/packages.go b/modules/setting/packages.go index 13599e5a63e7..ac0ad62bca3d 100644 --- a/modules/setting/packages.go +++ b/modules/setting/packages.go @@ -39,6 +39,7 @@ var ( LimitSizePub int64 LimitSizePyPI int64 LimitSizeRubyGems int64 + LimitSizeSwift int64 LimitSizeVagrant int64 }{ Enabled: true, @@ -81,6 +82,7 @@ func loadPackagesFrom(rootCfg ConfigProvider) { Packages.LimitSizePub = mustBytes(sec, "LIMIT_SIZE_PUB") Packages.LimitSizePyPI = mustBytes(sec, "LIMIT_SIZE_PYPI") Packages.LimitSizeRubyGems = mustBytes(sec, "LIMIT_SIZE_RUBYGEMS") + Packages.LimitSizeSwift = mustBytes(sec, "LIMIT_SIZE_SWIFT") Packages.LimitSizeVagrant = mustBytes(sec, "LIMIT_SIZE_VAGRANT") } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 677af1397ddb..e793c3ef0331 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3239,6 +3239,10 @@ rubygems.dependencies.development = Development Dependencies rubygems.required.ruby = Requires Ruby version rubygems.required.rubygems = Requires RubyGem version rubygems.documentation = For more information on the RubyGems registry, see the documentation. +swift.registry = Setup this registry from the command line: +swift.install = Add the package in your Package.swift file: +swift.install2 = and run the following command: +swift.documentation = For more information on the Swift registry, see the documentation. vagrant.install = To add a Vagrant box, run the following command: vagrant.documentation = For more information on the Vagrant registry, see the documentation. settings.link = Link this package to a repository diff --git a/public/img/svg/gitea-swift.svg b/public/img/svg/gitea-swift.svg new file mode 100644 index 000000000000..ebfea951da33 --- /dev/null +++ b/public/img/svg/gitea-swift.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 0e3d8b7a02d7..c0c7b117f696 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -28,6 +28,7 @@ import ( "code.gitea.io/gitea/routers/api/packages/pub" "code.gitea.io/gitea/routers/api/packages/pypi" "code.gitea.io/gitea/routers/api/packages/rubygems" + "code.gitea.io/gitea/routers/api/packages/swift" "code.gitea.io/gitea/routers/api/packages/vagrant" "code.gitea.io/gitea/services/auth" context_service "code.gitea.io/gitea/services/context" @@ -375,6 +376,41 @@ func CommonRoutes(ctx gocontext.Context) *web.Route { r.Delete("/yank", rubygems.DeletePackage) }, reqPackageAccess(perm.AccessModeWrite)) }, reqPackageAccess(perm.AccessModeRead)) + r.Group("/swift", func() { + r.Group("/{scope}/{name}", func() { + r.Group("", func() { + r.Get("", swift.EnumeratePackageVersions) + r.Get(".json", swift.EnumeratePackageVersions) + }, swift.CheckAcceptMediaType(swift.AcceptJSON)) + r.Group("/{version}", func() { + r.Get("/Package.swift", swift.CheckAcceptMediaType(swift.AcceptSwift), swift.DownloadManifest) + r.Put("", reqPackageAccess(perm.AccessModeWrite), swift.CheckAcceptMediaType(swift.AcceptJSON), swift.UploadPackageFile) + r.Get("", func(ctx *context.Context) { + // Can't use normal routes here: https://github.com/go-chi/chi/issues/781 + + version := ctx.Params("version") + if strings.HasSuffix(version, ".zip") { + swift.CheckAcceptMediaType(swift.AcceptZip)(ctx) + if ctx.Written() { + return + } + ctx.SetParams("version", version[:len(version)-4]) + swift.DownloadPackageFile(ctx) + } else { + swift.CheckAcceptMediaType(swift.AcceptJSON)(ctx) + if ctx.Written() { + return + } + if strings.HasSuffix(version, ".json") { + ctx.SetParams("version", version[:len(version)-5]) + } + swift.PackageVersionMetadata(ctx) + } + }) + }) + }) + r.Get("/identifiers", swift.CheckAcceptMediaType(swift.AcceptJSON), swift.LookupPackageIdentifiers) + }, reqPackageAccess(perm.AccessModeRead)) r.Group("/vagrant", func() { r.Group("/authenticate", func() { r.Get("", vagrant.CheckAuthenticate) diff --git a/routers/api/packages/swift/swift.go b/routers/api/packages/swift/swift.go new file mode 100644 index 000000000000..f78f703778ba --- /dev/null +++ b/routers/api/packages/swift/swift.go @@ -0,0 +1,464 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package swift + +import ( + "errors" + "fmt" + "io" + "net/http" + "regexp" + "sort" + "strings" + + packages_model "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/log" + packages_module "code.gitea.io/gitea/modules/packages" + swift_module "code.gitea.io/gitea/modules/packages/swift" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/routers/api/packages/helper" + packages_service "code.gitea.io/gitea/services/packages" + + "github.com/hashicorp/go-version" +) + +// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#35-api-versioning +const ( + AcceptJSON = "application/vnd.swift.registry.v1+json" + AcceptSwift = "application/vnd.swift.registry.v1+swift" + AcceptZip = "application/vnd.swift.registry.v1+zip" +) + +var ( + // https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#361-package-scope + scopePattern = regexp.MustCompile(`\A[a-zA-Z0-9][a-zA-Z0-9-]{0,38}\z`) + // https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#362-package-name + namePattern = regexp.MustCompile(`\A[a-zA-Z0-9][a-zA-Z0-9-_]{0,99}\z`) +) + +type headers struct { + Status int + ContentType string + Digest string + Location string + Link string +} + +// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#35-api-versioning +func setResponseHeaders(resp http.ResponseWriter, h *headers) { + if h.ContentType != "" { + resp.Header().Set("Content-Type", h.ContentType) + } + if h.Digest != "" { + resp.Header().Set("Digest", "sha256="+h.Digest) + } + if h.Location != "" { + resp.Header().Set("Location", h.Location) + } + if h.Link != "" { + resp.Header().Set("Link", h.Link) + } + resp.Header().Set("Content-Version", "1") + if h.Status != 0 { + resp.WriteHeader(h.Status) + } +} + +// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#33-error-handling +func apiError(ctx *context.Context, status int, obj interface{}) { + // https://www.rfc-editor.org/rfc/rfc7807 + type Problem struct { + Status int `json:"status"` + Detail string `json:"detail"` + } + + helper.LogAndProcessError(ctx, status, obj, func(message string) { + setResponseHeaders(ctx.Resp, &headers{ + Status: status, + ContentType: "application/problem+json", + }) + if err := json.NewEncoder(ctx.Resp).Encode(Problem{ + Status: status, + Detail: message, + }); err != nil { + log.Error("JSON encode: %v", err) + } + }) +} + +// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#35-api-versioning +func CheckAcceptMediaType(requiredAcceptHeader string) func(ctx *context.Context) { + return func(ctx *context.Context) { + accept := ctx.Req.Header.Get("Accept") + if accept != "" && accept != requiredAcceptHeader { + apiError(ctx, http.StatusBadRequest, fmt.Sprintf("Unexpected accept header. Should be '%s'.", requiredAcceptHeader)) + } + } +} + +func buildPackageID(scope, name string) string { + return scope + "." + name +} + +type Release struct { + URL string `json:"url"` +} + +type EnumeratePackageVersionsResponse struct { + Releases map[string]Release `json:"releases"` +} + +// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#41-list-package-releases +func EnumeratePackageVersions(ctx *context.Context) { + packageScope := ctx.Params("scope") + packageName := ctx.Params("name") + + pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeSwift, buildPackageID(packageScope, packageName)) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + if len(pvs) == 0 { + apiError(ctx, http.StatusNotFound, nil) + return + } + + pds, err := packages_model.GetPackageDescriptors(ctx, pvs) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + sort.Slice(pds, func(i, j int) bool { + return pds[i].SemVer.LessThan(pds[j].SemVer) + }) + + baseURL := fmt.Sprintf("%sapi/packages/%s/swift/%s/%s/", setting.AppURL, ctx.Package.Owner.LowerName, packageScope, packageName) + + releases := make(map[string]Release) + for _, pd := range pds { + version := pd.SemVer.String() + releases[version] = Release{ + URL: baseURL + version, + } + } + + setResponseHeaders(ctx.Resp, &headers{ + Link: fmt.Sprintf(`<%s%s>; rel="latest-version"`, baseURL, pds[len(pds)-1].Version.Version), + }) + + ctx.JSON(http.StatusOK, EnumeratePackageVersionsResponse{ + Releases: releases, + }) +} + +type Resource struct { + Name string `json:"id"` + Type string `json:"type"` + Checksum string `json:"checksum"` +} + +type PackageVersionMetadataResponse struct { + ID string `json:"id"` + Version string `json:"version"` + Resources []Resource `json:"resources"` + Metadata *swift_module.SoftwareSourceCode `json:"metadata"` +} + +// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#endpoint-2 +func PackageVersionMetadata(ctx *context.Context) { + id := buildPackageID(ctx.Params("scope"), ctx.Params("name")) + + pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeSwift, id, ctx.Params("version")) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + apiError(ctx, http.StatusNotFound, err) + } else { + apiError(ctx, http.StatusInternalServerError, err) + } + return + } + + pd, err := packages_model.GetPackageDescriptor(ctx, pv) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + metadata := pd.Metadata.(*swift_module.Metadata) + + setResponseHeaders(ctx.Resp, &headers{}) + + ctx.JSON(http.StatusOK, PackageVersionMetadataResponse{ + ID: id, + Version: pd.Version.Version, + Resources: []Resource{ + { + Name: "source-archive", + Type: "application/zip", + Checksum: pd.Files[0].Blob.HashSHA256, + }, + }, + Metadata: &swift_module.SoftwareSourceCode{ + Context: []string{"http://schema.org/"}, + Type: "SoftwareSourceCode", + Name: pd.PackageProperties.GetByName(swift_module.PropertyName), + Version: pd.Version.Version, + Description: metadata.Description, + Keywords: metadata.Keywords, + CodeRepository: metadata.RepositoryURL, + License: metadata.License, + ProgrammingLanguage: swift_module.ProgrammingLanguage{ + Type: "ComputerLanguage", + Name: "Swift", + URL: "https://swift.org", + }, + Author: swift_module.Person{ + Type: "Person", + GivenName: metadata.Author.GivenName, + MiddleName: metadata.Author.MiddleName, + FamilyName: metadata.Author.FamilyName, + }, + }, + }) +} + +// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#43-fetch-manifest-for-a-package-release +func DownloadManifest(ctx *context.Context) { + packageScope := ctx.Params("scope") + packageName := ctx.Params("name") + packageVersion := ctx.Params("version") + + pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeSwift, buildPackageID(packageScope, packageName), packageVersion) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + apiError(ctx, http.StatusNotFound, err) + } else { + apiError(ctx, http.StatusInternalServerError, err) + } + return + } + + pd, err := packages_model.GetPackageDescriptor(ctx, pv) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + swiftVersion := ctx.FormTrim("swift-version") + if swiftVersion != "" { + v, err := version.NewVersion(swiftVersion) + if err == nil { + swiftVersion = swift_module.TrimmedVersionString(v) + } + } + m, ok := pd.Metadata.(*swift_module.Metadata).Manifests[swiftVersion] + if !ok { + setResponseHeaders(ctx.Resp, &headers{ + Status: http.StatusSeeOther, + Location: fmt.Sprintf("%sapi/packages/%s/swift/%s/%s/%s/Package.swift", setting.AppURL, ctx.Package.Owner.LowerName, packageScope, packageName, packageVersion), + }) + return + } + + setResponseHeaders(ctx.Resp, &headers{}) + + filename := "Package.swift" + if swiftVersion != "" { + filename = fmt.Sprintf("Package@swift-%s.swift", swiftVersion) + } + + ctx.ServeContent(strings.NewReader(m.Content), &context.ServeHeaderOptions{ + ContentType: "text/x-swift", + Filename: filename, + LastModified: pv.CreatedUnix.AsLocalTime(), + }) +} + +// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#endpoint-6 +func UploadPackageFile(ctx *context.Context) { + packageScope := ctx.Params("scope") + packageName := ctx.Params("name") + + v, err := version.NewVersion(ctx.Params("version")) + + if !scopePattern.MatchString(packageScope) || !namePattern.MatchString(packageName) || err != nil { + apiError(ctx, http.StatusBadRequest, err) + return + } + + packageVersion := v.Core().String() + + file, _, err := ctx.Req.FormFile("source-archive") + if err != nil { + apiError(ctx, http.StatusBadRequest, err) + return + } + defer file.Close() + + buf, err := packages_module.CreateHashedBufferFromReader(file, 32*1024*1024) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + defer buf.Close() + + var mr io.Reader + metadata := ctx.Req.FormValue("metadata") + if metadata != "" { + mr = strings.NewReader(metadata) + } + + pck, err := swift_module.ParsePackage(buf, buf.Size(), mr) + if err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + 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 + } + + pv, _, err := packages_service.CreatePackageAndAddFile( + &packages_service.PackageCreationInfo{ + PackageInfo: packages_service.PackageInfo{ + Owner: ctx.Package.Owner, + PackageType: packages_model.TypeSwift, + Name: buildPackageID(packageScope, packageName), + Version: packageVersion, + }, + SemverCompatible: true, + Creator: ctx.Doer, + Metadata: pck.Metadata, + PackageProperties: map[string]string{ + swift_module.PropertyScope: packageScope, + swift_module.PropertyName: packageName, + }, + }, + &packages_service.PackageFileCreationInfo{ + PackageFileInfo: packages_service.PackageFileInfo{ + Filename: fmt.Sprintf("%s-%s.zip", packageName, packageVersion), + }, + Creator: ctx.Doer, + Data: buf, + IsLead: true, + }, + ) + if err != nil { + switch err { + case packages_model.ErrDuplicatePackageVersion: + 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 + } + + for _, url := range pck.RepositoryURLs { + _, err = packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, swift_module.PropertyRepositoryURL, url) + if err != nil { + log.Error("InsertProperty failed: %v", err) + } + } + + setResponseHeaders(ctx.Resp, &headers{}) + + ctx.Status(http.StatusCreated) +} + +// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#endpoint-4 +func DownloadPackageFile(ctx *context.Context) { + pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeSwift, buildPackageID(ctx.Params("scope"), ctx.Params("name")), ctx.Params("version")) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + apiError(ctx, http.StatusNotFound, err) + } else { + apiError(ctx, http.StatusInternalServerError, err) + } + return + } + + pd, err := packages_model.GetPackageDescriptor(ctx, pv) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + pf := pd.Files[0].File + + s, _, err := packages_service.GetPackageFileStream(ctx, pf) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + defer s.Close() + + setResponseHeaders(ctx.Resp, &headers{ + Digest: pd.Files[0].Blob.HashSHA256, + }) + + ctx.ServeContent(s, &context.ServeHeaderOptions{ + Filename: pf.Name, + ContentType: "application/zip", + LastModified: pf.CreatedUnix.AsLocalTime(), + }) +} + +type LookupPackageIdentifiersResponse struct { + Identifiers []string `json:"identifiers"` +} + +// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#endpoint-5 +func LookupPackageIdentifiers(ctx *context.Context) { + url := ctx.FormTrim("url") + if url == "" { + apiError(ctx, http.StatusBadRequest, nil) + return + } + + pvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{ + OwnerID: ctx.Package.Owner.ID, + Type: packages_model.TypeSwift, + Properties: map[string]string{ + swift_module.PropertyRepositoryURL: url, + }, + IsInternal: util.OptionalBoolFalse, + }) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + if len(pvs) == 0 { + apiError(ctx, http.StatusNotFound, nil) + return + } + + pds, err := packages_model.GetPackageDescriptors(ctx, pvs) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + identifiers := make([]string, 0, len(pds)) + for _, pd := range pds { + identifiers = append(identifiers, pd.Package.Name) + } + + setResponseHeaders(ctx.Resp, &headers{}) + + ctx.JSON(http.StatusOK, LookupPackageIdentifiersResponse{ + Identifiers: identifiers, + }) +} diff --git a/routers/api/v1/packages/package.go b/routers/api/v1/packages/package.go index ab077090d1c7..200dc5aaf140 100644 --- a/routers/api/v1/packages/package.go +++ b/routers/api/v1/packages/package.go @@ -40,7 +40,7 @@ func ListPackages(ctx *context.APIContext) { // in: query // description: package type filter // type: string - // enum: [cargo, chef, composer, conan, conda, container, generic, helm, maven, npm, nuget, pub, pypi, rubygems, vagrant] + // enum: [cargo, chef, composer, conan, conda, container, generic, helm, maven, npm, nuget, pub, pypi, rubygems, swift, vagrant] // - name: q // in: query // description: name filter diff --git a/services/forms/package_form.go b/services/forms/package_form.go index b22ed47c775a..699d0fe44f96 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(cargo,chef,composer,conan,conda,container,generic,helm,maven,npm,nuget,pub,pypi,rubygems,vagrant)"` + Type string `binding:"Required;In(cargo,chef,composer,conan,conda,container,generic,helm,maven,npm,nuget,pub,pypi,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/packages/packages.go b/services/packages/packages.go index 3abca7337c7d..dd5c63470b8b 100644 --- a/services/packages/packages.go +++ b/services/packages/packages.go @@ -361,6 +361,8 @@ func CheckSizeQuotaExceeded(ctx context.Context, doer, owner *user_model.User, p typeSpecificSize = setting.Packages.LimitSizePyPI case packages_model.TypeRubyGems: typeSpecificSize = setting.Packages.LimitSizeRubyGems + case packages_model.TypeSwift: + typeSpecificSize = setting.Packages.LimitSizeSwift case packages_model.TypeVagrant: typeSpecificSize = setting.Packages.LimitSizeVagrant } diff --git a/templates/package/content/swift.tmpl b/templates/package/content/swift.tmpl new file mode 100644 index 000000000000..3ff06483b83e --- /dev/null +++ b/templates/package/content/swift.tmpl @@ -0,0 +1,40 @@ +{{if eq .PackageDescriptor.Package.Type "swift"}} +

{{.locale.Tr "packages.installation"}}

+
+
+
+ +
swift package-registry set 
+
+
+ +
dependencies: [
+	.package(id: "{{.PackageDescriptor.Package.Name}}", from:"{{.PackageDescriptor.Version.Version}}")
+]
+
+
+ +
swift package resolve
+
+
+ +
+
+
+ + {{if .PackageDescriptor.Metadata.Description}} +

{{.locale.Tr "packages.about"}}

+
+ {{if .PackageDescriptor.Metadata.Description}}{{.PackageDescriptor.Metadata.Description}}{{end}} +
+ {{end}} + + {{if .PackageDescriptor.Metadata.Keywords}} +

{{.locale.Tr "packages.keywords"}}

+
+ {{range .PackageDescriptor.Metadata.Keywords}} + {{.}} + {{end}} +
+ {{end}} +{{end}} diff --git a/templates/package/metadata/swift.tmpl b/templates/package/metadata/swift.tmpl new file mode 100644 index 000000000000..8a9ab071fc59 --- /dev/null +++ b/templates/package/metadata/swift.tmpl @@ -0,0 +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 "mr-3"}} {{.locale.Tr "packages.details.project_site"}}
{{end}} +{{end}} diff --git a/templates/package/view.tmpl b/templates/package/view.tmpl index 839d9cf21af1..b2a2fb1e5d57 100644 --- a/templates/package/view.tmpl +++ b/templates/package/view.tmpl @@ -33,6 +33,7 @@ {{template "package/content/pub" .}} {{template "package/content/pypi" .}} {{template "package/content/rubygems" .}} + {{template "package/content/swift" .}} {{template "package/content/vagrant" .}}
@@ -59,6 +60,7 @@ {{template "package/metadata/pub" .}} {{template "package/metadata/pypi" .}} {{template "package/metadata/rubygems" .}} + {{template "package/metadata/swift" .}} {{template "package/metadata/vagrant" .}}
{{svg "octicon-database" 16 "gt-mr-3"}} {{FileSize .PackageDescriptor.CalculateBlobSize}}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index b64f6dcd87dc..9c46b25eafc0 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -2120,6 +2120,7 @@ "pub", "pypi", "rubygems", + "swift", "vagrant" ], "type": "string", diff --git a/tests/integration/api_packages_swift_test.go b/tests/integration/api_packages_swift_test.go new file mode 100644 index 000000000000..a3035ea60485 --- /dev/null +++ b/tests/integration/api_packages_swift_test.go @@ -0,0 +1,326 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "archive/zip" + "bytes" + "fmt" + "io" + "mime/multipart" + "net/http" + "strings" + "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" + swift_module "code.gitea.io/gitea/modules/packages/swift" + "code.gitea.io/gitea/modules/setting" + swift_router "code.gitea.io/gitea/routers/api/packages/swift" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func TestPackageSwift(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + packageScope := "test-scope" + packageName := "test_package" + packageID := packageScope + "." + packageName + packageVersion := "1.0.3" + packageAuthor := "KN4CK3R" + packageDescription := "Gitea Test Package" + packageRepositoryURL := "https://gitea.io/gitea/gitea" + contentManifest1 := "// swift-tools-version:5.7\n//\n// Package.swift" + contentManifest2 := "// swift-tools-version:5.6\n//\n// Package@swift-5.6.swift" + + url := fmt.Sprintf("/api/packages/%s/swift", user.Name) + + t.Run("CheckAcceptMediaType", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + for _, sub := range []string{ + "/scope/package", + "/scope/package.json", + "/scope/package/1.0.0", + "/scope/package/1.0.0.json", + "/scope/package/1.0.0.zip", + "/scope/package/1.0.0/Package.swift", + "/identifiers", + } { + req := NewRequest(t, "GET", url+sub) + req.Header.Add("Accept", "application/unknown") + resp := MakeRequest(t, req, http.StatusBadRequest) + + assert.Equal(t, "1", resp.Header().Get("Content-Version")) + assert.Equal(t, "application/problem+json", resp.Header().Get("Content-Type")) + } + + req := NewRequestWithBody(t, "PUT", url+"/scope/package/1.0.0", strings.NewReader("")) + req = AddBasicAuthHeader(req, user.Name) + req.Header.Add("Accept", "application/unknown") + resp := MakeRequest(t, req, http.StatusBadRequest) + + assert.Equal(t, "1", resp.Header().Get("Content-Version")) + assert.Equal(t, "application/problem+json", resp.Header().Get("Content-Type")) + }) + + t.Run("Upload", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + uploadPackage := func(t *testing.T, url string, expectedStatus int, sr io.Reader, metadata string) { + var body bytes.Buffer + mpw := multipart.NewWriter(&body) + + part, _ := mpw.CreateFormFile("source-archive", "source-archive.zip") + io.Copy(part, sr) + + if metadata != "" { + mpw.WriteField("metadata", metadata) + } + + mpw.Close() + + req := NewRequestWithBody(t, "PUT", url, &body) + req.Header.Add("Content-Type", mpw.FormDataContentType()) + req.Header.Add("Accept", swift_router.AcceptJSON) + req = AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, expectedStatus) + } + + createArchive := func(files map[string]string) *bytes.Buffer { + var buf bytes.Buffer + zw := zip.NewWriter(&buf) + for filename, content := range files { + w, _ := zw.Create(filename) + w.Write([]byte(content)) + } + zw.Close() + return &buf + } + + for _, triple := range []string{"/sc_ope/package/1.0.0", "/scope/pack~age/1.0.0", "/scope/package/1_0.0"} { + req := NewRequestWithBody(t, "PUT", url+triple, bytes.NewReader([]byte{})) + req = AddBasicAuthHeader(req, user.Name) + resp := MakeRequest(t, req, http.StatusBadRequest) + + assert.Equal(t, "1", resp.Header().Get("Content-Version")) + assert.Equal(t, "application/problem+json", resp.Header().Get("Content-Type")) + } + + uploadURL := fmt.Sprintf("%s/%s/%s/%s", url, packageScope, packageName, packageVersion) + + req := NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader([]byte{})) + MakeRequest(t, req, http.StatusUnauthorized) + + uploadPackage( + t, + uploadURL, + http.StatusCreated, + createArchive(map[string]string{ + "Package.swift": contentManifest1, + "Package@swift-5.6.swift": contentManifest2, + }), + `{"name":"`+packageName+`","version":"`+packageVersion+`","description":"`+packageDescription+`","codeRepository":"`+packageRepositoryURL+`","author":{"givenName":"`+packageAuthor+`"},"repositoryURLs":["`+packageRepositoryURL+`"]}`, + ) + + pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeSwift) + assert.NoError(t, err) + assert.Len(t, pvs, 1) + + pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0]) + assert.NoError(t, err) + assert.NotNil(t, pd.SemVer) + assert.Equal(t, packageID, pd.Package.Name) + assert.Equal(t, packageVersion, pd.Version.Version) + assert.IsType(t, &swift_module.Metadata{}, pd.Metadata) + metadata := pd.Metadata.(*swift_module.Metadata) + assert.Equal(t, packageDescription, metadata.Description) + assert.Len(t, metadata.Manifests, 2) + assert.Equal(t, contentManifest1, metadata.Manifests[""].Content) + assert.Equal(t, contentManifest2, metadata.Manifests["5.6"].Content) + assert.Len(t, pd.VersionProperties, 1) + assert.Equal(t, packageRepositoryURL, pd.VersionProperties.GetByName(swift_module.PropertyRepositoryURL)) + + pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID) + assert.NoError(t, err) + assert.Len(t, pfs, 1) + assert.Equal(t, fmt.Sprintf("%s-%s.zip", packageName, packageVersion), pfs[0].Name) + assert.True(t, pfs[0].IsLead) + + uploadPackage( + t, + uploadURL, + http.StatusConflict, + createArchive(map[string]string{ + "Package.swift": contentManifest1, + }), + "", + ) + }) + + t.Run("Download", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/%s.zip", url, packageScope, packageName, packageVersion)) + req = AddBasicAuthHeader(req, user.Name) + req.Header.Add("Accept", swift_router.AcceptZip) + resp := MakeRequest(t, req, http.StatusOK) + + assert.Equal(t, "1", resp.Header().Get("Content-Version")) + assert.Equal(t, "application/zip", resp.Header().Get("Content-Type")) + + pv, err := packages.GetVersionByNameAndVersion(db.DefaultContext, user.ID, packages.TypeSwift, packageID, packageVersion) + assert.NotNil(t, pv) + assert.NoError(t, err) + + pd, err := packages.GetPackageDescriptor(db.DefaultContext, pv) + assert.NoError(t, err) + assert.Equal(t, "sha256="+pd.Files[0].Blob.HashSHA256, resp.Header().Get("Digest")) + }) + + t.Run("EnumeratePackageVersions", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s", url, packageScope, packageName)) + req = AddBasicAuthHeader(req, user.Name) + req.Header.Add("Accept", swift_router.AcceptJSON) + resp := MakeRequest(t, req, http.StatusOK) + + versionURL := setting.AppURL + url[1:] + fmt.Sprintf("/%s/%s/%s", packageScope, packageName, packageVersion) + + assert.Equal(t, "1", resp.Header().Get("Content-Version")) + assert.Equal(t, fmt.Sprintf(`<%s>; rel="latest-version"`, versionURL), resp.Header().Get("Link")) + + body := resp.Body.String() + + var result *swift_router.EnumeratePackageVersionsResponse + DecodeJSON(t, resp, &result) + + assert.Len(t, result.Releases, 1) + assert.Contains(t, result.Releases, packageVersion) + assert.Equal(t, versionURL, result.Releases[packageVersion].URL) + + req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s.json", url, packageScope, packageName)) + req = AddBasicAuthHeader(req, user.Name) + resp = MakeRequest(t, req, http.StatusOK) + + assert.Equal(t, body, resp.Body.String()) + }) + + t.Run("PackageVersionMetadata", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/%s", url, packageScope, packageName, packageVersion)) + req = AddBasicAuthHeader(req, user.Name) + req.Header.Add("Accept", swift_router.AcceptJSON) + resp := MakeRequest(t, req, http.StatusOK) + + assert.Equal(t, "1", resp.Header().Get("Content-Version")) + + body := resp.Body.String() + + var result *swift_router.PackageVersionMetadataResponse + DecodeJSON(t, resp, &result) + + pv, err := packages.GetVersionByNameAndVersion(db.DefaultContext, user.ID, packages.TypeSwift, packageID, packageVersion) + assert.NotNil(t, pv) + assert.NoError(t, err) + + pd, err := packages.GetPackageDescriptor(db.DefaultContext, pv) + assert.NoError(t, err) + + assert.Equal(t, packageID, result.ID) + assert.Equal(t, packageVersion, result.Version) + assert.Len(t, result.Resources, 1) + assert.Equal(t, "source-archive", result.Resources[0].Name) + assert.Equal(t, "application/zip", result.Resources[0].Type) + assert.Equal(t, pd.Files[0].Blob.HashSHA256, result.Resources[0].Checksum) + assert.Equal(t, "SoftwareSourceCode", result.Metadata.Type) + assert.Equal(t, packageName, result.Metadata.Name) + assert.Equal(t, packageVersion, result.Metadata.Version) + assert.Equal(t, packageDescription, result.Metadata.Description) + assert.Equal(t, "Swift", result.Metadata.ProgrammingLanguage.Name) + assert.Equal(t, packageAuthor, result.Metadata.Author.GivenName) + + req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/%s.json", url, packageScope, packageName, packageVersion)) + req = AddBasicAuthHeader(req, user.Name) + resp = MakeRequest(t, req, http.StatusOK) + + assert.Equal(t, body, resp.Body.String()) + }) + + t.Run("DownloadManifest", func(t *testing.T) { + manifestURL := fmt.Sprintf("%s/%s/%s/%s/Package.swift", url, packageScope, packageName, packageVersion) + + t.Run("Default", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", manifestURL) + req = AddBasicAuthHeader(req, user.Name) + req.Header.Add("Accept", swift_router.AcceptSwift) + resp := MakeRequest(t, req, http.StatusOK) + + assert.Equal(t, "1", resp.Header().Get("Content-Version")) + assert.Equal(t, "text/x-swift", resp.Header().Get("Content-Type")) + assert.Equal(t, contentManifest1, resp.Body.String()) + }) + + t.Run("DifferentVersion", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", manifestURL+"?swift-version=5.6") + req = AddBasicAuthHeader(req, user.Name) + resp := MakeRequest(t, req, http.StatusOK) + + assert.Equal(t, "1", resp.Header().Get("Content-Version")) + assert.Equal(t, "text/x-swift", resp.Header().Get("Content-Type")) + assert.Equal(t, contentManifest2, resp.Body.String()) + + req = NewRequest(t, "GET", manifestURL+"?swift-version=5.6.0") + req = AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusOK) + }) + + t.Run("Redirect", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", manifestURL+"?swift-version=1.0") + req = AddBasicAuthHeader(req, user.Name) + resp := MakeRequest(t, req, http.StatusSeeOther) + + assert.Equal(t, "1", resp.Header().Get("Content-Version")) + assert.Equal(t, setting.AppURL+url[1:]+fmt.Sprintf("/%s/%s/%s/Package.swift", packageScope, packageName, packageVersion), resp.Header().Get("Location")) + }) + }) + + t.Run("LookupPackageIdentifiers", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", url+"/identifiers") + req.Header.Add("Accept", swift_router.AcceptJSON) + resp := MakeRequest(t, req, http.StatusBadRequest) + + assert.Equal(t, "1", resp.Header().Get("Content-Version")) + assert.Equal(t, "application/problem+json", resp.Header().Get("Content-Type")) + + req = NewRequest(t, "GET", url+"/identifiers?url=https://unknown.host/") + MakeRequest(t, req, http.StatusNotFound) + + req = NewRequest(t, "GET", url+"/identifiers?url="+packageRepositoryURL) + req.Header.Add("Accept", swift_router.AcceptJSON) + resp = MakeRequest(t, req, http.StatusOK) + + var result *swift_router.LookupPackageIdentifiersResponse + DecodeJSON(t, resp, &result) + + assert.Len(t, result.Identifiers, 1) + assert.Equal(t, packageID, result.Identifiers[0]) + }) +} diff --git a/web_src/svg/gitea-swift.svg b/web_src/svg/gitea-swift.svg new file mode 100644 index 000000000000..8af43d32e466 --- /dev/null +++ b/web_src/svg/gitea-swift.svg @@ -0,0 +1,5 @@ + + + + + From 5eea61dbc8f8e82e0dd05addf76751ee517459a0 Mon Sep 17 00:00:00 2001 From: sillyguodong <33891828+sillyguodong@users.noreply.github.com> Date: Tue, 14 Mar 2023 05:05:19 +0800 Subject: [PATCH 03/11] Fix missing commit status in PR which from forked repo (#23351) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit close: #23347 ### Reference and Inference According to Github REST API [doc](https://docs.github.com/en/rest/commits/statuses?apiVersion=2022-11-28#list-commit-statuses-for-a-reference): 1. The `Drone CI` that can create some commit status by [API](https://docs.github.com/en/rest/commits/statuses?apiVersion=2022-11-28#create-a-commit-status) is enabled in `go-gitea/gitea`. So I tried to call the API to get a commit status list of a PR which is commited to upstream repo(`go-gitea/gitea`). As a result, the API returned a array of commit status. ![image](https://user-images.githubusercontent.com/33891828/223913371-313d047a-5e2e-484c-b13e-dcd38748703e.png) 2. Then I tried to call the API to get commit status list of the reference which of the `SHA` is the same as step 1 in the repo which is forked from `go-gitea/gitea`. But I got a empty array. ![image](https://user-images.githubusercontent.com/33891828/223930827-17a64d3c-f466-4980-897c-77fe386c4d3b.png) So, I believe it that: 1. The commit status is not shared between upstream repo and forked repo. 2. The coomit status is bound to a repo that performs actions. (Gitea's logic is the same) ### Cause During debugging, I found it that commit status are not stored in the DB as expected. So, I located the following code: https://github.com/go-gitea/gitea/blob/8cadd51bf295e6ff36ac36efed68cc5de34c9382/services/actions/commit_status.go#L18-L26 When I create a PR, the type of `event` is `pull request`, not `push`. So the code return function directly. ### Screenshot ![image](https://user-images.githubusercontent.com/33891828/223939339-dadf539c-1fdd-40c4-96e9-2e4fa733f531.png) ![image](https://user-images.githubusercontent.com/33891828/223939519-edb02bf0-2478-4ea5-9366-be85468f02db.png) ![image](https://user-images.githubusercontent.com/33891828/223939557-ec6f1375-5536-400e-8987-fb7d2fd452fa.png) ### Other In this PR, I also fix the problem of missing icon which represents running in PRs list. ![image](https://user-images.githubusercontent.com/33891828/223939898-2a0339e4-713f-4c7b-9d99-2250a43f3457.png) ![image](https://user-images.githubusercontent.com/33891828/223939979-037a975f-5ced-480c-bac7-0ee00ebfff4b.png) --- models/actions/run.go | 11 +++++ services/actions/commit_status.go | 71 +++++++++++++++++++++---------- templates/repo/commit_status.tmpl | 3 ++ 3 files changed, 62 insertions(+), 23 deletions(-) diff --git a/models/actions/run.go b/models/actions/run.go index d5ab45a51958..a711cfee2ecd 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -128,6 +128,17 @@ func (run *ActionRun) GetPushEventPayload() (*api.PushPayload, error) { return nil, fmt.Errorf("event %s is not a push event", run.Event) } +func (run *ActionRun) GetPullRequestEventPayload() (*api.PullRequestPayload, error) { + if run.Event == webhook_module.HookEventPullRequest { + var payload api.PullRequestPayload + if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil { + return nil, err + } + return &payload, nil + } + return nil, fmt.Errorf("event %s is not a pull request event", run.Event) +} + func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) error { _, err := db.GetEngine(ctx).ID(repo.ID). SetExpr("num_action_runs", diff --git a/services/actions/commit_status.go b/services/actions/commit_status.go index 4f313493523e..84de106eeca3 100644 --- a/services/actions/commit_status.go +++ b/services/actions/commit_status.go @@ -21,35 +21,60 @@ func CreateCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er } run := job.Run - if run.Event != webhook_module.HookEventPush { - return nil - } + var ( + sha string + creatorID int64 + ) - payload, err := run.GetPushEventPayload() - if err != nil { - return fmt.Errorf("GetPushEventPayload: %w", err) - } + switch run.Event { + case webhook_module.HookEventPush: + payload, err := run.GetPushEventPayload() + if err != nil { + return fmt.Errorf("GetPushEventPayload: %w", err) + } - // Since the payload comes from json data, we should check if it's broken, or it will cause panic - switch { - case payload.Repo == nil: - return fmt.Errorf("repo is missing in event payload") - case payload.Pusher == nil: - return fmt.Errorf("pusher is missing in event payload") - case payload.HeadCommit == nil: - return fmt.Errorf("head commit is missing in event payload") - } + // Since the payload comes from json data, we should check if it's broken, or it will cause panic + switch { + case payload.Repo == nil: + return fmt.Errorf("repo is missing in event payload") + case payload.Pusher == nil: + return fmt.Errorf("pusher is missing in event payload") + case payload.HeadCommit == nil: + return fmt.Errorf("head commit is missing in event payload") + } - creator, err := user_model.GetUserByID(ctx, payload.Pusher.ID) - if err != nil { - return fmt.Errorf("GetUserByID: %w", err) + sha = payload.HeadCommit.ID + creatorID = payload.Pusher.ID + case webhook_module.HookEventPullRequest: + payload, err := run.GetPullRequestEventPayload() + if err != nil { + return fmt.Errorf("GetPullRequestEventPayload: %w", err) + } + + switch { + case payload.PullRequest == nil: + return fmt.Errorf("pull request is missing in event payload") + case payload.PullRequest.Head == nil: + return fmt.Errorf("head of pull request is missing in event payload") + case payload.PullRequest.Head.Repository == nil: + return fmt.Errorf("head repository of pull request is missing in event payload") + case payload.PullRequest.Head.Repository.Owner == nil: + return fmt.Errorf("owner of head repository of pull request is missing in evnt payload") + } + + sha = payload.PullRequest.Head.Sha + creatorID = payload.PullRequest.Head.Repository.Owner.ID + default: + return nil } repo := run.Repo - sha := payload.HeadCommit.ID ctxname := job.Name state := toCommitStatus(job.Status) - + creator, err := user_model.GetUserByID(ctx, creatorID) + if err != nil { + return fmt.Errorf("GetUserByID: %w", err) + } if statuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptions{}); err == nil { for _, v := range statuses { if v.Context == ctxname { @@ -65,14 +90,14 @@ func CreateCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er if err := git_model.NewCommitStatus(ctx, git_model.NewCommitStatusOptions{ Repo: repo, - SHA: payload.HeadCommit.ID, + SHA: sha, Creator: creator, CommitStatus: &git_model.CommitStatus{ SHA: sha, TargetURL: run.Link(), Description: "", Context: ctxname, - CreatorID: payload.Pusher.ID, + CreatorID: creatorID, State: state, }, }); err != nil { diff --git a/templates/repo/commit_status.tmpl b/templates/repo/commit_status.tmpl index fbf064527da4..470869b381c0 100644 --- a/templates/repo/commit_status.tmpl +++ b/templates/repo/commit_status.tmpl @@ -1,6 +1,9 @@ {{if eq .State "pending"}} {{svg "octicon-dot-fill" 18 "commit-status icon text yellow"}} {{end}} +{{if eq .State "running"}} + {{svg "octicon-dot-fill" 18 "commit-status icon text yellow"}} +{{end}} {{if eq .State "success"}} {{svg "octicon-check" 18 "commit-status icon text green"}} {{end}} From 8421b8264fd7715ec93b13f37be31a945faec556 Mon Sep 17 00:00:00 2001 From: Zettat123 Date: Tue, 14 Mar 2023 05:55:30 +0800 Subject: [PATCH 04/11] Handle missing `README` in create repos API (#23387) Close #22934 In `/user/repos` API (and other APIs related to creating repos), user can specify a readme template for auto init. At present, if the specified template does not exist, a `500` will be returned . This PR improved the logic and will return a `400` instead of `500`. --- routers/api/v1/admin/repo.go | 2 ++ routers/api/v1/repo/repo.go | 11 +++++++++++ templates/swagger/v1_json.tmpl | 9 +++++++++ 3 files changed, 22 insertions(+) diff --git a/routers/api/v1/admin/repo.go b/routers/api/v1/admin/repo.go index 83ed06e49bce..a4895f260bec 100644 --- a/routers/api/v1/admin/repo.go +++ b/routers/api/v1/admin/repo.go @@ -32,6 +32,8 @@ func CreateRepo(ctx *context.APIContext) { // responses: // "201": // "$ref": "#/responses/Repository" + // "400": + // "$ref": "#/responses/error" // "403": // "$ref": "#/responses/forbidden" // "404": diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 397600dc50a9..16608e5bbbdb 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -231,6 +231,13 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre if opt.AutoInit && opt.Readme == "" { opt.Readme = "Default" } + + // If the readme template does not exist, a 400 will be returned. + if opt.AutoInit && len(opt.Readme) > 0 && !util.SliceContains(repo_module.Readmes, opt.Readme) { + ctx.Error(http.StatusBadRequest, "", fmt.Errorf("readme template does not exist, available templates: %v", repo_module.Readmes)) + return + } + repo, err := repo_service.CreateRepository(ctx, ctx.Doer, owner, repo_module.CreateRepoOptions{ Name: opt.Name, Description: opt.Description, @@ -283,6 +290,8 @@ func Create(ctx *context.APIContext) { // responses: // "201": // "$ref": "#/responses/Repository" + // "400": + // "$ref": "#/responses/error" // "409": // description: The repository with the same name already exists. // "422": @@ -464,6 +473,8 @@ func CreateOrgRepo(ctx *context.APIContext) { // responses: // "201": // "$ref": "#/responses/Repository" + // "400": + // "$ref": "#/responses/error" // "404": // "$ref": "#/responses/notFound" // "403": diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 9c46b25eafc0..c304a7a4979c 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -713,6 +713,9 @@ "201": { "$ref": "#/responses/Repository" }, + "400": { + "$ref": "#/responses/error" + }, "403": { "$ref": "#/responses/forbidden" }, @@ -1926,6 +1929,9 @@ "201": { "$ref": "#/responses/Repository" }, + "400": { + "$ref": "#/responses/error" + }, "403": { "$ref": "#/responses/forbidden" }, @@ -13382,6 +13388,9 @@ "201": { "$ref": "#/responses/Repository" }, + "400": { + "$ref": "#/responses/error" + }, "409": { "description": "The repository with the same name already exists." }, From 8570593d5526812ee9adc95485a11f51d55575d6 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Mon, 13 Mar 2023 23:15:09 +0100 Subject: [PATCH 05/11] Add package registry architecture overview (#23445) As announced in #22810 I added a readme file to help understanding how the package registry archictecture works and how the go packages are related. --- routers/api/packages/README.md | 50 ++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 routers/api/packages/README.md diff --git a/routers/api/packages/README.md b/routers/api/packages/README.md new file mode 100644 index 000000000000..533a0d32f066 --- /dev/null +++ b/routers/api/packages/README.md @@ -0,0 +1,50 @@ +# Gitea Package Registry + +This document gives a brief overview how the package registry is organized in code. + +## Structure + +The package registry code is divided into multiple modules to split the functionality and make code reuse possible. + +| Module | Description | +| - | - | +| `models/packages` | Common methods and models used by all registry types | +| `models/packages/` | Methods used by specific registry type. There should be no need to use type specific models. | +| `modules/packages` | Common methods and types used by multiple registry types | +| `modules/packages/` | Registry type specific methods and types (e.g. metadata extraction of package files) | +| `routers/api/packages` | Route definitions for all registry types | +| `routers/api/packages/` | Route implementation for a specific registry type | +| `services/packages` | Helper methods used by registry types to handle common tasks like package creation and deletion in `routers` | +| `services/packages/` | Registry type specific methods used by `routers` and `services` | + +## Models + +Every package registry implementation uses the same underlaying models: + +| Model | Description | +| - | - | +| `Package` | The root of a package providing values fixed for every version (e.g. the package name) | +| `PackageVersion` | A version of a package containing metadata (e.g. the package description) | +| `PackageFile` | A file of a package describing its content (e.g. file name) | +| `PackageBlob` | The content of a file (may be shared by multiple files) | +| `PackageProperty` | Additional properties attached to `Package`, `PackageVersion` or `PackageFile` (e.g. used if metadata is needed for routing) | + +The following diagram shows the relationship between the models: +``` +Package <1---*> PackageVersion <1---*> PackageFile <*---1> PackageBlob +``` + +## Adding a new package registry type + +Before adding a new package registry type have a look at the existing implementation to get an impression of how it could work. +Most registry types offer endpoints to retrieve the metadata, upload and download package files. +The upload endpoint is often the heavy part because it must validate the uploaded blob, extract metadata and create the models. +The methods to validate and extract the metadata should be added in the `modules/packages/` package. +If the upload is valid the methods in `services/packages` allow to store the upload and create the corresponding models. +It depends if the registry type allows multiple files per package version which method should be called: +- `CreatePackageAndAddFile`: error if package version already exists +- `CreatePackageOrAddFileToExisting`: error if file already exists +- `AddFileToExistingPackage`: error if package version does not exist or file already exists + +`services/packages` also contains helper methods to download a file or to remove a package version. +There are no helper methods for metadata endpoints because they are very type specific. From 605fd15ad6eda19dba8f5e8a8f2e595e34e6c6ee Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Tue, 14 Mar 2023 00:16:09 +0000 Subject: [PATCH 06/11] [skip ci] Updated translations via Crowdin --- options/locale/locale_pt-BR.ini | 58 +++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index 55529df8829a..b34b5642dc74 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -247,6 +247,7 @@ default_enable_timetracking_popup=Habilitar o cronômetro para novos repositóri no_reply_address=Domínio de e-mail oculto no_reply_address_helper=Nome de domínio para usuários com um endereço de e-mail oculto. Por exemplo, o nome de usuário 'joe' será registrado no Git como 'joe@noreply.example.org' se o domínio de e-mail oculto estiver definido como 'noreply.example.org'. password_algorithm=Algoritmo Hash de Senha +invalid_password_algorithm=Algoritmo de hash de senha inválido password_algorithm_helper=Escolha o algoritmo de hash para as senhas. Diferentes algoritmos têm requerimentos e forças diversos. O `Argon2` possui boa qualidade, porém usa muita memória e pode ser inapropriado para sistemas com menos recursos. enable_update_checker=Habilitar Verificador de Atualizações enable_update_checker_helper=Procura por novas versões periodicamente conectando-se ao gitea.io. @@ -284,6 +285,7 @@ users=Usuários organizations=Organizações search=Pesquisar code=Código +search.type.tooltip=Tipo de pesquisa search.fuzzy=Similar search.fuzzy.tooltip=Incluir resultados que sejam próximos ao termo de busca search.match=Correspondência @@ -819,6 +821,7 @@ remove_account_link=Remover conta vinculada remove_account_link_desc=A exclusão da chave SSH revogará o acesso à sua conta. Continuar? remove_account_link_success=A conta vinculada foi removida. +hooks.desc=Adicionar webhooks que serão acionados para todos os repositórios pertencentes a este usuário. orgs_none=Você não é membro de nenhuma organização. repos_none=Você não possui nenhum repositório @@ -1231,6 +1234,7 @@ projects.column.color=Colorido projects.open=Abrir projects.close=Fechar projects.column.assigned_to=Atribuído a +projects.card_type.desc=Pré-visualizações de Cards projects.card_type.images_and_text=Imagens e Texto projects.card_type.text_only=Somente texto @@ -1399,6 +1403,7 @@ issues.label_title=Nome da etiqueta issues.label_description=Descrição da etiqueta issues.label_color=Cor da etiqueta issues.label_exclusive=Exclusivo +issues.label_exclusive_desc=Nomeie o rótulo escopo/item para torná-lo mutuamente exclusivo com outros rótulos do escopo/. issues.label_exclusive_warning=Quaisquer rótulos com escopo conflitantes serão removidos ao editar os rótulos de uma issue ou pull request. issues.label_count=%d etiquetas issues.label_open_issues=%d issues abertas @@ -1655,6 +1660,7 @@ pulls.merge_instruction_hint=`Você também pode ver as *, eventos para todos os branches serão relatados. Veja github.com/gobwas/glob documentação da sintaxe. Exemplos: master, {master,release*}. settings.authorization_header=Header de Autorização +settings.authorization_header_desc=Será incluído como header de autorização para solicitações quando estiver presente. Exemplos: %s. settings.active=Ativo settings.active_helper=Informações sobre eventos disparados serão enviadas para esta URL do webhook. settings.add_hook_success=O webhook foi adicionado. @@ -2124,6 +2132,7 @@ settings.dismiss_stale_approvals=Descartar aprovações obsoletas settings.dismiss_stale_approvals_desc=Quando novos commits que mudam o conteúdo do pull request são enviados para o branch, as antigas aprovações serão descartadas. settings.require_signed_commits=Exibir commits assinados settings.require_signed_commits_desc=Rejeitar pushes para este branch se não estiverem assinados ou não forem validáveis. +settings.protect_branch_name_pattern=Padrão de Nome de Branch Protegida settings.protect_protected_file_patterns=Padrões de arquivos protegidos (separados usando ponto e vírgula '\;'): settings.protect_protected_file_patterns_desc=Arquivos protegidos que não têm permissão para serem alterados diretamente, mesmo se o usuário tiver permissão para adicionar, editar ou apagar arquivos neste branch. Vários padrões podem ser separados usando ponto e vírgula ('\;'). Veja github.com/gobwas/glob documentação para sintaxe de padrões. Exemplos: .drone.yml, /docs/**/*.txt. settings.protect_unprotected_file_patterns=Padrões de arquivos desprotegidos (separados usando ponto e vírgula '\;'): @@ -2132,6 +2141,7 @@ settings.add_protected_branch=Habilitar proteção settings.delete_protected_branch=Desabilitar proteção settings.update_protect_branch_success=Proteção do branch '%s' foi atualizada. settings.remove_protected_branch_success=Proteção do branch '%s' foi desabilitada. +settings.remove_protected_branch_failed=Removendo regra de proteção de branch '%s' falhou. settings.protected_branch_deletion=Desabilitar proteção de branch settings.protected_branch_deletion_desc=Desabilitar a proteção de branch permite que os usuários com permissão de escrita realizem push. Continuar? settings.block_rejected_reviews=Bloquear merge em revisões rejeitadas @@ -2146,6 +2156,8 @@ settings.default_merge_style_desc=Estilo de merge padrão para pull requests: settings.choose_branch=Escolha um branch... settings.no_protected_branch=Não há branches protegidos. settings.edit_protected_branch=Editar +settings.protected_branch_required_rule_name=Nome da regra é obrigatório +settings.protected_branch_duplicate_rule_name=Regra com nome duplicado settings.protected_branch_required_approvals_min=Aprovações necessárias não podem ser negativas. settings.tags=Tags settings.tags.protection=Proteção das Tags @@ -2278,6 +2290,8 @@ release.edit_subheader=Lançamentos organizam versões do projeto. release.tag_name=Nome da tag release.target=Destino release.tag_helper=Escolha uma tag existente, ou crie uma nova tag. +release.tag_helper_new=Nova tag. Esta tag será criada a partir do alvo. +release.tag_helper_existing=Tag existente. release.title=Título release.content=Conteúdo release.prerelease_desc=Marcar como pré-lançamento @@ -2570,6 +2584,10 @@ dashboard.delete_old_actions=Excluir todas as ações antigas do banco de dados dashboard.delete_old_actions.started=A exclusão de todas as ações antigas do banco de dados foi iniciada. dashboard.update_checker=Verificador de atualização dashboard.delete_old_system_notices=Excluir todos os avisos de sistema antigos do banco de dados +dashboard.gc_lfs=Coletar lixos dos meta-objetos LFS +dashboard.stop_zombie_tasks=Parar tarefas zumbi +dashboard.stop_endless_tasks=Parar tarefas infinitas +dashboard.cancel_abandoned_jobs=Cancelar trabalhos abandonados users.user_manage_panel=Gerenciamento de conta de usuário users.new_account=Criar conta de usuário @@ -2658,6 +2676,7 @@ repos.size=Tamanho packages.package_manage_panel=Gerenciamento de Pacotes packages.total_size=Tamanho Total: %s +packages.unreferenced_size=Tamanho Não Referenciado: %s packages.owner=Proprietário packages.creator=Criador packages.name=Nome @@ -2751,6 +2770,8 @@ auths.oauth2_required_claim_value_helper=Defina este valor para permitir o login auths.oauth2_group_claim_name=Nome do claim que fornece os nomes dos grupos para esta fonte. (Opcional) auths.oauth2_admin_group=Valor do Claim de Grupo para os usuários administradores. (Opcional - requer nome do claim acima) auths.oauth2_restricted_group=Valor do Claim de Grupo para os usuários restritos. (Opcional - requer nome do claim acima) +auths.oauth2_map_group_to_team=Mapear grupos para Organizações. (Opcional - requer nome do claim acima) +auths.oauth2_map_group_to_team_removal=Remover usuários de equipes sincronizadas se o usuário não pertence ao grupo correspondente. auths.enable_auto_register=Habilitar cadastro automático auths.sspi_auto_create_users=Criar usuários automaticamente auths.sspi_auto_create_users_helper=Permitir que o método de autenticação SSPI crie automaticamente novas contas para usuários que fazem o login pela primeira vez @@ -2791,6 +2812,8 @@ auths.still_in_used=A fonte de autenticação ainda está em uso. Converta ou ex auths.deletion_success=A fonte de autenticação foi excluída. auths.login_source_exist=A fonte de autenticação '%s' já existe. auths.login_source_of_type_exist=Uma fonte de autenticação deste tipo já existe. +auths.unable_to_initialize_openid=Não é possível inicializar o Provedor OpenID Connect: %s +auths.invalid_openIdConnectAutoDiscoveryURL=URL do Auto Discovery inválida (deve ser uma URL válida, começando com http:// ou https://) config.server_config=Configuração do servidor config.app_name=Nome do servidor @@ -3039,6 +3062,7 @@ reopen_pull_request=`reabriu o pull request %[3]s#%[2]s` comment_issue=`comentou na issue %[3]s#%[2]s` comment_pull=`comentou no pull request %[3]s#%[2]s` merge_pull_request=`fez merge do pull request %[3]s#%[2]s` +auto_merge_pull_request=`fez merge automático do pull request %[3]s#%[2]s` transfer_repo=transferiu repositório de %s para %s push_tag=fez push da tag %[3]s to %[4]s delete_tag=excluiu tag %[2]s de %[3]s @@ -3148,10 +3172,12 @@ dependency.id=ID dependency.version=Versão cargo.registry=Configurar este registro no arquivo de configuração de Cargo (por exemplo ~/.cargo/config.toml): cargo.install=Para instalar o pacote usando Cargo, execute o seguinte comando: +cargo.documentation=Para obter mais informações sobre o registro Cargo, consulte a documentação. cargo.details.repository_site=Site do Repositório cargo.details.documentation_site=Site da Documentação chef.registry=Configure este registro em seu arquivo ~/.chef/config.rb: chef.install=Para instalar o pacote, execute o seguinte comando: +chef.documentation=Para obter mais informações sobre o registro Chef, consulte a documentação. composer.registry=Configure este registro em seu arquivo ~/.composer/config.json: composer.install=Para instalar o pacote usando o Composer, execute o seguinte comando: composer.documentation=Para obter mais informações sobre o registro do Composer, consulte a documentação. @@ -3224,6 +3250,15 @@ settings.delete.description=A exclusão de um pacote é permanente e não pode s settings.delete.notice=Você está prestes a excluir %s (%s). Esta operação é irreversível, tem certeza? settings.delete.success=O pacote foi excluído. settings.delete.error=Falha ao excluir o pacote. +owner.settings.cargo.title=Índice do Registro Cargo +owner.settings.cargo.initialize=Iniciar Índice +owner.settings.cargo.initialize.description=Para usar o registro Cargo é necessário um repositório git especial. Aqui você pode (re)criá-lo com a configuração necessária. +owner.settings.cargo.initialize.error=Falha ao inicializar índice Cargo: %v +owner.settings.cargo.initialize.success=O índice Cargo foi criado com sucesso. +owner.settings.cargo.rebuild=Reconstruir Índice +owner.settings.cargo.rebuild.description=Se o índice está fora de sincronia com os pacotes Cargo, você pode reconstruí-lo aqui. +owner.settings.cargo.rebuild.error=Falha ao reconstruir índice Cargo: %v +owner.settings.cargo.rebuild.success=O índice Cargo foi reconstruído com sucesso. owner.settings.cleanuprules.title=Gerenciar Regras de Limpeza owner.settings.cleanuprules.add=Adicionar Regra de Limpeza owner.settings.cleanuprules.edit=Editar Regra de Limpeza @@ -3232,6 +3267,7 @@ owner.settings.cleanuprules.preview=Pré-visualizar Regra de Limpeza owner.settings.cleanuprules.preview.overview=%d pacotes agendados para serem removidos. owner.settings.cleanuprules.preview.none=A regra de limpeza não corresponde a nenhum pacote. owner.settings.cleanuprules.enabled=Habilitado +owner.settings.cleanuprules.pattern_full_match=Aplicar padrão ao nome completo do pacote owner.settings.cleanuprules.keep.title=Versões que correspondem a estas regras são mantidas, mesmo se corresponderem a uma regra de remoção abaixo. owner.settings.cleanuprules.keep.count=Manter o mais recente owner.settings.cleanuprules.keep.count.1=1 versão por pacote @@ -3245,6 +3281,7 @@ owner.settings.cleanuprules.success.update=Regra de limpeza foi atualizada. owner.settings.cleanuprules.success.delete=Regra de limpeza foi excluída. owner.settings.chef.title=Registro Chef owner.settings.chef.keypair=Gerar par de chaves +owner.settings.chef.keypair.description=Gerar um par de chaves usado para autenticar no registro Chef. A chave anterior não pode ser usada depois. [secrets] secrets=Segredos @@ -3253,6 +3290,8 @@ none=Não há segredos ainda. value=Valor name=Nome creation=Adicionar Segredo +creation.name_placeholder=apenas caracteres alfanuméricos ou underline (_), não pode começar com GITEA_ ou GITHUB_ +creation.value_placeholder=Insira qualquer conteúdo. Espaços em branco no início e no fim serão omitidos. creation.success=O segredo '%s' foi adicionado. creation.failed=Falha ao adicionar segredo. deletion=Excluir segredo @@ -3274,6 +3313,10 @@ status.cancelled=Cancelado status.skipped=Ignorado status.blocked=Bloqueado +runners=Runners +runners.runner_manage_panel=Gerenciamento de Runners +runners.new=Criar novo Runner +runners.new_notice=Como iniciar um runner runners.status=Status runners.id=ID runners.name=Nome @@ -3281,21 +3324,36 @@ runners.owner_type=Tipo runners.description=Descrição runners.labels=Rótulos runners.last_online=Última Vez Online +runners.agent_labels=Etiquetas do Agente runners.custom_labels=Etiquetas Personalizadas runners.custom_labels_helper=Etiquetas personalizadas são etiquetas que são adicionadas manualmente por um administrador. Separe as etiquetas com vírgula. Espaço em branco no começo ou no final de cada etiqueta é ignorado. +runners.runner_title=Runner +runners.task_list=Tarefas recentes neste runner runners.task_list.run=Executar runners.task_list.status=Status runners.task_list.repository=Repositório runners.task_list.commit=Commit +runners.task_list.done_at=Feito em +runners.edit_runner=Editar Runner runners.update_runner=Atualizar as Alterações +runners.update_runner_success=Runner atualizado com sucesso +runners.update_runner_failed=Falha ao atualizar runner +runners.delete_runner=Deletar esse runner +runners.delete_runner_success=Runner excluído com sucesso +runners.delete_runner_failed=Falha ao excluir runner +runners.delete_runner_header=Confirme para excluir este runner +runners.delete_runner_notice=Se uma tarefa estiver sendo executada neste runner, ela será encerrada e marcada como falha. Pode quebrar o workflow de construção. +runners.none=Nenhum runner disponível runners.status.unspecified=Desconhecido runners.status.idle=Inativo runners.status.active=Ativo runners.status.offline=Offiline +runs.all_workflows=Todos os Workflows runs.open_tab=%d Aberto runs.closed_tab=%d Fechado runs.commit=Commit runs.pushed_by=Push realizado por +need_approval_desc=Precisa de aprovação para executar workflowa para pull request do fork. From 81fe5d61851c0e586af7d32c29171ceff9a571bb Mon Sep 17 00:00:00 2001 From: delvh Date: Tue, 14 Mar 2023 04:34:09 +0100 Subject: [PATCH 07/11] Convert `
` to ` -
{{.locale.Tr "admin.auths.delete"}}
+
diff --git a/templates/admin/emails/list.tmpl b/templates/admin/emails/list.tmpl index 091f5011f953..d8fa986cff3e 100644 --- a/templates/admin/emails/list.tmpl +++ b/templates/admin/emails/list.tmpl @@ -78,7 +78,7 @@ {{.locale.Tr "admin.emails.change_email_header"}}
-

{{.locale.Tr "admin.emails.change_email_text"}}

+

{{.locale.Tr "admin.emails.change_email_text"}}

{{$.CsrfTokenHtml}} @@ -93,11 +93,9 @@ -
-
{{$.locale.Tr "settings.cancel"}}
- +
+ {{template "base/delete_modal_actions" .}}
-
diff --git a/templates/admin/notice.tmpl b/templates/admin/notice.tmpl index a2c7ca2f6ae8..34bd83a21449 100644 --- a/templates/admin/notice.tmpl +++ b/templates/admin/notice.tmpl @@ -23,7 +23,7 @@
- +
{{.ID}} @@ -39,13 +39,11 @@ -
-
- {{.CsrfTokenHtml}} - -
-
- @@ -70,16 +61,7 @@ -
-
- {{svg "octicon-trash" 16 "gt-mr-2"}} - {{$.locale.Tr "modal.no"}} -
- -
+ {{template "base/delete_modal_actions" .}} diff --git a/templates/admin/user/edit.tmpl b/templates/admin/user/edit.tmpl index 5dd1f531fda9..73017e1b13f1 100644 --- a/templates/admin/user/edit.tmpl +++ b/templates/admin/user/edit.tmpl @@ -151,7 +151,7 @@
-
{{.locale.Tr "admin.users.delete_account"}}
+
@@ -189,7 +189,7 @@
@@ -213,16 +213,7 @@

{{.locale.Tr "admin.users.purge_help"}}

-
-
- {{svg "octicon-x"}} - {{.locale.Tr "modal.no"}} -
- -
+ {{template "base/delete_modal_actions" .}} {{template "base/footer" .}} diff --git a/templates/base/delete_modal_actions.tmpl b/templates/base/delete_modal_actions.tmpl index fb4d31270af5..29bf5f92fd53 100644 --- a/templates/base/delete_modal_actions.tmpl +++ b/templates/base/delete_modal_actions.tmpl @@ -1,10 +1,10 @@
-
+
-
+ +
+
diff --git a/templates/org/settings/delete.tmpl b/templates/org/settings/delete.tmpl index 669e393e1dec..69e226f410a7 100644 --- a/templates/org/settings/delete.tmpl +++ b/templates/org/settings/delete.tmpl @@ -19,9 +19,9 @@ -
+
+ diff --git a/templates/org/settings/labels.tmpl b/templates/org/settings/labels.tmpl index 5436bcba05ca..e04b39127162 100644 --- a/templates/org/settings/labels.tmpl +++ b/templates/org/settings/labels.tmpl @@ -11,7 +11,7 @@
-
{{.locale.Tr "repo.issues.new_label"}}
+
diff --git a/templates/package/settings.tmpl b/templates/package/settings.tmpl index dc12fb8207dd..875bf852bb98 100644 --- a/templates/package/settings.tmpl +++ b/templates/package/settings.tmpl @@ -57,10 +57,7 @@
{{.CsrfTokenHtml}} -
-
{{.locale.Tr "cancel"}}
- -
+ {{template "base/delete_modal_actions" .}}
diff --git a/templates/projects/list.tmpl b/templates/projects/list.tmpl index 4a21c0fd28ce..89c52dee6808 100644 --- a/templates/projects/list.tmpl +++ b/templates/projects/list.tmpl @@ -84,15 +84,6 @@

{{.locale.Tr "repo.projects.deletion_desc"}}

-
-
- - {{.locale.Tr "modal.no"}} -
-
- - {{.locale.Tr "modal.yes"}} -
-
+ {{template "base/delete_modal_actions" .}} {{end}} diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index 6867309510f8..b776f89efa39 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -29,7 +29,7 @@
-
{{$.locale.Tr "settings.cancel"}}
+
@@ -127,7 +127,7 @@
-
{{$.locale.Tr "settings.cancel"}}
+
@@ -144,7 +144,7 @@
-
{{$.locale.Tr "settings.cancel"}}
+
@@ -158,8 +158,8 @@ {{$.locale.Tr "repo.projects.column.deletion_desc"}} -
-
{{$.locale.Tr "settings.cancel"}}
+
{{/* TODO: convert to base/delete_modal_actions.tmpl */}} +
@@ -265,15 +265,6 @@

{{.locale.Tr "repo.projects.deletion_desc"}}

-
-
- - {{.locale.Tr "modal.no"}} -
-
- - {{.locale.Tr "modal.yes"}} -
-
+ {{template "base/delete_modal_actions" .}} {{end}} diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl index a093c19deb8c..7e8bf348a4d6 100644 --- a/templates/repo/branch/list.tmpl +++ b/templates/repo/branch/list.tmpl @@ -176,7 +176,7 @@
-
{{.locale.Tr "settings.cancel"}}
+
diff --git a/templates/repo/cite/cite_modal.tmpl b/templates/repo/cite/cite_modal.tmpl index 185b34173dc8..f00bab8859e4 100644 --- a/templates/repo/cite/cite_modal.tmpl +++ b/templates/repo/cite/cite_modal.tmpl @@ -15,8 +15,8 @@
-
+
+
diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl index f19a4d4223c7..ace5a410875e 100644 --- a/templates/repo/commit_page.tmpl +++ b/templates/repo/commit_page.tmpl @@ -96,7 +96,7 @@
-
{{.locale.Tr "settings.cancel"}}
+
@@ -121,7 +121,7 @@
-
{{.locale.Tr "settings.cancel"}}
+
diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl index afd471368fa8..e0c58896f0f0 100644 --- a/templates/repo/diff/box.tmpl +++ b/templates/repo/diff/box.tmpl @@ -107,8 +107,8 @@
{{if $showFileViewToggle}}
- {{svg "octicon-code"}} - {{svg "octicon-file"}} + +
{{end}} {{if $file.IsProtected}} @@ -200,8 +200,8 @@ {{$.locale.Tr "loading"}}
-
{{.locale.Tr "repo.issues.cancel"}}
-
{{.locale.Tr "repo.issues.save"}}
+ +
diff --git a/templates/repo/editor/edit.tmpl b/templates/repo/editor/edit.tmpl index 992ccee8e4ff..431033e18e8a 100644 --- a/templates/repo/editor/edit.tmpl +++ b/templates/repo/editor/edit.tmpl @@ -65,14 +65,14 @@

{{.locale.Tr "repo.editor.commit_empty_file_text"}}

-
+
-
+ +
+
diff --git a/templates/repo/editor/patch.tmpl b/templates/repo/editor/patch.tmpl index bbd5c2dbdef1..75a8b5d687d9 100644 --- a/templates/repo/editor/patch.tmpl +++ b/templates/repo/editor/patch.tmpl @@ -45,14 +45,14 @@

{{.locale.Tr "repo.editor.commit_empty_file_text"}}

-
- +
-
- + +
+
diff --git a/templates/repo/issue/labels.tmpl b/templates/repo/issue/labels.tmpl index 82cfcd071204..0a25d9c87ff1 100644 --- a/templates/repo/issue/labels.tmpl +++ b/templates/repo/issue/labels.tmpl @@ -6,7 +6,7 @@ {{template "repo/issue/navbar" .}} {{if and (or .CanWriteIssues .CanWritePulls) (not .Repository.IsArchived)}}
-
{{.locale.Tr "repo.issues.new_label"}}
+
{{end}} diff --git a/templates/repo/issue/labels/edit_delete_label.tmpl b/templates/repo/issue/labels/edit_delete_label.tmpl index 450061e8357c..38a948172ffc 100644 --- a/templates/repo/issue/labels/edit_delete_label.tmpl +++ b/templates/repo/issue/labels/edit_delete_label.tmpl @@ -6,16 +6,7 @@

{{.locale.Tr "repo.issues.label_deletion_desc"}}

-
-
- - {{.locale.Tr "modal.no"}} -
-
- - {{.locale.Tr "modal.yes"}} -
-
+ {{template "base/delete_modal_actions" .}}
-
+
-
+ +
+
diff --git a/templates/repo/issue/labels/label_new.tmpl b/templates/repo/issue/labels/label_new.tmpl index 62f7155b745c..c937f28e8aa2 100644 --- a/templates/repo/issue/labels/label_new.tmpl +++ b/templates/repo/issue/labels/label_new.tmpl @@ -36,12 +36,15 @@ +
-
+
-
+ +
+
diff --git a/templates/repo/issue/list.tmpl b/templates/repo/issue/list.tmpl index ca05264e77b6..36faf861136b 100644 --- a/templates/repo/issue/list.tmpl +++ b/templates/repo/issue/list.tmpl @@ -213,9 +213,9 @@ {{if not .Repository.IsArchived}} {{if .IsShowClosed}} -
{{.locale.Tr "repo.issues.action_open"}}
+ {{else}} -
{{.locale.Tr "repo.issues.action_close"}}
+ {{end}} diff --git a/templates/repo/issue/view_content/comments_delete_time.tmpl b/templates/repo/issue/view_content/comments_delete_time.tmpl index bc08d7fde78d..b79b7ae2be09 100644 --- a/templates/repo/issue/view_content/comments_delete_time.tmpl +++ b/templates/repo/issue/view_content/comments_delete_time.tmpl @@ -7,10 +7,7 @@ {{.ctxData.CsrfTokenHtml}}
{{.ctxData.locale.Tr "repo.issues.del_time"}}
-
-
{{.ctxData.locale.Tr "repo.issues.context.delete"}}
-
{{.ctxData.locale.Tr "repo.issues.add_time_cancel"}}
-
+ {{template "base/delete_modal_actions" .}} diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl index e58f94aff389..165dca7e0c16 100644 --- a/templates/repo/issue/view_content/sidebar.tmpl +++ b/templates/repo/issue/view_content/sidebar.tmpl @@ -346,8 +346,8 @@
-
{{.locale.Tr "repo.issues.add_time_short"}}
-
{{.locale.Tr "repo.issues.add_time_cancel"}}
+ +
@@ -532,14 +532,14 @@ {{end}}

-
+
-
+ +
+
{{end}} @@ -619,7 +619,7 @@ {{end}}
-
{{.locale.Tr "settings.cancel"}}
+
diff --git a/templates/repo/issue/view_content/update_branch_by_merge.tmpl b/templates/repo/issue/view_content/update_branch_by_merge.tmpl index 6d36a9b45f38..49e4467dc3f4 100644 --- a/templates/repo/issue/view_content/update_branch_by_merge.tmpl +++ b/templates/repo/issue/view_content/update_branch_by_merge.tmpl @@ -19,8 +19,8 @@ diff --git a/templates/repo/issue/view_title.tmpl b/templates/repo/issue/view_title.tmpl index f0ac1e021ec4..2a8381b0b47b 100644 --- a/templates/repo/issue/view_title.tmpl +++ b/templates/repo/issue/view_title.tmpl @@ -1,9 +1,7 @@
{{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}} -
- -
+ {{end}}

{{RenderIssueTitle $.Context .Issue.Title $.RepoLink $.Repository.ComposeMetas | RenderCodeBlock}} diff --git a/templates/repo/migrate/migrating.tmpl b/templates/repo/migrate/migrating.tmpl index a3552610c47d..cd3c5e754ecb 100644 --- a/templates/repo/migrate/migrating.tmpl +++ b/templates/repo/migrate/migrating.tmpl @@ -72,7 +72,7 @@

-
{{.locale.Tr "settings.cancel"}}
+
diff --git a/templates/repo/projects/list.tmpl b/templates/repo/projects/list.tmpl index f066f84ea2e8..6833b7d785a7 100644 --- a/templates/repo/projects/list.tmpl +++ b/templates/repo/projects/list.tmpl @@ -86,16 +86,7 @@

{{.locale.Tr "repo.projects.deletion_desc"}}

-
-
- - {{.locale.Tr "modal.no"}} -
-
- - {{.locale.Tr "modal.yes"}} -
-
+ {{template "base/delete_modal_actions" .}}
{{end}} {{template "base/footer" .}} diff --git a/templates/repo/projects/view.tmpl b/templates/repo/projects/view.tmpl index bef9cb9bf062..0248b9c6d2c7 100644 --- a/templates/repo/projects/view.tmpl +++ b/templates/repo/projects/view.tmpl @@ -33,7 +33,7 @@
-
{{$.locale.Tr "settings.cancel"}}
+
@@ -131,7 +131,7 @@
-
{{$.locale.Tr "settings.cancel"}}
+
@@ -148,7 +148,7 @@
-
{{$.locale.Tr "settings.cancel"}}
+
@@ -162,8 +162,8 @@ {{$.locale.Tr "repo.projects.column.deletion_desc"}} -
-
{{$.locale.Tr "settings.cancel"}}
+
{{/* TODO: Convert to base/delete_modal_actions.tmpl? */}} +
@@ -276,16 +276,7 @@

{{.locale.Tr "repo.projects.deletion_desc"}}

-
-
- - {{.locale.Tr "modal.no"}} -
-
- - {{.locale.Tr "modal.yes"}} -
-
+ {{template "base/delete_modal_actions" .}} {{end}} diff --git a/templates/repo/release/new.tmpl b/templates/repo/release/new.tmpl index d7c580fed969..8c4df98d1911 100644 --- a/templates/repo/release/new.tmpl +++ b/templates/repo/release/new.tmpl @@ -114,7 +114,7 @@ {{$.locale.Tr "repo.release.delete_release"}} {{if .IsDraft}} - + @@ -125,9 +125,9 @@ {{end}} {{else}} {{if not .tag_name}} - + {{end}} - + diff --git a/templates/repo/settings/deploy_keys.tmpl b/templates/repo/settings/deploy_keys.tmpl index 22fddeb4df67..ea4fba240edb 100644 --- a/templates/repo/settings/deploy_keys.tmpl +++ b/templates/repo/settings/deploy_keys.tmpl @@ -8,9 +8,9 @@ {{.locale.Tr "repo.settings.deploy_keys"}}
{{if not .DisableSSH}} -
{{.locale.Tr "repo.settings.add_deploy_key"}}
+ {{else}} -
{{.locale.Tr "settings.ssh_disabled"}}
+ {{end}}
@@ -85,15 +85,6 @@

{{.locale.Tr "repo.settings.deploy_key_deletion_desc"}}

-
-
- - {{.locale.Tr "modal.no"}} -
-
- - {{.locale.Tr "modal.yes"}} -
-
+ {{template "base/delete_modal_actions" .}} {{template "base/footer" .}} diff --git a/templates/repo/settings/lfs.tmpl b/templates/repo/settings/lfs.tmpl index 566a701efb2a..9a38d3234573 100644 --- a/templates/repo/settings/lfs.tmpl +++ b/templates/repo/settings/lfs.tmpl @@ -50,8 +50,8 @@

{{$.CsrfTokenHtml}} -
-
{{$.locale.Tr "settings.cancel"}}
+
{{/* TODO: Convert to base/delete_modal_actions */}} +
diff --git a/templates/repo/settings/lfs_pointers.tmpl b/templates/repo/settings/lfs_pointers.tmpl index 8eebc6e870a1..67021ba6cda6 100644 --- a/templates/repo/settings/lfs_pointers.tmpl +++ b/templates/repo/settings/lfs_pointers.tmpl @@ -49,9 +49,9 @@ {{ShortSha .Oid}} {{else}} - + {{end}} diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl index 030c77b881da..be07aeb0ffeb 100644 --- a/templates/repo/settings/options.tmpl +++ b/templates/repo/settings/options.tmpl @@ -825,7 +825,7 @@
-
{{.locale.Tr "settings.cancel"}}
+
@@ -856,7 +856,7 @@
-
{{.locale.Tr "settings.cancel"}}
+
@@ -892,7 +892,7 @@
-
{{.locale.Tr "settings.cancel"}}
+
@@ -926,7 +926,7 @@
-
{{.locale.Tr "settings.cancel"}}
+
@@ -958,7 +958,7 @@
-
{{.locale.Tr "settings.cancel"}}
+
@@ -988,10 +988,7 @@ {{.CsrfTokenHtml}} -
-
{{.locale.Tr "settings.cancel"}}
- -
+ {{template "base/delete_modal_actions" .}} {{end}} diff --git a/templates/repo/settings/webhook/delete_modal.tmpl b/templates/repo/settings/webhook/delete_modal.tmpl index fdc49ada4e37..f455899663e8 100644 --- a/templates/repo/settings/webhook/delete_modal.tmpl +++ b/templates/repo/settings/webhook/delete_modal.tmpl @@ -6,14 +6,5 @@

{{.locale.Tr "repo.settings.webhook_deletion_desc"}}

-
-
- - {{.locale.Tr "modal.no"}} -
-
- - {{.locale.Tr "modal.yes"}} -
-
+ {{template "base/delete_modal_actions" .}} diff --git a/templates/repo/unicode_escape_prompt.tmpl b/templates/repo/unicode_escape_prompt.tmpl index d55bd0150a1b..12eff6aebe3a 100644 --- a/templates/repo/unicode_escape_prompt.tmpl +++ b/templates/repo/unicode_escape_prompt.tmpl @@ -1,7 +1,7 @@ {{if .EscapeStatus}} {{if .EscapeStatus.HasInvisible}}
- {{svg "octicon-x" 16 "close inside"}} +
{{$.root.locale.Tr "repo.invisible_runes_header"}}
@@ -12,7 +12,7 @@
{{else if .EscapeStatus.HasAmbiguous}}
- {{svg "octicon-x" 16 "close inside"}} +
{{$.root.locale.Tr "repo.ambiguous_runes_header"}}
diff --git a/templates/shared/actions/runner_list.tmpl b/templates/shared/actions/runner_list.tmpl index eabddbb30cdc..30c52c01b457 100644 --- a/templates/shared/actions/runner_list.tmpl +++ b/templates/shared/actions/runner_list.tmpl @@ -20,9 +20,9 @@
-
+
+
diff --git a/templates/shared/secrets/add_list.tmpl b/templates/shared/secrets/add_list.tmpl index 9105b7ad9bf4..4aa5f0ccd578 100644 --- a/templates/shared/secrets/add_list.tmpl +++ b/templates/shared/secrets/add_list.tmpl @@ -1,7 +1,7 @@

{{.locale.Tr "secrets.secrets"}}
-
{{.locale.Tr "secrets.creation"}}
+

diff --git a/templates/user/auth/grant.tmpl b/templates/user/auth/grant.tmpl index c906db3e0a17..060b67527300 100644 --- a/templates/user/auth/grant.tmpl +++ b/templates/user/auth/grant.tmpl @@ -23,7 +23,7 @@ - + Cancel
diff --git a/templates/user/auth/webauthn_error.tmpl b/templates/user/auth/webauthn_error.tmpl index 447d289a2807..b6467de1aad8 100644 --- a/templates/user/auth/webauthn_error.tmpl +++ b/templates/user/auth/webauthn_error.tmpl @@ -17,6 +17,6 @@
-
{{.locale.Tr "cancel"}}
+
diff --git a/templates/user/settings/account.tmpl b/templates/user/settings/account.tmpl index 9a57bd572267..53f7d021e0b6 100644 --- a/templates/user/settings/account.tmpl +++ b/templates/user/settings/account.tmpl @@ -151,9 +151,9 @@
-
+
+ {{.locale.Tr "auth.forgot_password"}}
diff --git a/templates/user/settings/applications.tmpl b/templates/user/settings/applications.tmpl index b0cd37d44c47..18132c4a7534 100644 --- a/templates/user/settings/applications.tmpl +++ b/templates/user/settings/applications.tmpl @@ -276,15 +276,16 @@

{{.locale.Tr "settings.access_token_deletion_desc"}}

-
-
- + +
{{/* TODO: Convert to base/delete_modal_actions.tmpl */}} +
-
- + +
+
diff --git a/templates/user/settings/keys_gpg.tmpl b/templates/user/settings/keys_gpg.tmpl index c80890940a44..93ca12a0880c 100644 --- a/templates/user/settings/keys_gpg.tmpl +++ b/templates/user/settings/keys_gpg.tmpl @@ -1,7 +1,7 @@

{{.locale.Tr "settings.manage_gpg_keys"}}
-
{{.locale.Tr "settings.add_key"}}
+

diff --git a/templates/user/settings/keys_principal.tmpl b/templates/user/settings/keys_principal.tmpl index cc1152b739a6..8012b874cdcf 100644 --- a/templates/user/settings/keys_principal.tmpl +++ b/templates/user/settings/keys_principal.tmpl @@ -3,9 +3,9 @@ {{.locale.Tr "settings.manage_ssh_principals"}}
{{if not .DisableSSH}} -
{{.locale.Tr "settings.add_new_principal"}}
+ {{else}} -
{{.locale.Tr "settings.ssh_disabled"}}
+ {{end}}
diff --git a/templates/user/settings/keys_ssh.tmpl b/templates/user/settings/keys_ssh.tmpl index 891959d35145..1ff4dab34e31 100644 --- a/templates/user/settings/keys_ssh.tmpl +++ b/templates/user/settings/keys_ssh.tmpl @@ -2,11 +2,11 @@ {{.locale.Tr "settings.manage_ssh_keys"}}
{{if not .DisableSSH}} -
+
+ {{else}} -
{{.locale.Tr "settings.ssh_disabled"}}
+ {{end}}
diff --git a/templates/user/settings/repos.tmpl b/templates/user/settings/repos.tmpl index 902b3fb2f33b..2e107ca7fa1a 100644 --- a/templates/user/settings/repos.tmpl +++ b/templates/user/settings/repos.tmpl @@ -50,16 +50,7 @@ {{$.CsrfTokenHtml}} -
-
- - {{$.locale.Tr "modal.no"}} -
- -
+ {{template "base/delete_modal_actions" .}}
{{end}} @@ -77,16 +68,7 @@ {{$.CsrfTokenHtml}} -
-
- - {{$.locale.Tr "modal.no"}} -
- -
+ {{template "base/delete_modal_actions" .}} {{end}} diff --git a/templates/user/settings/security/twofa.tmpl b/templates/user/settings/security/twofa.tmpl index a4da94762886..1a0a8a6432c0 100644 --- a/templates/user/settings/security/twofa.tmpl +++ b/templates/user/settings/security/twofa.tmpl @@ -13,7 +13,7 @@
{{.CsrfTokenHtml}}

{{.locale.Tr "settings.twofa_disable_note"}}

-
{{$.locale.Tr "settings.twofa_disable"}}
+
{{else}}

{{.locale.Tr "settings.twofa_not_enrolled"}}

diff --git a/web_src/js/features/admin/common.js b/web_src/js/features/admin/common.js index d023e0bc3695..be5aa876a5db 100644 --- a/web_src/js/features/admin/common.js +++ b/web_src/js/features/admin/common.js @@ -198,7 +198,8 @@ export function initAdminCommon() { break; } }); - $('#delete-selection').on('click', function () { + $('#delete-selection').on('click', function (e) { + e.preventDefault(); const $this = $(this); $this.addClass('loading disabled'); const ids = []; diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js index 4fa6942467ee..0f36ce2bf84f 100644 --- a/web_src/js/features/common-global.js +++ b/web_src/js/features/common-global.js @@ -202,7 +202,8 @@ export function initGlobalDropzone() { } export function initGlobalLinkActions() { - function showDeletePopup() { + function showDeletePopup(e) { + e.preventDefault(); const $this = $(this); const dataArray = $this.data(); let filter = ''; @@ -243,10 +244,10 @@ export function initGlobalLinkActions() { }); } }).modal('show'); - return false; } - function showAddAllPopup() { + function showAddAllPopup(e) { + e.preventDefault(); const $this = $(this); let filter = ''; if ($this.attr('id')) { @@ -272,7 +273,6 @@ export function initGlobalLinkActions() { }); } }).modal('show'); - return false; } function linkAction(e) { @@ -318,13 +318,21 @@ export function initGlobalLinkActions() { } export function initGlobalButtons() { - $('.show-panel.button').on('click', function () { + // There are many "cancel button" elements in modal dialogs, Fomantic UI expects they are button-like elements but never submit a form. + // However, Gitea misuses the modal dialog and put the cancel buttons inside forms, so we must prevent the form submission. + // There are a few cancel buttons in non-modal forms, and there are some dynamically created forms (eg: the "Edit Issue Content") + $(document).on('click', 'form .ui.cancel.button', (e) => { + e.preventDefault(); + }); + + $('.show-panel.button').on('click', function (e) { + e.preventDefault(); showElem($(this).data('panel')); }); - $('.hide-panel.button').on('click', function (event) { + $('.hide-panel.button').on('click', function (e) { // a `.hide-panel.button` can hide a panel, by `data-panel="selector"` or `data-panel-closest="selector"` - event.preventDefault(); + e.preventDefault(); let sel = $(this).attr('data-panel'); if (sel) { hideElem($(sel)); @@ -339,7 +347,8 @@ export function initGlobalButtons() { alert('Nothing to hide'); }); - $('.show-modal').on('click', function () { + $('.show-modal').on('click', function (e) { + e.preventDefault(); const modalDiv = $($(this).attr('data-modal')); for (const attrib of this.attributes) { if (!attrib.name.startsWith('data-modal-')) { @@ -360,7 +369,8 @@ export function initGlobalButtons() { } }); - $('.delete-post.button').on('click', function () { + $('.delete-post.button').on('click', function (e) { + e.preventDefault(); const $this = $(this); $.post($this.attr('data-request-url'), { _csrf: csrfToken diff --git a/web_src/js/features/common-issue.js b/web_src/js/features/common-issue.js index 0965caef154f..ebc851d67633 100644 --- a/web_src/js/features/common-issue.js +++ b/web_src/js/features/common-issue.js @@ -34,6 +34,7 @@ export function initCommonIssue() { }); $('.issue-action').on('click', async function (e) { + e.preventDefault(); let action = this.getAttribute('data-action'); let elementId = this.getAttribute('data-element-id'); const url = this.getAttribute('data-url'); diff --git a/web_src/js/features/repo-issue.js b/web_src/js/features/repo-issue.js index 41c9dd118f2f..a8a27c257209 100644 --- a/web_src/js/features/repo-issue.js +++ b/web_src/js/features/repo-issue.js @@ -230,7 +230,8 @@ export function initRepoIssueStatusButton() { const value = easyMDE?.value() || $(this).val(); $statusButton.text($statusButton.data(value.length === 0 ? 'status' : 'status-and-comment')); }); - $statusButton.on('click', () => { + $statusButton.on('click', (e) => { + e.preventDefault(); $('#status').val($statusButton.data('status-val')); $('#comment-form').trigger('submit'); }); diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js index 70542ad883e9..5346a0d27484 100644 --- a/web_src/js/features/repo-legacy.js +++ b/web_src/js/features/repo-legacy.js @@ -412,7 +412,8 @@ async function onEditContent(event) { $saveButton.trigger('click'); }); - $editContentZone.find('.cancel.button').on('click', () => { + $editContentZone.find('.cancel.button').on('click', (e) => { + e.preventDefault(); showElem($renderContent); hideElem($editContentZone); if (dz) { diff --git a/web_src/svg/fontawesome-save.svg b/web_src/svg/fontawesome-save.svg new file mode 100644 index 000000000000..763d26abb1c3 --- /dev/null +++ b/web_src/svg/fontawesome-save.svg @@ -0,0 +1 @@ + \ No newline at end of file From b942838bd486f5d3919a14a128efe22fc55c6112 Mon Sep 17 00:00:00 2001 From: LeenHawk <127599173+LeenHawk@users.noreply.github.com> Date: Tue, 14 Mar 2023 11:38:20 +0800 Subject: [PATCH 08/11] Update localization.zh-cn.md (#23448) As title. --- docs/content/doc/features/localization.zh-cn.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/content/doc/features/localization.zh-cn.md b/docs/content/doc/features/localization.zh-cn.md index 6a6f63732665..44f9537f31b4 100644 --- a/docs/content/doc/features/localization.zh-cn.md +++ b/docs/content/doc/features/localization.zh-cn.md @@ -14,5 +14,17 @@ menu: --- # 本地化 +Gitea的本地化是通过我们的[Crowdin项目](https://crowdin.com/project/gitea)进行的。 -## TBD +对于对**英语翻译**的更改,可以发出pull-request,来更改[英语语言环境](https://github.com/go-gitea/gitea/blob/master/options/locale/locale_en-US.ini)中合适的关键字。 + +有关对**非英语**翻译的更改,请参阅上面的 Crowdin 项目。 + +## 支持的语言 +上述 Crowdin 项目中列出的任何语言一旦翻译了 25% 或更多都将得到支持。 + +翻译被接受后,它将在下一次 Crowdin 同步后反映在主存储库中,这通常是在任何 PR 合并之后。 + +在撰写本文时,这意味着更改后的翻译可能要到 Gitea 的下一个版本才会出现。 + +如果使用开发版本,则在同步更改内容后,它应该会在更新后立即显示。 From e82f1b15c7120ad13fd3b67cf7e2c6cb9915c22d Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Tue, 14 Mar 2023 12:09:06 +0800 Subject: [PATCH 09/11] Refactor dashboard repo list to Vue SFC (#23405) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Similar to #23394 The dashboard repo list mixes jQuery/Fomantic UI/Vue together, it's very diffcult to maintain and causes unfixable a11y problems. This PR uses two steps to refactor the repo list: 1. move `data-` attributes to JS object and use Vue data as much as possible https://github.com/go-gitea/gitea/pull/23405/commits/d3adc0dcacf7de87b9819277e6598ac3993bbfa3 2. move the code into a Vue SFC https://github.com/go-gitea/gitea/pull/23405/commits/7ebe55df6e67adfd272a4bf0a96ad6688edf661f Total: +516 −585 Screenshots:
![image](https://user-images.githubusercontent.com/2114189/224271457-a23e05be-d7d3-4247-a803-f0ee30c36f44.png) ![image](https://user-images.githubusercontent.com/2114189/224271504-76fbd3da-4d7a-4725-b0d1-fbff83caac63.png) ![image](https://user-images.githubusercontent.com/2114189/224271845-f007cadf-6c49-46bd-a65c-a3fc75bdba3b.png)
--------- Co-authored-by: John Olheiser --- templates/user/dashboard/repolist.tmpl | 233 +++------- web_src/js/components/DashboardRepoList.js | 345 -------------- web_src/js/components/DashboardRepoList.vue | 432 ++++++++++++++++++ .../js/components/RepoActivityTopAuthors.vue | 9 +- .../js/components/RepoBranchTagDropdown.js | 3 +- web_src/js/components/VueComponentLoader.js | 49 -- web_src/js/index.js | 4 +- web_src/js/svg.js | 26 +- 8 files changed, 516 insertions(+), 585 deletions(-) delete mode 100644 web_src/js/components/DashboardRepoList.js create mode 100644 web_src/js/components/DashboardRepoList.vue delete mode 100644 web_src/js/components/VueComponentLoader.js diff --git a/templates/user/dashboard/repolist.tmpl b/templates/user/dashboard/repolist.tmpl index 97234176bd21..0a8f427f9da1 100644 --- a/templates/user/dashboard/repolist.tmpl +++ b/templates/user/dashboard/repolist.tmpl @@ -1,181 +1,54 @@ -
- -
+ + +
diff --git a/web_src/js/components/DashboardRepoList.js b/web_src/js/components/DashboardRepoList.js deleted file mode 100644 index 2328cc83a90c..000000000000 --- a/web_src/js/components/DashboardRepoList.js +++ /dev/null @@ -1,345 +0,0 @@ -import {createApp, nextTick} from 'vue'; -import $ from 'jquery'; -import {initVueSvg, vueDelimiters} from './VueComponentLoader.js'; -import {initTooltip} from '../modules/tippy.js'; - -const {appSubUrl, assetUrlPrefix, pageData} = window.config; - -function initVueComponents(app) { - app.component('repo-search', { - delimiters: vueDelimiters, - props: { - searchLimit: { - type: Number, - default: 10 - }, - subUrl: { - type: String, - required: true - }, - uid: { - type: Number, - default: 0 - }, - teamId: { - type: Number, - required: false, - default: 0 - }, - organizations: { - type: Array, - default: () => [], - }, - isOrganization: { - type: Boolean, - default: true - }, - canCreateOrganization: { - type: Boolean, - default: false - }, - organizationsTotalCount: { - type: Number, - default: 0 - }, - moreReposLink: { - type: String, - default: '' - } - }, - - data() { - const params = new URLSearchParams(window.location.search); - - let tab = params.get('repo-search-tab'); - if (!tab) { - tab = 'repos'; - } - - let reposFilter = params.get('repo-search-filter'); - if (!reposFilter) { - reposFilter = 'all'; - } - - let privateFilter = params.get('repo-search-private'); - if (!privateFilter) { - privateFilter = 'both'; - } - - let archivedFilter = params.get('repo-search-archived'); - if (!archivedFilter) { - archivedFilter = 'unarchived'; - } - - let searchQuery = params.get('repo-search-query'); - if (!searchQuery) { - searchQuery = ''; - } - - let page = 1; - try { - page = parseInt(params.get('repo-search-page')); - } catch { - // noop - } - if (!page) { - page = 1; - } - - return { - hasMounted: false, // accessing $refs in computed() need to wait for mounted - tab, - repos: [], - reposTotalCount: 0, - reposFilter, - archivedFilter, - privateFilter, - page, - finalPage: 1, - searchQuery, - isLoading: false, - staticPrefix: assetUrlPrefix, - counts: {}, - repoTypes: { - all: { - searchMode: '', - }, - forks: { - searchMode: 'fork', - }, - mirrors: { - searchMode: 'mirror', - }, - sources: { - searchMode: 'source', - }, - collaborative: { - searchMode: 'collaborative', - }, - } - }; - }, - - computed: { - // used in `repolist.tmpl` - showMoreReposLink() { - return this.repos.length > 0 && this.repos.length < this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`]; - }, - searchURL() { - return `${this.subUrl}/repo/search?sort=updated&order=desc&uid=${this.uid}&team_id=${this.teamId}&q=${this.searchQuery - }&page=${this.page}&limit=${this.searchLimit}&mode=${this.repoTypes[this.reposFilter].searchMode - }${this.reposFilter !== 'all' ? '&exclusive=1' : '' - }${this.archivedFilter === 'archived' ? '&archived=true' : ''}${this.archivedFilter === 'unarchived' ? '&archived=false' : '' - }${this.privateFilter === 'private' ? '&is_private=true' : ''}${this.privateFilter === 'public' ? '&is_private=false' : '' - }`; - }, - repoTypeCount() { - return this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`]; - }, - checkboxArchivedFilterTitle() { - return this.hasMounted && this.$refs.checkboxArchivedFilter?.getAttribute(`data-title-${this.archivedFilter}`); - }, - checkboxArchivedFilterProps() { - return {checked: this.archivedFilter === 'archived', indeterminate: this.archivedFilter === 'both'}; - }, - checkboxPrivateFilterTitle() { - return this.hasMounted && this.$refs.checkboxPrivateFilter?.getAttribute(`data-title-${this.privateFilter}`); - }, - checkboxPrivateFilterProps() { - return {checked: this.privateFilter === 'private', indeterminate: this.privateFilter === 'both'}; - }, - }, - - mounted() { - const el = document.getElementById('dashboard-repo-list'); - this.changeReposFilter(this.reposFilter); - for (const elTooltip of el.querySelectorAll('.tooltip')) { - initTooltip(elTooltip); - } - $(el).find('.dropdown').dropdown(); - nextTick(() => { - this.$refs.search.focus(); - }); - - this.hasMounted = true; - }, - - methods: { - changeTab(t) { - this.tab = t; - this.updateHistory(); - }, - - changeReposFilter(filter) { - this.reposFilter = filter; - this.repos = []; - this.page = 1; - this.counts[`${filter}:${this.archivedFilter}:${this.privateFilter}`] = 0; - this.searchRepos(); - }, - - updateHistory() { - const params = new URLSearchParams(window.location.search); - - if (this.tab === 'repos') { - params.delete('repo-search-tab'); - } else { - params.set('repo-search-tab', this.tab); - } - - if (this.reposFilter === 'all') { - params.delete('repo-search-filter'); - } else { - params.set('repo-search-filter', this.reposFilter); - } - - if (this.privateFilter === 'both') { - params.delete('repo-search-private'); - } else { - params.set('repo-search-private', this.privateFilter); - } - - if (this.archivedFilter === 'unarchived') { - params.delete('repo-search-archived'); - } else { - params.set('repo-search-archived', this.archivedFilter); - } - - if (this.searchQuery === '') { - params.delete('repo-search-query'); - } else { - params.set('repo-search-query', this.searchQuery); - } - - if (this.page === 1) { - params.delete('repo-search-page'); - } else { - params.set('repo-search-page', `${this.page}`); - } - - const queryString = params.toString(); - if (queryString) { - window.history.replaceState({}, '', `?${queryString}`); - } else { - window.history.replaceState({}, '', window.location.pathname); - } - }, - - toggleArchivedFilter() { - if (this.archivedFilter === 'unarchived') { - this.archivedFilter = 'archived'; - } else if (this.archivedFilter === 'archived') { - this.archivedFilter = 'both'; - } else { // including both - this.archivedFilter = 'unarchived'; - } - this.page = 1; - this.repos = []; - this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`] = 0; - this.searchRepos(); - }, - - togglePrivateFilter() { - if (this.privateFilter === 'both') { - this.privateFilter = 'public'; - } else if (this.privateFilter === 'public') { - this.privateFilter = 'private'; - } else { // including private - this.privateFilter = 'both'; - } - this.page = 1; - this.repos = []; - this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`] = 0; - this.searchRepos(); - }, - - - changePage(page) { - this.page = page; - if (this.page > this.finalPage) { - this.page = this.finalPage; - } - if (this.page < 1) { - this.page = 1; - } - this.repos = []; - this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`] = 0; - this.searchRepos(); - }, - - async searchRepos() { - this.isLoading = true; - - const searchedMode = this.repoTypes[this.reposFilter].searchMode; - const searchedURL = this.searchURL; - const searchedQuery = this.searchQuery; - - let response, json; - try { - if (!this.reposTotalCount) { - const totalCountSearchURL = `${this.subUrl}/repo/search?count_only=1&uid=${this.uid}&team_id=${this.teamId}&q=&page=1&mode=`; - response = await fetch(totalCountSearchURL); - this.reposTotalCount = response.headers.get('X-Total-Count'); - } - - response = await fetch(searchedURL); - json = await response.json(); - } catch { - if (searchedURL === this.searchURL) { - this.isLoading = false; - } - return; - } - - if (searchedURL === this.searchURL) { - this.repos = json.data; - const count = response.headers.get('X-Total-Count'); - if (searchedQuery === '' && searchedMode === '' && this.archivedFilter === 'both') { - this.reposTotalCount = count; - } - this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`] = count; - this.finalPage = Math.ceil(count / this.searchLimit); - this.updateHistory(); - this.isLoading = false; - } - }, - - repoIcon(repo) { - if (repo.fork) { - return 'octicon-repo-forked'; - } else if (repo.mirror) { - return 'octicon-mirror'; - } else if (repo.template) { - return `octicon-repo-template`; - } else if (repo.private) { - return 'octicon-lock'; - } else if (repo.internal) { - return 'octicon-repo'; - } - return 'octicon-repo'; - } - }, - - template: document.getElementById('dashboard-repo-list-template'), - }); -} - -export function initDashboardRepoList() { - const el = document.getElementById('dashboard-repo-list'); - const dashboardRepoListData = pageData.dashboardRepoList || null; - if (!el || !dashboardRepoListData) return; - - const app = createApp({ - delimiters: vueDelimiters, - data() { - return { - searchLimit: dashboardRepoListData.searchLimit || 0, - subUrl: appSubUrl, - uid: dashboardRepoListData.uid || 0, - }; - }, - }); - initVueSvg(app); - initVueComponents(app); - app.mount(el); -} diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue new file mode 100644 index 000000000000..e295910fd086 --- /dev/null +++ b/web_src/js/components/DashboardRepoList.vue @@ -0,0 +1,432 @@ + + + diff --git a/web_src/js/components/RepoActivityTopAuthors.vue b/web_src/js/components/RepoActivityTopAuthors.vue index 37b6df91878f..294ee6f7bcce 100644 --- a/web_src/js/components/RepoActivityTopAuthors.vue +++ b/web_src/js/components/RepoActivityTopAuthors.vue @@ -51,7 +51,7 @@ diff --git a/web_src/js/components/RepoBranchTagDropdown.js b/web_src/js/components/RepoBranchTagDropdown.js index e1bf35c1294d..a8945b82d162 100644 --- a/web_src/js/components/RepoBranchTagDropdown.js +++ b/web_src/js/components/RepoBranchTagDropdown.js @@ -1,6 +1,5 @@ import {createApp, nextTick} from 'vue'; import $ from 'jquery'; -import {vueDelimiters} from './VueComponentLoader.js'; export function initRepoBranchTagDropdown(selector) { $(selector).each(function (dropdownIndex, elRoot) { @@ -39,7 +38,7 @@ export function initRepoBranchTagDropdown(selector) { } const view = createApp({ - delimiters: vueDelimiters, + delimiters: ['${', '}'], data() { return data; }, diff --git a/web_src/js/components/VueComponentLoader.js b/web_src/js/components/VueComponentLoader.js deleted file mode 100644 index 33ebf95eff96..000000000000 --- a/web_src/js/components/VueComponentLoader.js +++ /dev/null @@ -1,49 +0,0 @@ -import {createApp} from 'vue'; -import {svgs} from '../svg.js'; - -export const vueDelimiters = ['${', '}']; - -let vueEnvInited = false; -export function initVueEnv() { - if (vueEnvInited) return; - vueEnvInited = true; - - // As far as I could tell, this is no longer possible. - // But there seem not to be a guide what to do instead. - // const isProd = window.config.runModeIsProd; - // Vue.config.devtools = !isProd; -} - -let vueSvgInited = false; -export function initVueSvg(app) { - if (vueSvgInited) return; - vueSvgInited = true; - - // register svg icon vue components, e.g. - for (const [name, htmlString] of Object.entries(svgs)) { - const template = htmlString - .replace(/height="[0-9]+"/, 'v-bind:height="size"') - .replace(/width="[0-9]+"/, 'v-bind:width="size"'); - - app.component(name, { - props: { - size: { - type: String, - default: '16', - }, - }, - template, - }); - } -} - -export function initVueApp(el, opts = {}) { - if (typeof el === 'string') { - el = document.querySelector(el); - } - if (!el) return null; - - return createApp( - {delimiters: vueDelimiters, ...opts} - ).mount(el); -} diff --git a/web_src/js/index.js b/web_src/js/index.js index 6b4f4ef3ebe1..480661118bc6 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -2,9 +2,8 @@ import './bootstrap.js'; import $ from 'jquery'; -import {initVueEnv} from './components/VueComponentLoader.js'; import {initRepoActivityTopAuthorsChart} from './components/RepoActivityTopAuthors.vue'; -import {initDashboardRepoList} from './components/DashboardRepoList.js'; +import {initDashboardRepoList} from './components/DashboardRepoList.vue'; import {attachTribute} from './features/tribute.js'; import {initGlobalCopyToClipboardListener} from './features/clipboard.js'; @@ -100,7 +99,6 @@ $.fn.tab.settings.silent = true; // Disable the behavior of fomantic to toggle the checkbox when you press enter on a checkbox element. $.fn.checkbox.settings.enableEnterKey = false; -initVueEnv(); $(document).ready(() => { initGlobalCommon(); diff --git a/web_src/js/svg.js b/web_src/js/svg.js index 6476f16bfb3e..9eabca3fd3f4 100644 --- a/web_src/js/svg.js +++ b/web_src/js/svg.js @@ -31,8 +31,17 @@ import octiconSkip from '../../public/img/svg/octicon-skip.svg'; import octiconMeter from '../../public/img/svg/octicon-meter.svg'; import octiconBlocked from '../../public/img/svg/octicon-blocked.svg'; import octiconSync from '../../public/img/svg/octicon-sync.svg'; +import octiconFilter from '../../public/img/svg/octicon-filter.svg'; +import octiconPlus from '../../public/img/svg/octicon-plus.svg'; +import octiconSearch from '../../public/img/svg/octicon-search.svg'; +import octiconArchive from '../../public/img/svg/octicon-archive.svg'; +import octiconStar from '../../public/img/svg/octicon-star.svg'; +import giteaDoubleChevronLeft from '../../public/img/svg/gitea-double-chevron-left.svg'; +import giteaDoubleChevronRight from '../../public/img/svg/gitea-double-chevron-right.svg'; +import octiconChevronLeft from '../../public/img/svg/octicon-chevron-left.svg'; +import octiconOrganization from '../../public/img/svg/octicon-organization.svg'; -export const svgs = { +const svgs = { 'octicon-blocked': octiconBlocked, 'octicon-check-circle-fill': octiconCheckCircleFill, 'octicon-chevron-down': octiconChevronDown, @@ -66,14 +75,25 @@ export const svgs = { 'octicon-triangle-down': octiconTriangleDown, 'octicon-x': octiconX, 'octicon-x-circle-fill': octiconXCircleFill, + 'octicon-filter': octiconFilter, + 'octicon-plus': octiconPlus, + 'octicon-search': octiconSearch, + 'octicon-archive': octiconArchive, + 'octicon-star': octiconStar, + 'gitea-double-chevron-left': giteaDoubleChevronLeft, + 'gitea-double-chevron-right': giteaDoubleChevronRight, + 'octicon-chevron-left': octiconChevronLeft, + 'octicon-organization': octiconOrganization, }; +// TODO: use a more general approach to access SVG icons. At the moment, developers must check, pick and fill the names manually, most of the SVG icons in assets couldn't be used directly. + const parser = new DOMParser(); const serializer = new XMLSerializer(); -// retrieve a HTML string for given SVG icon name, size and additional classes +// retrieve an HTML string for given SVG icon name, size and additional classes export function svg(name, size = 16, className = '') { - if (!(name in svgs)) return ''; + if (!(name in svgs)) throw new Error(`Unknown SVG icon: ${name}`); if (size === 16 && !className) return svgs[name]; const document = parser.parseFromString(svgs[name], 'image/svg+xml'); From 0efa9d564941e6539df98ed4ddd906a05c1fa7e7 Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Tue, 14 Mar 2023 00:10:01 -0400 Subject: [PATCH 10/11] fix markdown lint issue (#23457) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI is failing with the following: ``` docs/content/doc/features/localization.zh-cn.md:16 MD022/blanks-around-headings/blanks-around-headers Headings should be surrounded by blank lines [Expected: 1; Actual: 0; Below] [Context: "# 本地化"] docs/content/doc/features/localization.zh-cn.md:23 MD022/blanks-around-headings/blanks-around-headers Headings should be surrounded by blank lines [Expected: 1; Actual: 0; Below] [Context: "## 支持的语言"] ``` This fixes that error --- docs/content/doc/features/localization.zh-cn.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/content/doc/features/localization.zh-cn.md b/docs/content/doc/features/localization.zh-cn.md index 44f9537f31b4..dd2dc1fa90f6 100644 --- a/docs/content/doc/features/localization.zh-cn.md +++ b/docs/content/doc/features/localization.zh-cn.md @@ -14,6 +14,7 @@ menu: --- # 本地化 + Gitea的本地化是通过我们的[Crowdin项目](https://crowdin.com/project/gitea)进行的。 对于对**英语翻译**的更改,可以发出pull-request,来更改[英语语言环境](https://github.com/go-gitea/gitea/blob/master/options/locale/locale_en-US.ini)中合适的关键字。 @@ -21,6 +22,7 @@ Gitea的本地化是通过我们的[Crowdin项目](https://crowdin.com/project/g 有关对**非英语**翻译的更改,请参阅上面的 Crowdin 项目。 ## 支持的语言 + 上述 Crowdin 项目中列出的任何语言一旦翻译了 25% 或更多都将得到支持。 翻译被接受后,它将在下一次 Crowdin 同步后反映在主存储库中,这通常是在任何 PR 合并之后。 From 6ff5400af91aefb02cbc7dd59f6be23cc2bf7865 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Tue, 14 Mar 2023 13:11:38 +0800 Subject: [PATCH 11/11] Make branches list page operations remember current page (#23420) Close #23411 Always pass "page" query parameter to backend, and make backend respect it. The `ctx.FormInt("limit")` is never used, so removed. --------- Co-authored-by: Jason Song Co-authored-by: Lunny Xiao --- modules/context/pagination.go | 7 ++++--- routers/web/repo/branch.go | 17 +++++++---------- templates/repo/branch/list.tmpl | 8 ++++---- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/modules/context/pagination.go b/modules/context/pagination.go index 3effd88f109c..5a88c92053aa 100644 --- a/modules/context/pagination.go +++ b/modules/context/pagination.go @@ -18,10 +18,11 @@ type Pagination struct { urlParams []string } -// NewPagination creates a new instance of the Pagination struct -func NewPagination(total, page, issueNum, numPages int) *Pagination { +// NewPagination creates a new instance of the Pagination struct. +// "pagingNum" is "page size" or "limit", "current" is "page" +func NewPagination(total, pagingNum, current, numPages int) *Pagination { p := &Pagination{} - p.Paginater = paginator.New(total, page, issueNum, numPages) + p.Paginater = paginator.New(total, pagingNum, current, numPages) return p } diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go index d23367e04790..9f2663431112 100644 --- a/routers/web/repo/branch.go +++ b/routers/web/repo/branch.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "net/http" + "net/url" "strings" "code.gitea.io/gitea/models" @@ -65,21 +66,17 @@ func Branches(ctx *context.Context) { if page <= 1 { page = 1 } + pageSize := setting.Git.BranchesRangeSize - limit := ctx.FormInt("limit") - if limit <= 0 || limit > setting.Git.BranchesRangeSize { - limit = setting.Git.BranchesRangeSize - } - - skip := (page - 1) * limit - log.Debug("Branches: skip: %d limit: %d", skip, limit) - defaultBranchBranch, branches, branchesCount := loadBranches(ctx, skip, limit) + skip := (page - 1) * pageSize + log.Debug("Branches: skip: %d limit: %d", skip, pageSize) + defaultBranchBranch, branches, branchesCount := loadBranches(ctx, skip, pageSize) if ctx.Written() { return } ctx.Data["Branches"] = branches ctx.Data["DefaultBranchBranch"] = defaultBranchBranch - pager := context.NewPagination(branchesCount, setting.Git.BranchesRangeSize, page, 5) + pager := context.NewPagination(branchesCount, pageSize, page, 5) pager.SetDefaultParams(ctx) ctx.Data["Page"] = pager @@ -165,7 +162,7 @@ func RestoreBranchPost(ctx *context.Context) { func redirect(ctx *context.Context) { ctx.JSON(http.StatusOK, map[string]interface{}{ - "redirect": ctx.Repo.RepoLink + "/branches", + "redirect": ctx.Repo.RepoLink + "/branches?page=" + url.QueryEscape(ctx.FormString("page")), }) } diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl index 7e8bf348a4d6..898be4d6bb98 100644 --- a/templates/repo/branch/list.tmpl +++ b/templates/repo/branch/list.tmpl @@ -81,9 +81,9 @@ {{if not .LatestPullRequest}} {{if .IsIncluded}} - + {{svg "octicon-git-pull-request"}} {{$.locale.Tr "repo.branch.included"}} - + {{else if and (not .IsDeleted) $.AllowsPulls (gt .CommitsAhead 0)}} @@ -123,13 +123,13 @@ {{end}} {{if and $.IsWriter (not $.IsMirror) (not $.Repository.IsArchived) (not .IsProtected)}} {{if .IsDeleted}} - {{else}} - {{end}}