From cdc9e91750036fc370db65a44618f3139db11ae1 Mon Sep 17 00:00:00 2001
From: FuXiaoHei
Date: Mon, 13 Mar 2023 18:23:51 +0800
Subject: [PATCH 01/94] add path prefix to ObjectStorage.Iterator (#23332)
Support to iterator subdirectory in ObjectStorage for
ObjectStorage.Iterator method.
It's required for https://github.com/go-gitea/gitea/pull/22738 to make
artifact files cleanable.
---------
Co-authored-by: Jason Song
Co-authored-by: Lunny Xiao
---
cmd/dump.go | 6 ++---
modules/doctor/storage.go | 2 +-
modules/storage/helper.go | 2 +-
modules/storage/helper_test.go | 2 +-
modules/storage/local.go | 8 +++++--
modules/storage/local_test.go | 42 ++++++++++++++++++++++++++++++++++
modules/storage/minio.go | 12 +++++++---
modules/storage/storage.go | 4 ++--
tests/test_utils.go | 4 ++--
9 files changed, 67 insertions(+), 15 deletions(-)
diff --git a/cmd/dump.go b/cmd/dump.go
index c802849f8e..00d279b991 100644
--- a/cmd/dump.go
+++ b/cmd/dump.go
@@ -250,7 +250,7 @@ func runDump(ctx *cli.Context) error {
if ctx.IsSet("skip-lfs-data") && ctx.Bool("skip-lfs-data") {
log.Info("Skip dumping LFS data")
- } else if err := storage.LFS.IterateObjects(func(objPath string, object storage.Object) error {
+ } else if err := storage.LFS.IterateObjects("", func(objPath string, object storage.Object) error {
info, err := object.Stat()
if err != nil {
return err
@@ -351,7 +351,7 @@ func runDump(ctx *cli.Context) error {
if ctx.IsSet("skip-attachment-data") && ctx.Bool("skip-attachment-data") {
log.Info("Skip dumping attachment data")
- } else if err := storage.Attachments.IterateObjects(func(objPath string, object storage.Object) error {
+ } else if err := storage.Attachments.IterateObjects("", func(objPath string, object storage.Object) error {
info, err := object.Stat()
if err != nil {
return err
@@ -364,7 +364,7 @@ func runDump(ctx *cli.Context) error {
if ctx.IsSet("skip-package-data") && ctx.Bool("skip-package-data") {
log.Info("Skip dumping package data")
- } else if err := storage.Packages.IterateObjects(func(objPath string, object storage.Object) error {
+ } else if err := storage.Packages.IterateObjects("", func(objPath string, object storage.Object) error {
info, err := object.Stat()
if err != nil {
return err
diff --git a/modules/doctor/storage.go b/modules/doctor/storage.go
index aa987de447..c20566d675 100644
--- a/modules/doctor/storage.go
+++ b/modules/doctor/storage.go
@@ -31,7 +31,7 @@ func commonCheckStorage(ctx context.Context, logger log.Logger, autofix bool, op
totalSize, orphanedSize := int64(0), int64(0)
var pathsToDelete []string
- if err := opts.storer.IterateObjects(func(p string, obj storage.Object) error {
+ if err := opts.storer.IterateObjects("", func(p string, obj storage.Object) error {
defer obj.Close()
totalCount++
diff --git a/modules/storage/helper.go b/modules/storage/helper.go
index 1ab99d98b3..d1959830b9 100644
--- a/modules/storage/helper.go
+++ b/modules/storage/helper.go
@@ -90,6 +90,6 @@ func (s discardStorage) URL(_, _ string) (*url.URL, error) {
return nil, fmt.Errorf("%s", s)
}
-func (s discardStorage) IterateObjects(_ func(string, Object) error) error {
+func (s discardStorage) IterateObjects(_ string, _ func(string, Object) error) error {
return fmt.Errorf("%s", s)
}
diff --git a/modules/storage/helper_test.go b/modules/storage/helper_test.go
index 7d74671c54..f4c2d0467f 100644
--- a/modules/storage/helper_test.go
+++ b/modules/storage/helper_test.go
@@ -42,7 +42,7 @@ func Test_discardStorage(t *testing.T) {
assert.Errorf(t, err, string(tt))
}
{
- err := tt.IterateObjects(func(_ string, _ Object) error { return nil })
+ err := tt.IterateObjects("", func(_ string, _ Object) error { return nil })
assert.Error(t, err, string(tt))
}
})
diff --git a/modules/storage/local.go b/modules/storage/local.go
index 05bf1fb28a..15f5761e8f 100644
--- a/modules/storage/local.go
+++ b/modules/storage/local.go
@@ -127,8 +127,12 @@ func (l *LocalStorage) URL(path, name string) (*url.URL, error) {
}
// IterateObjects iterates across the objects in the local storage
-func (l *LocalStorage) IterateObjects(fn func(path string, obj Object) error) error {
- return filepath.WalkDir(l.dir, func(path string, d os.DirEntry, err error) error {
+func (l *LocalStorage) IterateObjects(prefix string, fn func(path string, obj Object) error) error {
+ dir := l.dir
+ if prefix != "" {
+ dir = filepath.Join(l.dir, util.CleanPath(prefix))
+ }
+ return filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
diff --git a/modules/storage/local_test.go b/modules/storage/local_test.go
index 994c54e859..2b112df8f1 100644
--- a/modules/storage/local_test.go
+++ b/modules/storage/local_test.go
@@ -4,6 +4,10 @@
package storage
import (
+ "bytes"
+ "context"
+ "os"
+ "path/filepath"
"testing"
"github.com/stretchr/testify/assert"
@@ -50,3 +54,41 @@ func TestBuildLocalPath(t *testing.T) {
})
}
}
+
+func TestLocalStorageIterator(t *testing.T) {
+ dir := filepath.Join(os.TempDir(), "TestLocalStorageIteratorTestDir")
+ l, err := NewLocalStorage(context.Background(), LocalStorageConfig{Path: dir})
+ assert.NoError(t, err)
+
+ testFiles := [][]string{
+ {"a/1.txt", "a1"},
+ {"/a/1.txt", "aa1"}, // same as above, but with leading slash that will be trim
+ {"b/1.txt", "b1"},
+ {"b/2.txt", "b2"},
+ {"b/3.txt", "b3"},
+ {"b/x 4.txt", "bx4"},
+ }
+ for _, f := range testFiles {
+ _, err = l.Save(f[0], bytes.NewBufferString(f[1]), -1)
+ assert.NoError(t, err)
+ }
+
+ expectedList := map[string][]string{
+ "a": {"a/1.txt"},
+ "b": {"b/1.txt", "b/2.txt", "b/3.txt", "b/x 4.txt"},
+ "": {"a/1.txt", "b/1.txt", "b/2.txt", "b/3.txt", "b/x 4.txt"},
+ "/": {"a/1.txt", "b/1.txt", "b/2.txt", "b/3.txt", "b/x 4.txt"},
+ "a/b/../../a": {"a/1.txt"},
+ }
+ for dir, expected := range expectedList {
+ count := 0
+ err = l.IterateObjects(dir, func(path string, f Object) error {
+ defer f.Close()
+ assert.Contains(t, expected, path)
+ count++
+ return nil
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, count, len(expected))
+ }
+}
diff --git a/modules/storage/minio.go b/modules/storage/minio.go
index 24da14b634..8cc06bcdd3 100644
--- a/modules/storage/minio.go
+++ b/modules/storage/minio.go
@@ -209,12 +209,18 @@ func (m *MinioStorage) URL(path, name string) (*url.URL, error) {
}
// IterateObjects iterates across the objects in the miniostorage
-func (m *MinioStorage) IterateObjects(fn func(path string, obj Object) error) error {
+func (m *MinioStorage) IterateObjects(prefix string, fn func(path string, obj Object) error) error {
opts := minio.GetObjectOptions{}
lobjectCtx, cancel := context.WithCancel(m.ctx)
defer cancel()
+
+ basePath := m.basePath
+ if prefix != "" {
+ basePath = m.buildMinioPath(prefix)
+ }
+
for mObjInfo := range m.client.ListObjects(lobjectCtx, m.bucket, minio.ListObjectsOptions{
- Prefix: m.basePath,
+ Prefix: basePath,
Recursive: true,
}) {
object, err := m.client.GetObject(lobjectCtx, m.bucket, mObjInfo.Key, opts)
@@ -223,7 +229,7 @@ func (m *MinioStorage) IterateObjects(fn func(path string, obj Object) error) er
}
if err := func(object *minio.Object, fn func(path string, obj Object) error) error {
defer object.Close()
- return fn(strings.TrimPrefix(mObjInfo.Key, m.basePath), &minioObject{object})
+ return fn(strings.TrimPrefix(mObjInfo.Key, basePath), &minioObject{object})
}(object, fn); err != nil {
return convertMinioErr(err)
}
diff --git a/modules/storage/storage.go b/modules/storage/storage.go
index d8998b1922..caecab306e 100644
--- a/modules/storage/storage.go
+++ b/modules/storage/storage.go
@@ -65,7 +65,7 @@ type ObjectStorage interface {
Stat(path string) (os.FileInfo, error)
Delete(path string) error
URL(path, name string) (*url.URL, error)
- IterateObjects(func(path string, obj Object) error) error
+ IterateObjects(path string, iterator func(path string, obj Object) error) error
}
// Copy copies a file from source ObjectStorage to dest ObjectStorage
@@ -87,7 +87,7 @@ func Copy(dstStorage ObjectStorage, dstPath string, srcStorage ObjectStorage, sr
// Clean delete all the objects in this storage
func Clean(storage ObjectStorage) error {
- return storage.IterateObjects(func(path string, obj Object) error {
+ return storage.IterateObjects("", func(path string, obj Object) error {
_ = obj.Close()
return storage.Delete(path)
})
diff --git a/tests/test_utils.go b/tests/test_utils.go
index e3e5becfbe..102dd3d298 100644
--- a/tests/test_utils.go
+++ b/tests/test_utils.go
@@ -201,7 +201,7 @@ func PrepareTestEnv(t testing.TB, skip ...int) func() {
lfsFixtures, err := storage.NewStorage("", storage.LocalStorageConfig{Path: path.Join(filepath.Dir(setting.AppPath), "tests/gitea-lfs-meta")})
assert.NoError(t, err)
assert.NoError(t, storage.Clean(storage.LFS))
- assert.NoError(t, lfsFixtures.IterateObjects(func(path string, _ storage.Object) error {
+ assert.NoError(t, lfsFixtures.IterateObjects("", func(path string, _ storage.Object) error {
_, err := storage.Copy(storage.LFS, path, lfsFixtures, path)
return err
}))
@@ -258,7 +258,7 @@ func ResetFixtures(t *testing.T) {
lfsFixtures, err := storage.NewStorage("", storage.LocalStorageConfig{Path: path.Join(filepath.Dir(setting.AppPath), "tests/gitea-lfs-meta")})
assert.NoError(t, err)
assert.NoError(t, storage.Clean(storage.LFS))
- assert.NoError(t, lfsFixtures.IterateObjects(func(path string, _ storage.Object) error {
+ assert.NoError(t, lfsFixtures.IterateObjects("", func(path string, _ storage.Object) error {
_, err := storage.Copy(storage.LFS, path, lfsFixtures, path)
return err
}))
From d74a7efb60f94a4b8e6e5f65332f94f1be31b761 Mon Sep 17 00:00:00 2001
From: yp05327 <576951401@qq.com>
Date: Mon, 13 Mar 2023 20:31:41 +0900
Subject: [PATCH 02/94] Use context for `RepositoryList.LoadAttributes`
(#23435)
---
models/repo/repo_list.go | 10 +++-------
routers/api/v1/user/repo.go | 2 +-
routers/web/user/notification.go | 2 +-
3 files changed, 5 insertions(+), 9 deletions(-)
diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go
index d9cd905a19..92b9c15b4b 100644
--- a/models/repo/repo_list.go
+++ b/models/repo/repo_list.go
@@ -62,7 +62,8 @@ func RepositoryListOfMap(repoMap map[int64]*Repository) RepositoryList {
return RepositoryList(ValuesRepository(repoMap))
}
-func (repos RepositoryList) loadAttributes(ctx context.Context) error {
+// LoadAttributes loads the attributes for the given RepositoryList
+func (repos RepositoryList) LoadAttributes(ctx context.Context) error {
if len(repos) == 0 {
return nil
}
@@ -107,11 +108,6 @@ func (repos RepositoryList) loadAttributes(ctx context.Context) error {
return nil
}
-// LoadAttributes loads the attributes for the given RepositoryList
-func (repos RepositoryList) LoadAttributes() error {
- return repos.loadAttributes(db.DefaultContext)
-}
-
// SearchRepoOptions holds the search options
type SearchRepoOptions struct {
db.ListOptions
@@ -547,7 +543,7 @@ func SearchRepositoryByCondition(ctx context.Context, opts *SearchRepoOptions, c
}
if loadAttributes {
- if err := repos.loadAttributes(ctx); err != nil {
+ if err := repos.LoadAttributes(ctx); err != nil {
return nil, 0, fmt.Errorf("LoadAttributes: %w", err)
}
}
diff --git a/routers/api/v1/user/repo.go b/routers/api/v1/user/repo.go
index dcb14780a7..7a8978cc4e 100644
--- a/routers/api/v1/user/repo.go
+++ b/routers/api/v1/user/repo.go
@@ -31,7 +31,7 @@ func listUserRepos(ctx *context.APIContext, u *user_model.User, private bool) {
return
}
- if err := repos.LoadAttributes(); err != nil {
+ if err := repos.LoadAttributes(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "RepositoryList.LoadAttributes", err)
return
}
diff --git a/routers/web/user/notification.go b/routers/web/user/notification.go
index e96b3dd27a..e12b41e649 100644
--- a/routers/web/user/notification.go
+++ b/routers/web/user/notification.go
@@ -117,7 +117,7 @@ func getNotifications(ctx *context.Context) {
return
}
notifications = notifications.Without(failures)
- if err := repos.LoadAttributes(); err != nil { // TODO
+ if err := repos.LoadAttributes(ctx); err != nil {
ctx.ServerError("LoadAttributes", err)
return
}
From 0a6f6354bbc03d4a3ad382a432113301e9ea9d86 Mon Sep 17 00:00:00 2001
From: John Olheiser
Date: Mon, 13 Mar 2023 14:41:38 -0500
Subject: [PATCH 03/94] 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 1fbdab3e55..4192d8654d 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 cb88e175ea..b64f6dcd87 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 04/94] Add Swift package registry (#22404)
This PR adds a [Swift](https://www.swift.org/) package registry.

---
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 b2b5af0af8..e53ed7ad9f 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 b67d6cdf5f..4b9c519cd8 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 f93fec6393..08da8ced48 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 0000000000..61a4c9a55d
--- /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 f4be21e74e..06699b5d57 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 32f30fab9b..ccc9257c31 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 0000000000..24c4262ab7
--- /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 0000000000..3913c2355b
--- /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 13599e5a63..ac0ad62bca 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 677af1397d..e793c3ef03 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 0000000000..ebfea951da
--- /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 0e3d8b7a02..c0c7b117f6 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 0000000000..f78f703778
--- /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 ab077090d1..200dc5aaf1 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 b22ed47c77..699d0fe44f 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 3abca7337c..dd5c63470b 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 0000000000..3ff06483b8
--- /dev/null
+++ b/templates/package/content/swift.tmpl
@@ -0,0 +1,40 @@
+{{if eq .PackageDescriptor.Package.Type "swift"}}
+
+
+
+ {{if .PackageDescriptor.Metadata.Description}}
+
+
+ {{if .PackageDescriptor.Metadata.Description}}{{.PackageDescriptor.Metadata.Description}}{{end}}
+
+ {{end}}
+
+ {{if .PackageDescriptor.Metadata.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 0000000000..8a9ab071fc
--- /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}}{{end}}
+{{end}}
diff --git a/templates/package/view.tmpl b/templates/package/view.tmpl
index 839d9cf21a..b2a2fb1e5d 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 b64f6dcd87..9c46b25eaf 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 0000000000..a3035ea604
--- /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 0000000000..8af43d32e4
--- /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 05/94] 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.

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.

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



### Other
In this PR, I also fix the problem of missing icon which represents
running in PRs list.


---
models/actions/run.go | 11 +++++
services/actions/commit_status.go | 75 ++++++++++++++++++++-----------
templates/repo/commit_status.tmpl | 3 ++
3 files changed, 64 insertions(+), 25 deletions(-)
diff --git a/models/actions/run.go b/models/actions/run.go
index d5ab45a519..a711cfee2e 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 4f31349352..84de106eec 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 {
+ var (
+ sha string
+ creatorID int64
+ )
+
+ 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")
+ }
+
+ 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
}
- 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")
- }
-
- creator, err := user_model.GetUserByID(ctx, payload.Pusher.ID)
+ repo := run.Repo
+ ctxname := job.Name
+ state := toCommitStatus(job.Status)
+ creator, err := user_model.GetUserByID(ctx, creatorID)
if err != nil {
return fmt.Errorf("GetUserByID: %w", err)
}
-
- repo := run.Repo
- sha := payload.HeadCommit.ID
- ctxname := job.Name
- state := toCommitStatus(job.Status)
-
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 fbf064527d..470869b381 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 06/94] 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 83ed06e49b..a4895f260b 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 397600dc50..16608e5bbb 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 9c46b25eaf..c304a7a497 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 07/94] 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 0000000000..533a0d32f0
--- /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 08/94] [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 55529df882..b34b5642dc 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 09/94] Convert `
-
{{.locale.Tr "admin.emails.change_email_text"}}
+
{{.locale.Tr "admin.emails.change_email_text"}}
diff --git a/templates/admin/notice.tmpl b/templates/admin/notice.tmpl
index a2c7ca2f6a..34bd83a214 100644
--- a/templates/admin/notice.tmpl
+++ b/templates/admin/notice.tmpl
@@ -23,7 +23,7 @@
-
+
{{.ID}}
@@ -39,13 +39,11 @@
-
-
-
-
diff --git a/templates/admin/repo/unadopted.tmpl b/templates/admin/repo/unadopted.tmpl
index 2629056d1d..7ff41d503a 100644
--- a/templates/admin/repo/unadopted.tmpl
+++ b/templates/admin/repo/unadopted.tmpl
@@ -43,16 +43,7 @@
-
-
- {{svg "octicon-trash" 16 "gt-mr-2"}}
- {{$.locale.Tr "modal.no"}}
-
-
- {{svg "octicon-check" 16 "gt-mr-2"}}
- {{$.locale.Tr "modal.yes"}}
-
-
+ {{template "base/delete_modal_actions" .}}
{{svg "octicon-x"}} {{$.locale.Tr "repo.delete_preexisting_label"}}
@@ -70,16 +61,7 @@
-
-
- {{svg "octicon-trash" 16 "gt-mr-2"}}
- {{$.locale.Tr "modal.no"}}
-
-
- {{svg "octicon-check" 16 "gt-mr-2"}}
- {{$.locale.Tr "modal.yes"}}
-
-
+ {{template "base/delete_modal_actions" .}}
diff --git a/templates/admin/user/edit.tmpl b/templates/admin/user/edit.tmpl
index 5dd1f531fd..73017e1b13 100644
--- a/templates/admin/user/edit.tmpl
+++ b/templates/admin/user/edit.tmpl
@@ -151,7 +151,7 @@
{{.locale.Tr "admin.users.update_profile"}}
-
{{.locale.Tr "admin.users.delete_account"}}
+
{{.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"}}
-
-
- {{svg "octicon-check"}}
- {{.locale.Tr "modal.yes"}}
-
-
+ {{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 fb4d31270a..29bf5f92fd 100644
--- a/templates/base/delete_modal_actions.tmpl
+++ b/templates/base/delete_modal_actions.tmpl
@@ -1,10 +1,10 @@
-
+
{{svg "octicon-x"}}
{{.locale.Tr "modal.no"}}
-
-
+
+
{{svg "octicon-check"}}
{{.locale.Tr "modal.yes"}}
-
+
diff --git a/templates/org/settings/delete.tmpl b/templates/org/settings/delete.tmpl
index 669e393e1d..69e226f410 100644
--- a/templates/org/settings/delete.tmpl
+++ b/templates/org/settings/delete.tmpl
@@ -19,9 +19,9 @@
{{.locale.Tr "org.org_name_holder"}}
-
+
{{.locale.Tr "org.settings.confirm_delete_account"}}
-
+
diff --git a/templates/org/settings/labels.tmpl b/templates/org/settings/labels.tmpl
index 5436bcba05..e04b391271 100644
--- a/templates/org/settings/labels.tmpl
+++ b/templates/org/settings/labels.tmpl
@@ -11,7 +11,7 @@
-
{{.locale.Tr "repo.issues.new_label"}}
+
{{.locale.Tr "repo.issues.new_label"}}
diff --git a/templates/package/settings.tmpl b/templates/package/settings.tmpl
index dc12fb8207..875bf852bb 100644
--- a/templates/package/settings.tmpl
+++ b/templates/package/settings.tmpl
@@ -57,10 +57,7 @@
diff --git a/templates/projects/list.tmpl b/templates/projects/list.tmpl
index 4a21c0fd28..89c52dee68 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 6867309510..b776f89efa 100644
--- a/templates/projects/view.tmpl
+++ b/templates/projects/view.tmpl
@@ -29,7 +29,7 @@
-
{{$.locale.Tr "settings.cancel"}}
+
{{$.locale.Tr "settings.cancel"}}
{{$.locale.Tr "repo.projects.column.new_submit"}}
@@ -127,7 +127,7 @@
-
{{$.locale.Tr "settings.cancel"}}
+
{{$.locale.Tr "settings.cancel"}}
{{$.locale.Tr "repo.projects.column.edit"}}
@@ -144,7 +144,7 @@
-
{{$.locale.Tr "settings.cancel"}}
+
{{$.locale.Tr "settings.cancel"}}
{{$.locale.Tr "repo.projects.column.set_default"}}
@@ -158,8 +158,8 @@
{{$.locale.Tr "repo.projects.column.deletion_desc"}}
-
-
{{$.locale.Tr "settings.cancel"}}
+
{{/* TODO: convert to base/delete_modal_actions.tmpl */}}
+ {{$.locale.Tr "settings.cancel"}}
{{$.locale.Tr "repo.projects.column.delete"}}
@@ -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 a093c19deb..7e8bf348a4 100644
--- a/templates/repo/branch/list.tmpl
+++ b/templates/repo/branch/list.tmpl
@@ -176,7 +176,7 @@
-
{{.locale.Tr "settings.cancel"}}
+
{{.locale.Tr "settings.cancel"}}
{{.locale.Tr "repo.branch.confirm_create_branch"}}
diff --git a/templates/repo/cite/cite_modal.tmpl b/templates/repo/cite/cite_modal.tmpl
index 185b34173d..f00bab8859 100644
--- a/templates/repo/cite/cite_modal.tmpl
+++ b/templates/repo/cite/cite_modal.tmpl
@@ -15,8 +15,8 @@
-
+
{{.locale.Tr "cancel"}}
-
+
diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl
index f19a4d4223..ace5a41087 100644
--- a/templates/repo/commit_page.tmpl
+++ b/templates/repo/commit_page.tmpl
@@ -96,7 +96,7 @@
-
{{.locale.Tr "settings.cancel"}}
+
{{.locale.Tr "settings.cancel"}}
{{.locale.Tr "repo.branch.confirm_create_branch"}}
@@ -121,7 +121,7 @@
-
{{.locale.Tr "settings.cancel"}}
+
{{.locale.Tr "settings.cancel"}}
{{.locale.Tr "repo.tag.confirm_create_tag"}}
diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl
index afd471368f..e0c58896f0 100644
--- a/templates/repo/diff/box.tmpl
+++ b/templates/repo/diff/box.tmpl
@@ -107,8 +107,8 @@
-
{{.locale.Tr "repo.issues.cancel"}}
-
{{.locale.Tr "repo.issues.save"}}
+
{{.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 992ccee8e4..431033e18e 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"}}
-
+
{{.locale.Tr "repo.editor.cancel"}}
-
-
+
+
{{.locale.Tr "repo.editor.commit_changes"}}
-
+
diff --git a/templates/repo/editor/patch.tmpl b/templates/repo/editor/patch.tmpl
index bbd5c2dbde..75a8b5d687 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"}}
-
-
+
+ {{svg "octicon-x"}}
{{.locale.Tr "repo.editor.cancel"}}
-
-
-
+
+
+ {{svg "fontawesome-save"}}
{{.locale.Tr "repo.editor.commit_changes"}}
-
+
diff --git a/templates/repo/issue/labels.tmpl b/templates/repo/issue/labels.tmpl
index 82cfcd0712..0a25d9c87f 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"}}
+
{{.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 450061e835..38a948172f 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" .}}
@@ -61,11 +52,13 @@
-
+
+ {{svg "octicon-x"}}
{{.locale.Tr "cancel"}}
-
-
+
+
+
{{.locale.Tr "save"}}
-
+
diff --git a/templates/repo/issue/labels/label_new.tmpl b/templates/repo/issue/labels/label_new.tmpl
index 62f7155b74..c937f28e8a 100644
--- a/templates/repo/issue/labels/label_new.tmpl
+++ b/templates/repo/issue/labels/label_new.tmpl
@@ -36,12 +36,15 @@
+
-
+
+ {{svg "octicon-x"}}
{{.locale.Tr "cancel"}}
-
-
+
+
+ {{svg "octicon-check"}}
{{.locale.Tr "repo.issues.create_label"}}
-
+
diff --git a/templates/repo/issue/list.tmpl b/templates/repo/issue/list.tmpl
index ca05264e77..36faf86113 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"}}
+ {{.locale.Tr "repo.issues.action_open"}}
{{else}}
- {{.locale.Tr "repo.issues.action_close"}}
+ {{.locale.Tr "repo.issues.action_close"}}
{{end}}
diff --git a/templates/repo/issue/milestone_issues.tmpl b/templates/repo/issue/milestone_issues.tmpl
index 2918c4359f..b4f036d32a 100644
--- a/templates/repo/issue/milestone_issues.tmpl
+++ b/templates/repo/issue/milestone_issues.tmpl
@@ -149,9 +149,9 @@
-
{{$.locale.Tr "settings.cancel"}}
+
{{$.locale.Tr "settings.cancel"}}
{{$.locale.Tr "ok"}}
diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl
index e58f94aff3..165dca7e0c 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"}}
+
{{.locale.Tr "repo.issues.add_time_short"}}
+
{{.locale.Tr "repo.issues.add_time_cancel"}}
{{.locale.Tr "repo.issues.add_time_short"}}
@@ -532,14 +532,14 @@
{{end}}
-
+
{{svg "octicon-x"}}
{{.locale.Tr "repo.issues.dependency.cancel"}}
-
-
+
+
{{svg "octicon-check"}}
{{.locale.Tr "repo.issues.dependency.remove"}}
-
+
{{end}}
@@ -619,7 +619,7 @@
{{end}}
-
{{.locale.Tr "settings.cancel"}}
+
{{.locale.Tr "settings.cancel"}}
{{if .Issue.IsLocked}}
{{.locale.Tr "repo.issues.unlock_confirm"}}
@@ -655,7 +655,7 @@
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 6d36a9b45f..49e4467dc3 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 @@
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
diff --git a/templates/repo/issue/view_title.tmpl b/templates/repo/issue/view_title.tmpl
index f0ac1e021e..2a8381b0b4 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)}}
-
- {{.locale.Tr "repo.issues.edit"}}
-
+
{{.locale.Tr "repo.issues.edit"}}
{{end}}
{{RenderIssueTitle $.Context .Issue.Title $.RepoLink $.Repository.ComposeMetas | RenderCodeBlock}}
diff --git a/templates/repo/migrate/migrating.tmpl b/templates/repo/migrate/migrating.tmpl
index a3552610c4..cd3c5e754e 100644
--- a/templates/repo/migrate/migrating.tmpl
+++ b/templates/repo/migrate/migrating.tmpl
@@ -72,7 +72,7 @@
-
{{.locale.Tr "settings.cancel"}}
+
{{.locale.Tr "settings.cancel"}}
{{.locale.Tr "repo.settings.confirm_delete"}}
diff --git a/templates/repo/projects/list.tmpl b/templates/repo/projects/list.tmpl
index f066f84ea2..6833b7d785 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 bef9cb9bf0..0248b9c6d2 100644
--- a/templates/repo/projects/view.tmpl
+++ b/templates/repo/projects/view.tmpl
@@ -33,7 +33,7 @@
-
{{$.locale.Tr "settings.cancel"}}
+
{{$.locale.Tr "settings.cancel"}}
{{$.locale.Tr "repo.projects.column.new_submit"}}
@@ -131,7 +131,7 @@
-
{{$.locale.Tr "settings.cancel"}}
+
{{$.locale.Tr "settings.cancel"}}
{{$.locale.Tr "repo.projects.column.edit"}}
@@ -148,7 +148,7 @@
-
{{$.locale.Tr "settings.cancel"}}
+
{{$.locale.Tr "settings.cancel"}}
{{$.locale.Tr "repo.projects.column.set_default"}}
@@ -162,8 +162,8 @@
{{$.locale.Tr "repo.projects.column.deletion_desc"}}
-
-
{{$.locale.Tr "settings.cancel"}}
+
{{/* TODO: Convert to base/delete_modal_actions.tmpl? */}}
+ {{$.locale.Tr "settings.cancel"}}
{{$.locale.Tr "repo.projects.column.delete"}}
@@ -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 d7c580fed9..8c4df98d19 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}}
-
+ {{.locale.Tr "repo.release.save_draft"}}
{{.locale.Tr "repo.release.publish"}}
@@ -125,9 +125,9 @@
{{end}}
{{else}}
{{if not .tag_name}}
-
+ {{.locale.Tr "repo.release.add_tag"}}
{{end}}
-
+ {{.locale.Tr "repo.release.save_draft"}}
{{.locale.Tr "repo.release.publish"}}
diff --git a/templates/repo/settings/deploy_keys.tmpl b/templates/repo/settings/deploy_keys.tmpl
index 22fddeb4df..ea4fba240e 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"}}
+
{{.locale.Tr "repo.settings.add_deploy_key"}}
{{else}}
-
{{.locale.Tr "settings.ssh_disabled"}}
+
{{.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 566a701efb..9a38d32345 100644
--- a/templates/repo/settings/lfs.tmpl
+++ b/templates/repo/settings/lfs.tmpl
@@ -50,8 +50,8 @@
@@ -856,7 +856,7 @@
-
{{.locale.Tr "settings.cancel"}}
+
{{.locale.Tr "settings.cancel"}}
{{.locale.Tr "repo.settings.convert_fork_confirm"}}
@@ -892,7 +892,7 @@
-
{{.locale.Tr "settings.cancel"}}
+
{{.locale.Tr "settings.cancel"}}
{{.locale.Tr "repo.settings.transfer_perform"}}
@@ -926,7 +926,7 @@
-
{{.locale.Tr "settings.cancel"}}
+
{{.locale.Tr "settings.cancel"}}
{{.locale.Tr "repo.settings.confirm_delete"}}
@@ -958,7 +958,7 @@
-
{{.locale.Tr "settings.cancel"}}
+
{{.locale.Tr "settings.cancel"}}
{{.locale.Tr "repo.settings.confirm_wiki_delete"}}
@@ -988,10 +988,7 @@
{{.CsrfTokenHtml}}
-
-
{{.locale.Tr "settings.cancel"}}
-
{{.locale.Tr "modal.yes"}}
-
+ {{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 fdc49ada4e..f455899663 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 d55bd0150a..12eff6aebe 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"}}
+ {{svg "octicon-x" 16 "close inside"}}
@@ -12,7 +12,7 @@
{{else if .EscapeStatus.HasAmbiguous}}
- {{svg "octicon-x" 16 "close inside"}}
+ {{svg "octicon-x" 16 "close inside"}}
diff --git a/templates/shared/actions/runner_list.tmpl b/templates/shared/actions/runner_list.tmpl
index eabddbb30c..30c52c01b4 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 9105b7ad9b..4aa5f0ccd5 100644
--- a/templates/shared/secrets/add_list.tmpl
+++ b/templates/shared/secrets/add_list.tmpl
@@ -1,7 +1,7 @@
diff --git a/templates/user/auth/grant.tmpl b/templates/user/auth/grant.tmpl
index c906db3e0a..060b675273 100644
--- a/templates/user/auth/grant.tmpl
+++ b/templates/user/auth/grant.tmpl
@@ -23,7 +23,7 @@
-
+
{{.locale.Tr "auth.authorize_application"}}
Cancel
diff --git a/templates/user/auth/webauthn_error.tmpl b/templates/user/auth/webauthn_error.tmpl
index 447d289a28..b6467de1aa 100644
--- a/templates/user/auth/webauthn_error.tmpl
+++ b/templates/user/auth/webauthn_error.tmpl
@@ -17,6 +17,6 @@
{{.locale.Tr "webauthn_reload"}}
-
{{.locale.Tr "cancel"}}
+
{{.locale.Tr "cancel"}}
diff --git a/templates/user/settings/account.tmpl b/templates/user/settings/account.tmpl
index 9a57bd5722..53f7d021e0 100644
--- a/templates/user/settings/account.tmpl
+++ b/templates/user/settings/account.tmpl
@@ -151,9 +151,9 @@
diff --git a/templates/user/settings/applications.tmpl b/templates/user/settings/applications.tmpl
index b0cd37d44c..18132c4a75 100644
--- a/templates/user/settings/applications.tmpl
+++ b/templates/user/settings/applications.tmpl
@@ -276,15 +276,16 @@
{{.locale.Tr "settings.access_token_deletion_desc"}}
-
diff --git a/templates/user/settings/keys_gpg.tmpl b/templates/user/settings/keys_gpg.tmpl
index c80890940a..93ca12a088 100644
--- a/templates/user/settings/keys_gpg.tmpl
+++ b/templates/user/settings/keys_gpg.tmpl
@@ -1,7 +1,7 @@
diff --git a/templates/user/settings/keys_principal.tmpl b/templates/user/settings/keys_principal.tmpl
index cc1152b739..8012b874cd 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"}}
+
{{.locale.Tr "settings.add_new_principal"}}
{{else}}
-
{{.locale.Tr "settings.ssh_disabled"}}
+
{{.locale.Tr "settings.ssh_disabled"}}
{{end}}
diff --git a/templates/user/settings/keys_ssh.tmpl b/templates/user/settings/keys_ssh.tmpl
index 891959d351..1ff4dab34e 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}}
-
+
{{.locale.Tr "settings.add_key"}}
-
+
{{else}}
-
{{.locale.Tr "settings.ssh_disabled"}}
+
{{.locale.Tr "settings.ssh_disabled"}}
{{end}}
diff --git a/templates/user/settings/repos.tmpl b/templates/user/settings/repos.tmpl
index 902b3fb2f3..2e107ca7fa 100644
--- a/templates/user/settings/repos.tmpl
+++ b/templates/user/settings/repos.tmpl
@@ -50,16 +50,7 @@
{{$.CsrfTokenHtml}}
-
-
-
- {{$.locale.Tr "modal.no"}}
-
-
-
- {{$.locale.Tr "modal.yes"}}
-
-
+ {{template "base/delete_modal_actions" .}}
{{end}}
@@ -77,16 +68,7 @@
{{$.CsrfTokenHtml}}
-
-
-
- {{$.locale.Tr "modal.no"}}
-
-
-
- {{$.locale.Tr "modal.yes"}}
-
-
+ {{template "base/delete_modal_actions" .}}
{{end}}
diff --git a/templates/user/settings/security/twofa.tmpl b/templates/user/settings/security/twofa.tmpl
index a4da947628..1a0a8a6432 100644
--- a/templates/user/settings/security/twofa.tmpl
+++ b/templates/user/settings/security/twofa.tmpl
@@ -13,7 +13,7 @@
{{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 d023e0bc36..be5aa876a5 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 4fa6942467..0f36ce2bf8 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 0965caef15..ebc851d676 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 41c9dd118f..a8a27c2572 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 70542ad883..5346a0d274 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 0000000000..763d26abb1
--- /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 10/94] 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 6a6f637326..44f9537f31 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 11/94] 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:



---------
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 97234176bd..0a8f427f9d 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 2328cc83a9..0000000000
--- 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 0000000000..e295910fd0
--- /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 37b6df9187..294ee6f7bc 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 e1bf35c129..a8945b82d1 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 33ebf95eff..0000000000
--- 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 6b4f4ef3eb..480661118b 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 6476f16bfb..9eabca3fd3 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 12/94] 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 44f9537f31..dd2dc1fa90 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 13/94] 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 3effd88f10..5a88c92053 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 d23367e047..9f26634311 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 7e8bf348a4..898be4d6bb 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)}}
{{if $.CanPull}}{{$.locale.Tr "repo.pulls.compare_changes"}}{{else}}{{$.locale.Tr "action.compare_branch"}}{{end}}
@@ -123,13 +123,13 @@
{{end}}
{{if and $.IsWriter (not $.IsMirror) (not $.Repository.IsArchived) (not .IsProtected)}}
{{if .IsDeleted}}
-
+
{{svg "octicon-reply"}}
{{else}}
-
+
{{svg "octicon-trash"}}
{{end}}
From aac07d010f261c00fb3bd9644c71dc108c668c11 Mon Sep 17 00:00:00 2001
From: yp05327 <576951401@qq.com>
Date: Tue, 14 Mar 2023 16:27:03 +0900
Subject: [PATCH 14/94] Add workflow error notification in ui (#23404)


---------
Co-authored-by: techknowlogick
Co-authored-by: techknowlogick
Co-authored-by: Lunny Xiao
---
modules/actions/workflows.go | 40 ++++++++++++++++++++---------
options/locale/locale_en-US.ini | 2 ++
routers/web/repo/actions/actions.go | 26 ++++++++++++++++---
templates/repo/actions/list.tmpl | 12 ++++++++-
4 files changed, 64 insertions(+), 16 deletions(-)
diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go
index 7f0e6e4564..98fc831c31 100644
--- a/modules/actions/workflows.go
+++ b/modules/actions/workflows.go
@@ -44,6 +44,32 @@ func ListWorkflows(commit *git.Commit) (git.Entries, error) {
return ret, nil
}
+func GetContentFromEntry(entry *git.TreeEntry) ([]byte, error) {
+ f, err := entry.Blob().DataAsync()
+ if err != nil {
+ return nil, err
+ }
+ content, err := io.ReadAll(f)
+ _ = f.Close()
+ if err != nil {
+ return nil, err
+ }
+ return content, nil
+}
+
+func GetEventsFromContent(content []byte) ([]*jobparser.Event, error) {
+ workflow, err := model.ReadWorkflow(bytes.NewReader(content))
+ if err != nil {
+ return nil, err
+ }
+ events, err := jobparser.ParseRawOn(&workflow.RawOn)
+ if err != nil {
+ return nil, err
+ }
+
+ return events, nil
+}
+
func DetectWorkflows(commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader) (map[string][]byte, error) {
entries, err := ListWorkflows(commit)
if err != nil {
@@ -52,21 +78,11 @@ func DetectWorkflows(commit *git.Commit, triggedEvent webhook_module.HookEventTy
workflows := make(map[string][]byte, len(entries))
for _, entry := range entries {
- f, err := entry.Blob().DataAsync()
+ content, err := GetContentFromEntry(entry)
if err != nil {
return nil, err
}
- content, err := io.ReadAll(f)
- _ = f.Close()
- if err != nil {
- return nil, err
- }
- workflow, err := model.ReadWorkflow(bytes.NewReader(content))
- if err != nil {
- log.Warn("ignore invalid workflow %q: %v", entry.Name(), err)
- continue
- }
- events, err := jobparser.ParseRawOn(&workflow.RawOn)
+ events, err := GetEventsFromContent(content)
if err != nil {
log.Warn("ignore invalid workflow %q: %v", entry.Name(), err)
continue
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index e793c3ef03..afcf9ade04 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -3360,5 +3360,7 @@ runs.open_tab = %d Open
runs.closed_tab = %d Closed
runs.commit = Commit
runs.pushed_by = Pushed by
+runs.valid_workflow_helper = Workflow config file is valid.
+runs.invalid_workflow_helper = Workflow config file is invalid. Please check your config file: %s
need_approval_desc = Need approval to run workflows for fork pull request.
diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go
index 0e7a95ed07..dd2dc55bd5 100644
--- a/routers/web/repo/actions/actions.go
+++ b/routers/web/repo/actions/actions.go
@@ -23,6 +23,12 @@ const (
tplViewActions base.TplName = "repo/actions/view"
)
+type Workflow struct {
+ Entry git.TreeEntry
+ IsInvalid bool
+ ErrMsg string
+}
+
// MustEnableActions check if actions are enabled in settings
func MustEnableActions(ctx *context.Context) {
if !setting.Actions.Enabled {
@@ -47,7 +53,7 @@ func List(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("actions.actions")
ctx.Data["PageIsActions"] = true
- var workflows git.Entries
+ var workflows []Workflow
if empty, err := ctx.Repo.GitRepo.IsEmpty(); err != nil {
ctx.Error(http.StatusInternalServerError, err.Error())
return
@@ -62,13 +68,27 @@ func List(ctx *context.Context) {
ctx.Error(http.StatusInternalServerError, err.Error())
return
}
- workflows, err = actions.ListWorkflows(commit)
+ entries, err := actions.ListWorkflows(commit)
if err != nil {
ctx.Error(http.StatusInternalServerError, err.Error())
return
}
+ workflows = make([]Workflow, 0, len(entries))
+ for _, entry := range entries {
+ workflow := Workflow{Entry: *entry}
+ content, err := actions.GetContentFromEntry(entry)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, err.Error())
+ return
+ }
+ _, err = actions.GetEventsFromContent(content)
+ if err != nil {
+ workflow.IsInvalid = true
+ workflow.ErrMsg = err.Error()
+ }
+ workflows = append(workflows, workflow)
+ }
}
-
ctx.Data["workflows"] = workflows
ctx.Data["RepoLink"] = ctx.Repo.Repository.Link()
diff --git a/templates/repo/actions/list.tmpl b/templates/repo/actions/list.tmpl
index c5abff5251..5e8313fa5e 100644
--- a/templates/repo/actions/list.tmpl
+++ b/templates/repo/actions/list.tmpl
@@ -9,7 +9,17 @@
{{.locale.Tr "actions.runs.all_workflows"}}
{{range .workflows}}
- {{.Name}}
+ {{.Entry.Name}}
+ {{if .IsInvalid}}
+
+
+
+ {{else}}
+
+
+
+ {{end}}
+
{{end}}
From 03591f0f95823a0b1dcca969d2a3ed505c7e6d73 Mon Sep 17 00:00:00 2001
From: techknowlogick
Date: Tue, 14 Mar 2023 03:45:21 -0400
Subject: [PATCH 15/94] add user rename endpoint to admin api (#22789)
this is a simple endpoint that adds the ability to rename users to the
admin API.
Note: this is not in a mergeable state. It would be better if this was
handled by a PATCH/POST to the /api/v1/admin/users/{username} endpoint
and the username is modified.
---------
Co-authored-by: Jason Song
---
models/issues/pull.go | 4 +-
models/user/user.go | 4 +-
modules/structs/user.go | 9 +++++
routers/api/v1/admin/user.go | 58 +++++++++++++++++++++++++++++
routers/api/v1/api.go | 1 +
routers/api/v1/swagger/options.go | 3 ++
routers/web/org/setting.go | 2 +-
routers/web/user/setting/profile.go | 52 ++++++++------------------
services/agit/agit.go | 4 +-
services/user/rename.go | 41 ++++++++++++++++++++
services/user/user.go | 16 ++++++++
templates/swagger/v1_json.tmpl | 56 ++++++++++++++++++++++++++++
12 files changed, 206 insertions(+), 44 deletions(-)
create mode 100644 services/user/rename.go
diff --git a/models/issues/pull.go b/models/issues/pull.go
index 6a1dc31556..a15ebec0b5 100644
--- a/models/issues/pull.go
+++ b/models/issues/pull.go
@@ -660,10 +660,10 @@ func GetPullRequestByIssueID(ctx context.Context, issueID int64) (*PullRequest,
// GetAllUnmergedAgitPullRequestByPoster get all unmerged agit flow pull request
// By poster id.
-func GetAllUnmergedAgitPullRequestByPoster(uid int64) ([]*PullRequest, error) {
+func GetAllUnmergedAgitPullRequestByPoster(ctx context.Context, uid int64) ([]*PullRequest, error) {
pulls := make([]*PullRequest, 0, 10)
- err := db.GetEngine(db.DefaultContext).
+ err := db.GetEngine(ctx).
Where("has_merged=? AND flow = ? AND issue.is_closed=? AND issue.poster_id=?",
false, PullRequestFlowAGit, false, uid).
Join("INNER", "issue", "issue.id=pull_request.issue_id").
diff --git a/models/user/user.go b/models/user/user.go
index 454779b9ea..82c2d3b6cd 100644
--- a/models/user/user.go
+++ b/models/user/user.go
@@ -742,13 +742,13 @@ func VerifyUserActiveCode(code string) (user *User) {
}
// ChangeUserName changes all corresponding setting from old user name to new one.
-func ChangeUserName(u *User, newUserName string) (err error) {
+func ChangeUserName(ctx context.Context, u *User, newUserName string) (err error) {
oldUserName := u.Name
if err = IsUsableUsername(newUserName); err != nil {
return err
}
- ctx, committer, err := db.TxContext(db.DefaultContext)
+ ctx, committer, err := db.TxContext(ctx)
if err != nil {
return err
}
diff --git a/modules/structs/user.go b/modules/structs/user.go
index c5e96f3356..f68b92ac06 100644
--- a/modules/structs/user.go
+++ b/modules/structs/user.go
@@ -93,3 +93,12 @@ type UserSettingsOptions struct {
HideEmail *bool `json:"hide_email"`
HideActivity *bool `json:"hide_activity"`
}
+
+// RenameUserOption options when renaming a user
+type RenameUserOption struct {
+ // New username for this user. This name cannot be in use yet by any other user.
+ //
+ // required: true
+ // unique: true
+ NewName string `json:"new_username" binding:"Required"`
+}
diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go
index 4192d8654d..369d13943a 100644
--- a/routers/api/v1/admin/user.go
+++ b/routers/api/v1/admin/user.go
@@ -461,3 +461,61 @@ func GetAllUsers(ctx *context.APIContext) {
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, &results)
}
+
+// RenameUser api for renaming a user
+func RenameUser(ctx *context.APIContext) {
+ // swagger:operation POST /admin/users/{username}/rename admin adminRenameUser
+ // ---
+ // summary: Rename a user
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: username
+ // in: path
+ // description: existing username of user
+ // type: string
+ // required: true
+ // - name: body
+ // in: body
+ // required: true
+ // schema:
+ // "$ref": "#/definitions/RenameUserOption"
+ // responses:
+ // "204":
+ // "$ref": "#/responses/empty"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "422":
+ // "$ref": "#/responses/validationError"
+
+ if ctx.ContextUser.IsOrganization() {
+ ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("%s is an organization not a user", ctx.ContextUser.Name))
+ return
+ }
+
+ newName := web.GetForm(ctx).(*api.RenameUserOption).NewName
+
+ if strings.EqualFold(newName, ctx.ContextUser.Name) {
+ // Noop as username is not changed
+ ctx.Status(http.StatusNoContent)
+ return
+ }
+
+ // Check if user name has been changed
+ if err := user_service.RenameUser(ctx, ctx.ContextUser, newName); err != nil {
+ switch {
+ case user_model.IsErrUserAlreadyExist(err):
+ ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("form.username_been_taken"))
+ case db.IsErrNameReserved(err):
+ ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("user.form.name_reserved", newName))
+ case db.IsErrNamePatternNotAllowed(err):
+ ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("user.form.name_pattern_not_allowed", newName))
+ case db.IsErrNameCharsNotAllowed(err):
+ ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("user.form.name_chars_not_allowed", newName))
+ default:
+ ctx.ServerError("ChangeUserName", err)
+ }
+ return
+ }
+ ctx.Status(http.StatusNoContent)
+}
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index 735939a551..7001dc72ac 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -1257,6 +1257,7 @@ func Routes(ctx gocontext.Context) *web.Route {
m.Get("/orgs", org.ListUserOrgs)
m.Post("/orgs", bind(api.CreateOrgOption{}), admin.CreateOrg)
m.Post("/repos", bind(api.CreateRepoOption{}), admin.CreateRepo)
+ m.Post("/rename", bind(api.RenameUserOption{}), admin.RenameUser)
}, context_service.UserAssignmentAPI())
})
m.Group("/unadopted", func() {
diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go
index 979b184075..0c8d3d353f 100644
--- a/routers/api/v1/swagger/options.go
+++ b/routers/api/v1/swagger/options.go
@@ -48,6 +48,9 @@ type swaggerParameterBodies struct {
// in:body
CreateKeyOption api.CreateKeyOption
+ // in:body
+ RenameUserOption api.RenameUserOption
+
// in:body
CreateLabelOption api.CreateLabelOption
// in:body
diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go
index b57ebfbcda..654e9000fa 100644
--- a/routers/web/org/setting.go
+++ b/routers/web/org/setting.go
@@ -79,7 +79,7 @@ func SettingsPost(ctx *context.Context) {
ctx.Data["OrgName"] = true
ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplSettingsOptions, &form)
return
- } else if err = user_model.ChangeUserName(org.AsUser(), form.Name); err != nil {
+ } else if err = user_model.ChangeUserName(ctx, org.AsUser(), form.Name); err != nil {
switch {
case db.IsErrNameReserved(err):
ctx.Data["OrgName"] = true
diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go
index f0f053a514..f500be7632 100644
--- a/routers/web/user/setting/profile.go
+++ b/routers/web/user/setting/profile.go
@@ -27,9 +27,7 @@ import (
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware"
- "code.gitea.io/gitea/services/agit"
"code.gitea.io/gitea/services/forms"
- container_service "code.gitea.io/gitea/services/packages/container"
user_service "code.gitea.io/gitea/services/user"
)
@@ -57,45 +55,25 @@ func HandleUsernameChange(ctx *context.Context, user *user_model.User, newName s
return fmt.Errorf(ctx.Tr("form.username_change_not_local_user"))
}
- // Check if user name has been changed
- if user.LowerName != strings.ToLower(newName) {
- if err := user_model.ChangeUserName(user, newName); err != nil {
- switch {
- case user_model.IsErrUserAlreadyExist(err):
- ctx.Flash.Error(ctx.Tr("form.username_been_taken"))
- case user_model.IsErrEmailAlreadyUsed(err):
- ctx.Flash.Error(ctx.Tr("form.email_been_used"))
- case db.IsErrNameReserved(err):
- ctx.Flash.Error(ctx.Tr("user.form.name_reserved", newName))
- case db.IsErrNamePatternNotAllowed(err):
- ctx.Flash.Error(ctx.Tr("user.form.name_pattern_not_allowed", newName))
- case db.IsErrNameCharsNotAllowed(err):
- ctx.Flash.Error(ctx.Tr("user.form.name_chars_not_allowed", newName))
- default:
- ctx.ServerError("ChangeUserName", err)
- }
- return err
+ // rename user
+ if err := user_service.RenameUser(ctx, user, newName); err != nil {
+ switch {
+ case user_model.IsErrUserAlreadyExist(err):
+ ctx.Flash.Error(ctx.Tr("form.username_been_taken"))
+ case user_model.IsErrEmailAlreadyUsed(err):
+ ctx.Flash.Error(ctx.Tr("form.email_been_used"))
+ case db.IsErrNameReserved(err):
+ ctx.Flash.Error(ctx.Tr("user.form.name_reserved", newName))
+ case db.IsErrNamePatternNotAllowed(err):
+ ctx.Flash.Error(ctx.Tr("user.form.name_pattern_not_allowed", newName))
+ case db.IsErrNameCharsNotAllowed(err):
+ ctx.Flash.Error(ctx.Tr("user.form.name_chars_not_allowed", newName))
+ default:
+ ctx.ServerError("ChangeUserName", err)
}
- } else {
- if err := repo_model.UpdateRepositoryOwnerNames(user.ID, newName); err != nil {
- ctx.ServerError("UpdateRepository", err)
- return err
- }
- }
-
- // update all agit flow pull request header
- err := agit.UserNameChanged(user, newName)
- if err != nil {
- ctx.ServerError("agit.UserNameChanged", err)
return err
}
- if err := container_service.UpdateRepositoryNames(ctx, user, newName); err != nil {
- ctx.ServerError("UpdateRepositoryNames", err)
- return err
- }
-
- log.Trace("User name changed: %s -> %s", user.Name, newName)
return nil
}
diff --git a/services/agit/agit.go b/services/agit/agit.go
index b61cb6f3f5..32fc3cba4a 100644
--- a/services/agit/agit.go
+++ b/services/agit/agit.go
@@ -226,8 +226,8 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
}
// UserNameChanged handle user name change for agit flow pull
-func UserNameChanged(user *user_model.User, newName string) error {
- pulls, err := issues_model.GetAllUnmergedAgitPullRequestByPoster(user.ID)
+func UserNameChanged(ctx context.Context, user *user_model.User, newName string) error {
+ pulls, err := issues_model.GetAllUnmergedAgitPullRequestByPoster(ctx, user.ID)
if err != nil {
return err
}
diff --git a/services/user/rename.go b/services/user/rename.go
new file mode 100644
index 0000000000..af195d7d76
--- /dev/null
+++ b/services/user/rename.go
@@ -0,0 +1,41 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/services/agit"
+ container_service "code.gitea.io/gitea/services/packages/container"
+)
+
+func renameUser(ctx context.Context, u *user_model.User, newUserName string) error {
+ if u.IsOrganization() {
+ return fmt.Errorf("cannot rename organization")
+ }
+
+ if err := user_model.ChangeUserName(ctx, u, newUserName); err != nil {
+ return err
+ }
+
+ if err := agit.UserNameChanged(ctx, u, newUserName); err != nil {
+ return err
+ }
+ if err := container_service.UpdateRepositoryNames(ctx, u, newUserName); err != nil {
+ return err
+ }
+
+ u.Name = newUserName
+ u.LowerName = strings.ToLower(newUserName)
+ if err := user_model.UpdateUser(ctx, u, false); err != nil {
+ return err
+ }
+
+ log.Trace("User name changed: %s -> %s", u.Name, newUserName)
+ return nil
+}
diff --git a/services/user/user.go b/services/user/user.go
index f0b8fe1c31..d52a2f404b 100644
--- a/services/user/user.go
+++ b/services/user/user.go
@@ -27,6 +27,22 @@ import (
"code.gitea.io/gitea/services/packages"
)
+// RenameUser renames a user
+func RenameUser(ctx context.Context, u *user_model.User, newUserName string) error {
+ ctx, committer, err := db.TxContext(ctx)
+ if err != nil {
+ return err
+ }
+ defer committer.Close()
+ if err := renameUser(ctx, u, newUserName); err != nil {
+ return err
+ }
+ if err := committer.Commit(); err != nil {
+ return err
+ }
+ return err
+}
+
// DeleteUser completely and permanently deletes everything of a user,
// but issues/comments/pulls will be kept and shown as someone has been deleted,
// unless the user is younger than USER_DELETE_WITH_COMMENTS_MAX_DAYS.
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index c304a7a497..7dc7f563c2 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -679,6 +679,46 @@
}
}
},
+ "/admin/users/{username}/rename": {
+ "post": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "admin"
+ ],
+ "summary": "Rename a user",
+ "operationId": "adminRenameUser",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "existing username of user",
+ "name": "username",
+ "in": "path",
+ "required": true
+ },
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/RenameUserOption"
+ }
+ }
+ ],
+ "responses": {
+ "204": {
+ "$ref": "#/responses/empty"
+ },
+ "403": {
+ "$ref": "#/responses/forbidden"
+ },
+ "422": {
+ "$ref": "#/responses/validationError"
+ }
+ }
+ }
+ },
"/admin/users/{username}/repos": {
"post": {
"consumes": [
@@ -19105,6 +19145,22 @@
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
+ "RenameUserOption": {
+ "description": "RenameUserOption options when renaming a user",
+ "type": "object",
+ "required": [
+ "new_username"
+ ],
+ "properties": {
+ "new_username": {
+ "description": "New username for this user. This name cannot be in use yet by any other user.",
+ "type": "string",
+ "uniqueItems": true,
+ "x-go-name": "NewName"
+ }
+ },
+ "x-go-package": "code.gitea.io/gitea/modules/structs"
+ },
"RepoCollaboratorPermission": {
"description": "RepoCollaboratorPermission to get repository permission for a collaborator",
"type": "object",
From d56bb7420184c0c2f451f4bcaa96c9b3b00c393d Mon Sep 17 00:00:00 2001
From: techknowlogick
Date: Tue, 14 Mar 2023 03:54:40 -0400
Subject: [PATCH 16/94] add admin API email endpoints (#22792)
add email endpoint to admin API to ensure API parity with admin
dashboard.
---
modules/structs/user_email.go | 3 ++
routers/api/v1/admin/email.go | 87 ++++++++++++++++++++++++++++++++++
routers/api/v1/api.go | 4 ++
services/convert/convert.go | 11 +++++
templates/swagger/v1_json.tmpl | 83 ++++++++++++++++++++++++++++++++
5 files changed, 188 insertions(+)
create mode 100644 routers/api/v1/admin/email.go
diff --git a/modules/structs/user_email.go b/modules/structs/user_email.go
index 6a11e040af..9319667e8f 100644
--- a/modules/structs/user_email.go
+++ b/modules/structs/user_email.go
@@ -1,4 +1,5 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
+// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package structs
@@ -9,6 +10,8 @@ type Email struct {
Email string `json:"email"`
Verified bool `json:"verified"`
Primary bool `json:"primary"`
+ UserID int64 `json:"user_id"`
+ UserName string `json:"username"`
}
// CreateEmailOption options when creating email addresses
diff --git a/routers/api/v1/admin/email.go b/routers/api/v1/admin/email.go
new file mode 100644
index 0000000000..8d0491e070
--- /dev/null
+++ b/routers/api/v1/admin/email.go
@@ -0,0 +1,87 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package admin
+
+import (
+ "net/http"
+
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/context"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/convert"
+)
+
+// GetAllEmails
+func GetAllEmails(ctx *context.APIContext) {
+ // swagger:operation GET /admin/emails admin adminGetAllEmails
+ // ---
+ // summary: List all emails
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: page
+ // in: query
+ // description: page number of results to return (1-based)
+ // type: integer
+ // - name: limit
+ // in: query
+ // description: page size of results
+ // type: integer
+ // responses:
+ // "200":
+ // "$ref": "#/responses/EmailList"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+
+ listOptions := utils.GetListOptions(ctx)
+
+ emails, maxResults, err := user_model.SearchEmails(&user_model.SearchEmailOptions{
+ Keyword: ctx.Params(":email"),
+ ListOptions: listOptions,
+ })
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "GetAllEmails", err)
+ return
+ }
+
+ results := make([]*api.Email, len(emails))
+ for i := range emails {
+ results[i] = convert.ToEmailSearch(emails[i])
+ }
+
+ ctx.SetLinkHeader(int(maxResults), listOptions.PageSize)
+ ctx.SetTotalCountHeader(maxResults)
+ ctx.JSON(http.StatusOK, &results)
+}
+
+// SearchEmail
+func SearchEmail(ctx *context.APIContext) {
+ // swagger:operation GET /admin/emails/search admin adminSearchEmails
+ // ---
+ // summary: Search all emails
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: q
+ // in: query
+ // description: keyword
+ // type: string
+ // - name: page
+ // in: query
+ // description: page number of results to return (1-based)
+ // type: integer
+ // - name: limit
+ // in: query
+ // description: page size of results
+ // type: integer
+ // responses:
+ // "200":
+ // "$ref": "#/responses/EmailList"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+
+ ctx.SetParams(":email", ctx.FormTrim("q"))
+ GetAllEmails(ctx)
+}
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index 7001dc72ac..5c32164fa7 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -1260,6 +1260,10 @@ func Routes(ctx gocontext.Context) *web.Route {
m.Post("/rename", bind(api.RenameUserOption{}), admin.RenameUser)
}, context_service.UserAssignmentAPI())
})
+ m.Group("/emails", func() {
+ m.Get("", admin.GetAllEmails)
+ m.Get("/search", admin.SearchEmail)
+ })
m.Group("/unadopted", func() {
m.Get("", admin.ListUnadoptedRepositories)
m.Post("/{username}/{reponame}", admin.AdoptRepository)
diff --git a/services/convert/convert.go b/services/convert/convert.go
index 5f2100a039..bce0e7ba21 100644
--- a/services/convert/convert.go
+++ b/services/convert/convert.go
@@ -38,6 +38,17 @@ func ToEmail(email *user_model.EmailAddress) *api.Email {
}
}
+// ToEmail convert models.EmailAddress to api.Email
+func ToEmailSearch(email *user_model.SearchEmailResult) *api.Email {
+ return &api.Email{
+ Email: email.Email,
+ Verified: email.IsActivated,
+ Primary: email.IsPrimary,
+ UserID: email.UID,
+ UserName: email.Name,
+ }
+}
+
// ToBranch convert a git.Commit and git.Branch to an api.Branch
func ToBranch(ctx context.Context, repo *repo_model.Repository, b *git.Branch, c *git.Commit, bp *git_model.ProtectedBranch, user *user_model.User, isRepoAdmin bool) (*api.Branch, error) {
if bp == nil {
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 7dc7f563c2..9c89b21fca 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -138,6 +138,80 @@
}
}
},
+ "/admin/emails": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "admin"
+ ],
+ "summary": "List all emails",
+ "operationId": "adminGetAllEmails",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "page number of results to return (1-based)",
+ "name": "page",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "page size of results",
+ "name": "limit",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/EmailList"
+ },
+ "403": {
+ "$ref": "#/responses/forbidden"
+ }
+ }
+ }
+ },
+ "/admin/emails/search": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "admin"
+ ],
+ "summary": "Search all emails",
+ "operationId": "adminSearchEmails",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "keyword",
+ "name": "q",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "page number of results to return (1-based)",
+ "name": "page",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "page size of results",
+ "name": "limit",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/EmailList"
+ },
+ "403": {
+ "$ref": "#/responses/forbidden"
+ }
+ }
+ }
+ },
"/admin/hooks": {
"get": {
"produces": [
@@ -16999,6 +17073,15 @@
"type": "boolean",
"x-go-name": "Primary"
},
+ "user_id": {
+ "type": "integer",
+ "format": "int64",
+ "x-go-name": "UserID"
+ },
+ "username": {
+ "type": "string",
+ "x-go-name": "UserName"
+ },
"verified": {
"type": "boolean",
"x-go-name": "Verified"
From ac8d71ff07a3354a27d6a5daab45d1e79e242269 Mon Sep 17 00:00:00 2001
From: wxiaoguang
Date: Tue, 14 Mar 2023 17:51:20 +0800
Subject: [PATCH 17/94] Refactor branch/tag selector to Vue SFC (#23421)
Follow #23394
There were many bad smells in old code. This PR only moves the code into
Vue SFC, doesn't touch the unrelated logic.
update: after
https://github.com/go-gitea/gitea/pull/23421/commits/5f23218c851e12132f538a404c946bbf6ff38e62
, there should be no usage of the vue-rumtime-compiler anymore
(hopefully), so I think this PR could close #19851
---------
Co-authored-by: Lunny Xiao
---
templates/repo/branch_dropdown.tmpl | 98 ++----
web_src/js/components/DashboardRepoList.vue | 2 +-
.../js/components/PullRequestMergeForm.vue | 7 +-
.../js/components/RepoBranchTagDropdown.js | 208 -------------
.../js/components/RepoBranchTagSelector.vue | 293 ++++++++++++++++++
web_src/js/features/repo-findfile.js | 7 +-
web_src/js/features/repo-findfile.test.js | 7 +-
web_src/js/features/repo-legacy.js | 4 +-
web_src/js/svg.js | 18 +-
web_src/js/utils/url.js | 3 +
web_src/js/utils/url.test.js | 7 +
web_src/less/_base.less | 4 -
web_src/less/_repository.less | 6 -
webpack.config.js | 4 +
14 files changed, 359 insertions(+), 309 deletions(-)
delete mode 100644 web_src/js/components/RepoBranchTagDropdown.js
create mode 100644 web_src/js/components/RepoBranchTagSelector.vue
create mode 100644 web_src/js/utils/url.js
create mode 100644 web_src/js/utils/url.test.js
diff --git a/templates/repo/branch_dropdown.tmpl b/templates/repo/branch_dropdown.tmpl
index 8e81373aec..1ec4b7ef16 100644
--- a/templates/repo/branch_dropdown.tmpl
+++ b/templates/repo/branch_dropdown.tmpl
@@ -1,6 +1,20 @@
-{{$release := .release}}
-{{$defaultBranch := $.root.BranchName}}{{if and .root.IsViewTag (not .noTag)}}{{$defaultBranch = .root.TagName}}{{end}}{{if eq $defaultBranch ""}}{{$defaultBranch = $.root.Repository.DefaultBranch}}{{end}}
-{{$type := ""}}{{if and .root.IsViewTag (not .noTag)}}{{$type = "tag"}}{{else if .root.IsViewBranch}}{{$type = "branch"}}{{else}}{{$type = "tree"}}{{end}}
+{{$defaultBranch := $.root.BranchName}}
+{{if and .root.IsViewTag (not .noTag)}}
+ {{$defaultBranch = .root.TagName}}
+{{end}}
+{{if eq $defaultBranch ""}}
+ {{$defaultBranch = $.root.Repository.DefaultBranch}}
+{{end}}
+
+{{$type := ""}}
+{{if and .root.IsViewTag (not .noTag)}}
+ {{$type = "tag"}}
+{{else if .root.IsViewBranch}}
+ {{$type = "branch"}}
+{{else}}
+ {{$type = "tree"}}
+{{end}}
+
{{$showBranchesInDropdown := not .root.HideBranchesInDropdown}}
-