diff --git a/.eslintrc.yaml b/.eslintrc.yaml index c4e0b9de8cbf..d47d8c0349e9 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -16,6 +16,8 @@ plugins: - eslint-plugin-sonarjs - eslint-plugin-custom-elements - eslint-plugin-regexp + - eslint-plugin-wc + - eslint-plugin-eslint-comments env: es2022: true @@ -85,6 +87,15 @@ rules: dot-notation: [0] eol-last: [2] eqeqeq: [2] + eslint-comments/disable-enable-pair: [2] + eslint-comments/no-aggregating-enable: [2] + eslint-comments/no-duplicate-disable: [2] + eslint-comments/no-restricted-disable: [0] + eslint-comments/no-unlimited-disable: [2] + eslint-comments/no-unused-disable: [2] + eslint-comments/no-unused-enable: [2] + eslint-comments/no-use: [0] + eslint-comments/require-description: [0] for-direction: [2] func-call-spacing: [2, never] func-name-matching: [2] @@ -717,6 +728,15 @@ rules: use-isnan: [2] valid-typeof: [2, {requireStringLiterals: true}] vars-on-top: [0] + wc/attach-shadow-constructor: [2] + wc/guard-super-call: [2] + wc/no-closed-shadow-root: [2] + wc/no-constructor-attributes: [2] + wc/no-constructor-params: [2] + wc/no-invalid-element-name: [0] # covered by custom-elements/valid-tag-name + wc/no-self-class: [2] + wc/no-typos: [2] + wc/require-listener-teardown: [2] wrap-iife: [2, inside] wrap-regex: [0] yield-star-spacing: [2, after] diff --git a/.github/workflows/cron-translations.yml b/.github/workflows/cron-translations.yml index 97e1ff9739b3..bc24dd485456 100644 --- a/.github/workflows/cron-translations.yml +++ b/.github/workflows/cron-translations.yml @@ -38,6 +38,7 @@ jobs: env: CROWDIN_KEY: ${{ secrets.CROWDIN_KEY }} PLUGIN_UPLOAD: true + PLUGIN_EXPORT_DIR: options/locale/ PLUGIN_IGNORE_BRANCH: true PLUGIN_PROJECT_IDENTIFIER: gitea PLUGIN_FILES: | diff --git a/.github/workflows/pull-db-tests.yml b/.github/workflows/pull-db-tests.yml index 4011b4201be2..3446b711559d 100644 --- a/.github/workflows/pull-db-tests.yml +++ b/.github/workflows/pull-db-tests.yml @@ -102,6 +102,13 @@ jobs: --health-retries 10 ports: - 6379:6379 + minio: + image: bitnami/minio:2021.3.17 + env: + MINIO_ACCESS_KEY: 123456 + MINIO_SECRET_KEY: 12345678 + ports: + - "9000:9000" steps: - uses: actions/checkout@v3 - uses: actions/setup-go@v4 diff --git a/Dockerfile b/Dockerfile index 3623085e4744..06481cdf5aac 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ #Build stage -FROM docker.io/library/golang:1.20-alpine3.17 AS build-env +FROM docker.io/library/golang:1.20-alpine3.18 AS build-env ARG GOPROXY ENV GOPROXY ${GOPROXY:-direct} @@ -23,7 +23,7 @@ RUN if [ -n "${GITEA_VERSION}" ]; then git checkout "${GITEA_VERSION}"; fi \ # Begin env-to-ini build RUN go build contrib/environment-to-ini/environment-to-ini.go -FROM docker.io/library/alpine:3.17 +FROM docker.io/library/alpine:3.18 LABEL maintainer="maintainers@gitea.io" EXPOSE 22 3000 diff --git a/Dockerfile.rootless b/Dockerfile.rootless index 67bd9c4880fb..aa74d3598750 100644 --- a/Dockerfile.rootless +++ b/Dockerfile.rootless @@ -1,5 +1,5 @@ #Build stage -FROM docker.io/library/golang:1.20-alpine3.17 AS build-env +FROM docker.io/library/golang:1.20-alpine3.18 AS build-env ARG GOPROXY ENV GOPROXY ${GOPROXY:-direct} @@ -23,7 +23,7 @@ RUN if [ -n "${GITEA_VERSION}" ]; then git checkout "${GITEA_VERSION}"; fi \ # Begin env-to-ini build RUN go build contrib/environment-to-ini/environment-to-ini.go -FROM docker.io/library/alpine:3.17 +FROM docker.io/library/alpine:3.18 LABEL maintainer="maintainers@gitea.io" EXPOSE 2222 3000 diff --git a/cmd/migrate_storage.go b/cmd/migrate_storage.go index 9798913705f2..291e7695b5e4 100644 --- a/cmd/migrate_storage.go +++ b/cmd/migrate_storage.go @@ -8,6 +8,7 @@ import ( "fmt" "strings" + actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" "code.gitea.io/gitea/models/migrations" @@ -32,7 +33,7 @@ var CmdMigrateStorage = cli.Command{ cli.StringFlag{ Name: "type, t", Value: "", - Usage: "Type of stored files to copy. Allowed types: 'attachments', 'lfs', 'avatars', 'repo-avatars', 'repo-archivers', 'packages'", + Usage: "Type of stored files to copy. Allowed types: 'attachments', 'lfs', 'avatars', 'repo-avatars', 'repo-archivers', 'packages', 'actions-log'", }, cli.StringFlag{ Name: "storage, s", @@ -134,6 +135,22 @@ func migratePackages(ctx context.Context, dstStorage storage.ObjectStorage) erro }) } +func migrateActionsLog(ctx context.Context, dstStorage storage.ObjectStorage) error { + return db.Iterate(ctx, nil, func(ctx context.Context, task *actions_model.ActionTask) error { + if task.LogExpired { + // the log has been cleared + return nil + } + if !task.LogInStorage { + // running tasks store logs in DBFS + return nil + } + p := task.LogFilename + _, err := storage.Copy(dstStorage, p, storage.Actions, p) + return err + }) +} + func runMigrateStorage(ctx *cli.Context) error { stdCtx, cancel := installSignals() defer cancel() @@ -201,6 +218,7 @@ func runMigrateStorage(ctx *cli.Context) error { "repo-avatars": migrateRepoAvatars, "repo-archivers": migrateRepoArchivers, "packages": migratePackages, + "actions-log": migrateActionsLog, } tp := strings.ToLower(ctx.String("type")) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 3ceb53dcd0a3..592257b3b9d7 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -940,6 +940,9 @@ ROUTER = console ;; Force ssh:// clone url instead of scp-style uri when default SSH port is used ;USE_COMPAT_SSH_URI = false ;; +;; Value for the "go get" request returns the repository url as https or ssh, default is https +;GO_GET_CLONE_URL_PROTOCOL = https +;; ;; Close issues as long as a commit on any branch marks it as fixed ;; Comma separated list of globally disabled repo units. Allowed values: repo.issues, repo.ext_issues, repo.pulls, repo.wiki, repo.ext_wiki, repo.projects, repo.packages, repo.actions. ;DISABLED_REPO_UNITS = @@ -1770,16 +1773,19 @@ ROUTER = console ;; Max Width and Height of uploaded avatars. ;; This is to limit the amount of RAM used when resizing the image. ;AVATAR_MAX_WIDTH = 4096 -;AVATAR_MAX_HEIGHT = 3072 +;AVATAR_MAX_HEIGHT = 4096 ;; ;; The multiplication factor for rendered avatar images. ;; Larger values result in finer rendering on HiDPI devices. -;AVATAR_RENDERED_SIZE_FACTOR = 3 +;AVATAR_RENDERED_SIZE_FACTOR = 2 ;; ;; Maximum allowed file size for uploaded avatars. ;; This is to limit the amount of RAM used when resizing the image. ;AVATAR_MAX_FILE_SIZE = 1048576 ;; +;; If the uploaded file is not larger than this byte size, the image will be used as is, without resizing/converting. +;AVATAR_MAX_ORIGIN_SIZE = 262144 +;; ;; Chinese users can choose "duoshuo" ;; or a custom avatar source, like: http://cn.gravatar.com/avatar/ ;GRAVATAR_SOURCE = gravatar @@ -2439,6 +2445,8 @@ ROUTER = console ;LIMIT_TOTAL_OWNER_COUNT = -1 ;; Maximum size of packages a single owner can use (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) ;LIMIT_TOTAL_OWNER_SIZE = -1 +;; Maximum size of an Alpine upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) +;LIMIT_SIZE_ALPINE = -1 ;; Maximum size of a Cargo upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) ;LIMIT_SIZE_CARGO = -1 ;; Maximum size of a Chef upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) @@ -2455,6 +2463,8 @@ ROUTER = console ;LIMIT_SIZE_DEBIAN = -1 ;; Maximum size of a Generic upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) ;LIMIT_SIZE_GENERIC = -1 +;; Maximum size of a Go upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) +;LIMIT_SIZE_GO = -1 ;; Maximum size of a Helm upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) ;LIMIT_SIZE_HELM = -1 ;; Maximum size of a Maven upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) diff --git a/docs/content/doc/administration/config-cheat-sheet.en-us.md b/docs/content/doc/administration/config-cheat-sheet.en-us.md index c1befed48980..27e74f2a2596 100644 --- a/docs/content/doc/administration/config-cheat-sheet.en-us.md +++ b/docs/content/doc/administration/config-cheat-sheet.en-us.md @@ -95,6 +95,8 @@ In addition there is _`StaticRootPath`_ which can be set as a built-in at build HTTP protocol. - `USE_COMPAT_SSH_URI`: **false**: Force ssh:// clone url instead of scp-style uri when default SSH port is used. +- `GO_GET_CLONE_URL_PROTOCOL`: **https**: Value for the "go get" request returns the repository url as https or ssh + default is https. - `ACCESS_CONTROL_ALLOW_ORIGIN`: **\**: Value for Access-Control-Allow-Origin header, default is not to present. **WARNING**: This maybe harmful to you website if you do not give it a right value. @@ -790,9 +792,10 @@ and - `AVATAR_STORAGE_TYPE`: **default**: Storage type defined in `[storage.xxx]`. Default is `default` which will read `[storage]` if no section `[storage]` will be a type `local`. - `AVATAR_UPLOAD_PATH`: **data/avatars**: Path to store user avatar image files. - `AVATAR_MAX_WIDTH`: **4096**: Maximum avatar image width in pixels. -- `AVATAR_MAX_HEIGHT`: **3072**: Maximum avatar image height in pixels. -- `AVATAR_MAX_FILE_SIZE`: **1048576** (1Mb): Maximum avatar image file size in bytes. -- `AVATAR_RENDERED_SIZE_FACTOR`: **3**: The multiplication factor for rendered avatar images. Larger values result in finer rendering on HiDPI devices. +- `AVATAR_MAX_HEIGHT`: **4096**: Maximum avatar image height in pixels. +- `AVATAR_MAX_FILE_SIZE`: **1048576** (1MiB): Maximum avatar image file size in bytes. +- `AVATAR_MAX_ORIGIN_SIZE`: **262144** (256KiB): If the uploaded file is not larger than this byte size, the image will be used as is, without resizing/converting. +- `AVATAR_RENDERED_SIZE_FACTOR`: **2**: The multiplication factor for rendered avatar images. Larger values result in finer rendering on HiDPI devices. - `REPOSITORY_AVATAR_STORAGE_TYPE`: **default**: Storage type defined in `[storage.xxx]`. Default is `default` which will read `[storage]` if no section `[storage]` will be a type `local`. - `REPOSITORY_AVATAR_UPLOAD_PATH`: **data/repo-avatars**: Path to store repository avatar image files. @@ -1016,7 +1019,7 @@ Default templates for project boards: - `RUN_AT_START`: **false**: Run tasks at start up time (if ENABLED). - `NOTICE_ON_SUCCESS`: **false**: Set to true to switch on success notices. - `SCHEDULE`: **@every 168h**: Cron syntax to set how often to check. -- `OLDER_THAN`: **@every 8760h**: any action older than this expression will be deleted from database, suggest using `8760h` (1 year) because that's the max length of heatmap. +- `OLDER_THAN`: **8760h**: any action older than this expression will be deleted from database, suggest using `8760h` (1 year) because that's the max length of heatmap. #### Cron - Check for new Gitea versions (`cron.update_checker`) @@ -1032,7 +1035,7 @@ Default templates for project boards: - `RUN_AT_START`: **false**: Run tasks at start up time (if ENABLED). - `NO_SUCCESS_NOTICE`: **false**: Set to true to switch off success notices. - `SCHEDULE`: **@every 168h**: Cron syntax to set how often to check. -- `OLDER_THAN`: **@every 8760h**: any system notice older than this expression will be deleted from database. +- `OLDER_THAN`: **8760h**: any system notice older than this expression will be deleted from database. #### Cron - Garbage collect LFS pointers in repositories (`cron.gc_lfs`) @@ -1211,6 +1214,7 @@ Task queue configuration has been moved to `queue.task`. However, the below conf - `CHUNKED_UPLOAD_PATH`: **tmp/package-upload**: Path for chunked uploads. Defaults to `APP_DATA_PATH` + `tmp/package-upload` - `LIMIT_TOTAL_OWNER_COUNT`: **-1**: Maximum count of package versions a single owner can have (`-1` means no limits) - `LIMIT_TOTAL_OWNER_SIZE`: **-1**: Maximum size of packages a single owner can use (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) +- `LIMIT_SIZE_ALPINE`: **-1**: Maximum size of an Alpine upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) - `LIMIT_SIZE_CARGO`: **-1**: Maximum size of a Cargo upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) - `LIMIT_SIZE_CHEF`: **-1**: Maximum size of a Chef upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) - `LIMIT_SIZE_COMPOSER`: **-1**: Maximum size of a Composer upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) @@ -1219,6 +1223,7 @@ Task queue configuration has been moved to `queue.task`. However, the below conf - `LIMIT_SIZE_CONTAINER`: **-1**: Maximum size of a Container upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) - `LIMIT_SIZE_DEBIAN`: **-1**: Maximum size of a Debian upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) - `LIMIT_SIZE_GENERIC`: **-1**: Maximum size of a Generic upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) +- `LIMIT_SIZE_GO`: **-1**: Maximum size of a Go upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) - `LIMIT_SIZE_HELM`: **-1**: Maximum size of a Helm upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) - `LIMIT_SIZE_MAVEN`: **-1**: Maximum size of a Maven upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) - `LIMIT_SIZE_NPM`: **-1**: Maximum size of a npm upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) diff --git a/docs/content/doc/administration/config-cheat-sheet.zh-cn.md b/docs/content/doc/administration/config-cheat-sheet.zh-cn.md index 41eed612acc5..c672b61598fd 100644 --- a/docs/content/doc/administration/config-cheat-sheet.zh-cn.md +++ b/docs/content/doc/administration/config-cheat-sheet.zh-cn.md @@ -214,8 +214,8 @@ menu: - `AVATAR_STORAGE_TYPE`: **local**: 头像存储类型,可以为 `local` 或 `minio`,分别支持本地文件系统和 minio 兼容的API。 - `AVATAR_UPLOAD_PATH`: **data/avatars**: 存储头像的文件系统路径。 - `AVATAR_MAX_WIDTH`: **4096**: 头像最大宽度,单位像素。 -- `AVATAR_MAX_HEIGHT`: **3072**: 头像最大高度,单位像素。 -- `AVATAR_MAX_FILE_SIZE`: **1048576** (1Mb): 头像最大大小。 +- `AVATAR_MAX_HEIGHT`: **4096**: 头像最大高度,单位像素。 +- `AVATAR_MAX_FILE_SIZE`: **1048576** (1MiB): 头像最大大小。 - `REPOSITORY_AVATAR_STORAGE_TYPE`: **local**: 仓库头像存储类型,可以为 `local` 或 `minio`,分别支持本地文件系统和 minio 兼容的API。 - `REPOSITORY_AVATAR_UPLOAD_PATH`: **data/repo-avatars**: 存储仓库头像的路径。 diff --git a/docs/content/doc/usage/packages/alpine.en-us.md b/docs/content/doc/usage/packages/alpine.en-us.md new file mode 100644 index 000000000000..aeb86093f039 --- /dev/null +++ b/docs/content/doc/usage/packages/alpine.en-us.md @@ -0,0 +1,133 @@ +--- +date: "2023-03-25T00:00:00+00:00" +title: "Alpine Packages Repository" +slug: "packages/alpine" +draft: false +toc: false +menu: + sidebar: + parent: "packages" + name: "Alpine" + weight: 4 + identifier: "alpine" +--- + +# Alpine Packages Repository + +Publish [Alpine](https://pkgs.alpinelinux.org/) packages for your user or organization. + +**Table of Contents** + +{{< toc >}} + +## Requirements + +To work with the Alpine registry, you need to use a HTTP client like `curl` to upload and a package manager like `apk` to consume packages. + +The following examples use `apk`. + +## Configuring the package registry + +To register the Alpine registry add the url to the list of known apk sources (`/etc/apk/repositories`): + +``` +https://gitea.example.com/api/packages/{owner}/alpine// +``` + +| Placeholder | Description | +| ------------ | ----------- | +| `owner` | The owner of the packages. | +| `branch` | The branch to use. | +| `repository` | The repository to use. | + +If the registry is private, provide credentials in the url. You can use a password or a [personal access token]({{< relref "doc/development/api-usage.en-us.md#authentication" >}}): + +``` +https://{username}:{your_password_or_token}@gitea.example.com/api/packages/{owner}/alpine// +``` + +The Alpine registry files are signed with a RSA key which must be known to apk. Download the public key and store it in `/etc/apk/keys/`: + +```shell +curl -JO https://gitea.example.com/api/packages/{owner}/alpine/key +``` + +Afterwards update the local package index: + +```shell +apk update +``` + +## Publish a package + +To publish an Alpine package (`*.apk`), perform a HTTP `PUT` operation with the package content in the request body. + +``` +PUT https://gitea.example.com/api/packages/{owner}/alpine/{branch}/{repository} +``` + +| Parameter | Description | +| ------------ | ----------- | +| `owner` | The owner of the package. | +| `branch` | The branch may match the release version of the OS, ex: `v3.17`. | +| `repository` | The repository can be used [to group packages](https://wiki.alpinelinux.org/wiki/Repositories) or just `main` or similar. | + +Example request using HTTP Basic authentication: + +```shell +curl --user your_username:your_password_or_token \ + --upload-file path/to/file.apk \ + https://gitea.example.com/api/packages/testuser/alpine/v3.17/main +``` + +If you are using 2FA or OAuth use a [personal access token]({{< relref "doc/development/api-usage.en-us.md#authentication" >}}) instead of the password. +You cannot publish a file with the same name twice to a package. You must delete the existing package file first. + +The server responds with the following HTTP Status codes. + +| HTTP Status Code | Meaning | +| ----------------- | ------- | +| `201 Created` | The package has been published. | +| `400 Bad Request` | The package name, version, branch, repository or architecture are invalid. | +| `409 Conflict` | A package file with the same combination of parameters exist already in the package. | + +## Delete a package + +To delete an Alpine package perform a HTTP `DELETE` operation. This will delete the package version too if there is no file left. + +``` +DELETE https://gitea.example.com/api/packages/{owner}/alpine/{branch}/{repository}/{architecture}/{filename} +``` + +| Parameter | Description | +| -------------- | ----------- | +| `owner` | The owner of the package. | +| `branch` | The branch to use. | +| `repository` | The repository to use. | +| `architecture` | The package architecture. | +| `filename` | The file to delete. + +Example request using HTTP Basic authentication: + +```shell +curl --user your_username:your_token_or_password -X DELETE \ + https://gitea.example.com/api/packages/testuser/alpine/v3.17/main/test-package-1.0.0.apk +``` + +The server responds with the following HTTP Status codes. + +| HTTP Status Code | Meaning | +| ----------------- | ------- | +| `204 No Content` | Success | +| `404 Not Found` | The package or file was not found. | + +## Install a package + +To install a package from the Alpine registry, execute the following commands: + +```shell +# use latest version +apk add {package_name} +# use specific version +apk add {package_name}={package_version} +``` diff --git a/docs/content/doc/usage/packages/debian.en-us.md b/docs/content/doc/usage/packages/debian.en-us.md index 7506b5ce2fdf..dc73da27cf97 100644 --- a/docs/content/doc/usage/packages/debian.en-us.md +++ b/docs/content/doc/usage/packages/debian.en-us.md @@ -83,7 +83,7 @@ curl --user your_username:your_password_or_token \ If you are using 2FA or OAuth use a [personal access token]({{< relref "doc/development/api-usage.en-us.md#authentication" >}}) instead of the password. You cannot publish a file with the same name twice to a package. You must delete the existing package version first. -The server reponds with the following HTTP Status codes. +The server responds with the following HTTP Status codes. | HTTP Status Code | Meaning | | ----------------- | ------- | @@ -115,7 +115,7 @@ curl --user your_username:your_token_or_password -X DELETE \ https://gitea.example.com/api/packages/testuser/debian/pools/bionic/main/test-package/1.0.0/amd64 ``` -The server reponds with the following HTTP Status codes. +The server responds with the following HTTP Status codes. | HTTP Status Code | Meaning | | ----------------- | ------- | diff --git a/docs/content/doc/usage/packages/generic.en-us.md b/docs/content/doc/usage/packages/generic.en-us.md index fbfe42d50a66..447eb692fd4d 100644 --- a/docs/content/doc/usage/packages/generic.en-us.md +++ b/docs/content/doc/usage/packages/generic.en-us.md @@ -51,7 +51,7 @@ curl --user your_username:your_password_or_token \ If you are using 2FA or OAuth use a [personal access token]({{< relref "doc/development/api-usage.en-us.md#authentication" >}}) instead of the password. -The server reponds with the following HTTP Status codes. +The server responds with the following HTTP Status codes. | HTTP Status Code | Meaning | | ----------------- | ------- | @@ -83,7 +83,7 @@ curl --user your_username:your_token_or_password \ https://gitea.example.com/api/packages/testuser/generic/test_package/1.0.0/file.bin ``` -The server reponds with the following HTTP Status codes. +The server responds with the following HTTP Status codes. | HTTP Status Code | Meaning | | ----------------- | ------- | @@ -111,7 +111,7 @@ curl --user your_username:your_token_or_password -X DELETE \ https://gitea.example.com/api/packages/testuser/generic/test_package/1.0.0 ``` -The server reponds with the following HTTP Status codes. +The server responds with the following HTTP Status codes. | HTTP Status Code | Meaning | | ----------------- | ------- | @@ -140,7 +140,7 @@ curl --user your_username:your_token_or_password -X DELETE \ https://gitea.example.com/api/packages/testuser/generic/test_package/1.0.0/file.bin ``` -The server reponds with the following HTTP Status codes. +The server responds with the following HTTP Status codes. | HTTP Status Code | Meaning | | ----------------- | ------- | diff --git a/docs/content/doc/usage/packages/go.en-us.md b/docs/content/doc/usage/packages/go.en-us.md new file mode 100644 index 000000000000..92f5eb5e939b --- /dev/null +++ b/docs/content/doc/usage/packages/go.en-us.md @@ -0,0 +1,77 @@ +--- +date: "2023-05-10T00:00:00+00:00" +title: "Go Packages Repository" +slug: "go" +weight: 45 +draft: false +toc: false +menu: + sidebar: + parent: "packages" + name: "Go" + weight: 45 + identifier: "go" +--- + +# Go Packages Repository + +Publish Go packages for your user or organization. + +**Table of Contents** + +{{< toc >}} + +## Publish a package + +To publish a Go package perform a HTTP `PUT` operation with the package content in the request body. +You cannot publish a package if a package of the same name and version already exists. You must delete the existing package first. +The package must follow the [documented structure](https://go.dev/ref/mod#zip-files). + +``` +PUT https://gitea.example.com/api/packages/{owner}/go/upload +``` + +| Parameter | Description | +| --------- | ----------- | +| `owner` | The owner of the package. | + +To authenticate to the package registry, you need to provide [custom HTTP headers or use HTTP Basic authentication]({{< relref "doc/development/api-usage.en-us.md#authentication" >}}): + +```shell +curl --user your_username:your_password_or_token \ + --upload-file path/to/file.zip \ + https://gitea.example.com/api/packages/testuser/go/upload +``` + +If you are using 2FA or OAuth use a [personal access token]({{< relref "doc/development/api-usage.en-us.md#authentication" >}}) instead of the password. + +The server responds with the following HTTP Status codes. + +| HTTP Status Code | Meaning | +| ----------------- | ------- | +| `201 Created` | The package has been published. | +| `400 Bad Request` | The package is invalid. | +| `409 Conflict` | A package with the same name exist already. | + +## Install a package + +To install a Go package instruct Go to use the package registry as proxy: + +```shell +# use latest version +GOPROXY=https://gitea.example.com/api/packages/{owner}/go go install {package_name} +# or +GOPROXY=https://gitea.example.com/api/packages/{owner}/go go install {package_name}@latest +# use specific version +GOPROXY=https://gitea.example.com/api/packages/{owner}/go go install {package_name}@{package_version} +``` + +| Parameter | Description | +| ----------------- | ----------- | +| `owner` | The owner of the package. | +| `package_name` | The package name. | +| `package_version` | The package version. | + +If the owner of the packages is private you need to [provide credentials](https://go.dev/ref/mod#private-module-proxy-auth). + +More information about the `GOPROXY` environment variable and how to protect against data leaks can be found in [the documentation](https://go.dev/ref/mod#private-modules). diff --git a/docs/content/doc/usage/packages/overview.en-us.md b/docs/content/doc/usage/packages/overview.en-us.md index 8a70a352eb3a..87164e35d886 100644 --- a/docs/content/doc/usage/packages/overview.en-us.md +++ b/docs/content/doc/usage/packages/overview.en-us.md @@ -27,6 +27,7 @@ The following package managers are currently supported: | Name | Language | Package client | | ---- | -------- | -------------- | +| [Alpine]({{< relref "doc/usage/packages/alpine.en-us.md" >}}) | - | `apk` | | [Cargo]({{< relref "doc/usage/packages/cargo.en-us.md" >}}) | Rust | `cargo` | | [Chef]({{< relref "doc/usage/packages/chef.en-us.md" >}}) | - | `knife` | | [Composer]({{< relref "doc/usage/packages/composer.en-us.md" >}}) | PHP | `composer` | diff --git a/docs/content/doc/usage/packages/rpm.en-us.md b/docs/content/doc/usage/packages/rpm.en-us.md index 2f9bb539bef7..7b256046c546 100644 --- a/docs/content/doc/usage/packages/rpm.en-us.md +++ b/docs/content/doc/usage/packages/rpm.en-us.md @@ -69,7 +69,7 @@ curl --user your_username:your_password_or_token \ If you are using 2FA or OAuth use a [personal access token]({{< relref "doc/development/api-usage.en-us.md#authentication" >}}) instead of the password. You cannot publish a file with the same name twice to a package. You must delete the existing package version first. -The server reponds with the following HTTP Status codes. +The server responds with the following HTTP Status codes. | HTTP Status Code | Meaning | | ----------------- | ------- | @@ -99,7 +99,7 @@ curl --user your_username:your_token_or_password -X DELETE \ https://gitea.example.com/api/packages/testuser/rpm/test-package/1.0.0/x86_64 ``` -The server reponds with the following HTTP Status codes. +The server responds with the following HTTP Status codes. | HTTP Status Code | Meaning | | ----------------- | ------- | diff --git a/docs/content/doc/usage/packages/storage.en-us.md b/docs/content/doc/usage/packages/storage.en-us.md index 15481ba7a395..598a636f5e30 100644 --- a/docs/content/doc/usage/packages/storage.en-us.md +++ b/docs/content/doc/usage/packages/storage.en-us.md @@ -9,7 +9,7 @@ menu: sidebar: parent: "packages" name: "Storage" - weight: 5 + weight: 2 identifier: "storage" --- diff --git a/models/actions/run_list.go b/models/actions/run_list.go index bc69c658409a..56de8eb9169c 100644 --- a/models/actions/run_list.go +++ b/models/actions/run_list.go @@ -10,7 +10,6 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/container" - "code.gitea.io/gitea/modules/util" "xorm.io/builder" ) @@ -45,6 +44,9 @@ func (runs RunList) LoadTriggerUser(ctx context.Context) error { run.TriggerUser = user_model.NewActionsUser() } else { run.TriggerUser = users[run.TriggerUserID] + if run.TriggerUser == nil { + run.TriggerUser = user_model.NewGhostUser() + } } } return nil @@ -66,7 +68,6 @@ type FindRunOptions struct { db.ListOptions RepoID int64 OwnerID int64 - IsClosed util.OptionalBool WorkflowFileName string TriggerUserID int64 Approved bool // not util.OptionalBool, it works only when it's true @@ -80,14 +81,6 @@ func (opts FindRunOptions) toConds() builder.Cond { if opts.OwnerID > 0 { cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) } - if opts.IsClosed.IsFalse() { - cond = cond.And(builder.Eq{"status": StatusWaiting}.Or( - builder.Eq{"status": StatusRunning})) - } else if opts.IsClosed.IsTrue() { - cond = cond.And( - builder.Neq{"status": StatusWaiting}.And( - builder.Neq{"status": StatusRunning})) - } if opts.WorkflowFileName != "" { cond = cond.And(builder.Eq{"workflow_id": opts.WorkflowFileName}) } diff --git a/models/git/commit_status.go b/models/git/commit_status.go index 82cbb2363739..6028e4664932 100644 --- a/models/git/commit_status.go +++ b/models/git/commit_status.go @@ -23,6 +23,7 @@ import ( api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" + "xorm.io/builder" "xorm.io/xorm" ) @@ -240,6 +241,55 @@ func GetLatestCommitStatus(ctx context.Context, repoID int64, sha string, listOp return statuses, count, db.GetEngine(ctx).In("id", ids).Find(&statuses) } +// GetLatestCommitStatusForPairs returns all statuses with a unique context for a given list of repo-sha pairs +func GetLatestCommitStatusForPairs(ctx context.Context, repoIDsToLatestCommitSHAs map[int64]string, listOptions db.ListOptions) (map[int64][]*CommitStatus, error) { + type result struct { + ID int64 + RepoID int64 + } + + results := make([]result, 0, len(repoIDsToLatestCommitSHAs)) + + sess := db.GetEngine(ctx).Table(&CommitStatus{}) + + // Create a disjunction of conditions for each repoID and SHA pair + conds := make([]builder.Cond, 0, len(repoIDsToLatestCommitSHAs)) + for repoID, sha := range repoIDsToLatestCommitSHAs { + conds = append(conds, builder.Eq{"repo_id": repoID, "sha": sha}) + } + sess = sess.Where(builder.Or(conds...)). + Select("max( id ) as id, repo_id"). + GroupBy("context_hash, repo_id").OrderBy("max( id ) desc") + + sess = db.SetSessionPagination(sess, &listOptions) + + err := sess.Find(&results) + if err != nil { + return nil, err + } + + ids := make([]int64, 0, len(results)) + repoStatuses := make(map[int64][]*CommitStatus) + for _, result := range results { + ids = append(ids, result.ID) + } + + statuses := make([]*CommitStatus, 0, len(ids)) + if len(ids) > 0 { + err = db.GetEngine(ctx).In("id", ids).Find(&statuses) + if err != nil { + return nil, err + } + + // Group the statuses by repo ID + for _, status := range statuses { + repoStatuses[status.RepoID] = append(repoStatuses[status.RepoID], status) + } + } + + return repoStatuses, nil +} + // FindRepoRecentCommitStatusContexts returns repository's recent commit status contexts func FindRepoRecentCommitStatusContexts(ctx context.Context, repoID int64, before time.Duration) ([]string, error) { start := timeutil.TimeStampNow().AddDuration(-before) diff --git a/models/packages/alpine/search.go b/models/packages/alpine/search.go new file mode 100644 index 000000000000..77eccb90ed5e --- /dev/null +++ b/models/packages/alpine/search.go @@ -0,0 +1,53 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package alpine + +import ( + "context" + + packages_model "code.gitea.io/gitea/models/packages" + alpine_module "code.gitea.io/gitea/modules/packages/alpine" +) + +// GetBranches gets all available branches +func GetBranches(ctx context.Context, ownerID int64) ([]string, error) { + return packages_model.GetDistinctPropertyValues( + ctx, + packages_model.TypeAlpine, + ownerID, + packages_model.PropertyTypeFile, + alpine_module.PropertyBranch, + nil, + ) +} + +// GetRepositories gets all available repositories for the given branch +func GetRepositories(ctx context.Context, ownerID int64, branch string) ([]string, error) { + return packages_model.GetDistinctPropertyValues( + ctx, + packages_model.TypeAlpine, + ownerID, + packages_model.PropertyTypeFile, + alpine_module.PropertyRepository, + &packages_model.DistinctPropertyDependency{ + Name: alpine_module.PropertyBranch, + Value: branch, + }, + ) +} + +// GetArchitectures gets all available architectures for the given repository +func GetArchitectures(ctx context.Context, ownerID int64, repository string) ([]string, error) { + return packages_model.GetDistinctPropertyValues( + ctx, + packages_model.TypeAlpine, + ownerID, + packages_model.PropertyTypeFile, + alpine_module.PropertyArchitecture, + &packages_model.DistinctPropertyDependency{ + Name: alpine_module.PropertyRepository, + Value: repository, + }, + ) +} diff --git a/models/packages/debian/search.go b/models/packages/debian/search.go index 332a4f7040c5..c63a31930462 100644 --- a/models/packages/debian/search.go +++ b/models/packages/debian/search.go @@ -88,44 +88,42 @@ func SearchLatestPackages(ctx context.Context, opts *PackageSearchOptions) ([]*p // GetDistributions gets all available distributions func GetDistributions(ctx context.Context, ownerID int64) ([]string, error) { - return getDistinctPropertyValues(ctx, ownerID, "", debian_module.PropertyDistribution) + return packages.GetDistinctPropertyValues( + ctx, + packages.TypeDebian, + ownerID, + packages.PropertyTypeFile, + debian_module.PropertyDistribution, + nil, + ) } // GetComponents gets all available components for the given distribution func GetComponents(ctx context.Context, ownerID int64, distribution string) ([]string, error) { - return getDistinctPropertyValues(ctx, ownerID, distribution, debian_module.PropertyComponent) + return packages.GetDistinctPropertyValues( + ctx, + packages.TypeDebian, + ownerID, + packages.PropertyTypeFile, + debian_module.PropertyComponent, + &packages.DistinctPropertyDependency{ + Name: debian_module.PropertyDistribution, + Value: distribution, + }, + ) } // GetArchitectures gets all available architectures for the given distribution func GetArchitectures(ctx context.Context, ownerID int64, distribution string) ([]string, error) { - return getDistinctPropertyValues(ctx, ownerID, distribution, debian_module.PropertyArchitecture) -} - -func getDistinctPropertyValues(ctx context.Context, ownerID int64, distribution, propName string) ([]string, error) { - var cond builder.Cond = builder.Eq{ - "package_property.ref_type": packages.PropertyTypeFile, - "package_property.name": propName, - "package.type": packages.TypeDebian, - "package.owner_id": ownerID, - } - if distribution != "" { - innerCond := builder. - Expr("pp.ref_id = package_property.ref_id"). - And(builder.Eq{ - "pp.ref_type": packages.PropertyTypeFile, - "pp.name": debian_module.PropertyDistribution, - "pp.value": distribution, - }) - cond = cond.And(builder.Exists(builder.Select("pp.ref_id").From("package_property pp").Where(innerCond))) - } - - values := make([]string, 0, 5) - return values, db.GetEngine(ctx). - Table("package_property"). - Distinct("package_property.value"). - Join("INNER", "package_file", "package_file.id = package_property.ref_id"). - Join("INNER", "package_version", "package_version.id = package_file.version_id"). - Join("INNER", "package", "package.id = package_version.package_id"). - Where(cond). - Find(&values) + return packages.GetDistinctPropertyValues( + ctx, + packages.TypeDebian, + ownerID, + packages.PropertyTypeFile, + debian_module.PropertyArchitecture, + &packages.DistinctPropertyDependency{ + Name: debian_module.PropertyDistribution, + Value: distribution, + }, + ) } diff --git a/models/packages/descriptor.go b/models/packages/descriptor.go index 1cac2eb02210..8e0165086639 100644 --- a/models/packages/descriptor.go +++ b/models/packages/descriptor.go @@ -12,6 +12,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/packages/alpine" "code.gitea.io/gitea/modules/packages/cargo" "code.gitea.io/gitea/modules/packages/chef" "code.gitea.io/gitea/modules/packages/composer" @@ -136,6 +137,8 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc var metadata interface{} switch p.Type { + case TypeAlpine: + metadata = &alpine.VersionMetadata{} case TypeCargo: metadata = &cargo.Metadata{} case TypeChef: @@ -152,6 +155,8 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc metadata = &debian.Metadata{} case TypeGeneric: // generic packages have no metadata + case TypeGo: + // go packages have no metadata case TypeHelm: metadata = &helm.Metadata{} case TypeNuGet: diff --git a/models/packages/package.go b/models/packages/package.go index a817ab6ff195..2dfed7804627 100644 --- a/models/packages/package.go +++ b/models/packages/package.go @@ -30,6 +30,7 @@ type Type string // List of supported packages const ( + TypeAlpine Type = "alpine" TypeCargo Type = "cargo" TypeChef Type = "chef" TypeComposer Type = "composer" @@ -38,6 +39,7 @@ const ( TypeContainer Type = "container" TypeDebian Type = "debian" TypeGeneric Type = "generic" + TypeGo Type = "go" TypeHelm Type = "helm" TypeMaven Type = "maven" TypeNpm Type = "npm" @@ -51,6 +53,7 @@ const ( ) var TypeList = []Type{ + TypeAlpine, TypeCargo, TypeChef, TypeComposer, @@ -59,6 +62,7 @@ var TypeList = []Type{ TypeContainer, TypeDebian, TypeGeneric, + TypeGo, TypeHelm, TypeMaven, TypeNpm, @@ -74,6 +78,8 @@ var TypeList = []Type{ // Name gets the name of the package type func (pt Type) Name() string { switch pt { + case TypeAlpine: + return "Alpine" case TypeCargo: return "Cargo" case TypeChef: @@ -90,6 +96,8 @@ func (pt Type) Name() string { return "Debian" case TypeGeneric: return "Generic" + case TypeGo: + return "Go" case TypeHelm: return "Helm" case TypeMaven: @@ -117,6 +125,8 @@ func (pt Type) Name() string { // SVGName gets the name of the package type svg image func (pt Type) SVGName() string { switch pt { + case TypeAlpine: + return "gitea-alpine" case TypeCargo: return "gitea-cargo" case TypeChef: @@ -133,6 +143,8 @@ func (pt Type) SVGName() string { return "gitea-debian" case TypeGeneric: return "octicon-package" + case TypeGo: + return "gitea-go" case TypeHelm: return "gitea-helm" case TypeMaven: diff --git a/models/packages/package_property.go b/models/packages/package_property.go index e03b12c9df4d..e0170016cfc9 100644 --- a/models/packages/package_property.go +++ b/models/packages/package_property.go @@ -7,6 +7,8 @@ import ( "context" "code.gitea.io/gitea/models/db" + + "xorm.io/builder" ) func init() { @@ -81,3 +83,39 @@ func DeletePropertyByName(ctx context.Context, refType PropertyType, refID int64 _, err := db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ? AND name = ?", refType, refID, name).Delete(&PackageProperty{}) return err } + +type DistinctPropertyDependency struct { + Name string + Value string +} + +// GetDistinctPropertyValues returns all distinct property values for a given type. +// Optional: Search only in dependence of another property. +func GetDistinctPropertyValues(ctx context.Context, packageType Type, ownerID int64, refType PropertyType, propertyName string, dep *DistinctPropertyDependency) ([]string, error) { + var cond builder.Cond = builder.Eq{ + "package_property.ref_type": refType, + "package_property.name": propertyName, + "package.type": packageType, + "package.owner_id": ownerID, + } + if dep != nil { + innerCond := builder. + Expr("pp.ref_id = package_property.ref_id"). + And(builder.Eq{ + "pp.ref_type": refType, + "pp.name": dep.Name, + "pp.value": dep.Value, + }) + cond = cond.And(builder.Exists(builder.Select("pp.ref_id").From("package_property pp").Where(innerCond))) + } + + values := make([]string, 0, 5) + return values, db.GetEngine(ctx). + Table("package_property"). + Distinct("package_property.value"). + Join("INNER", "package_file", "package_file.id = package_property.ref_id"). + Join("INNER", "package_version", "package_version.id = package_file.version_id"). + Join("INNER", "package", "package.id = package_version.package_id"). + Where(cond). + Find(&values) +} diff --git a/models/repo/repo.go b/models/repo/repo.go index 2e8c28cbb3a8..7cbd5867b7fb 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -547,16 +547,9 @@ func ComposeHTTPSCloneURL(owner, repo string) string { return fmt.Sprintf("%s%s/%s.git", setting.AppURL, url.PathEscape(owner), url.PathEscape(repo)) } -func (repo *Repository) cloneLink(isWiki bool) *CloneLink { - repoName := repo.Name - if isWiki { - repoName += ".wiki" - } - +func ComposeSSHCloneURL(ownerName, repoName string) string { sshUser := setting.SSH.User - cl := new(CloneLink) - // if we have a ipv6 literal we need to put brackets around it // for the git cloning to work. sshDomain := setting.SSH.Domain @@ -566,12 +559,25 @@ func (repo *Repository) cloneLink(isWiki bool) *CloneLink { } if setting.SSH.Port != 22 { - cl.SSH = fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, net.JoinHostPort(setting.SSH.Domain, strconv.Itoa(setting.SSH.Port)), url.PathEscape(repo.OwnerName), url.PathEscape(repoName)) - } else if setting.Repository.UseCompatSSHURI { - cl.SSH = fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, sshDomain, url.PathEscape(repo.OwnerName), url.PathEscape(repoName)) - } else { - cl.SSH = fmt.Sprintf("%s@%s:%s/%s.git", sshUser, sshDomain, url.PathEscape(repo.OwnerName), url.PathEscape(repoName)) + return fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, + net.JoinHostPort(setting.SSH.Domain, strconv.Itoa(setting.SSH.Port)), + url.PathEscape(ownerName), + url.PathEscape(repoName)) + } + if setting.Repository.UseCompatSSHURI { + return fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, sshDomain, url.PathEscape(ownerName), url.PathEscape(repoName)) } + return fmt.Sprintf("%s@%s:%s/%s.git", sshUser, sshDomain, url.PathEscape(ownerName), url.PathEscape(repoName)) +} + +func (repo *Repository) cloneLink(isWiki bool) *CloneLink { + repoName := repo.Name + if isWiki { + repoName += ".wiki" + } + + cl := new(CloneLink) + cl.SSH = ComposeSSHCloneURL(repo.OwnerName, repoName) cl.HTTPS = ComposeHTTPSCloneURL(repo.OwnerName, repoName) return cl } diff --git a/modules/avatar/avatar.go b/modules/avatar/avatar.go index c166f144042a..10de85b74eb3 100644 --- a/modules/avatar/avatar.go +++ b/modules/avatar/avatar.go @@ -5,13 +5,14 @@ package avatar import ( "bytes" + "errors" "fmt" "image" "image/color" + "image/png" _ "image/gif" // for processing gif images _ "image/jpeg" // for processing jpeg images - _ "image/png" // for processing png images "code.gitea.io/gitea/modules/avatar/identicon" "code.gitea.io/gitea/modules/setting" @@ -22,8 +23,11 @@ import ( _ "golang.org/x/image/webp" // for processing webp images ) -// AvatarSize returns avatar's size -const AvatarSize = 290 +// DefaultAvatarSize is the target CSS pixel size for avatar generation. It is +// multiplied by setting.Avatar.RenderedSizeFactor and the resulting size is the +// usual size of avatar image saved on server, unless the original file is smaller +// than the size after resizing. +const DefaultAvatarSize = 256 // RandomImageSize generates and returns a random avatar image unique to input data // in custom size (height and width). @@ -39,28 +43,44 @@ func RandomImageSize(size int, data []byte) (image.Image, error) { // RandomImage generates and returns a random avatar image unique to input data // in default size (height and width). func RandomImage(data []byte) (image.Image, error) { - return RandomImageSize(AvatarSize, data) + return RandomImageSize(DefaultAvatarSize*setting.Avatar.RenderedSizeFactor, data) } -// Prepare accepts a byte slice as input, validates it contains an image of an -// acceptable format, and crops and resizes it appropriately. -func Prepare(data []byte) (*image.Image, error) { - imgCfg, _, err := image.DecodeConfig(bytes.NewReader(data)) +// processAvatarImage process the avatar image data, crop and resize it if necessary. +// the returned data could be the original image if no processing is needed. +func processAvatarImage(data []byte, maxOriginSize int64) ([]byte, error) { + imgCfg, imgType, err := image.DecodeConfig(bytes.NewReader(data)) if err != nil { - return nil, fmt.Errorf("DecodeConfig: %w", err) + return nil, fmt.Errorf("image.DecodeConfig: %w", err) } + + // for safety, only accept known types explicitly + if imgType != "png" && imgType != "jpeg" && imgType != "gif" && imgType != "webp" { + return nil, errors.New("unsupported avatar image type") + } + + // do not process image which is too large, it would consume too much memory if imgCfg.Width > setting.Avatar.MaxWidth { - return nil, fmt.Errorf("Image width is too large: %d > %d", imgCfg.Width, setting.Avatar.MaxWidth) + return nil, fmt.Errorf("image width is too large: %d > %d", imgCfg.Width, setting.Avatar.MaxWidth) } if imgCfg.Height > setting.Avatar.MaxHeight { - return nil, fmt.Errorf("Image height is too large: %d > %d", imgCfg.Height, setting.Avatar.MaxHeight) + return nil, fmt.Errorf("image height is too large: %d > %d", imgCfg.Height, setting.Avatar.MaxHeight) + } + + // If the origin is small enough, just use it, then APNG could be supported, + // otherwise, if the image is processed later, APNG loses animation. + // And one more thing, webp is not fully supported, for animated webp, image.DecodeConfig works but Decode fails. + // So for animated webp, if the uploaded file is smaller than maxOriginSize, it will be used, if it's larger, there will be an error. + if len(data) < int(maxOriginSize) { + return data, nil } img, _, err := image.Decode(bytes.NewReader(data)) if err != nil { - return nil, fmt.Errorf("Decode: %w", err) + return nil, fmt.Errorf("image.Decode: %w", err) } + // try to crop and resize the origin image if necessary if imgCfg.Width != imgCfg.Height { var newSize, ax, ay int if imgCfg.Width > imgCfg.Height { @@ -74,13 +94,33 @@ func Prepare(data []byte) (*image.Image, error) { img, err = cutter.Crop(img, cutter.Config{ Width: newSize, Height: newSize, - Anchor: image.Point{ax, ay}, + Anchor: image.Point{X: ax, Y: ay}, }) if err != nil { return nil, err } } - img = resize.Resize(AvatarSize, AvatarSize, img, resize.Bilinear) - return &img, nil + targetSize := uint(DefaultAvatarSize * setting.Avatar.RenderedSizeFactor) + img = resize.Resize(targetSize, targetSize, img, resize.Bilinear) + + // try to encode the cropped/resized image to png + bs := bytes.Buffer{} + if err = png.Encode(&bs, img); err != nil { + return nil, err + } + resized := bs.Bytes() + + // usually the png compression is not good enough, use the original image (no cropping/resizing) if the origin is smaller + if len(data) <= len(resized) { + return data, nil + } + + return resized, nil +} + +// ProcessAvatarImage process the avatar image data, crop and resize it if necessary. +// the returned data could be the original image if no processing is needed. +func ProcessAvatarImage(data []byte) ([]byte, error) { + return processAvatarImage(data, setting.Avatar.MaxOriginSize) } diff --git a/modules/avatar/avatar_test.go b/modules/avatar/avatar_test.go index 5ef4ed379bec..a721c7786807 100644 --- a/modules/avatar/avatar_test.go +++ b/modules/avatar/avatar_test.go @@ -4,6 +4,9 @@ package avatar import ( + "bytes" + "image" + "image/png" "os" "testing" @@ -25,49 +28,109 @@ func Test_RandomImage(t *testing.T) { assert.NoError(t, err) } -func Test_PrepareWithPNG(t *testing.T) { +func Test_ProcessAvatarPNG(t *testing.T) { setting.Avatar.MaxWidth = 4096 setting.Avatar.MaxHeight = 4096 data, err := os.ReadFile("testdata/avatar.png") assert.NoError(t, err) - imgPtr, err := Prepare(data) + _, err = processAvatarImage(data, 262144) assert.NoError(t, err) - - assert.Equal(t, 290, (*imgPtr).Bounds().Max.X) - assert.Equal(t, 290, (*imgPtr).Bounds().Max.Y) } -func Test_PrepareWithJPEG(t *testing.T) { +func Test_ProcessAvatarJPEG(t *testing.T) { setting.Avatar.MaxWidth = 4096 setting.Avatar.MaxHeight = 4096 data, err := os.ReadFile("testdata/avatar.jpeg") assert.NoError(t, err) - imgPtr, err := Prepare(data) + _, err = processAvatarImage(data, 262144) assert.NoError(t, err) - - assert.Equal(t, 290, (*imgPtr).Bounds().Max.X) - assert.Equal(t, 290, (*imgPtr).Bounds().Max.Y) } -func Test_PrepareWithInvalidImage(t *testing.T) { +func Test_ProcessAvatarInvalidData(t *testing.T) { setting.Avatar.MaxWidth = 5 setting.Avatar.MaxHeight = 5 - _, err := Prepare([]byte{}) - assert.EqualError(t, err, "DecodeConfig: image: unknown format") + _, err := processAvatarImage([]byte{}, 12800) + assert.EqualError(t, err, "image.DecodeConfig: image: unknown format") } -func Test_PrepareWithInvalidImageSize(t *testing.T) { +func Test_ProcessAvatarInvalidImageSize(t *testing.T) { setting.Avatar.MaxWidth = 5 setting.Avatar.MaxHeight = 5 data, err := os.ReadFile("testdata/avatar.png") assert.NoError(t, err) - _, err = Prepare(data) - assert.EqualError(t, err, "Image width is too large: 10 > 5") + _, err = processAvatarImage(data, 12800) + assert.EqualError(t, err, "image width is too large: 10 > 5") +} + +func Test_ProcessAvatarImage(t *testing.T) { + setting.Avatar.MaxWidth = 4096 + setting.Avatar.MaxHeight = 4096 + scaledSize := DefaultAvatarSize * setting.Avatar.RenderedSizeFactor + + newImgData := func(size int, optHeight ...int) []byte { + width := size + height := size + if len(optHeight) == 1 { + height = optHeight[0] + } + img := image.NewRGBA(image.Rect(0, 0, width, height)) + bs := bytes.Buffer{} + err := png.Encode(&bs, img) + assert.NoError(t, err) + return bs.Bytes() + } + + // if origin image canvas is too large, crop and resize it + origin := newImgData(500, 600) + result, err := processAvatarImage(origin, 0) + assert.NoError(t, err) + assert.NotEqual(t, origin, result) + decoded, err := png.Decode(bytes.NewReader(result)) + assert.NoError(t, err) + assert.EqualValues(t, scaledSize, decoded.Bounds().Max.X) + assert.EqualValues(t, scaledSize, decoded.Bounds().Max.Y) + + // if origin image is smaller than the default size, use the origin image + origin = newImgData(1) + result, err = processAvatarImage(origin, 0) + assert.NoError(t, err) + assert.Equal(t, origin, result) + + // use the origin image if the origin is smaller + origin = newImgData(scaledSize + 100) + result, err = processAvatarImage(origin, 0) + assert.NoError(t, err) + assert.Less(t, len(result), len(origin)) + + // still use the origin image if the origin doesn't exceed the max-origin-size + origin = newImgData(scaledSize + 100) + result, err = processAvatarImage(origin, 262144) + assert.NoError(t, err) + assert.Equal(t, origin, result) + + // allow to use known image format (eg: webp) if it is small enough + origin, err = os.ReadFile("testdata/animated.webp") + assert.NoError(t, err) + result, err = processAvatarImage(origin, 262144) + assert.NoError(t, err) + assert.Equal(t, origin, result) + + // do not support unknown image formats, eg: SVG may contain embedded JS + origin = []byte("") + _, err = processAvatarImage(origin, 262144) + assert.ErrorContains(t, err, "image: unknown format") + + // make sure the canvas size limit works + setting.Avatar.MaxWidth = 5 + setting.Avatar.MaxHeight = 5 + origin = newImgData(10) + _, err = processAvatarImage(origin, 262144) + assert.ErrorContains(t, err, "image width is too large: 10 > 5") } diff --git a/modules/avatar/testdata/animated.webp b/modules/avatar/testdata/animated.webp new file mode 100644 index 000000000000..4c05f4695cb6 Binary files /dev/null and b/modules/avatar/testdata/animated.webp differ diff --git a/modules/context/context_test.go b/modules/context/context_test.go index a6facc97880f..033ce2ef0ad5 100644 --- a/modules/context/context_test.go +++ b/modules/context/context_test.go @@ -5,16 +5,16 @@ package context import ( "net/http" + "net/http/httptest" "testing" - "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/setting" "github.com/stretchr/testify/assert" ) func TestRemoveSessionCookieHeader(t *testing.T) { - w := httplib.NewMockResponseWriter() + w := httptest.NewRecorder() w.Header().Add("Set-Cookie", (&http.Cookie{Name: setting.SessionConfig.CookieName, Value: "foo"}).String()) w.Header().Add("Set-Cookie", (&http.Cookie{Name: "other", Value: "bar"}).String()) assert.Len(t, w.Header().Values("Set-Cookie"), 2) diff --git a/modules/context/repo.go b/modules/context/repo.go index 84e07ab4228d..b20ea26e4eee 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -319,7 +319,14 @@ func EarlyResponseForGoGetMeta(ctx *Context) { ctx.PlainText(http.StatusBadRequest, "invalid repository path") return } - goImportContent := fmt.Sprintf("%s git %s", ComposeGoGetImport(username, reponame), repo_model.ComposeHTTPSCloneURL(username, reponame)) + + var cloneURL string + if setting.Repository.GoGetCloneURLProtocol == "ssh" { + cloneURL = repo_model.ComposeSSHCloneURL(username, reponame) + } else { + cloneURL = repo_model.ComposeHTTPSCloneURL(username, reponame) + } + goImportContent := fmt.Sprintf("%s git %s", ComposeGoGetImport(username, reponame), cloneURL) htmlMeta := fmt.Sprintf(``, html.EscapeString(goImportContent)) ctx.PlainText(http.StatusOK, htmlMeta) } diff --git a/modules/git/repo_branch.go b/modules/git/repo_branch.go index 14dcf14d8a0d..3bb6ef5223a2 100644 --- a/modules/git/repo_branch.go +++ b/modules/git/repo_branch.go @@ -106,6 +106,17 @@ func GetBranchesByPath(ctx context.Context, path string, skip, limit int) ([]*Br return gitRepo.GetBranches(skip, limit) } +// GetBranchCommitID returns a branch commit ID by its name +func GetBranchCommitID(ctx context.Context, path, branch string) (string, error) { + gitRepo, err := OpenRepository(ctx, path) + if err != nil { + return "", err + } + defer gitRepo.Close() + + return gitRepo.GetBranchCommitID(branch) +} + // GetBranches returns a slice of *git.Branch func (repo *Repository) GetBranches(skip, limit int) ([]*Branch, int, error) { brs, countAll, err := repo.GetBranchNames(skip, limit) diff --git a/modules/httpcache/httpcache.go b/modules/httpcache/httpcache.go index 46e0152ef455..001ac0641537 100644 --- a/modules/httpcache/httpcache.go +++ b/modules/httpcache/httpcache.go @@ -4,10 +4,8 @@ package httpcache import ( - "encoding/base64" - "fmt" + "io" "net/http" - "os" "strconv" "strings" "time" @@ -37,38 +35,9 @@ func SetCacheControlInHeader(h http.Header, maxAge time.Duration, additionalDire h.Set("Cache-Control", strings.Join(append(directives, additionalDirectives...), ", ")) } -// generateETag generates an ETag based on size, filename and file modification time -func generateETag(fi os.FileInfo) string { - etag := fmt.Sprint(fi.Size()) + fi.Name() + fi.ModTime().UTC().Format(http.TimeFormat) - return `"` + base64.StdEncoding.EncodeToString([]byte(etag)) + `"` -} - -// HandleTimeCache handles time-based caching for a HTTP request -func HandleTimeCache(req *http.Request, w http.ResponseWriter, fi os.FileInfo) (handled bool) { - return HandleGenericTimeCache(req, w, fi.ModTime()) -} - -// HandleGenericTimeCache handles time-based caching for a HTTP request -func HandleGenericTimeCache(req *http.Request, w http.ResponseWriter, lastModified time.Time) (handled bool) { +func ServeContentWithCacheControl(w http.ResponseWriter, req *http.Request, name string, modTime time.Time, content io.ReadSeeker) { SetCacheControlInHeader(w.Header(), setting.StaticCacheTime) - - ifModifiedSince := req.Header.Get("If-Modified-Since") - if ifModifiedSince != "" { - t, err := time.Parse(http.TimeFormat, ifModifiedSince) - if err == nil && lastModified.Unix() <= t.Unix() { - w.WriteHeader(http.StatusNotModified) - return true - } - } - - w.Header().Set("Last-Modified", lastModified.Format(http.TimeFormat)) - return false -} - -// HandleFileETagCache handles ETag-based caching for a HTTP request -func HandleFileETagCache(req *http.Request, w http.ResponseWriter, fi os.FileInfo) (handled bool) { - etag := generateETag(fi) - return HandleGenericETagCache(req, w, etag) + http.ServeContent(w, req, name, modTime, content) } // HandleGenericETagCache handles ETag-based caching for a HTTP request. diff --git a/modules/httpcache/httpcache_test.go b/modules/httpcache/httpcache_test.go index 624d2f4d4bd1..d81f06097c12 100644 --- a/modules/httpcache/httpcache_test.go +++ b/modules/httpcache/httpcache_test.go @@ -6,23 +6,12 @@ package httpcache import ( "net/http" "net/http/httptest" - "os" "strings" "testing" - "time" "github.com/stretchr/testify/assert" ) -type mockFileInfo struct{} - -func (m mockFileInfo) Name() string { return "gitea.test" } -func (m mockFileInfo) Size() int64 { return int64(10) } -func (m mockFileInfo) Mode() os.FileMode { return os.ModePerm } -func (m mockFileInfo) ModTime() time.Time { return time.Time{} } -func (m mockFileInfo) IsDir() bool { return false } -func (m mockFileInfo) Sys() interface{} { return nil } - func countFormalHeaders(h http.Header) (c int) { for k := range h { // ignore our headers for internal usage @@ -34,52 +23,6 @@ func countFormalHeaders(h http.Header) (c int) { return c } -func TestHandleFileETagCache(t *testing.T) { - fi := mockFileInfo{} - etag := `"MTBnaXRlYS50ZXN0TW9uLCAwMSBKYW4gMDAwMSAwMDowMDowMCBHTVQ="` - - t.Run("No_If-None-Match", func(t *testing.T) { - req := &http.Request{Header: make(http.Header)} - w := httptest.NewRecorder() - - handled := HandleFileETagCache(req, w, fi) - - assert.False(t, handled) - assert.Equal(t, 2, countFormalHeaders(w.Header())) - assert.Contains(t, w.Header(), "Cache-Control") - assert.Contains(t, w.Header(), "Etag") - assert.Equal(t, etag, w.Header().Get("Etag")) - }) - t.Run("Wrong_If-None-Match", func(t *testing.T) { - req := &http.Request{Header: make(http.Header)} - w := httptest.NewRecorder() - - req.Header.Set("If-None-Match", `"wrong etag"`) - - handled := HandleFileETagCache(req, w, fi) - - assert.False(t, handled) - assert.Equal(t, 2, countFormalHeaders(w.Header())) - assert.Contains(t, w.Header(), "Cache-Control") - assert.Contains(t, w.Header(), "Etag") - assert.Equal(t, etag, w.Header().Get("Etag")) - }) - t.Run("Correct_If-None-Match", func(t *testing.T) { - req := &http.Request{Header: make(http.Header)} - w := httptest.NewRecorder() - - req.Header.Set("If-None-Match", etag) - - handled := HandleFileETagCache(req, w, fi) - - assert.True(t, handled) - assert.Equal(t, 1, countFormalHeaders(w.Header())) - assert.Contains(t, w.Header(), "Etag") - assert.Equal(t, etag, w.Header().Get("Etag")) - assert.Equal(t, http.StatusNotModified, w.Code) - }) -} - func TestHandleGenericETagCache(t *testing.T) { etag := `"test"` diff --git a/modules/httplib/mock.go b/modules/httplib/mock.go deleted file mode 100644 index 7d284e86fb92..000000000000 --- a/modules/httplib/mock.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package httplib - -import ( - "bytes" - "net/http" -) - -type MockResponseWriter struct { - header http.Header - - StatusCode int - BodyBuffer bytes.Buffer -} - -func (m *MockResponseWriter) Header() http.Header { - return m.header -} - -func (m *MockResponseWriter) Write(bytes []byte) (int, error) { - if m.StatusCode == 0 { - m.StatusCode = http.StatusOK - } - return m.BodyBuffer.Write(bytes) -} - -func (m *MockResponseWriter) WriteHeader(statusCode int) { - m.StatusCode = statusCode -} - -func NewMockResponseWriter() *MockResponseWriter { - return &MockResponseWriter{header: http.Header{}} -} diff --git a/modules/httplib/serve_test.go b/modules/httplib/serve_test.go index 0768f1c71393..fed4611d2179 100644 --- a/modules/httplib/serve_test.go +++ b/modules/httplib/serve_test.go @@ -6,6 +6,7 @@ package httplib import ( "fmt" "net/http" + "net/http/httptest" "net/url" "os" "strings" @@ -25,12 +26,12 @@ func TestServeContentByReader(t *testing.T) { r.Header.Set("Range", fmt.Sprintf("bytes=%s", rangeStr)) } reader := strings.NewReader(data) - w := NewMockResponseWriter() + w := httptest.NewRecorder() ServeContentByReader(r, w, "test", int64(len(data)), reader) - assert.Equal(t, expectedStatusCode, w.StatusCode) + assert.Equal(t, expectedStatusCode, w.Code) if expectedStatusCode == http.StatusPartialContent || expectedStatusCode == http.StatusOK { assert.Equal(t, fmt.Sprint(len(expectedContent)), w.Header().Get("Content-Length")) - assert.Equal(t, expectedContent, w.BodyBuffer.String()) + assert.Equal(t, expectedContent, w.Body.String()) } } @@ -76,12 +77,12 @@ func TestServeContentByReadSeeker(t *testing.T) { } defer seekReader.Close() - w := NewMockResponseWriter() + w := httptest.NewRecorder() ServeContentByReadSeeker(r, w, "test", time.Time{}, seekReader) - assert.Equal(t, expectedStatusCode, w.StatusCode) + assert.Equal(t, expectedStatusCode, w.Code) if expectedStatusCode == http.StatusPartialContent || expectedStatusCode == http.StatusOK { assert.Equal(t, fmt.Sprint(len(expectedContent)), w.Header().Get("Content-Length")) - assert.Equal(t, expectedContent, w.BodyBuffer.String()) + assert.Equal(t, expectedContent, w.Body.String()) } } diff --git a/modules/packages/alpine/metadata.go b/modules/packages/alpine/metadata.go new file mode 100644 index 000000000000..c2d0caffa125 --- /dev/null +++ b/modules/packages/alpine/metadata.go @@ -0,0 +1,236 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package alpine + +import ( + "archive/tar" + "bufio" + "compress/gzip" + "crypto/sha1" + "encoding/base64" + "io" + "strconv" + "strings" + + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/validation" +) + +var ( + ErrMissingPKGINFOFile = util.NewInvalidArgumentErrorf("PKGINFO file is missing") + ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid") + ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid") +) + +const ( + PropertyMetadata = "alpine.metadata" + PropertyBranch = "alpine.branch" + PropertyRepository = "alpine.repository" + PropertyArchitecture = "alpine.architecture" + + SettingKeyPrivate = "alpine.key.private" + SettingKeyPublic = "alpine.key.public" + + RepositoryPackage = "_alpine" + RepositoryVersion = "_repository" +) + +// https://wiki.alpinelinux.org/wiki/Apk_spec + +// Package represents an Alpine package +type Package struct { + Name string + Version string + VersionMetadata VersionMetadata + FileMetadata FileMetadata +} + +// Metadata of an Alpine package +type VersionMetadata struct { + Description string `json:"description,omitempty"` + License string `json:"license,omitempty"` + ProjectURL string `json:"project_url,omitempty"` + Maintainer string `json:"maintainer,omitempty"` +} + +type FileMetadata struct { + Checksum string `json:"checksum"` + Packager string `json:"packager,omitempty"` + BuildDate int64 `json:"build_date,omitempty"` + Size int64 `json:"size,omitempty"` + Architecture string `json:"architecture,omitempty"` + Origin string `json:"origin,omitempty"` + CommitHash string `json:"commit_hash,omitempty"` + InstallIf string `json:"install_if,omitempty"` + Provides []string `json:"provides,omitempty"` + Dependencies []string `json:"dependencies,omitempty"` +} + +// ParsePackage parses the Alpine package file +func ParsePackage(r io.Reader) (*Package, error) { + // Alpine packages are concated .tar.gz streams. Usually the first stream contains the package metadata. + + br := bufio.NewReader(r) // needed for gzip Multistream + + h := sha1.New() + + gzr, err := gzip.NewReader(&teeByteReader{br, h}) + if err != nil { + return nil, err + } + defer gzr.Close() + + for { + gzr.Multistream(false) + + tr := tar.NewReader(gzr) + for { + hd, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + + if hd.Name == ".PKGINFO" { + p, err := ParsePackageInfo(tr) + if err != nil { + return nil, err + } + + // drain the reader + for { + if _, err := tr.Next(); err != nil { + break + } + } + + p.FileMetadata.Checksum = "Q1" + base64.StdEncoding.EncodeToString(h.Sum(nil)) + + return p, nil + } + } + + h = sha1.New() + + err = gzr.Reset(&teeByteReader{br, h}) + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + } + + return nil, ErrMissingPKGINFOFile +} + +// ParsePackageInfo parses a PKGINFO file to retrieve the metadata of an Alpine package +func ParsePackageInfo(r io.Reader) (*Package, error) { + p := &Package{} + + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := scanner.Text() + + if strings.HasPrefix(line, "#") { + continue + } + + i := strings.IndexRune(line, '=') + if i == -1 { + continue + } + + key := strings.TrimSpace(line[:i]) + value := strings.TrimSpace(line[i+1:]) + + switch key { + case "pkgname": + p.Name = value + case "pkgver": + p.Version = value + case "pkgdesc": + p.VersionMetadata.Description = value + case "url": + p.VersionMetadata.ProjectURL = value + case "builddate": + n, err := strconv.ParseInt(value, 10, 64) + if err == nil { + p.FileMetadata.BuildDate = n + } + case "size": + n, err := strconv.ParseInt(value, 10, 64) + if err == nil { + p.FileMetadata.Size = n + } + case "arch": + p.FileMetadata.Architecture = value + case "origin": + p.FileMetadata.Origin = value + case "commit": + p.FileMetadata.CommitHash = value + case "maintainer": + p.VersionMetadata.Maintainer = value + case "packager": + p.FileMetadata.Packager = value + case "license": + p.VersionMetadata.License = value + case "install_if": + p.FileMetadata.InstallIf = value + case "provides": + if value != "" { + p.FileMetadata.Provides = append(p.FileMetadata.Provides, value) + } + case "depend": + if value != "" { + p.FileMetadata.Dependencies = append(p.FileMetadata.Dependencies, value) + } + } + } + if err := scanner.Err(); err != nil { + return nil, err + } + + if p.Name == "" { + return nil, ErrInvalidName + } + + if p.Version == "" { + return nil, ErrInvalidVersion + } + + if !validation.IsValidURL(p.VersionMetadata.ProjectURL) { + p.VersionMetadata.ProjectURL = "" + } + + return p, nil +} + +// Same as io.TeeReader but implements io.ByteReader +type teeByteReader struct { + r *bufio.Reader + w io.Writer +} + +func (t *teeByteReader) Read(p []byte) (int, error) { + n, err := t.r.Read(p) + if n > 0 { + if n, err := t.w.Write(p[:n]); err != nil { + return n, err + } + } + return n, err +} + +func (t *teeByteReader) ReadByte() (byte, error) { + b, err := t.r.ReadByte() + if err == nil { + if _, err := t.w.Write([]byte{b}); err != nil { + return 0, err + } + } + return b, err +} diff --git a/modules/packages/alpine/metadata_test.go b/modules/packages/alpine/metadata_test.go new file mode 100644 index 000000000000..2a3c48ffb9a2 --- /dev/null +++ b/modules/packages/alpine/metadata_test.go @@ -0,0 +1,143 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package alpine + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "io" + "testing" + + "github.com/stretchr/testify/assert" +) + +const ( + packageName = "gitea" + packageVersion = "1.0.1" + packageDescription = "Package Description" + packageProjectURL = "https://gitea.io" + packageMaintainer = "KN4CK3R " +) + +func createPKGINFOContent(name, version string) []byte { + return []byte(`pkgname = ` + name + ` +pkgver = ` + version + ` +pkgdesc = ` + packageDescription + ` +url = ` + packageProjectURL + ` +# comment +builddate = 1678834800 +packager = Gitea +size = 123456 +arch = aarch64 +origin = origin +commit = 1111e709613fbc979651b09ac2bc27c6591a9999 +maintainer = ` + packageMaintainer + ` +license = MIT +depend = common +install_if = value +depend = gitea +provides = common +provides = gitea`) +} + +func TestParsePackage(t *testing.T) { + createPackage := func(name string, content []byte) io.Reader { + names := []string{"first.stream", name} + contents := [][]byte{{0}, content} + + var buf bytes.Buffer + zw := gzip.NewWriter(&buf) + + for i := range names { + if i != 0 { + zw.Close() + zw.Reset(&buf) + } + + tw := tar.NewWriter(zw) + hdr := &tar.Header{ + Name: names[i], + Mode: 0o600, + Size: int64(len(contents[i])), + } + tw.WriteHeader(hdr) + tw.Write(contents[i]) + tw.Close() + } + + zw.Close() + + return &buf + } + + t.Run("MissingPKGINFOFile", func(t *testing.T) { + data := createPackage("dummy.txt", []byte{}) + + pp, err := ParsePackage(data) + assert.Nil(t, pp) + assert.ErrorIs(t, err, ErrMissingPKGINFOFile) + }) + + t.Run("InvalidPKGINFOFile", func(t *testing.T) { + data := createPackage(".PKGINFO", []byte{}) + + pp, err := ParsePackage(data) + assert.Nil(t, pp) + assert.ErrorIs(t, err, ErrInvalidName) + }) + + t.Run("Valid", func(t *testing.T) { + data := createPackage(".PKGINFO", createPKGINFOContent(packageName, packageVersion)) + + p, err := ParsePackage(data) + assert.NoError(t, err) + assert.NotNil(t, p) + + assert.Equal(t, "Q1SRYURM5+uQDqfHSwTnNIOIuuDVQ=", p.FileMetadata.Checksum) + }) +} + +func TestParsePackageInfo(t *testing.T) { + t.Run("InvalidName", func(t *testing.T) { + data := createPKGINFOContent("", packageVersion) + + p, err := ParsePackageInfo(bytes.NewReader(data)) + assert.Nil(t, p) + assert.ErrorIs(t, err, ErrInvalidName) + }) + + t.Run("InvalidVersion", func(t *testing.T) { + data := createPKGINFOContent(packageName, "") + + p, err := ParsePackageInfo(bytes.NewReader(data)) + assert.Nil(t, p) + assert.ErrorIs(t, err, ErrInvalidVersion) + }) + + t.Run("Valid", func(t *testing.T) { + data := createPKGINFOContent(packageName, packageVersion) + + p, err := ParsePackageInfo(bytes.NewReader(data)) + assert.NoError(t, err) + assert.NotNil(t, p) + + assert.Equal(t, packageName, p.Name) + assert.Equal(t, packageVersion, p.Version) + assert.Equal(t, packageDescription, p.VersionMetadata.Description) + assert.Equal(t, packageMaintainer, p.VersionMetadata.Maintainer) + assert.Equal(t, packageProjectURL, p.VersionMetadata.ProjectURL) + assert.Equal(t, "MIT", p.VersionMetadata.License) + assert.Empty(t, p.FileMetadata.Checksum) + assert.Equal(t, "Gitea ", p.FileMetadata.Packager) + assert.EqualValues(t, 1678834800, p.FileMetadata.BuildDate) + assert.EqualValues(t, 123456, p.FileMetadata.Size) + assert.Equal(t, "aarch64", p.FileMetadata.Architecture) + assert.Equal(t, "origin", p.FileMetadata.Origin) + assert.Equal(t, "1111e709613fbc979651b09ac2bc27c6591a9999", p.FileMetadata.CommitHash) + assert.Equal(t, "value", p.FileMetadata.InstallIf) + assert.ElementsMatch(t, []string{"common", "gitea"}, p.FileMetadata.Provides) + assert.ElementsMatch(t, []string{"common", "gitea"}, p.FileMetadata.Dependencies) + }) +} diff --git a/modules/packages/goproxy/metadata.go b/modules/packages/goproxy/metadata.go new file mode 100644 index 000000000000..40f7d2050803 --- /dev/null +++ b/modules/packages/goproxy/metadata.go @@ -0,0 +1,94 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package goproxy + +import ( + "archive/zip" + "fmt" + "io" + "path" + "strings" + + "code.gitea.io/gitea/modules/util" +) + +const ( + PropertyGoMod = "go.mod" + + maxGoModFileSize = 16 * 1024 * 1024 // https://go.dev/ref/mod#zip-path-size-constraints +) + +var ( + ErrInvalidStructure = util.NewInvalidArgumentErrorf("package has invalid structure") + ErrGoModFileTooLarge = util.NewInvalidArgumentErrorf("go.mod file is too large") +) + +type Package struct { + Name string + Version string + GoMod string +} + +// ParsePackage parses the Go package file +// https://go.dev/ref/mod#zip-files +func ParsePackage(r io.ReaderAt, size int64) (*Package, error) { + archive, err := zip.NewReader(r, size) + if err != nil { + return nil, err + } + + var p *Package + + for _, file := range archive.File { + nameAndVersion := path.Dir(file.Name) + + parts := strings.SplitN(nameAndVersion, "@", 2) + if len(parts) != 2 { + continue + } + + versionParts := strings.SplitN(parts[1], "/", 2) + + if p == nil { + p = &Package{ + Name: strings.TrimSuffix(nameAndVersion, "@"+parts[1]), + Version: versionParts[0], + } + } + + if len(versionParts) > 1 { + // files are expected in the "root" folder + continue + } + + if path.Base(file.Name) == "go.mod" { + if file.UncompressedSize64 > maxGoModFileSize { + return nil, ErrGoModFileTooLarge + } + + f, err := archive.Open(file.Name) + if err != nil { + return nil, err + } + defer f.Close() + + bytes, err := io.ReadAll(&io.LimitedReader{R: f, N: maxGoModFileSize}) + if err != nil { + return nil, err + } + + p.GoMod = string(bytes) + + return p, nil + } + } + + if p == nil { + return nil, ErrInvalidStructure + } + + p.GoMod = fmt.Sprintf("module %s", p.Name) + + return p, nil +} diff --git a/modules/packages/goproxy/metadata_test.go b/modules/packages/goproxy/metadata_test.go new file mode 100644 index 000000000000..4e7f394f8bce --- /dev/null +++ b/modules/packages/goproxy/metadata_test.go @@ -0,0 +1,75 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package goproxy + +import ( + "archive/zip" + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +const ( + packageName = "gitea.com/go-gitea/gitea" + packageVersion = "v0.0.1" +) + +func TestParsePackage(t *testing.T) { + createArchive := func(files map[string][]byte) *bytes.Reader { + var buf bytes.Buffer + zw := zip.NewWriter(&buf) + for name, content := range files { + w, _ := zw.Create(name) + w.Write(content) + } + zw.Close() + return bytes.NewReader(buf.Bytes()) + } + + t.Run("EmptyPackage", func(t *testing.T) { + data := createArchive(nil) + + p, err := ParsePackage(data, int64(data.Len())) + assert.Nil(t, p) + assert.ErrorIs(t, err, ErrInvalidStructure) + }) + + t.Run("InvalidNameOrVersionStructure", func(t *testing.T) { + data := createArchive(map[string][]byte{ + packageName + "/" + packageVersion + "/go.mod": {}, + }) + + p, err := ParsePackage(data, int64(data.Len())) + assert.Nil(t, p) + assert.ErrorIs(t, err, ErrInvalidStructure) + }) + + t.Run("GoModFileInWrongDirectory", func(t *testing.T) { + data := createArchive(map[string][]byte{ + packageName + "@" + packageVersion + "/subdir/go.mod": {}, + }) + + p, err := ParsePackage(data, int64(data.Len())) + assert.NotNil(t, p) + assert.NoError(t, err) + assert.Equal(t, packageName, p.Name) + assert.Equal(t, packageVersion, p.Version) + assert.Equal(t, "module gitea.com/go-gitea/gitea", p.GoMod) + }) + + t.Run("Valid", func(t *testing.T) { + data := createArchive(map[string][]byte{ + packageName + "@" + packageVersion + "/subdir/go.mod": []byte("invalid"), + packageName + "@" + packageVersion + "/go.mod": []byte("valid"), + }) + + p, err := ParsePackage(data, int64(data.Len())) + assert.NotNil(t, p) + assert.NoError(t, err) + assert.Equal(t, packageName, p.Name) + assert.Equal(t, packageVersion, p.Version) + assert.Equal(t, "valid", p.GoMod) + }) +} diff --git a/modules/public/public.go b/modules/public/public.go index 0c0e6dc1cc80..ed38d85cfaee 100644 --- a/modules/public/public.go +++ b/modules/public/public.go @@ -97,10 +97,6 @@ func handleRequest(w http.ResponseWriter, req *http.Request, fs http.FileSystem, return true } - if httpcache.HandleFileETagCache(req, w, fi) { - return true - } - serveContent(w, req, fi, fi.ModTime(), f) return true } @@ -124,11 +120,11 @@ func serveContent(w http.ResponseWriter, req *http.Request, fi os.FileInfo, modt w.Header().Set("Content-Type", "application/octet-stream") } w.Header().Set("Content-Encoding", "gzip") - http.ServeContent(w, req, fi.Name(), modtime, rdGzip) + httpcache.ServeContentWithCacheControl(w, req, fi.Name(), modtime, rdGzip) return } } - http.ServeContent(w, req, fi.Name(), modtime, content) + httpcache.ServeContentWithCacheControl(w, req, fi.Name(), modtime, content) return } diff --git a/modules/repository/commits_test.go b/modules/repository/commits_test.go index a407083f3a89..b6ad967d4ce5 100644 --- a/modules/repository/commits_test.go +++ b/modules/repository/commits_test.go @@ -6,6 +6,7 @@ package repository import ( "crypto/md5" "fmt" + "strconv" "testing" "time" @@ -136,13 +137,11 @@ func TestPushCommits_AvatarLink(t *testing.T) { enableGravatar(t) assert.Equal(t, - "https://secure.gravatar.com/avatar/ab53a2911ddf9b4817ac01ddcd3d975f?d=identicon&s=84", + "https://secure.gravatar.com/avatar/ab53a2911ddf9b4817ac01ddcd3d975f?d=identicon&s="+strconv.Itoa(28*setting.Avatar.RenderedSizeFactor), pushCommits.AvatarLink(db.DefaultContext, "user2@example.com")) assert.Equal(t, - "https://secure.gravatar.com/avatar/"+ - fmt.Sprintf("%x", md5.Sum([]byte("nonexistent@example.com")))+ - "?d=identicon&s=84", + fmt.Sprintf("https://secure.gravatar.com/avatar/%x?d=identicon&s=%d", md5.Sum([]byte("nonexistent@example.com")), 28*setting.Avatar.RenderedSizeFactor), pushCommits.AvatarLink(db.DefaultContext, "nonexistent@example.com")) } diff --git a/modules/setting/packages.go b/modules/setting/packages.go index 00d8b6122f3b..a9b91adf1621 100644 --- a/modules/setting/packages.go +++ b/modules/setting/packages.go @@ -24,6 +24,7 @@ var ( LimitTotalOwnerCount int64 LimitTotalOwnerSize int64 + LimitSizeAlpine int64 LimitSizeCargo int64 LimitSizeChef int64 LimitSizeComposer int64 @@ -32,6 +33,7 @@ var ( LimitSizeContainer int64 LimitSizeDebian int64 LimitSizeGeneric int64 + LimitSizeGo int64 LimitSizeHelm int64 LimitSizeMaven int64 LimitSizeNpm int64 @@ -69,6 +71,7 @@ func loadPackagesFrom(rootCfg ConfigProvider) { } Packages.LimitTotalOwnerSize = mustBytes(sec, "LIMIT_TOTAL_OWNER_SIZE") + Packages.LimitSizeAlpine = mustBytes(sec, "LIMIT_SIZE_ALPINE") Packages.LimitSizeCargo = mustBytes(sec, "LIMIT_SIZE_CARGO") Packages.LimitSizeChef = mustBytes(sec, "LIMIT_SIZE_CHEF") Packages.LimitSizeComposer = mustBytes(sec, "LIMIT_SIZE_COMPOSER") @@ -77,6 +80,7 @@ func loadPackagesFrom(rootCfg ConfigProvider) { Packages.LimitSizeContainer = mustBytes(sec, "LIMIT_SIZE_CONTAINER") Packages.LimitSizeDebian = mustBytes(sec, "LIMIT_SIZE_DEBIAN") Packages.LimitSizeGeneric = mustBytes(sec, "LIMIT_SIZE_GENERIC") + Packages.LimitSizeGo = mustBytes(sec, "LIMIT_SIZE_GO") Packages.LimitSizeHelm = mustBytes(sec, "LIMIT_SIZE_HELM") Packages.LimitSizeMaven = mustBytes(sec, "LIMIT_SIZE_MAVEN") Packages.LimitSizeNpm = mustBytes(sec, "LIMIT_SIZE_NPM") diff --git a/modules/setting/picture.go b/modules/setting/picture.go index 6d7c8b33ce46..64d9a608e651 100644 --- a/modules/setting/picture.go +++ b/modules/setting/picture.go @@ -3,21 +3,23 @@ package setting -// settings +// Avatar settings + var ( - // Picture settings Avatar = struct { Storage MaxWidth int MaxHeight int MaxFileSize int64 + MaxOriginSize int64 RenderedSizeFactor int }{ MaxWidth: 4096, - MaxHeight: 3072, + MaxHeight: 4096, MaxFileSize: 1048576, - RenderedSizeFactor: 3, + MaxOriginSize: 262144, + RenderedSizeFactor: 2, } GravatarSource string @@ -44,9 +46,10 @@ func loadPictureFrom(rootCfg ConfigProvider) { Avatar.Storage = getStorage(rootCfg, "avatars", storageType, avatarSec) Avatar.MaxWidth = sec.Key("AVATAR_MAX_WIDTH").MustInt(4096) - Avatar.MaxHeight = sec.Key("AVATAR_MAX_HEIGHT").MustInt(3072) + Avatar.MaxHeight = sec.Key("AVATAR_MAX_HEIGHT").MustInt(4096) Avatar.MaxFileSize = sec.Key("AVATAR_MAX_FILE_SIZE").MustInt64(1048576) - Avatar.RenderedSizeFactor = sec.Key("AVATAR_RENDERED_SIZE_FACTOR").MustInt(3) + Avatar.MaxOriginSize = sec.Key("AVATAR_MAX_ORIGIN_SIZE").MustInt64(262144) + Avatar.RenderedSizeFactor = sec.Key("AVATAR_RENDERED_SIZE_FACTOR").MustInt(2) switch source := sec.Key("GRAVATAR_SOURCE").MustString("gravatar"); source { case "duoshuo": @@ -94,5 +97,5 @@ func loadRepoAvatarFrom(rootCfg ConfigProvider) { RepoAvatar.Storage = getStorage(rootCfg, "repo-avatars", storageType, repoAvatarSec) RepoAvatar.Fallback = sec.Key("REPOSITORY_AVATAR_FALLBACK").MustString("none") - RepoAvatar.FallbackImage = sec.Key("REPOSITORY_AVATAR_FALLBACK_IMAGE").MustString("/assets/img/repo_default.png") + RepoAvatar.FallbackImage = sec.Key("REPOSITORY_AVATAR_FALLBACK_IMAGE").MustString(AppSubURL + "/assets/img/repo_default.png") } diff --git a/modules/setting/repository.go b/modules/setting/repository.go index 56e7e6f4aca5..153307a0b63d 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -36,6 +36,7 @@ var ( DisableHTTPGit bool AccessControlAllowOrigin string UseCompatSSHURI bool + GoGetCloneURLProtocol string DefaultCloseIssuesViaCommitsInAnyBranch bool EnablePushCreateUser bool EnablePushCreateOrg bool @@ -273,6 +274,7 @@ func loadRepositoryFrom(rootCfg ConfigProvider) { sec := rootCfg.Section("repository") Repository.DisableHTTPGit = sec.Key("DISABLE_HTTP_GIT").MustBool() Repository.UseCompatSSHURI = sec.Key("USE_COMPAT_SSH_URI").MustBool() + Repository.GoGetCloneURLProtocol = sec.Key("GO_GET_CLONE_URL_PROTOCOL").MustString("https") Repository.MaxCreationLimit = sec.Key("MAX_CREATION_LIMIT").MustInt(-1) Repository.DefaultBranch = sec.Key("DEFAULT_BRANCH").MustString(Repository.DefaultBranch) RepoRootPath = sec.Key("ROOT").MustString(path.Join(AppDataPath, "gitea-repositories")) diff --git a/modules/storage/local.go b/modules/storage/local.go index d22974a65add..73ef306979ab 100644 --- a/modules/storage/local.go +++ b/modules/storage/local.go @@ -133,8 +133,8 @@ func (l *LocalStorage) URL(path, name string) (*url.URL, error) { } // IterateObjects iterates across the objects in the local storage -func (l *LocalStorage) IterateObjects(prefix string, fn func(path string, obj Object) error) error { - dir := l.buildLocalPath(prefix) +func (l *LocalStorage) IterateObjects(dirName string, fn func(path string, obj Object) error) error { + dir := l.buildLocalPath(dirName) 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 0873f8e14ef0..1c4b856ab6af 100644 --- a/modules/storage/local_test.go +++ b/modules/storage/local_test.go @@ -4,8 +4,6 @@ package storage import ( - "bytes" - "context" "os" "path/filepath" "testing" @@ -57,38 +55,5 @@ 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.Len(t, expected, count) - } + testStorageIterator(t, string(LocalStorageType), LocalStorageConfig{Path: dir}) } diff --git a/modules/storage/minio.go b/modules/storage/minio.go index 250f17827ff5..c78f351e9ce0 100644 --- a/modules/storage/minio.go +++ b/modules/storage/minio.go @@ -129,7 +129,11 @@ func NewMinioStorage(ctx context.Context, cfg interface{}) (ObjectStorage, error } func (m *MinioStorage) buildMinioPath(p string) string { - return util.PathJoinRelX(m.basePath, p) + p = util.PathJoinRelX(m.basePath, p) + if p == "." { + p = "" // minio doesn't use dot as relative path + } + return p } // Open opens a file @@ -224,14 +228,15 @@ func (m *MinioStorage) URL(path, name string) (*url.URL, error) { } // IterateObjects iterates across the objects in the miniostorage -func (m *MinioStorage) IterateObjects(prefix string, fn func(path string, obj Object) error) error { +func (m *MinioStorage) IterateObjects(dirName 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) + if dirName != "" { + // ending slash is required for avoiding matching like "foo/" and "foobar/" with prefix "foo" + basePath = m.buildMinioPath(dirName) + "/" } for mObjInfo := range m.client.ListObjects(lobjectCtx, m.bucket, minio.ListObjectsOptions{ @@ -244,7 +249,7 @@ func (m *MinioStorage) IterateObjects(prefix string, fn func(path string, obj Ob } if err := func(object *minio.Object, fn func(path string, obj Object) error) error { defer object.Close() - return fn(strings.TrimPrefix(mObjInfo.Key, basePath), &minioObject{object}) + return fn(strings.TrimPrefix(mObjInfo.Key, m.basePath), &minioObject{object}) }(object, fn); err != nil { return convertMinioErr(err) } diff --git a/modules/storage/minio_test.go b/modules/storage/minio_test.go new file mode 100644 index 000000000000..bee1b8631860 --- /dev/null +++ b/modules/storage/minio_test.go @@ -0,0 +1,18 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package storage + +import ( + "testing" +) + +func TestMinioStorageIterator(t *testing.T) { + testStorageIterator(t, string(MinioStorageType), MinioStorageConfig{ + Endpoint: "127.0.0.1:9000", + AccessKeyID: "123456", + SecretAccessKey: "12345678", + Bucket: "gitea", + Location: "us-east-1", + }) +} diff --git a/modules/storage/storage_test.go b/modules/storage/storage_test.go new file mode 100644 index 000000000000..b517a9e71a16 --- /dev/null +++ b/modules/storage/storage_test.go @@ -0,0 +1,49 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package storage + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +func testStorageIterator(t *testing.T, typStr string, cfg interface{}) { + l, err := NewStorage(typStr, cfg) + 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 + {"ab/1.txt", "ab1"}, + {"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", "ab/1.txt"}, + "/": {"a/1.txt", "b/1.txt", "b/2.txt", "b/3.txt", "b/x 4.txt", "ab/1.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.Len(t, expected, count) + } +} diff --git a/modules/testlogger/testlogger.go b/modules/testlogger/testlogger.go index bf912f41dcf4..cc80e86c81cb 100644 --- a/modules/testlogger/testlogger.go +++ b/modules/testlogger/testlogger.go @@ -58,7 +58,9 @@ func (w *testLoggerWriterCloser) Write(p []byte) (int, error) { } if t == nil || *t == nil { - return fmt.Fprintf(os.Stdout, "??? [Unknown Test] %s\n", p) + // if there is no running test, the log message should be outputted to console, to avoid losing important information. + // the "???" prefix is used to match the "===" and "+++" in PrintCurrentTest + return fmt.Fprintf(os.Stdout, "??? [TestLogger] %s\n", p) } defer func() { diff --git a/modules/util/keypair.go b/modules/util/keypair.go index 5a3ce715a40f..97f2d9ebca2d 100644 --- a/modules/util/keypair.go +++ b/modules/util/keypair.go @@ -4,10 +4,13 @@ package util import ( + "crypto" "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/pem" + + "github.com/minio/sha256-simd" ) // GenerateKeyPair generates a public and private keypair @@ -43,3 +46,16 @@ func pemBlockForPub(pub *rsa.PublicKey) (string, error) { }) return string(pubBytes), nil } + +// CreatePublicKeyFingerprint creates a fingerprint of the given key. +// The fingerprint is the sha256 sum of the PKIX structure of the key. +func CreatePublicKeyFingerprint(key crypto.PublicKey) ([]byte, error) { + bytes, err := x509.MarshalPKIXPublicKey(key) + if err != nil { + return nil, err + } + + checksum := sha256.Sum256(bytes) + + return checksum[:], nil +} diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index d7c392a624e5..906a32dc2d73 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3212,6 +3212,15 @@ versions = Versions versions.view_all = View all dependency.id = ID dependency.version = Version +alpine.registry = Setup this registry by adding the url in your /etc/apk/repositories file: +alpine.registry.key = Download the registry public RSA key into the /etc/apk/keys/ folder to verify the index signature: +alpine.registry.info = Choose $branch and $repository from the list below. +alpine.install = To install the package, run the following command: +alpine.documentation = For more information on the Alpine registry, see the documentation. +alpine.repository = Repository Info +alpine.repository.branches = Branches +alpine.repository.repositories = Repositories +alpine.repository.architectures = Architectures cargo.registry = Setup this registry in the Cargo configuration file (for example ~/.cargo/config.toml): cargo.install = To install the package using Cargo, run the following command: cargo.documentation = For more information on the Cargo registry, see the documentation. @@ -3254,6 +3263,8 @@ debian.repository.components = Components debian.repository.architectures = Architectures generic.download = Download package from the command line: generic.documentation = For more information on the generic registry, see the documentation. +go.install = Install the package from the command line: +go.documentation = For more information on the Go registry, see the documentation. helm.registry = Setup this registry from the command line: helm.install = To install the package, run the following command: helm.documentation = For more information on the Helm registry, see the documentation. @@ -3410,8 +3421,6 @@ runners.version = Version runners.reset_registration_token_success = Runner registration token reset successfully runs.all_workflows = All Workflows -runs.open_tab = %d Open -runs.closed_tab = %d Closed runs.commit = Commit runs.pushed_by = Pushed by runs.invalid_workflow_helper = Workflow config file is invalid. Please check your config file: %s diff --git a/package-lock.json b/package-lock.json index 73468344d3f5..4754a8a8268d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,9 +7,9 @@ "name": "gitea", "license": "MIT", "dependencies": { - "@citation-js/core": "0.6.5", - "@citation-js/plugin-bibtex": "0.6.6", - "@citation-js/plugin-csl": "0.6.7", + "@citation-js/core": "0.6.8", + "@citation-js/plugin-bibtex": "0.6.8", + "@citation-js/plugin-csl": "0.6.8", "@citation-js/plugin-software-formats": "0.6.1", "@claviska/jquery-minicolors": "2.3.6", "@github/markdown-toolbar-element": "2.1.1", @@ -17,7 +17,7 @@ "@github/text-expander-element": "2.3.0", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@primer/octicons": "19.1.0", - "@vue/compiler-sfc": "3.2.47", + "@vue/compiler-sfc": "3.3.2", "@webcomponents/custom-elements": "1.6.0", "add-asset-webpack-plugin": "2.0.1", "ansi-to-html": "0.7.2", @@ -29,7 +29,7 @@ "esbuild-loader": "3.0.1", "escape-goat": "4.0.0", "fast-glob": "3.2.12", - "jquery": "3.6.4", + "jquery": "3.7.0", "jquery.are-you-sure": "1.9.0", "katex": "0.16.7", "license-checker-webpack-plugin": "0.2.1", @@ -44,12 +44,12 @@ "tippy.js": "6.3.7", "tributejs": "5.1.3", "uint8-to-base64": "0.2.0", - "vue": "3.2.47", + "vue": "3.3.2", "vue-bar-graph": "2.0.0", - "vue-loader": "17.1.0", + "vue-loader": "17.1.1", "vue3-calendar-heatmap": "2.0.5", - "webpack": "5.82.0", - "webpack-cli": "5.1.0", + "webpack": "5.82.1", + "webpack-cli": "5.1.1", "workbox-routing": "6.5.4", "workbox-strategies": "6.5.4", "wrap-ansi": "8.1.0" @@ -58,16 +58,18 @@ "@playwright/test": "1.33.0", "@rollup/pluginutils": "5.0.2", "@stoplight/spectral-cli": "6.6.0", - "@vitejs/plugin-vue": "4.2.1", + "@vitejs/plugin-vue": "4.2.3", "eslint": "8.40.0", "eslint-plugin-custom-elements": "0.0.8", + "eslint-plugin-eslint-comments": "3.2.0", "eslint-plugin-import": "2.27.5", "eslint-plugin-jquery": "1.5.1", "eslint-plugin-no-jquery": "2.7.0", - "eslint-plugin-regexp": "1.14.0", + "eslint-plugin-regexp": "1.15.0", "eslint-plugin-sonarjs": "0.19.0", "eslint-plugin-unicorn": "47.0.0", - "eslint-plugin-vue": "9.11.1", + "eslint-plugin-vue": "9.12.0", + "eslint-plugin-wc": "1.5.0", "jsdom": "22.0.0", "markdownlint-cli": "0.34.0", "stylelint": "15.6.1", @@ -81,9 +83,9 @@ } }, "node_modules/@asyncapi/specs": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@asyncapi/specs/-/specs-4.2.1.tgz", - "integrity": "sha512-NeOgMxl1J00PbsmStbDFE6OVA6rE5S3xv0Twm70MwMigsE28hdG9iH/+2SrMBeD/NFKVlryHcEuLtAPoqiCCFg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@asyncapi/specs/-/specs-4.3.1.tgz", + "integrity": "sha512-EfexhJu/lwF8OdQDm28NKLJHFkx0Gb6O+rcezhZYLPIoNYKXJMh2J1vFGpwmfAcTTh+ffK44Oc2Hs1Q4sLBp+A==", "dev": true, "dependencies": { "@types/json-schema": "^7.0.11" @@ -223,9 +225,9 @@ "integrity": "sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg==" }, "node_modules/@citation-js/core": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/@citation-js/core/-/core-0.6.5.tgz", - "integrity": "sha512-YmfL3wby/oLgggs3hqRcllL0xYOUzTaABChTEEbcfXwrvIstgHzODG1tcPAVg/EVuVH151uMR9xttuzu+Lbxcg==", + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@citation-js/core/-/core-0.6.8.tgz", + "integrity": "sha512-EqnEj+0OR9t0pU0d/CQ4TKNSxxALTSm5gXxg56dQ2nnod9esIwNVDWztX5LXstiHj7k8VjUSo/TZw7O2AXFdBQ==", "dependencies": { "@citation-js/date": "^0.5.0", "@citation-js/name": "^0.4.2", @@ -253,9 +255,9 @@ } }, "node_modules/@citation-js/plugin-bibtex": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/@citation-js/plugin-bibtex/-/plugin-bibtex-0.6.6.tgz", - "integrity": "sha512-hDcMK+e+WaA8f3b+SkIVB+41w39Yf3AVGQ6Ee1amC4KCF5kS6IoiAC5dUquCZoaJCrh5PEg1fyX2tuii6WkOhA==", + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@citation-js/plugin-bibtex/-/plugin-bibtex-0.6.8.tgz", + "integrity": "sha512-ERCHi4TTZ/8TM/Y6lul/pcLHfMltcZqn0BrATCXwVrndwXdUfj8Z6bQdXgaOAhHjo7ngp9T0DC7skgofEGBSTA==", "dependencies": { "@citation-js/date": "^0.5.0", "@citation-js/name": "^0.4.2", @@ -281,9 +283,9 @@ } }, "node_modules/@citation-js/plugin-csl": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/@citation-js/plugin-csl/-/plugin-csl-0.6.7.tgz", - "integrity": "sha512-cgMCaujDaSnYPHmmyk5vp7UTVmEEkToqh1gFH5OGUOlLOaFmRTQn8kssMrVQR/mBZlQmMeoh5NYVktNeo5eDCg==", + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@citation-js/plugin-csl/-/plugin-csl-0.6.8.tgz", + "integrity": "sha512-X3cTCKWvhD580YSVcS55XhwVgPppzDIiaYgiJopAa/MAvzogt1ODqy6dbBPtb07ttdVQm8VG8FHRD2LsGViTGg==", "dependencies": { "@citation-js/date": "^0.5.0", "citeproc": "^2.4.6" @@ -1630,9 +1632,9 @@ } }, "node_modules/@stoplight/types": { - "version": "13.14.0", - "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-13.14.0.tgz", - "integrity": "sha512-RqF5Cyl5227fRPIaWa5ptMvAtNUIM4yCzSUoERUXAarxpTcOrjMJ52p9lV9XrQtpLhLNMrWn08+h0ap10R22ig==", + "version": "13.15.0", + "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-13.15.0.tgz", + "integrity": "sha512-pBLjVRrWGVd+KzTbL3qrmufSKIEp0UfziDBdt/nrTHPKrlrtVwaHdrrQMcpM23yJDU1Wcg4cHvhIuGtKCT5OmA==", "dev": true, "dependencies": { "@types/json-schema": "^7.0.4", @@ -1764,9 +1766,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.1.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.1.1.tgz", - "integrity": "sha512-uKBEevTNb+l6/aCQaKVnUModfEMjAl98lw2Si9P5y4hLu9tm6AlX2ZIoXZX6Wh9lJueYPrGPKk5WMCNHg/u6/A==" + "version": "20.1.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.1.3.tgz", + "integrity": "sha512-NP2yfZpgmf2eDRPmgGq+fjGjSwFgYbihA8/gK+ey23qT9RkxsgNTZvGOEpXgzIGqesTYkElELLgtKoMQTys5vA==" }, "node_modules/@types/normalize-package-data": { "version": "2.4.1", @@ -1809,9 +1811,9 @@ "dev": true }, "node_modules/@vitejs/plugin-vue": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.2.1.tgz", - "integrity": "sha512-ZTZjzo7bmxTRTkb8GSTwkPOYDIP7pwuyV+RV53c9PYUouwcbkIZIvWvNWlX2b1dYZqtOv7D6iUAnJLVNGcLrSw==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.2.3.tgz", + "integrity": "sha512-R6JDUfiZbJA9cMiguQ7jxALsgiprjBeHL5ikpXfJCH62pPHtI+JdJ5xWj6Ev73yXSlYl86+blXn1kZHQ7uElxw==", "dev": true, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -1930,240 +1932,257 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.2.47", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.47.tgz", - "integrity": "sha512-p4D7FDnQb7+YJmO2iPEv0SQNeNzcbHdGByJDsT4lynf63AFkOTFN07HsiRSvjGo0QrxR/o3d0hUyNCUnBU2Tig==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.2.tgz", + "integrity": "sha512-CKZWo1dzsQYTNTft7whzjL0HsrEpMfiK7pjZ2WFE3bC1NA7caUjWioHSK+49y/LK7Bsm4poJZzAMnvZMQ7OTeg==", "dependencies": { - "@babel/parser": "^7.16.4", - "@vue/shared": "3.2.47", + "@babel/parser": "^7.21.3", + "@vue/shared": "3.3.2", "estree-walker": "^2.0.2", - "source-map": "^0.6.1" + "source-map-js": "^1.0.2" } }, "node_modules/@vue/compiler-dom": { - "version": "3.2.47", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.47.tgz", - "integrity": "sha512-dBBnEHEPoftUiS03a4ggEig74J2YBZ2UIeyfpcRM2tavgMWo4bsEfgCGsu+uJIL/vax9S+JztH8NmQerUo7shQ==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.2.tgz", + "integrity": "sha512-6gS3auANuKXLw0XH6QxkWqyPYPunziS2xb6VRenM3JY7gVfZcJvkCBHkb5RuNY1FCbBO3lkIi0CdXUCW1c7SXw==", "dependencies": { - "@vue/compiler-core": "3.2.47", - "@vue/shared": "3.2.47" + "@vue/compiler-core": "3.3.2", + "@vue/shared": "3.3.2" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.2.47", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.47.tgz", - "integrity": "sha512-rog05W+2IFfxjMcFw10tM9+f7i/+FFpZJJ5XHX72NP9eC2uRD+42M3pYcQqDXVYoj74kHMSEdQ/WmCjt8JFksQ==", - "dependencies": { - "@babel/parser": "^7.16.4", - "@vue/compiler-core": "3.2.47", - "@vue/compiler-dom": "3.2.47", - "@vue/compiler-ssr": "3.2.47", - "@vue/reactivity-transform": "3.2.47", - "@vue/shared": "3.2.47", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.2.tgz", + "integrity": "sha512-jG4jQy28H4BqzEKsQqqW65BZgmo3vzdLHTBjF+35RwtDdlFE+Fk1VWJYUnDMMqkFBo6Ye1ltSKVOMPgkzYj7SQ==", + "dependencies": { + "@babel/parser": "^7.20.15", + "@vue/compiler-core": "3.3.2", + "@vue/compiler-dom": "3.3.2", + "@vue/compiler-ssr": "3.3.2", + "@vue/reactivity-transform": "3.3.2", + "@vue/shared": "3.3.2", "estree-walker": "^2.0.2", - "magic-string": "^0.25.7", + "magic-string": "^0.30.0", "postcss": "^8.1.10", - "source-map": "^0.6.1" + "source-map-js": "^1.0.2" + } + }, + "node_modules/@vue/compiler-sfc/node_modules/magic-string": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz", + "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.2.47", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.47.tgz", - "integrity": "sha512-wVXC+gszhulcMD8wpxMsqSOpvDZ6xKXSVWkf50Guf/S+28hTAXPDYRTbLQ3EDkOP5Xz/+SY37YiwDquKbJOgZw==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.2.tgz", + "integrity": "sha512-K8OfY5FQtZaSOJHHe8xhEfIfLrefL/Y9frv4k4NsyQL3+0lRKxr9QuJhfdBDjkl7Fhz8CzKh63mULvmOfx3l2w==", "dependencies": { - "@vue/compiler-dom": "3.2.47", - "@vue/shared": "3.2.47" + "@vue/compiler-dom": "3.3.2", + "@vue/shared": "3.3.2" } }, "node_modules/@vue/reactivity": { - "version": "3.2.47", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.47.tgz", - "integrity": "sha512-7khqQ/75oyyg+N/e+iwV6lpy1f5wq759NdlS1fpAhFXa8VeAIKGgk2E/C4VF59lx5b+Ezs5fpp/5WsRYXQiKxQ==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.2.tgz", + "integrity": "sha512-yX8C4uTgg2Tdj+512EEMnMKbLveoITl7YdQX35AYgx8vBvQGszKiiCN46g4RY6/deeo/5DLbeUUGxCq1qWMf5g==", "dependencies": { - "@vue/shared": "3.2.47" + "@vue/shared": "3.3.2" } }, "node_modules/@vue/reactivity-transform": { - "version": "3.2.47", - "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.47.tgz", - "integrity": "sha512-m8lGXw8rdnPVVIdIFhf0LeQ/ixyHkH5plYuS83yop5n7ggVJU+z5v0zecwEnX7fa7HNLBhh2qngJJkxpwEEmYA==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.2.tgz", + "integrity": "sha512-iu2WaQvlJHdnONrsyv4ibIEnSsuKF+aHFngGj/y1lwpHQtalpVhKg9wsKMoiKXS9zPNjG9mNKzJS9vudvjzvyg==", "dependencies": { - "@babel/parser": "^7.16.4", - "@vue/compiler-core": "3.2.47", - "@vue/shared": "3.2.47", + "@babel/parser": "^7.20.15", + "@vue/compiler-core": "3.3.2", + "@vue/shared": "3.3.2", "estree-walker": "^2.0.2", - "magic-string": "^0.25.7" + "magic-string": "^0.30.0" + } + }, + "node_modules/@vue/reactivity-transform/node_modules/magic-string": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz", + "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" } }, "node_modules/@vue/runtime-core": { - "version": "3.2.47", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.47.tgz", - "integrity": "sha512-RZxbLQIRB/K0ev0K9FXhNbBzT32H9iRtYbaXb0ZIz2usLms/D55dJR2t6cIEUn6vyhS3ALNvNthI+Q95C+NOpA==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.2.tgz", + "integrity": "sha512-qSl95qj0BvKfcsO+hICqFEoLhJn6++HtsPxmTkkadFbuhe3uQfJ8HmQwvEr7xbxBd2rcJB6XOJg7nWAn/ymC5A==", "dependencies": { - "@vue/reactivity": "3.2.47", - "@vue/shared": "3.2.47" + "@vue/reactivity": "3.3.2", + "@vue/shared": "3.3.2" } }, "node_modules/@vue/runtime-dom": { - "version": "3.2.47", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.47.tgz", - "integrity": "sha512-ArXrFTjS6TsDei4qwNvgrdmHtD930KgSKGhS5M+j8QxXrDJYLqYw4RRcDy1bz1m1wMmb6j+zGLifdVHtkXA7gA==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.2.tgz", + "integrity": "sha512-+drStsJT+0mtgHdarT7cXZReCcTFfm6ptxMrz0kAW5hms6UNBd8Q1pi4JKlncAhu+Ld/TevsSp7pqAZxBBoGng==", "dependencies": { - "@vue/runtime-core": "3.2.47", - "@vue/shared": "3.2.47", - "csstype": "^2.6.8" + "@vue/runtime-core": "3.3.2", + "@vue/shared": "3.3.2", + "csstype": "^3.1.1" } }, - "node_modules/@vue/runtime-dom/node_modules/csstype": { - "version": "2.6.21", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", - "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==" - }, "node_modules/@vue/server-renderer": { - "version": "3.2.47", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.2.47.tgz", - "integrity": "sha512-dN9gc1i8EvmP9RCzvneONXsKfBRgqFeFZLurmHOveL7oH6HiFXJw5OGu294n1nHc/HMgTy6LulU/tv5/A7f/LA==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.2.tgz", + "integrity": "sha512-QCwh6OGwJg6GDLE0fbQhRTR6tnU+XDJ1iCsTYHXBiezCXAhqMygFRij7BiLF4ytvvHcg5kX9joX5R5vP85++wg==", "dependencies": { - "@vue/compiler-ssr": "3.2.47", - "@vue/shared": "3.2.47" + "@vue/compiler-ssr": "3.3.2", + "@vue/shared": "3.3.2" }, "peerDependencies": { - "vue": "3.2.47" + "vue": "3.3.2" } }, "node_modules/@vue/shared": { - "version": "3.2.47", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.47.tgz", - "integrity": "sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ==" + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.2.tgz", + "integrity": "sha512-0rFu3h8JbclbnvvKrs7Fe5FNGV9/5X2rPD7KmOzhLSUAiQH5//Hq437Gv0fR5Mev3u/nbtvmLl8XgwCU20/ZfQ==" }, "node_modules/@webassemblyjs/ast": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.5.tgz", - "integrity": "sha512-LHY/GSAZZRpsNQH+/oHqhRQ5FT7eoULcBqgfyTB5nQHogFnK3/7QoN7dLnwSE/JkUAF0SrRuclT7ODqMFtWxxQ==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", + "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.5", - "@webassemblyjs/helper-wasm-bytecode": "1.11.5" + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" } }, "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.5.tgz", - "integrity": "sha512-1j1zTIC5EZOtCplMBG/IEwLtUojtwFVwdyVMbL/hwWqbzlQoJsWCOavrdnLkemwNoC/EOwtUFch3fuo+cbcXYQ==" + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==" }, "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.5.tgz", - "integrity": "sha512-L65bDPmfpY0+yFrsgz8b6LhXmbbs38OnwDCf6NpnMUYqa+ENfE5Dq9E42ny0qz/PdR0LJyq/T5YijPnU8AXEpA==" + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==" }, "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.5.tgz", - "integrity": "sha512-fDKo1gstwFFSfacIeH5KfwzjykIE6ldh1iH9Y/8YkAZrhmu4TctqYjSh7t0K2VyDSXOZJ1MLhht/k9IvYGcIxg==" + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", + "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==" }, "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.5.tgz", - "integrity": "sha512-DhykHXM0ZABqfIGYNv93A5KKDw/+ywBFnuWybZZWcuzWHfbp21wUfRkbtz7dMGwGgT4iXjWuhRMA2Mzod6W4WA==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.5", - "@webassemblyjs/helper-api-error": "1.11.5", + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.5.tgz", - "integrity": "sha512-oC4Qa0bNcqnjAowFn7MPCETQgDYytpsfvz4ujZz63Zu/a/v71HeCAAmZsgZ3YVKec3zSPYytG3/PrRCqbtcAvA==" + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==" }, "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.5.tgz", - "integrity": "sha512-uEoThA1LN2NA+K3B9wDo3yKlBfVtC6rh0i4/6hvbz071E8gTNZD/pT0MsBf7MeD6KbApMSkaAK0XeKyOZC7CIA==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", + "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", "dependencies": { - "@webassemblyjs/ast": "1.11.5", - "@webassemblyjs/helper-buffer": "1.11.5", - "@webassemblyjs/helper-wasm-bytecode": "1.11.5", - "@webassemblyjs/wasm-gen": "1.11.5" + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6" } }, "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.5.tgz", - "integrity": "sha512-37aGq6qVL8A8oPbPrSGMBcp38YZFXcHfiROflJn9jxSdSMMM5dS5P/9e2/TpaJuhE+wFrbukN2WI6Hw9MH5acg==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "node_modules/@webassemblyjs/leb128": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.5.tgz", - "integrity": "sha512-ajqrRSXaTJoPW+xmkfYN6l8VIeNnR4vBOTQO9HzR7IygoCcKWkICbKFbVTNMjMgMREqXEr0+2M6zukzM47ZUfQ==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", "dependencies": { "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/utf8": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.5.tgz", - "integrity": "sha512-WiOhulHKTZU5UPlRl53gHR8OxdGsSOxqfpqWeA2FmcwBMaoEdz6b2x2si3IwC9/fSPLfe8pBMRTHVMk5nlwnFQ==" + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==" }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.5.tgz", - "integrity": "sha512-C0p9D2fAu3Twwqvygvf42iGCQ4av8MFBLiTb+08SZ4cEdwzWx9QeAHDo1E2k+9s/0w1DM40oflJOpkZ8jW4HCQ==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", + "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", "dependencies": { - "@webassemblyjs/ast": "1.11.5", - "@webassemblyjs/helper-buffer": "1.11.5", - "@webassemblyjs/helper-wasm-bytecode": "1.11.5", - "@webassemblyjs/helper-wasm-section": "1.11.5", - "@webassemblyjs/wasm-gen": "1.11.5", - "@webassemblyjs/wasm-opt": "1.11.5", - "@webassemblyjs/wasm-parser": "1.11.5", - "@webassemblyjs/wast-printer": "1.11.5" + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-opt": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6", + "@webassemblyjs/wast-printer": "1.11.6" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.5.tgz", - "integrity": "sha512-14vteRlRjxLK9eSyYFvw1K8Vv+iPdZU0Aebk3j6oB8TQiQYuO6hj9s4d7qf6f2HJr2khzvNldAFG13CgdkAIfA==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", + "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", "dependencies": { - "@webassemblyjs/ast": "1.11.5", - "@webassemblyjs/helper-wasm-bytecode": "1.11.5", - "@webassemblyjs/ieee754": "1.11.5", - "@webassemblyjs/leb128": "1.11.5", - "@webassemblyjs/utf8": "1.11.5" + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.5.tgz", - "integrity": "sha512-tcKwlIXstBQgbKy1MlbDMlXaxpucn42eb17H29rawYLxm5+MsEmgPzeCP8B1Cl69hCice8LeKgZpRUAPtqYPgw==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", + "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", "dependencies": { - "@webassemblyjs/ast": "1.11.5", - "@webassemblyjs/helper-buffer": "1.11.5", - "@webassemblyjs/wasm-gen": "1.11.5", - "@webassemblyjs/wasm-parser": "1.11.5" + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.5.tgz", - "integrity": "sha512-SVXUIwsLQlc8srSD7jejsfTU83g7pIGr2YYNb9oHdtldSxaOhvA5xwvIiWIfcX8PlSakgqMXsLpLfbbJ4cBYew==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", + "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", "dependencies": { - "@webassemblyjs/ast": "1.11.5", - "@webassemblyjs/helper-api-error": "1.11.5", - "@webassemblyjs/helper-wasm-bytecode": "1.11.5", - "@webassemblyjs/ieee754": "1.11.5", - "@webassemblyjs/leb128": "1.11.5", - "@webassemblyjs/utf8": "1.11.5" + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" } }, "node_modules/@webassemblyjs/wast-printer": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.5.tgz", - "integrity": "sha512-f7Pq3wvg3GSPUPzR0F6bmI89Hdb+u9WXrSKc4v+N0aV0q6r42WoF92Jp2jEorBEBRoRNXgjp53nBniDXcqZYPA==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", + "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", "dependencies": { - "@webassemblyjs/ast": "1.11.5", + "@webassemblyjs/ast": "1.11.6", "@xtuc/long": "4.2.2" } }, @@ -2197,9 +2216,9 @@ } }, "node_modules/@webpack-cli/serve": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.3.tgz", - "integrity": "sha512-Bwxd73pHuYc0cyl7vulPp2I6kAYtmJPkfUivbts7by6wDAVyFdKzGX3AksbvCRyNVFUJu7o2ZTcWXdT90T3qbg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.4.tgz", + "integrity": "sha512-0xRgjgDLdz6G7+vvDLlaRpFatJaJ69uTalZLRSMX5B3VUrDmXcrVA3+6fXXQgmYz7bY9AAgs348XQdmtLsK41A==", "engines": { "node": ">=14.15.0" }, @@ -2253,9 +2272,9 @@ } }, "node_modules/acorn-import-assertions": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", - "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", "peerDependencies": { "acorn": "^8" } @@ -4032,9 +4051,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.386", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.386.tgz", - "integrity": "sha512-w0VD4WR225nuNsz6FokDaqugxzue6iUVBo8QfUrl2Y6nWHxtBUhjWDnUaG/1v5oWeFPLMJAQk3zKHTHW/P8+Og==" + "version": "1.4.392", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.392.tgz", + "integrity": "sha512-TXQOMW9tnhIms3jGy/lJctLjICOgyueZFJ1KUtm6DTQ+QpxX3p7ZBwB6syuZ9KBuT5S4XX7bgY1ECPgfxKUdOg==" }, "node_modules/elkjs": { "version": "0.8.2", @@ -4064,9 +4083,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.13.0.tgz", - "integrity": "sha512-eyV8f0y1+bzyfh8xAwW/WTSZpLbjhqc4ne9eGSH4Zo2ejdyiNG9pU6mf9DG8a7+Auk6MFTlNOT4Y2y/9k8GKVg==", + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.14.0.tgz", + "integrity": "sha512-+DCows0XNwLDcUhbFJPdlQEVnT2zXlCv7hPxemTz86/O+B/hCQ+mb7ydkPKiflpVraqLPCAfu7lDy+hBXueojw==", "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -4501,6 +4520,34 @@ "eslint": ">=4.19.0" } }, + "node_modules/eslint-plugin-eslint-comments": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-3.2.0.tgz", + "integrity": "sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5", + "ignore": "^5.0.5" + }, + "engines": { + "node": ">=6.5.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=4.19.1" + } + }, + "node_modules/eslint-plugin-eslint-comments/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/eslint-plugin-import": { "version": "2.27.5", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz", @@ -4579,9 +4626,9 @@ } }, "node_modules/eslint-plugin-regexp": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-regexp/-/eslint-plugin-regexp-1.14.0.tgz", - "integrity": "sha512-5+bBSsRTTtkSf8+/iNSjiOW6qbjAdGyqv88HxPaBNFKxROK+UAdOGDl5Jr+csV5wW2BuOOvaG82zsvTriQBRFA==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-regexp/-/eslint-plugin-regexp-1.15.0.tgz", + "integrity": "sha512-YEtQPfdudafU7RBIFci81R/Q1yErm0mVh3BkGnXD2Dk8DLwTFdc2ITYH1wCnHKim2gnHfPFgrkh+b2ozyyU7ag==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", @@ -4646,9 +4693,9 @@ } }, "node_modules/eslint-plugin-vue": { - "version": "9.11.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.11.1.tgz", - "integrity": "sha512-SNtBGDrRkPUFsREswPceqdvZ7YVdWY+iCYiDC+RoxwVieeQ7GJU1FLDlkcaYTOD2os/YuVgI1Fdu8YGM7fmoow==", + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.12.0.tgz", + "integrity": "sha512-xH8PgpDW2WwmFSmRfs/3iWogef1CJzQqX264I65zz77jDuxF2yLy7+GA2diUM8ZNATuSl1+UehMQkb5YEyau5w==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.3.0", @@ -4666,6 +4713,19 @@ "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/eslint-plugin-wc": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-wc/-/eslint-plugin-wc-1.5.0.tgz", + "integrity": "sha512-KFSfiHDol/LeV7U6IX8GdgpGf/s3wG8FTG120Rml/hGNB/DkCuGYQhlf0VgdBdf7gweem8Nlsh5o64HNdj+qPA==", + "dev": true, + "dependencies": { + "is-valid-element-name": "^1.0.0", + "js-levenshtein-esm": "^1.2.0" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, "node_modules/eslint-scope": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", @@ -5977,6 +6037,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-valid-element-name": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-valid-element-name/-/is-valid-element-name-1.0.0.tgz", + "integrity": "sha512-GZITEJY2LkSjQfaIPBha7eyZv+ge0PhBR7KITeCCWvy7VBQrCUdFkvpI+HrAPQjVtVjy1LvlEkqQTHckoszruw==", + "dev": true, + "dependencies": { + "is-potential-custom-element-name": "^1.0.0" + } + }, "node_modules/is-weakref": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", @@ -6054,9 +6123,9 @@ } }, "node_modules/jquery": { - "version": "3.6.4", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.4.tgz", - "integrity": "sha512-v28EW9DWDFpzcD9O5iyJXg3R3+q+mET5JhnjJzQUZMHOv67bpSIHq81GEYpPNZHG+XXHsfSme3nxp/hndKEcsQ==" + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.0.tgz", + "integrity": "sha512-umpJ0/k8X0MvD1ds0P9SfowREz2LenHsQaxSohMZ5OMNEU2r0tf8pdeEFTHMFxWVxKNyU9rTtK3CWzUCTKJUeQ==" }, "node_modules/jquery.are-you-sure": { "version": "1.9.0", @@ -6069,6 +6138,12 @@ "node": ">=0.8.0" } }, + "node_modules/js-levenshtein-esm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/js-levenshtein-esm/-/js-levenshtein-esm-1.2.0.tgz", + "integrity": "sha512-fzreKVq1eD7eGcQr7MtRpQH94f8gIfhdrc7yeih38xh684TNMK9v5aAu2wxfIRMk/GpAJRrzcirMAPIaSDaByQ==", + "dev": true + }, "node_modules/js-sdsl": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", @@ -6492,6 +6567,7 @@ "version": "0.25.9", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dev": true, "dependencies": { "sourcemap-codec": "^1.4.8" } @@ -6591,9 +6667,9 @@ } }, "node_modules/markdownlint-cli/node_modules/glob": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.2.2.tgz", - "integrity": "sha512-Xsa0BcxIC6th9UwNjZkhrMtNo/MnyRL8jGCP+uEwhA5oFOCY1f2s1/oNKY47xQ0Bg5nkjsfAEIej1VeH62bDDQ==", + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.2.3.tgz", + "integrity": "sha512-Kb4rfmBVE3eQTAimgmeqc2LwSnN0wIOkkUL6HmxEFxNJ4fHghYHVbFba/HcGcRjE6s9KoMNK3rSOwkL4PioZjg==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", @@ -6909,15 +6985,15 @@ } }, "node_modules/mlly": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.2.0.tgz", - "integrity": "sha512-+c7A3CV0KGdKcylsI6khWyts/CYrGTrRVo4R/I7u/cUsy0Conxa6LUhiEzVKIw14lc2L5aiO4+SeVe4TeGRKww==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.2.1.tgz", + "integrity": "sha512-1aMEByaWgBPEbWV2BOPEMySRrzl7rIHXmQxam4DM8jVjalTQDjpN2ZKOLUrwyhfZQO7IXHml2StcHMhooDeEEQ==", "dev": true, "dependencies": { "acorn": "^8.8.2", "pathe": "^1.1.0", - "pkg-types": "^1.0.2", - "ufo": "^1.1.1" + "pkg-types": "^1.0.3", + "ufo": "^1.1.2" } }, "node_modules/monaco-editor": { @@ -7015,9 +7091,9 @@ } }, "node_modules/node-fetch": { - "version": "2.6.10", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.10.tgz", - "integrity": "sha512-5YytjUVbwzjE/BX4N62vnPPkGNxlJPwdA9/ArUc4pcM6cYS4Hinuv4VazzwjMGgnWuiQqcemOanib/5PpcsGug==", + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -7386,12 +7462,12 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-scurry": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.7.0.tgz", - "integrity": "sha512-UkZUeDjczjYRE495+9thsgcVgsaCPkaw80slmfVFgllxY+IO8ubTsOpFVjDPROBqJdHfVPUFRHPBV/WciOVfWg==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.8.0.tgz", + "integrity": "sha512-IjTrKseM404/UAWA8bBbL3Qp6O2wXkanuIE3seCxBH7ctRuvH1QRawy1N3nVDHGkdeZsjOsSe/8AQBL/VQCy2g==", "dev": true, "dependencies": { - "lru-cache": "^9.0.0", + "lru-cache": "^9.1.1", "minipass": "^5.0.0" }, "engines": { @@ -8520,9 +8596,9 @@ "dev": true }, "node_modules/signal-exit": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.1.tgz", - "integrity": "sha512-uUWsN4aOxJAS8KOuf3QMyFtgm1pkb6I+KRZbRF/ghdf5T7sM+B1lLLzPDxswUjkmHyxQAVzEgG35E3NzDM9GVw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.2.tgz", + "integrity": "sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==", "dev": true, "engines": { "node": ">=14" @@ -8661,7 +8737,8 @@ "version": "1.4.8", "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", - "deprecated": "Please use @jridgewell/sourcemap-codec instead" + "deprecated": "Please use @jridgewell/sourcemap-codec instead", + "dev": true }, "node_modules/spdx-compare": { "version": "1.0.0", @@ -9130,9 +9207,9 @@ } }, "node_modules/terser": { - "version": "5.17.2", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.2.tgz", - "integrity": "sha512-1D1aGbOF1Mnayq5PvfMc0amAR1y5Z1nrZaGCvI5xsdEfZEVte8okonk02OiaK5fw5hG1GWuuVsakOnpZW8y25A==", + "version": "5.17.3", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.3.tgz", + "integrity": "sha512-AudpAZKmZHkG9jueayypz4duuCFJMMNGRMwaPvQKWfxKedh8Z2x3OCoDqIIi1xx5+iwx1u6Au8XQcc9Lke65Yg==", "dependencies": { "@jridgewell/source-map": "^0.3.2", "acorn": "^8.5.0", @@ -9678,9 +9755,9 @@ } }, "node_modules/vite/node_modules/rollup": { - "version": "3.21.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.21.5.tgz", - "integrity": "sha512-a4NTKS4u9PusbUJcfF4IMxuqjFzjm6ifj76P54a7cKnvVzJaG12BLVR+hgU2YDGHzyMMQNxLAZWuALsn8q2oQg==", + "version": "3.21.6", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.21.6.tgz", + "integrity": "sha512-SXIICxvxQxR3D4dp/3LDHZIJPC8a4anKMHd4E3Jiz2/JnY+2bEjqrOokAauc5ShGVNFHlEFjBXAXlaxkJqIqSg==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -9800,15 +9877,15 @@ } }, "node_modules/vue": { - "version": "3.2.47", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.2.47.tgz", - "integrity": "sha512-60188y/9Dc9WVrAZeUVSDxRQOZ+z+y5nO2ts9jWXSTkMvayiWxCWOWtBQoYjLeccfXkiiPZWAHcV+WTPhkqJHQ==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.2.tgz", + "integrity": "sha512-98hJcAhyDwZoOo2flAQBSPVYG/o0HA9ivIy2ktHshjE+6/q8IMQ+kvDKQzOZTFPxvnNMcGM+zS2A00xeZMA7tA==", "dependencies": { - "@vue/compiler-dom": "3.2.47", - "@vue/compiler-sfc": "3.2.47", - "@vue/runtime-dom": "3.2.47", - "@vue/server-renderer": "3.2.47", - "@vue/shared": "3.2.47" + "@vue/compiler-dom": "3.3.2", + "@vue/compiler-sfc": "3.3.2", + "@vue/runtime-dom": "3.3.2", + "@vue/server-renderer": "3.3.2", + "@vue/shared": "3.3.2" } }, "node_modules/vue-bar-graph": { @@ -9845,9 +9922,9 @@ } }, "node_modules/vue-loader": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-17.1.0.tgz", - "integrity": "sha512-zAjrT+TNWTpgRODxqDfzbDyvuTf5kCP9xmMk8aspQKuYNnTY2r0XK/bHu1DKLpSpk0I6fkQph5OLKB7HcRIPZw==", + "version": "17.1.1", + "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-17.1.1.tgz", + "integrity": "sha512-qpqEVkKdrAsgyIBMHaiXurDeCuBWqRyKqg2GI4aG3NbggEls+BLqTZdqahbJJh7fm83sz+iz3gg6eDWdbNlG7Q==", "dependencies": { "chalk": "^4.1.0", "hash-sum": "^2.0.0", @@ -9916,9 +9993,9 @@ } }, "node_modules/webpack": { - "version": "5.82.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.82.0.tgz", - "integrity": "sha512-iGNA2fHhnDcV1bONdUu554eZx+XeldsaeQ8T67H6KKHl2nUSwX8Zm7cmzOA46ox/X1ARxf7Bjv8wQ/HsB5fxBg==", + "version": "5.82.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.82.1.tgz", + "integrity": "sha512-C6uiGQJ+Gt4RyHXXYt+v9f+SN1v83x68URwgxNQ98cvH8kxiuywWGP4XeNZ1paOzZ63aY3cTciCEQJNFUljlLw==", "dependencies": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.0", @@ -9929,7 +10006,7 @@ "acorn-import-assertions": "^1.7.6", "browserslist": "^4.14.5", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.13.0", + "enhanced-resolve": "^5.14.0", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", @@ -9962,14 +10039,14 @@ } }, "node_modules/webpack-cli": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.0.tgz", - "integrity": "sha512-a7KRJnCxejFoDpYTOwzm5o21ZXMaNqtRlvS183XzGDUPRdVEzJNImcQokqYZ8BNTnk9DkKiuWxw75+DCCoZ26w==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.1.tgz", + "integrity": "sha512-OLJwVMoXnXYH2ncNGU8gxVpUtm3ybvdioiTvHgUyBuyMLKiVvWy+QObzBsMtp5pH7qQoEuWgeEUQ/sU3ZJFzAw==", "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.0", "@webpack-cli/info": "^2.0.1", - "@webpack-cli/serve": "^2.0.3", + "@webpack-cli/serve": "^2.0.4", "colorette": "^2.0.14", "commander": "^10.0.1", "cross-spawn": "^7.0.3", diff --git a/package.json b/package.json index fe637e938994..21c39aae9cdc 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,9 @@ "node": ">= 16.0.0" }, "dependencies": { - "@citation-js/core": "0.6.5", - "@citation-js/plugin-bibtex": "0.6.6", - "@citation-js/plugin-csl": "0.6.7", + "@citation-js/core": "0.6.8", + "@citation-js/plugin-bibtex": "0.6.8", + "@citation-js/plugin-csl": "0.6.8", "@citation-js/plugin-software-formats": "0.6.1", "@claviska/jquery-minicolors": "2.3.6", "@github/markdown-toolbar-element": "2.1.1", @@ -17,7 +17,7 @@ "@github/text-expander-element": "2.3.0", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@primer/octicons": "19.1.0", - "@vue/compiler-sfc": "3.2.47", + "@vue/compiler-sfc": "3.3.2", "@webcomponents/custom-elements": "1.6.0", "add-asset-webpack-plugin": "2.0.1", "ansi-to-html": "0.7.2", @@ -29,7 +29,7 @@ "esbuild-loader": "3.0.1", "escape-goat": "4.0.0", "fast-glob": "3.2.12", - "jquery": "3.6.4", + "jquery": "3.7.0", "jquery.are-you-sure": "1.9.0", "katex": "0.16.7", "license-checker-webpack-plugin": "0.2.1", @@ -44,12 +44,12 @@ "tippy.js": "6.3.7", "tributejs": "5.1.3", "uint8-to-base64": "0.2.0", - "vue": "3.2.47", + "vue": "3.3.2", "vue-bar-graph": "2.0.0", - "vue-loader": "17.1.0", + "vue-loader": "17.1.1", "vue3-calendar-heatmap": "2.0.5", - "webpack": "5.82.0", - "webpack-cli": "5.1.0", + "webpack": "5.82.1", + "webpack-cli": "5.1.1", "workbox-routing": "6.5.4", "workbox-strategies": "6.5.4", "wrap-ansi": "8.1.0" @@ -58,16 +58,18 @@ "@playwright/test": "1.33.0", "@rollup/pluginutils": "5.0.2", "@stoplight/spectral-cli": "6.6.0", - "@vitejs/plugin-vue": "4.2.1", + "@vitejs/plugin-vue": "4.2.3", "eslint": "8.40.0", "eslint-plugin-custom-elements": "0.0.8", + "eslint-plugin-eslint-comments": "3.2.0", "eslint-plugin-import": "2.27.5", "eslint-plugin-jquery": "1.5.1", "eslint-plugin-no-jquery": "2.7.0", - "eslint-plugin-regexp": "1.14.0", + "eslint-plugin-regexp": "1.15.0", "eslint-plugin-sonarjs": "0.19.0", "eslint-plugin-unicorn": "47.0.0", - "eslint-plugin-vue": "9.11.1", + "eslint-plugin-vue": "9.12.0", + "eslint-plugin-wc": "1.5.0", "jsdom": "22.0.0", "markdownlint-cli": "0.34.0", "stylelint": "15.6.1", diff --git a/public/img/svg/gitea-alpine.svg b/public/img/svg/gitea-alpine.svg new file mode 100644 index 000000000000..1c878013ac10 --- /dev/null +++ b/public/img/svg/gitea-alpine.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/svg/gitea-go.svg b/public/img/svg/gitea-go.svg new file mode 100644 index 000000000000..a432bdbf21b8 --- /dev/null +++ b/public/img/svg/gitea-go.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/routers/api/packages/alpine/alpine.go b/routers/api/packages/alpine/alpine.go new file mode 100644 index 000000000000..9a551a219b64 --- /dev/null +++ b/routers/api/packages/alpine/alpine.go @@ -0,0 +1,253 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package alpine + +import ( + "crypto/x509" + "encoding/hex" + "encoding/pem" + "errors" + "fmt" + "io" + "net/http" + "strings" + + packages_model "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/json" + packages_module "code.gitea.io/gitea/modules/packages" + alpine_module "code.gitea.io/gitea/modules/packages/alpine" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/routers/api/packages/helper" + packages_service "code.gitea.io/gitea/services/packages" + alpine_service "code.gitea.io/gitea/services/packages/alpine" +) + +func apiError(ctx *context.Context, status int, obj interface{}) { + helper.LogAndProcessError(ctx, status, obj, func(message string) { + ctx.PlainText(status, message) + }) +} + +func GetRepositoryKey(ctx *context.Context) { + _, pub, err := alpine_service.GetOrCreateKeyPair(ctx.Package.Owner.ID) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + pubPem, _ := pem.Decode([]byte(pub)) + if pubPem == nil { + apiError(ctx, http.StatusInternalServerError, "failed to decode private key pem") + return + } + + pubKey, err := x509.ParsePKIXPublicKey(pubPem.Bytes) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + fingerprint, err := util.CreatePublicKeyFingerprint(pubKey) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + ctx.ServeContent(strings.NewReader(pub), &context.ServeHeaderOptions{ + ContentType: "application/x-pem-file", + Filename: fmt.Sprintf("%s@%s.rsa.pub", ctx.Package.Owner.LowerName, hex.EncodeToString(fingerprint)), + }) +} + +func GetRepositoryFile(ctx *context.Context) { + pv, err := alpine_service.GetOrCreateRepositoryVersion(ctx.Package.Owner.ID) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + s, pf, err := packages_service.GetFileStreamByPackageVersion( + ctx, + pv, + &packages_service.PackageFileInfo{ + Filename: alpine_service.IndexFilename, + CompositeKey: fmt.Sprintf("%s|%s|%s", ctx.Params("branch"), ctx.Params("repository"), ctx.Params("architecture")), + }, + ) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + apiError(ctx, http.StatusNotFound, err) + } else { + apiError(ctx, http.StatusInternalServerError, err) + } + return + } + defer s.Close() + + ctx.ServeContent(s, &context.ServeHeaderOptions{ + Filename: pf.Name, + LastModified: pf.CreatedUnix.AsLocalTime(), + }) +} + +func UploadPackageFile(ctx *context.Context) { + branch := strings.TrimSpace(ctx.Params("branch")) + repository := strings.TrimSpace(ctx.Params("repository")) + if branch == "" || repository == "" { + apiError(ctx, http.StatusBadRequest, "invalid branch or repository") + return + } + + upload, close, err := ctx.UploadStream() + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + if close { + defer upload.Close() + } + + buf, err := packages_module.CreateHashedBufferFromReader(upload) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + defer buf.Close() + + pck, err := alpine_module.ParsePackage(buf) + if err != nil { + if errors.Is(err, util.ErrInvalidArgument) || err == io.EOF { + apiError(ctx, http.StatusBadRequest, err) + } else { + apiError(ctx, http.StatusInternalServerError, err) + } + return + } + + if _, err := buf.Seek(0, io.SeekStart); err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + fileMetadataRaw, err := json.Marshal(pck.FileMetadata) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + _, _, err = packages_service.CreatePackageOrAddFileToExisting( + &packages_service.PackageCreationInfo{ + PackageInfo: packages_service.PackageInfo{ + Owner: ctx.Package.Owner, + PackageType: packages_model.TypeAlpine, + Name: pck.Name, + Version: pck.Version, + }, + Creator: ctx.Doer, + Metadata: pck.VersionMetadata, + }, + &packages_service.PackageFileCreationInfo{ + PackageFileInfo: packages_service.PackageFileInfo{ + Filename: fmt.Sprintf("%s-%s.apk", pck.Name, pck.Version), + CompositeKey: fmt.Sprintf("%s|%s|%s", branch, repository, pck.FileMetadata.Architecture), + }, + Creator: ctx.Doer, + Data: buf, + IsLead: true, + Properties: map[string]string{ + alpine_module.PropertyBranch: branch, + alpine_module.PropertyRepository: repository, + alpine_module.PropertyArchitecture: pck.FileMetadata.Architecture, + alpine_module.PropertyMetadata: string(fileMetadataRaw), + }, + }, + ) + if err != nil { + switch err { + case packages_model.ErrDuplicatePackageVersion, packages_model.ErrDuplicatePackageFile: + apiError(ctx, http.StatusBadRequest, err) + case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize: + apiError(ctx, http.StatusForbidden, err) + default: + apiError(ctx, http.StatusInternalServerError, err) + } + return + } + + if err := alpine_service.BuildSpecificRepositoryFiles(ctx, ctx.Package.Owner.ID, branch, repository, pck.FileMetadata.Architecture); err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + ctx.Status(http.StatusCreated) +} + +func DownloadPackageFile(ctx *context.Context) { + pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{ + OwnerID: ctx.Package.Owner.ID, + PackageType: packages_model.TypeAlpine, + Query: ctx.Params("filename"), + CompositeKey: fmt.Sprintf("%s|%s|%s", ctx.Params("branch"), ctx.Params("repository"), ctx.Params("architecture")), + }) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + if len(pfs) != 1 { + apiError(ctx, http.StatusNotFound, nil) + return + } + + s, pf, err := packages_service.GetPackageFileStream(ctx, pfs[0]) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + apiError(ctx, http.StatusNotFound, err) + } else { + apiError(ctx, http.StatusInternalServerError, err) + } + return + } + defer s.Close() + + ctx.ServeContent(s, &context.ServeHeaderOptions{ + Filename: pf.Name, + LastModified: pf.CreatedUnix.AsLocalTime(), + }) +} + +func DeletePackageFile(ctx *context.Context) { + branch, repository, architecture := ctx.Params("branch"), ctx.Params("repository"), ctx.Params("architecture") + + pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{ + OwnerID: ctx.Package.Owner.ID, + PackageType: packages_model.TypeAlpine, + Query: ctx.Params("filename"), + CompositeKey: fmt.Sprintf("%s|%s|%s", branch, repository, architecture), + }) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + if len(pfs) != 1 { + apiError(ctx, http.StatusNotFound, nil) + return + } + + if err := packages_service.RemovePackageFileAndVersionIfUnreferenced(ctx.Doer, pfs[0]); err != nil { + if errors.Is(err, util.ErrNotExist) { + apiError(ctx, http.StatusNotFound, err) + } else { + apiError(ctx, http.StatusInternalServerError, err) + } + return + } + + if err := alpine_service.BuildSpecificRepositoryFiles(ctx, ctx.Package.Owner.ID, branch, repository, architecture); err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + ctx.Status(http.StatusNoContent) +} diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index 9b24918f5139..aaceb8a92b30 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/api/packages/alpine" "code.gitea.io/gitea/routers/api/packages/cargo" "code.gitea.io/gitea/routers/api/packages/chef" "code.gitea.io/gitea/routers/api/packages/composer" @@ -23,6 +24,7 @@ import ( "code.gitea.io/gitea/routers/api/packages/container" "code.gitea.io/gitea/routers/api/packages/debian" "code.gitea.io/gitea/routers/api/packages/generic" + "code.gitea.io/gitea/routers/api/packages/goproxy" "code.gitea.io/gitea/routers/api/packages/helm" "code.gitea.io/gitea/routers/api/packages/maven" "code.gitea.io/gitea/routers/api/packages/npm" @@ -107,6 +109,19 @@ func CommonRoutes(ctx gocontext.Context) *web.Route { }) r.Group("/{username}", func() { + r.Group("/alpine", func() { + r.Get("/key", alpine.GetRepositoryKey) + r.Group("/{branch}/{repository}", func() { + r.Put("", reqPackageAccess(perm.AccessModeWrite), alpine.UploadPackageFile) + r.Group("/{architecture}", func() { + r.Get("/APKINDEX.tar.gz", alpine.GetRepositoryFile) + r.Group("/{filename}", func() { + r.Get("", alpine.DownloadPackageFile) + r.Delete("", reqPackageAccess(perm.AccessModeWrite), alpine.DeletePackageFile) + }) + }) + }) + }, reqPackageAccess(perm.AccessModeRead)) r.Group("/cargo", func() { r.Group("/api/v1/crates", func() { r.Get("", cargo.SearchPackages) @@ -298,6 +313,64 @@ func CommonRoutes(ctx gocontext.Context) *web.Route { }, reqPackageAccess(perm.AccessModeWrite)) }) }, reqPackageAccess(perm.AccessModeRead)) + r.Group("/go", func() { + r.Put("/upload", reqPackageAccess(perm.AccessModeWrite), goproxy.UploadPackage) + r.Get("/sumdb/sum.golang.org/supported", func(ctx *context.Context) { + ctx.Status(http.StatusNotFound) + }) + + // Manual mapping of routes because the package name contains slashes which chi does not support + // https://go.dev/ref/mod#goproxy-protocol + r.Get("/*", func(ctx *context.Context) { + path := ctx.Params("*") + + if strings.HasSuffix(path, "/@latest") { + ctx.SetParams("name", path[:len(path)-len("/@latest")]) + ctx.SetParams("version", "latest") + + goproxy.PackageVersionMetadata(ctx) + return + } + + parts := strings.SplitN(path, "/@v/", 2) + if len(parts) != 2 { + ctx.Status(http.StatusNotFound) + return + } + + ctx.SetParams("name", parts[0]) + + // /@v/list + if parts[1] == "list" { + goproxy.EnumeratePackageVersions(ctx) + return + } + + // /@v/.zip + if strings.HasSuffix(parts[1], ".zip") { + ctx.SetParams("version", parts[1][:len(parts[1])-len(".zip")]) + + goproxy.DownloadPackageFile(ctx) + return + } + // /@v/.info + if strings.HasSuffix(parts[1], ".info") { + ctx.SetParams("version", parts[1][:len(parts[1])-len(".info")]) + + goproxy.PackageVersionMetadata(ctx) + return + } + // /@v/.mod + if strings.HasSuffix(parts[1], ".mod") { + ctx.SetParams("version", parts[1][:len(parts[1])-len(".mod")]) + + goproxy.PackageVersionGoModContent(ctx) + return + } + + ctx.Status(http.StatusNotFound) + }) + }, reqPackageAccess(perm.AccessModeRead)) r.Group("/generic", func() { r.Group("/{packagename}/{packageversion}", func() { r.Delete("", reqPackageAccess(perm.AccessModeWrite), generic.DeletePackage) diff --git a/routers/api/packages/goproxy/goproxy.go b/routers/api/packages/goproxy/goproxy.go new file mode 100644 index 000000000000..d0bc9c1e98c3 --- /dev/null +++ b/routers/api/packages/goproxy/goproxy.go @@ -0,0 +1,226 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package goproxy + +import ( + "errors" + "fmt" + "io" + "net/http" + "sort" + "time" + + packages_model "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/modules/context" + packages_module "code.gitea.io/gitea/modules/packages" + goproxy_module "code.gitea.io/gitea/modules/packages/goproxy" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/routers/api/packages/helper" + packages_service "code.gitea.io/gitea/services/packages" +) + +func apiError(ctx *context.Context, status int, obj interface{}) { + helper.LogAndProcessError(ctx, status, obj, func(message string) { + ctx.PlainText(status, message) + }) +} + +func EnumeratePackageVersions(ctx *context.Context) { + pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeGo, ctx.Params("name")) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + if len(pvs) == 0 { + apiError(ctx, http.StatusNotFound, err) + return + } + + sort.Slice(pvs, func(i, j int) bool { + return pvs[i].CreatedUnix < pvs[j].CreatedUnix + }) + + ctx.Resp.Header().Set("Content-Type", "text/plain;charset=utf-8") + + for _, pv := range pvs { + fmt.Fprintln(ctx.Resp, pv.Version) + } +} + +func PackageVersionMetadata(ctx *context.Context) { + pv, err := resolvePackage(ctx, ctx.Package.Owner.ID, 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 + } + + ctx.JSON(http.StatusOK, struct { + Version string `json:"Version"` + Time time.Time `json:"Time"` + }{ + Version: pv.Version, + Time: pv.CreatedUnix.AsLocalTime(), + }) +} + +func PackageVersionGoModContent(ctx *context.Context) { + pv, err := resolvePackage(ctx, ctx.Package.Owner.ID, 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 + } + + pps, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeVersion, pv.ID, goproxy_module.PropertyGoMod) + if err != nil || len(pps) != 1 { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + ctx.PlainText(http.StatusOK, pps[0].Value) +} + +func DownloadPackageFile(ctx *context.Context) { + pv, err := resolvePackage(ctx, ctx.Package.Owner.ID, 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 + } + + pfs, err := packages_model.GetFilesByVersionID(ctx, pv.ID) + if err != nil || len(pfs) != 1 { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + s, _, err := packages_service.GetPackageFileStream(ctx, pfs[0]) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + apiError(ctx, http.StatusNotFound, err) + } else { + apiError(ctx, http.StatusInternalServerError, err) + } + return + } + defer s.Close() + + ctx.ServeContent(s, &context.ServeHeaderOptions{ + Filename: pfs[0].Name, + LastModified: pfs[0].CreatedUnix.AsLocalTime(), + }) +} + +func resolvePackage(ctx *context.Context, ownerID int64, name, version string) (*packages_model.PackageVersion, error) { + var pv *packages_model.PackageVersion + + if version == "latest" { + pvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{ + OwnerID: ownerID, + Type: packages_model.TypeGo, + Name: packages_model.SearchValue{ + Value: name, + ExactMatch: true, + }, + IsInternal: util.OptionalBoolFalse, + Sort: packages_model.SortCreatedDesc, + }) + if err != nil { + return nil, err + } + + if len(pvs) != 1 { + return nil, packages_model.ErrPackageNotExist + } + + pv = pvs[0] + } else { + var err error + pv, err = packages_model.GetVersionByNameAndVersion(ctx, ownerID, packages_model.TypeGo, name, version) + if err != nil { + return nil, err + } + } + + return pv, nil +} + +func UploadPackage(ctx *context.Context) { + upload, close, err := ctx.UploadStream() + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + if close { + defer upload.Close() + } + + buf, err := packages_module.CreateHashedBufferFromReader(upload) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + defer buf.Close() + + pck, err := goproxy_module.ParsePackage(buf, buf.Size()) + 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 + } + + _, _, err = packages_service.CreatePackageAndAddFile( + &packages_service.PackageCreationInfo{ + PackageInfo: packages_service.PackageInfo{ + Owner: ctx.Package.Owner, + PackageType: packages_model.TypeGo, + Name: pck.Name, + Version: pck.Version, + }, + Creator: ctx.Doer, + VersionProperties: map[string]string{ + goproxy_module.PropertyGoMod: pck.GoMod, + }, + }, + &packages_service.PackageFileCreationInfo{ + PackageFileInfo: packages_service.PackageFileInfo{ + Filename: fmt.Sprintf("%v.zip", pck.Version), + }, + 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 + } + + ctx.Status(http.StatusCreated) +} diff --git a/routers/api/v1/org/team.go b/routers/api/v1/org/team.go index 024fee34693c..8b7d7b195151 100644 --- a/routers/api/v1/org/team.go +++ b/routers/api/v1/org/team.go @@ -126,7 +126,7 @@ func GetTeam(ctx *context.APIContext) { // "200": // "$ref": "#/responses/Team" - apiTeam, err := convert.ToTeam(ctx, ctx.Org.Team) + apiTeam, err := convert.ToTeam(ctx, ctx.Org.Team, true) if err != nil { ctx.InternalServerError(err) return diff --git a/routers/api/v1/packages/package.go b/routers/api/v1/packages/package.go index e0811f8665ef..0c9a13428150 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, debian, generic, helm, maven, npm, nuget, pub, pypi, rpm, rubygems, swift, vagrant] + // enum: [alpine, cargo, chef, composer, conan, conda, container, debian, generic, go, helm, maven, npm, nuget, pub, pypi, rpm, rubygems, swift, vagrant] // - name: q // in: query // description: name filter diff --git a/routers/web/base.go b/routers/web/base.go index b9b59583896f..1f6c4fbfc595 100644 --- a/routers/web/base.go +++ b/routers/web/base.go @@ -6,7 +6,6 @@ package web import ( "errors" "fmt" - "io" "net/http" "os" "path" @@ -76,12 +75,6 @@ func storageHandler(storageSetting setting.Storage, prefix string, objStore stor } fi, err := objStore.Stat(rPath) - if err == nil && httpcache.HandleTimeCache(req, w, fi) { - return - } - - // If we have matched and access to release or issue - fr, err := objStore.Open(rPath) if err != nil { if os.IsNotExist(err) || errors.Is(err, os.ErrNotExist) { log.Warn("Unable to find %s %s", prefix, rPath) @@ -92,14 +85,15 @@ func storageHandler(storageSetting setting.Storage, prefix string, objStore stor http.Error(w, fmt.Sprintf("Error whilst opening %s %s", prefix, rPath), http.StatusInternalServerError) return } - defer fr.Close() - _, err = io.Copy(w, fr) + fr, err := objStore.Open(rPath) if err != nil { - log.Error("Error whilst rendering %s %s. Error: %v", prefix, rPath, err) - http.Error(w, fmt.Sprintf("Error whilst rendering %s %s", prefix, rPath), http.StatusInternalServerError) + log.Error("Error whilst opening %s %s. Error: %v", prefix, rPath, err) + http.Error(w, fmt.Sprintf("Error whilst opening %s %s", prefix, rPath), http.StatusInternalServerError) return } + defer fr.Close() + httpcache.ServeContentWithCacheControl(w, req, path.Base(rPath), fi.ModTime(), fr) }) } } diff --git a/routers/web/goget.go b/routers/web/goget.go index fb8afae9991a..c5b8b6cbc015 100644 --- a/routers/web/goget.go +++ b/routers/web/goget.go @@ -66,7 +66,14 @@ func goGet(ctx *context.Context) { } goGetImport := context.ComposeGoGetImport(ownerName, trimmedRepoName) - goImportContent := fmt.Sprintf("%s git %s", goGetImport, repo_model.ComposeHTTPSCloneURL(ownerName, repoName) /*CloneLink*/) + + var cloneURL string + if setting.Repository.GoGetCloneURLProtocol == "ssh" { + cloneURL = repo_model.ComposeSSHCloneURL(ownerName, repoName) + } else { + cloneURL = repo_model.ComposeHTTPSCloneURL(ownerName, repoName) + } + goImportContent := fmt.Sprintf("%s git %s", goGetImport, cloneURL /*CloneLink*/) goSourceContent := fmt.Sprintf("%s _ %s %s", goGetImport, prefix+"{/dir}" /*GoDocDirectory*/, prefix+"{/dir}/{file}#L{line}" /*GoDocFile*/) goGetCli := fmt.Sprintf("go get %s%s", insecure, goGetImport) diff --git a/routers/web/misc/misc.go b/routers/web/misc/misc.go index 582179990a4c..6ed3b5c3adb8 100644 --- a/routers/web/misc/misc.go +++ b/routers/web/misc/misc.go @@ -5,13 +5,13 @@ package misc import ( "net/http" - "os" "path" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/httpcache" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" ) func SSHInfo(rw http.ResponseWriter, req *http.Request) { @@ -34,11 +34,8 @@ func DummyOK(w http.ResponseWriter, req *http.Request) { } func RobotsTxt(w http.ResponseWriter, req *http.Request) { - filePath := path.Join(setting.CustomPath, "robots.txt") - fi, err := os.Stat(filePath) - if err == nil && httpcache.HandleTimeCache(req, w, fi) { - return - } + filePath := util.FilePathJoinAbs(setting.CustomPath, "robots.txt") + httpcache.SetCacheControlInHeader(w.Header(), setting.StaticCacheTime) http.ServeFile(w, req, filePath) } diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go index 8a44e836c504..87ff07d5ebf2 100644 --- a/routers/web/repo/actions/actions.go +++ b/routers/web/repo/actions/actions.go @@ -16,7 +16,6 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/convert" "github.com/nektos/act/pkg/model" @@ -61,12 +60,7 @@ func List(ctx *context.Context) { ctx.Error(http.StatusInternalServerError, err.Error()) return } else if !empty { - defaultBranch, err := ctx.Repo.GitRepo.GetDefaultBranch() - if err != nil { - ctx.Error(http.StatusInternalServerError, err.Error()) - return - } - commit, err := ctx.Repo.GitRepo.GetBranchCommit(defaultBranch) + commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) if err != nil { ctx.Error(http.StatusInternalServerError, err.Error()) return @@ -143,37 +137,6 @@ func List(ctx *context.Context) { WorkflowFileName: workflow, } - // open counts - opts.IsClosed = util.OptionalBoolFalse - numOpenRuns, err := actions_model.CountRuns(ctx, opts) - if err != nil { - ctx.Error(http.StatusInternalServerError, err.Error()) - return - } - ctx.Data["NumOpenActionRuns"] = numOpenRuns - - // closed counts - opts.IsClosed = util.OptionalBoolTrue - numClosedRuns, err := actions_model.CountRuns(ctx, opts) - if err != nil { - ctx.Error(http.StatusInternalServerError, err.Error()) - return - } - ctx.Data["NumClosedActionRuns"] = numClosedRuns - - opts.IsClosed = util.OptionalBoolNone - isShowClosed := ctx.FormString("state") == "closed" - if len(ctx.FormString("state")) == 0 && numOpenRuns == 0 && numClosedRuns != 0 { - isShowClosed = true - } - - if isShowClosed { - opts.IsClosed = util.OptionalBoolTrue - ctx.Data["IsShowClosed"] = true - } else { - opts.IsClosed = util.OptionalBoolFalse - } - runs, total, err := actions_model.FindRuns(ctx, opts) if err != nil { ctx.Error(http.StatusInternalServerError, err.Error()) @@ -194,7 +157,6 @@ func List(ctx *context.Context) { pager := context.NewPagination(int(total), opts.PageSize, opts.Page, 5) pager.SetDefaultParams(ctx) pager.AddParamString("workflow", workflow) - pager.AddParamString("state", ctx.FormString("state")) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplListActions) diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index 2f87e190228a..8944890f6ad0 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" + git_model "code.gitea.io/gitea/models/git" "code.gitea.io/gitea/models/organization" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" @@ -576,23 +577,44 @@ func SearchRepo(ctx *context.Context) { return } - results := make([]*api.Repository, len(repos)) + // collect the latest commit of each repo + // at most there are dozens of repos (limited by MaxResponseItems), so it's not a big problem at the moment + repoIDsToLatestCommitSHAs := make(map[int64]string, len(repos)) + for _, repo := range repos { + commitID, err := repo_service.GetBranchCommitID(ctx, repo, repo.DefaultBranch) + if err != nil { + continue + } + repoIDsToLatestCommitSHAs[repo.ID] = commitID + } + + // call the database O(1) times to get the commit statuses for all repos + repoToItsLatestCommitStatuses, err := git_model.GetLatestCommitStatusForPairs(ctx, repoIDsToLatestCommitSHAs, db.ListOptions{}) + if err != nil { + log.Error("GetLatestCommitStatusForPairs: %v", err) + return + } + + results := make([]*repo_service.WebSearchRepository, len(repos)) for i, repo := range repos { - results[i] = &api.Repository{ - ID: repo.ID, - FullName: repo.FullName(), - Fork: repo.IsFork, - Private: repo.IsPrivate, - Template: repo.IsTemplate, - Mirror: repo.IsMirror, - Stars: repo.NumStars, - HTMLURL: repo.HTMLURL(), - Link: repo.Link(), - Internal: !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePrivate, + results[i] = &repo_service.WebSearchRepository{ + Repository: &api.Repository{ + ID: repo.ID, + FullName: repo.FullName(), + Fork: repo.IsFork, + Private: repo.IsPrivate, + Template: repo.IsTemplate, + Mirror: repo.IsMirror, + Stars: repo.NumStars, + HTMLURL: repo.HTMLURL(), + Link: repo.Link(), + Internal: !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePrivate, + }, + LatestCommitStatus: git_model.CalcCommitStatus(repoToItsLatestCommitStatuses[repo.ID]), } } - ctx.JSON(http.StatusOK, api.SearchResults{ + ctx.JSON(http.StatusOK, repo_service.WebSearchResults{ OK: true, Data: results, }) diff --git a/routers/web/user/package.go b/routers/web/user/package.go index 37ee0b86319b..81a26da82728 100644 --- a/routers/web/user/package.go +++ b/routers/web/user/package.go @@ -17,6 +17,7 @@ import ( "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" + alpine_module "code.gitea.io/gitea/modules/packages/alpine" debian_module "code.gitea.io/gitea/modules/packages/debian" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" @@ -168,6 +169,27 @@ func ViewPackageVersion(ctx *context.Context) { switch pd.Package.Type { case packages_model.TypeContainer: ctx.Data["RegistryHost"] = setting.Packages.RegistryHost + case packages_model.TypeAlpine: + branches := make(container.Set[string]) + repositories := make(container.Set[string]) + architectures := make(container.Set[string]) + + for _, f := range pd.Files { + for _, pp := range f.Properties { + switch pp.Name { + case alpine_module.PropertyBranch: + branches.Add(pp.Value) + case alpine_module.PropertyRepository: + repositories.Add(pp.Value) + case alpine_module.PropertyArchitecture: + architectures.Add(pp.Value) + } + } + } + + ctx.Data["Branches"] = branches.Values() + ctx.Data["Repositories"] = repositories.Values() + ctx.Data["Architectures"] = architectures.Values() case packages_model.TypeDebian: distributions := make(container.Set[string]) components := make(container.Set[string]) diff --git a/routers/web/web.go b/routers/web/web.go index 8784b7c5f7f5..58623b4c67ff 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -112,6 +112,7 @@ func Routes(ctx gocontext.Context) *web.Route { routes.RouteMethods("/avatars/*", "GET, HEAD", storageHandler(setting.Avatar.Storage, "avatars", storage.Avatars)) routes.RouteMethods("/repo-avatars/*", "GET, HEAD", storageHandler(setting.RepoAvatar.Storage, "repo-avatars", storage.RepoAvatars)) routes.RouteMethods("/apple-touch-icon.png", "GET, HEAD", misc.StaticRedirect("/assets/img/apple-touch-icon.png")) + routes.RouteMethods("/apple-touch-icon-precomposed.png", "GET, HEAD", misc.StaticRedirect("/assets/img/apple-touch-icon.png")) routes.RouteMethods("/favicon.ico", "GET, HEAD", misc.StaticRedirect("/assets/img/favicon.png")) _ = templates.HTMLRenderer() diff --git a/services/auth/source/oauth2/jwtsigningkey.go b/services/auth/source/oauth2/jwtsigningkey.go index ed60952ac783..ed0fc67ca0f9 100644 --- a/services/auth/source/oauth2/jwtsigningkey.go +++ b/services/auth/source/oauth2/jwtsigningkey.go @@ -23,7 +23,6 @@ import ( "code.gitea.io/gitea/modules/util" "github.com/golang-jwt/jwt/v4" - "github.com/minio/sha256-simd" ) // ErrInvalidAlgorithmType represents an invalid algorithm error. @@ -82,7 +81,7 @@ type rsaSingingKey struct { } func newRSASingingKey(signingMethod jwt.SigningMethod, key *rsa.PrivateKey) (rsaSingingKey, error) { - kid, err := createPublicKeyFingerprint(key.Public().(*rsa.PublicKey)) + kid, err := util.CreatePublicKeyFingerprint(key.Public().(*rsa.PublicKey)) if err != nil { return rsaSingingKey{}, err } @@ -133,7 +132,7 @@ type eddsaSigningKey struct { } func newEdDSASingingKey(signingMethod jwt.SigningMethod, key ed25519.PrivateKey) (eddsaSigningKey, error) { - kid, err := createPublicKeyFingerprint(key.Public().(ed25519.PublicKey)) + kid, err := util.CreatePublicKeyFingerprint(key.Public().(ed25519.PublicKey)) if err != nil { return eddsaSigningKey{}, err } @@ -184,7 +183,7 @@ type ecdsaSingingKey struct { } func newECDSASingingKey(signingMethod jwt.SigningMethod, key *ecdsa.PrivateKey) (ecdsaSingingKey, error) { - kid, err := createPublicKeyFingerprint(key.Public().(*ecdsa.PublicKey)) + kid, err := util.CreatePublicKeyFingerprint(key.Public().(*ecdsa.PublicKey)) if err != nil { return ecdsaSingingKey{}, err } @@ -229,19 +228,6 @@ func (key ecdsaSingingKey) PreProcessToken(token *jwt.Token) { token.Header["kid"] = key.id } -// createPublicKeyFingerprint creates a fingerprint of the given key. -// The fingerprint is the sha256 sum of the PKIX structure of the key. -func createPublicKeyFingerprint(key interface{}) ([]byte, error) { - bytes, err := x509.MarshalPKIXPublicKey(key) - if err != nil { - return nil, err - } - - checksum := sha256.Sum256(bytes) - - return checksum[:], nil -} - // CreateJWTSigningKey creates a signing key from an algorithm / key pair. func CreateJWTSigningKey(algorithm string, key interface{}) (JWTSigningKey, error) { var signingMethod jwt.SigningMethod diff --git a/services/forms/package_form.go b/services/forms/package_form.go index 30296971079c..dfec98fff455 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,debian,generic,helm,maven,npm,nuget,pub,pypi,rpm,rubygems,swift,vagrant)"` + Type string `binding:"Required;In(alpine,cargo,chef,composer,conan,conda,container,debian,generic,go,helm,maven,npm,nuget,pub,pypi,rpm,rubygems,swift,vagrant)"` KeepCount int `binding:"In(0,1,5,10,25,50,100)"` KeepPattern string `binding:"RegexPattern"` RemoveDays int `binding:"In(0,7,14,30,60,90,180)"` diff --git a/services/packages/alpine/repository.go b/services/packages/alpine/repository.go new file mode 100644 index 000000000000..5264bd6c4a8c --- /dev/null +++ b/services/packages/alpine/repository.go @@ -0,0 +1,328 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package alpine + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/x509" + "encoding/hex" + "encoding/pem" + "errors" + "fmt" + "io" + "strings" + + packages_model "code.gitea.io/gitea/models/packages" + alpine_model "code.gitea.io/gitea/models/packages/alpine" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/json" + packages_module "code.gitea.io/gitea/modules/packages" + alpine_module "code.gitea.io/gitea/modules/packages/alpine" + "code.gitea.io/gitea/modules/util" + packages_service "code.gitea.io/gitea/services/packages" +) + +const IndexFilename = "APKINDEX.tar.gz" + +// GetOrCreateRepositoryVersion gets or creates the internal repository package +// The Alpine registry needs multiple index files which are stored in this package. +func GetOrCreateRepositoryVersion(ownerID int64) (*packages_model.PackageVersion, error) { + return packages_service.GetOrCreateInternalPackageVersion(ownerID, packages_model.TypeAlpine, alpine_module.RepositoryPackage, alpine_module.RepositoryVersion) +} + +// GetOrCreateKeyPair gets or creates the RSA keys used to sign repository files +func GetOrCreateKeyPair(ownerID int64) (string, string, error) { + priv, err := user_model.GetSetting(ownerID, alpine_module.SettingKeyPrivate) + if err != nil && !errors.Is(err, util.ErrNotExist) { + return "", "", err + } + + pub, err := user_model.GetSetting(ownerID, alpine_module.SettingKeyPublic) + if err != nil && !errors.Is(err, util.ErrNotExist) { + return "", "", err + } + + if priv == "" || pub == "" { + priv, pub, err = util.GenerateKeyPair(4096) + if err != nil { + return "", "", err + } + + if err := user_model.SetUserSetting(ownerID, alpine_module.SettingKeyPrivate, priv); err != nil { + return "", "", err + } + + if err := user_model.SetUserSetting(ownerID, alpine_module.SettingKeyPublic, pub); err != nil { + return "", "", err + } + } + + return priv, pub, nil +} + +// BuildAllRepositoryFiles (re)builds all repository files for every available distributions, components and architectures +func BuildAllRepositoryFiles(ctx context.Context, ownerID int64) error { + pv, err := GetOrCreateRepositoryVersion(ownerID) + if err != nil { + return err + } + + // 1. Delete all existing repository files + pfs, err := packages_model.GetFilesByVersionID(ctx, pv.ID) + if err != nil { + return err + } + + for _, pf := range pfs { + if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypeFile, pf.ID); err != nil { + return err + } + if err := packages_model.DeleteFileByID(ctx, pf.ID); err != nil { + return err + } + } + + // 2. (Re)Build repository files for existing packages + branches, err := alpine_model.GetBranches(ctx, ownerID) + if err != nil { + return err + } + for _, branch := range branches { + repositories, err := alpine_model.GetRepositories(ctx, ownerID, branch) + if err != nil { + return err + } + for _, repository := range repositories { + architectures, err := alpine_model.GetArchitectures(ctx, ownerID, repository) + if err != nil { + return err + } + for _, architecture := range architectures { + if err := buildPackagesIndex(ctx, ownerID, pv, branch, repository, architecture); err != nil { + return fmt.Errorf("failed to build repository files [%s/%s/%s]: %w", branch, repository, architecture, err) + } + } + } + } + + return nil +} + +// BuildSpecificRepositoryFiles builds index files for the repository +func BuildSpecificRepositoryFiles(ctx context.Context, ownerID int64, branch, repository, architecture string) error { + pv, err := GetOrCreateRepositoryVersion(ownerID) + if err != nil { + return err + } + + return buildPackagesIndex(ctx, ownerID, pv, branch, repository, architecture) +} + +type packageData struct { + Package *packages_model.Package + Version *packages_model.PackageVersion + Blob *packages_model.PackageBlob + VersionMetadata *alpine_module.VersionMetadata + FileMetadata *alpine_module.FileMetadata +} + +type packageCache = map[*packages_model.PackageFile]*packageData + +// https://wiki.alpinelinux.org/wiki/Apk_spec#APKINDEX_Format +func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *packages_model.PackageVersion, branch, repository, architecture string) error { + pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{ + OwnerID: ownerID, + PackageType: packages_model.TypeAlpine, + Query: "%.apk", + Properties: map[string]string{ + alpine_module.PropertyBranch: branch, + alpine_module.PropertyRepository: repository, + alpine_module.PropertyArchitecture: architecture, + }, + }) + if err != nil { + return err + } + + // Delete the package indices if there are no packages + if len(pfs) == 0 { + pf, err := packages_model.GetFileForVersionByName(ctx, repoVersion.ID, IndexFilename, fmt.Sprintf("%s|%s|%s", branch, repository, architecture)) + if err != nil && !errors.Is(err, util.ErrNotExist) { + return err + } + + if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypeFile, pf.ID); err != nil { + return err + } + return packages_model.DeleteFileByID(ctx, pf.ID) + } + + // Cache data needed for all repository files + cache := make(packageCache) + for _, pf := range pfs { + pv, err := packages_model.GetVersionByID(ctx, pf.VersionID) + if err != nil { + return err + } + p, err := packages_model.GetPackageByID(ctx, pv.PackageID) + if err != nil { + return err + } + pb, err := packages_model.GetBlobByID(ctx, pf.BlobID) + if err != nil { + return err + } + pps, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, pf.ID, alpine_module.PropertyMetadata) + if err != nil { + return err + } + + pd := &packageData{ + Package: p, + Version: pv, + Blob: pb, + } + + if err := json.Unmarshal([]byte(pv.MetadataJSON), &pd.VersionMetadata); err != nil { + return err + } + if len(pps) > 0 { + if err := json.Unmarshal([]byte(pps[0].Value), &pd.FileMetadata); err != nil { + return err + } + } + + cache[pf] = pd + } + + var buf bytes.Buffer + for _, pf := range pfs { + pd := cache[pf] + + fmt.Fprintf(&buf, "C:%s\n", pd.FileMetadata.Checksum) + fmt.Fprintf(&buf, "P:%s\n", pd.Package.Name) + fmt.Fprintf(&buf, "V:%s\n", pd.Version.Version) + fmt.Fprintf(&buf, "A:%s\n", pd.FileMetadata.Architecture) + if pd.VersionMetadata.Description != "" { + fmt.Fprintf(&buf, "T:%s\n", pd.VersionMetadata.Description) + } + if pd.VersionMetadata.ProjectURL != "" { + fmt.Fprintf(&buf, "U:%s\n", pd.VersionMetadata.ProjectURL) + } + if pd.VersionMetadata.License != "" { + fmt.Fprintf(&buf, "L:%s\n", pd.VersionMetadata.License) + } + fmt.Fprintf(&buf, "S:%d\n", pd.Blob.Size) + fmt.Fprintf(&buf, "I:%d\n", pd.FileMetadata.Size) + fmt.Fprintf(&buf, "o:%s\n", pd.FileMetadata.Origin) + fmt.Fprintf(&buf, "m:%s\n", pd.VersionMetadata.Maintainer) + fmt.Fprintf(&buf, "t:%d\n", pd.FileMetadata.BuildDate) + if pd.FileMetadata.CommitHash != "" { + fmt.Fprintf(&buf, "c:%s\n", pd.FileMetadata.CommitHash) + } + if len(pd.FileMetadata.Dependencies) > 0 { + fmt.Fprintf(&buf, "D:%s\n", strings.Join(pd.FileMetadata.Dependencies, " ")) + } + if len(pd.FileMetadata.Provides) > 0 { + fmt.Fprintf(&buf, "p:%s\n", strings.Join(pd.FileMetadata.Provides, " ")) + } + fmt.Fprint(&buf, "\n") + } + + unsignedIndexContent, _ := packages_module.NewHashedBuffer() + h := sha1.New() + + if err := writeGzipStream(io.MultiWriter(unsignedIndexContent, h), "APKINDEX", buf.Bytes(), true); err != nil { + return err + } + + priv, _, err := GetOrCreateKeyPair(ownerID) + if err != nil { + return err + } + + privPem, _ := pem.Decode([]byte(priv)) + if privPem == nil { + return fmt.Errorf("failed to decode private key pem") + } + + privKey, err := x509.ParsePKCS1PrivateKey(privPem.Bytes) + if err != nil { + return err + } + + sign, err := rsa.SignPKCS1v15(rand.Reader, privKey, crypto.SHA1, h.Sum(nil)) + if err != nil { + return err + } + + owner, err := user_model.GetUserByID(ctx, ownerID) + if err != nil { + return err + } + + fingerprint, err := util.CreatePublicKeyFingerprint(&privKey.PublicKey) + if err != nil { + return err + } + + signedIndexContent, _ := packages_module.NewHashedBuffer() + + if err := writeGzipStream( + signedIndexContent, + fmt.Sprintf(".SIGN.RSA.%s@%s.rsa.pub", owner.LowerName, hex.EncodeToString(fingerprint)), + sign, + false, + ); err != nil { + return err + } + + if _, err := io.Copy(signedIndexContent, unsignedIndexContent); err != nil { + return err + } + + _, err = packages_service.AddFileToPackageVersionInternal( + repoVersion, + &packages_service.PackageFileCreationInfo{ + PackageFileInfo: packages_service.PackageFileInfo{ + Filename: IndexFilename, + CompositeKey: fmt.Sprintf("%s|%s|%s", branch, repository, architecture), + }, + Creator: user_model.NewGhostUser(), + Data: signedIndexContent, + IsLead: false, + OverwriteExisting: true, + }, + ) + return err +} + +func writeGzipStream(w io.Writer, filename string, content []byte, addTarEnd bool) error { + zw := gzip.NewWriter(w) + defer zw.Close() + + tw := tar.NewWriter(zw) + if addTarEnd { + defer tw.Close() + } + hdr := &tar.Header{ + Name: filename, + Mode: 0o600, + Size: int64(len(content)), + } + if err := tw.WriteHeader(hdr); err != nil { + return err + } + if _, err := tw.Write(content); err != nil { + return err + } + return nil +} diff --git a/services/packages/packages.go b/services/packages/packages.go index 535f2fac8e32..9d5ce04a0e39 100644 --- a/services/packages/packages.go +++ b/services/packages/packages.go @@ -351,6 +351,8 @@ func CheckSizeQuotaExceeded(ctx context.Context, doer, owner *user_model.User, p var typeSpecificSize int64 switch packageType { + case packages_model.TypeAlpine: + typeSpecificSize = setting.Packages.LimitSizeAlpine case packages_model.TypeCargo: typeSpecificSize = setting.Packages.LimitSizeCargo case packages_model.TypeChef: @@ -367,6 +369,8 @@ func CheckSizeQuotaExceeded(ctx context.Context, doer, owner *user_model.User, p typeSpecificSize = setting.Packages.LimitSizeDebian case packages_model.TypeGeneric: typeSpecificSize = setting.Packages.LimitSizeGeneric + case packages_model.TypeGo: + typeSpecificSize = setting.Packages.LimitSizeGo case packages_model.TypeHelm: typeSpecificSize = setting.Packages.LimitSizeHelm case packages_model.TypeMaven: @@ -486,6 +490,47 @@ func RemovePackageVersion(doer *user_model.User, pv *packages_model.PackageVersi return nil } +// RemovePackageFileAndVersionIfUnreferenced deletes the package file and the version if there are no referenced files afterwards +func RemovePackageFileAndVersionIfUnreferenced(doer *user_model.User, pf *packages_model.PackageFile) error { + var pd *packages_model.PackageDescriptor + + if err := db.WithTx(db.DefaultContext, func(ctx context.Context) error { + if err := DeletePackageFile(ctx, pf); err != nil { + return err + } + + has, err := packages_model.HasVersionFileReferences(ctx, pf.VersionID) + if err != nil { + return err + } + if !has { + pv, err := packages_model.GetVersionByID(ctx, pf.VersionID) + if err != nil { + return err + } + + pd, err = packages_model.GetPackageDescriptor(ctx, pv) + if err != nil { + return err + } + + if err := DeletePackageVersionAndReferences(ctx, pv); err != nil { + return err + } + } + + return nil + }); err != nil { + return err + } + + if pd != nil { + notification.NotifyPackageDelete(db.DefaultContext, doer, pd) + } + + return nil +} + // DeletePackageVersionAndReferences deletes the package version and its properties and files func DeletePackageVersionAndReferences(ctx context.Context, pv *packages_model.PackageVersion) error { if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypeVersion, pv.ID); err != nil { diff --git a/services/repository/avatar.go b/services/repository/avatar.go index 74e5de877e0c..38c2621bc4d1 100644 --- a/services/repository/avatar.go +++ b/services/repository/avatar.go @@ -6,7 +6,6 @@ package repository import ( "context" "fmt" - "image/png" "io" "strconv" "strings" @@ -21,7 +20,7 @@ import ( // UploadAvatar saves custom avatar for repository. // FIXME: split uploads to different subdirs in case we have massive number of repos. func UploadAvatar(ctx context.Context, repo *repo_model.Repository, data []byte) error { - m, err := avatar.Prepare(data) + avatarData, err := avatar.ProcessAvatarImage(data) if err != nil { return err } @@ -47,9 +46,7 @@ func UploadAvatar(ctx context.Context, repo *repo_model.Repository, data []byte) } if err := storage.SaveFrom(storage.RepoAvatars, repo.CustomAvatarRelativePath(), func(w io.Writer) error { - if err := png.Encode(w, *m); err != nil { - log.Error("Encode: %v", err) - } + _, err := w.Write(avatarData) return err }); err != nil { return fmt.Errorf("UploadAvatar %s failed: Failed to remove old repo avatar %s: %w", repo.RepoPath(), newAvatar, err) diff --git a/services/repository/branch.go b/services/repository/branch.go index a085026ae156..cafad34cef17 100644 --- a/services/repository/branch.go +++ b/services/repository/branch.go @@ -53,6 +53,10 @@ func GetBranches(ctx context.Context, repo *repo_model.Repository, skip, limit i return git.GetBranchesByPath(ctx, repo.RepoPath(), skip, limit) } +func GetBranchCommitID(ctx context.Context, repo *repo_model.Repository, branch string) (string, error) { + return git.GetBranchCommitID(ctx, repo.RepoPath(), branch) +} + // checkBranchName validates branch name with existing repository branches func checkBranchName(ctx context.Context, repo *repo_model.Repository, name string) error { _, err := git.WalkReferences(ctx, repo.RepoPath(), func(_, refName string) error { diff --git a/services/repository/repository.go b/services/repository/repository.go index 0d6529383cc8..0914a8f6ec6a 100644 --- a/services/repository/repository.go +++ b/services/repository/repository.go @@ -9,6 +9,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" packages_model "code.gitea.io/gitea/models/packages" @@ -20,9 +21,22 @@ import ( "code.gitea.io/gitea/modules/notification" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/structs" pull_service "code.gitea.io/gitea/services/pull" ) +// WebSearchRepository represents a repository returned by web search +type WebSearchRepository struct { + Repository *structs.Repository `json:"repository"` + LatestCommitStatus *git.CommitStatus `json:"latest_commit_status"` +} + +// WebSearchResults results of a successful web search +type WebSearchResults struct { + OK bool `json:"ok"` + Data []*WebSearchRepository `json:"data"` +} + // CreateRepository creates a repository for the user/organization. func CreateRepository(ctx context.Context, doer, owner *user_model.User, opts repo_module.CreateRepoOptions) (*repo_model.Repository, error) { repo, err := repo_module.CreateRepository(doer, owner, opts) diff --git a/services/user/user.go b/services/user/user.go index d52a2f404bcf..5148f2168d58 100644 --- a/services/user/user.go +++ b/services/user/user.go @@ -6,7 +6,6 @@ package user import ( "context" "fmt" - "image/png" "io" "time" @@ -244,7 +243,7 @@ func DeleteInactiveUsers(ctx context.Context, olderThan time.Duration) error { // UploadAvatar saves custom avatar for user. func UploadAvatar(u *user_model.User, data []byte) error { - m, err := avatar.Prepare(data) + avatarData, err := avatar.ProcessAvatarImage(data) if err != nil { return err } @@ -262,9 +261,7 @@ func UploadAvatar(u *user_model.User, data []byte) error { } if err := storage.SaveFrom(storage.Avatars, u.CustomAvatarRelativePath(), func(w io.Writer) error { - if err := png.Encode(w, *m); err != nil { - log.Error("Encode: %v", err) - } + _, err := w.Write(avatarData) return err }); err != nil { return fmt.Errorf("Failed to create dir %s: %w", u.CustomAvatarRelativePath(), err) diff --git a/templates/devtest/gitea-ui.tmpl b/templates/devtest/gitea-ui.tmpl index 2fe478a07d8a..c00d12217db9 100644 --- a/templates/devtest/gitea-ui.tmpl +++ b/templates/devtest/gitea-ui.tmpl @@ -1,5 +1,38 @@ {{template "base/head" .}}
+
+

Button

+
+ Style: + + + + +
+
+ State: + +
+
+
+ + + + + +
+
This is a button
+
+ +

Tooltip

diff --git a/templates/org/home.tmpl b/templates/org/home.tmpl index 27f0df0b385e..97047c71cc01 100644 --- a/templates/org/home.tmpl +++ b/templates/org/home.tmpl @@ -10,7 +10,7 @@ {{if .Org.Visibility.IsPrivate}}{{.locale.Tr "org.settings.visibility.private_shortname"}}{{end}} {{if .EnableFeed}} - {{svg "octicon-rss" 24}} + {{svg "octicon-rss" 24}} {{end}}
{{if $.RenderedDescription}}
{{$.RenderedDescription|Str2html}}
{{end}} diff --git a/templates/package/content/alpine.tmpl b/templates/package/content/alpine.tmpl new file mode 100644 index 000000000000..97e2289ad8f7 --- /dev/null +++ b/templates/package/content/alpine.tmpl @@ -0,0 +1,52 @@ +{{if eq .PackageDescriptor.Package.Type "alpine"}} +

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

+
+
+
+ +
/$branch/$repository
+

{{.locale.Tr "packages.alpine.registry.info" | Safe}}

+
+
+ +
curl -JO 
+
+
+ +
+
sudo apk add {{$.PackageDescriptor.Package.Name}}={{$.PackageDescriptor.Version.Version}}
+
+
+
+ +
+
+
+ +

{{.locale.Tr "packages.alpine.repository"}}

+
+ + + + + + + + + + + + + + + +
{{.locale.Tr "packages.alpine.repository.branches"}}
{{StringUtils.Join .Branches ", "}}
{{.locale.Tr "packages.alpine.repository.repositories"}}
{{StringUtils.Join .Repositories ", "}}
{{.locale.Tr "packages.alpine.repository.architectures"}}
{{StringUtils.Join .Architectures ", "}}
+
+ + {{if .PackageDescriptor.Metadata.Description}} +

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

+
+ {{.PackageDescriptor.Metadata.Description}} +
+ {{end}} +{{end}} diff --git a/templates/package/content/go.tmpl b/templates/package/content/go.tmpl new file mode 100644 index 000000000000..2343d945b3a7 --- /dev/null +++ b/templates/package/content/go.tmpl @@ -0,0 +1,14 @@ +{{if eq .PackageDescriptor.Package.Type "go"}} +

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

+
+
+
+ +
GOPROXY= go install {{$.PackageDescriptor.Package.Name}}@{{$.PackageDescriptor.Version.Version}}
+
+
+ +
+
+
+{{end}} diff --git a/templates/package/metadata/alpine.tmpl b/templates/package/metadata/alpine.tmpl new file mode 100644 index 000000000000..9011bfce10ba --- /dev/null +++ b/templates/package/metadata/alpine.tmpl @@ -0,0 +1,5 @@ +{{if eq .PackageDescriptor.Package.Type "alpine"}} + {{if .PackageDescriptor.Metadata.Maintainer}}
{{svg "octicon-person" 16 "mr-3"}} {{.PackageDescriptor.Metadata.Maintainer}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "mr-3"}} {{.locale.Tr "packages.details.project_site"}}
{{end}} + {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.License}}
{{end}} +{{end}} diff --git a/templates/package/view.tmpl b/templates/package/view.tmpl index 36637772cdbb..5285a0838d5a 100644 --- a/templates/package/view.tmpl +++ b/templates/package/view.tmpl @@ -19,6 +19,7 @@
+ {{template "package/content/alpine" .}} {{template "package/content/cargo" .}} {{template "package/content/chef" .}} {{template "package/content/composer" .}} @@ -27,6 +28,7 @@ {{template "package/content/container" .}} {{template "package/content/debian" .}} {{template "package/content/generic" .}} + {{template "package/content/go" .}} {{template "package/content/helm" .}} {{template "package/content/maven" .}} {{template "package/content/npm" .}} @@ -48,6 +50,7 @@ {{end}}
{{svg "octicon-calendar" 16 "gt-mr-3"}} {{TimeSinceUnix .PackageDescriptor.Version.CreatedUnix $.locale}}
{{svg "octicon-download" 16 "gt-mr-3"}} {{.PackageDescriptor.Version.DownloadCount}}
+ {{template "package/metadata/alpine" .}} {{template "package/metadata/cargo" .}} {{template "package/metadata/chef" .}} {{template "package/metadata/composer" .}} diff --git a/templates/repo/actions/list.tmpl b/templates/repo/actions/list.tmpl index 2885aa0fbfc9..ca97b67faaa0 100644 --- a/templates/repo/actions/list.tmpl +++ b/templates/repo/actions/list.tmpl @@ -19,11 +19,6 @@
-
-
- {{template "repo/actions/openclose" .}} -
-
{{template "repo/actions/runs_list" .}}
diff --git a/templates/repo/actions/openclose.tmpl b/templates/repo/actions/openclose.tmpl deleted file mode 100644 index 6874115a19ff..000000000000 --- a/templates/repo/actions/openclose.tmpl +++ /dev/null @@ -1,10 +0,0 @@ - diff --git a/templates/repo/actions/runs_list.tmpl b/templates/repo/actions/runs_list.tmpl index caa14b339051..fdef2e6446c4 100644 --- a/templates/repo/actions/runs_list.tmpl +++ b/templates/repo/actions/runs_list.tmpl @@ -1,4 +1,4 @@ -
+
{{range .Runs}}
  • diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl index 199d4489a9e0..938825892103 100644 --- a/templates/repo/branch/list.tmpl +++ b/templates/repo/branch/list.tmpl @@ -27,7 +27,7 @@ {{if and $.IsWriter (not $.Repository.IsArchived) (not .IsDeleted)}} - {{else}} - {{end}} diff --git a/templates/repo/commits_list_small.tmpl b/templates/repo/commits_list_small.tmpl index 5bc159de8178..900d853601aa 100644 --- a/templates/repo/commits_list_small.tmpl +++ b/templates/repo/commits_list_small.tmpl @@ -15,7 +15,7 @@ {{$commitLink:= printf "%s/commit/%s" $.comment.Issue.PullRequest.BaseRepo.Link (PathEscape .ID.String)}} - + {{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses "root" $.root}} {{$class := "ui sha label"}} {{if .Signature}} @@ -32,7 +32,7 @@ {{$class = (printf "%s%s" $class " isWarning")}} {{end}} {{end}} - + {{ShortSha .ID.String}} {{if .Signature}} {{template "repo/shabox_badge" dict "root" $.root "verification" .Verification}} diff --git a/templates/repo/diff/new_review.tmpl b/templates/repo/diff/new_review.tmpl index bb97303034fe..afb82a8d3d7a 100644 --- a/templates/repo/diff/new_review.tmpl +++ b/templates/repo/diff/new_review.tmpl @@ -9,13 +9,15 @@
    {{.CsrfTokenHtml}} -
    + -
    +
    {{template "shared/combomarkdowneditor" (dict "locale" $.locale + "MarkdownPreviewUrl" (print .Repository.Link "/markup") + "MarkdownPreviewContext" .RepoLink "TextareaName" "content" "TextareaPlaceholder" ($.locale.Tr "repo.diff.review.placeholder") "DropzoneParentContainer" "form" diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl index b2fd0710afb0..03f5a2e78fed 100644 --- a/templates/repo/header.tmpl +++ b/templates/repo/header.tmpl @@ -36,7 +36,7 @@ {{end}}
    {{if $.EnableFeed}} - {{svg "octicon-rss" 18}} + {{svg "octicon-rss" 18}} {{end}}
    {{if $.IsPullMirror}} diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl index e7a76cdaf56c..7c084b06867e 100644 --- a/templates/repo/home.tmpl +++ b/templates/repo/home.tmpl @@ -30,7 +30,7 @@
    {{range .Topics}}{{.Name}}{{end}} - {{if and .Permission.IsAdmin (not .Repository.IsArchived)}}{{end}} + {{if and .Permission.IsAdmin (not .Repository.IsArchived)}}{{end}}
    {{end}} {{if and .Permission.IsAdmin (not .Repository.IsArchived)}} diff --git a/templates/repo/issue/view_content.tmpl b/templates/repo/issue/view_content.tmpl index e9a82425ba72..c54b29dcd648 100644 --- a/templates/repo/issue/view_content.tmpl +++ b/templates/repo/issue/view_content.tmpl @@ -11,10 +11,12 @@
    {{if .Issue.OriginalAuthor}} - + + + {{else}} - {{avatar $.Context .Issue.Poster}} + {{avatar $.Context .Issue.Poster 40}} {{end}}
    @@ -33,7 +35,7 @@ {{else}} - {{avatar $.Context .Issue.Poster}} + {{avatar $.Context .Issue.Poster 24}} {{template "shared/user/authorlink" .Issue.Poster}} @@ -93,7 +95,7 @@ {{if and (or .IsRepoAdmin .HasIssuesOrPullsWritePermission (not .Issue.IsLocked)) (not .Repository.IsArchived)}}
    - {{avatar $.Context .SignedUser}} + {{avatar $.Context .SignedUser 40}}
    diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index ab499f346afd..32ce8bad9476 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -15,10 +15,12 @@ {{if eq .Type 0}}
    {{if .OriginalAuthor}} - + + + {{else}} - {{avatar $.Context .Poster}} + {{avatar $.Context .Poster 40}} {{end}}
    @@ -38,7 +40,7 @@ {{else}} {{if gt .Poster.ID 0}} - {{avatar $.Context .Poster}} + {{avatar $.Context .Poster 24}} {{end}} @@ -375,8 +377,8 @@ {{/* Some timeline avatars need a offset to correctly allign with their speech bubble. The condition depends on review type and for positive reviews whether there is a comment element or not */}} - - {{avatar $.Context .Poster}} + + {{avatar $.Context .Poster 40}} {{end}} {{svg (printf "octicon-%s" .Review.Type.Icon)}} @@ -413,7 +415,7 @@
    {{if gt .Poster.ID 0}} - {{avatar $.Context .Poster}} + {{avatar $.Context .Poster 24}} {{end}} @@ -482,7 +484,7 @@ {{range $filename, $lines := .Review.CodeComments}} {{range $line, $comms := $lines}}
    -
    +
    {{$invalid := (index $comms 0).Invalidated}} {{$resolved := (index $comms 0).IsResolved}} {{$resolveDoer := (index $comms 0).ResolveDoer}} @@ -541,7 +543,7 @@
    {{if not .OriginalAuthor}} - {{avatar $.Context .Poster}} + {{avatar $.Context .Poster 20}} {{end}} @@ -768,7 +770,7 @@
    - + {{svg "octicon-x" 16}} {{template "shared/user/avatarlink" dict "Context" $.Context "user" .Poster}} @@ -789,7 +791,7 @@
    {{if gt .Poster.ID 0}} - {{avatar $.Context .Poster}} + {{avatar $.Context .Poster 24}} {{end}} diff --git a/templates/repo/issue/view_content/pull.tmpl b/templates/repo/issue/view_content/pull.tmpl index 907ee1985755..5325c710b4a8 100644 --- a/templates/repo/issue/view_content/pull.tmpl +++ b/templates/repo/issue/view_content/pull.tmpl @@ -115,7 +115,7 @@ {{- else if .Issue.PullRequest.IsChecking}}yellow {{- else if .Issue.PullRequest.IsEmpty}}grey {{- else if .Issue.PullRequest.CanAutoMerge}}green - {{- else}}red{{end}}">{{svg "octicon-git-merge" 32}} + {{- else}}red{{end}}">{{svg "octicon-git-merge" 40}}
    {{template "repo/pulls/status" .}} {{$showGeneralMergeForm := false}} diff --git a/templates/repo/release_tag_header.tmpl b/templates/repo/release_tag_header.tmpl index 6d022eebccac..d8f60a2c6cf4 100644 --- a/templates/repo/release_tag_header.tmpl +++ b/templates/repo/release_tag_header.tmpl @@ -11,7 +11,7 @@ {{end}} {{if .EnableFeed}} - {{svg "octicon-rss" 18}} + {{svg "octicon-rss" 18}} {{end}}
    {{if and (not .PageIsTagList) .CanCreateRelease}} diff --git a/templates/shared/issuelist.tmpl b/templates/shared/issuelist.tmpl index 3b354710817c..71d9059c1e18 100644 --- a/templates/shared/issuelist.tmpl +++ b/templates/shared/issuelist.tmpl @@ -153,7 +153,7 @@ diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 35cbc71c8baf..3859eb5567a8 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -2409,6 +2409,7 @@ }, { "enum": [ + "alpine", "cargo", "chef", "composer", @@ -2417,6 +2418,7 @@ "container", "debian", "generic", + "go", "helm", "maven", "npm", diff --git a/templates/user/notification/notification_div.tmpl b/templates/user/notification/notification_div.tmpl index 6047f34580e5..6693b2e57d0a 100644 --- a/templates/user/notification/notification_div.tmpl +++ b/templates/user/notification/notification_div.tmpl @@ -82,7 +82,7 @@ {{$.CsrfTokenHtml}} -
    diff --git a/templates/user/settings/applications_oauth2_edit_form.tmpl b/templates/user/settings/applications_oauth2_edit_form.tmpl index 1a8336b15065..8a9579c5c51d 100644 --- a/templates/user/settings/applications_oauth2_edit_form.tmpl +++ b/templates/user/settings/applications_oauth2_edit_form.tmpl @@ -27,7 +27,7 @@ {{.CsrfTokenHtml}} {{.locale.Tr "settings.oauth2_regenerate_secret_hint"}} - +
    diff --git a/tests/integration/api_packages_alpine_test.go b/tests/integration/api_packages_alpine_test.go new file mode 100644 index 000000000000..473dcc042495 --- /dev/null +++ b/tests/integration/api_packages_alpine_test.go @@ -0,0 +1,229 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "archive/tar" + "bufio" + "bytes" + "compress/gzip" + "encoding/base64" + "fmt" + "io" + "net/http" + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + alpine_module "code.gitea.io/gitea/modules/packages/alpine" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func TestPackageAlpine(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + packageName := "gitea-test" + packageVersion := "1.4.1-r3" + + base64AlpinePackageContent := `H4sIAAAAAAACA9ML9nT30wsKdtTLzjNJzjYuckjPLElN1DUzMUxMNTa11CsqTtQrKE1ioAAYAIGZ +iQmYBgJ02hDENjQxMTAzMzQ1MTVjMDA0MTQ1ZlAwYKADKC0uSSxSUGAYoWDm4sZZtypv75+q2fVT +POD1bKkFB22ms+g1z+H4dk7AhC3HwUSj9EbT0Rk3Dn55dHxy/K7Q+Nl/i+L7Z036ypcRvvpZuMiN +s7wbZL/klqRGGshv9Gi0qHTgTZfw3HytnJdx9c3NTRp/PHn+Z50uq2pjkilzjtpfd+uzQMw1M7cY +i9RXJasnT2M+vDXCesLK7MilJt8sGplj4xUlLMUun9SzY+phFpxWxRXa06AseV9WvzH3jtGGoL5A +vQkea+VKPj5R+Cb461tIk97qpa9nJYsJujTNl2B/J1P52H/D2rPr/j19uU8p7cMSq5tmXk51ReXl +F/Yddr9XsMpEwFKlXSPo3QSGwnCOG8y2uadjm6ui998WYXNYubjg78N3a7bnXjhrl5fB8voI++LI +1FP5W44e2xf4Ou2wrtyic1Onz7MzMV5ksuno2V/LVG4eN/15X/n2/2vJ2VV+T68aT327dOrhd6e6 +q5Y0V82Y83tdqkFa8TW2BvGCZ0ds/iibHVpzKuPcuSULO63/bNmfrnhjWqXzhMSXTb5Cv4vPaxSL +8LFMdqmxbN7+Y+Yi0ZyZhz4UxexLuHHFd1VFvk+kwvniq3P+f9rh52InWnL8Lpvedcecoh1GFSc5 +xZ9VBGex2V269HZfwxSVCvP35wQfi2xKX+lYMXtF48n1R65O2PLWpm69RdESMa79dlrTGazsZacu +MbMLeSSScPORZde76/MBV6SFJAAEAAAfiwgAAAAAAAID7VRLaxsxEN6zfoUgZ++OVq+1aUIhUDeY +pKa49FhmJdkW3ofRysXpr69220t9SCk0gZJ+IGaY56eBmbxY4/m9Q+vCUOTr1fLu4d2H7O8CEpQQ +k0y4lAClypgQoBSTQqoMGBMgMnrOXgCnIWJIVLLXCcaoib5110CSij/V7D9eCZ5p5f9o/5VkF/tf +MqUzCi+5/6Hv41Nxv/Nffu4fwRVdus4FjM7S+pFiffKNpTxnkMMsALmin5PnHgMtS8rkgvGFBPpp +c0tLKDk5HnYdto5e052PDmfRDXE0fnUh2VgucjYLU5h1g0mm5RhGNymMrtEccOfIKTTJsY/xOCyK +YqqT+74gExWbmI2VlJ6LeQUcyPFH2lh/9SBuV/wjfXPohDnw8HZKviGD/zYmCZgrgsHsk36u1Bcl +SB/8zne/0jV92/qYbKRF38X0niiemN2QxhvXDWOL+7tNGhGeYt+m22mwaR6pddGZNM8FSeRxj8PY +X7PaqdqAVlqWXHKnmQGmK43VlqNlILRilbBSMI2jV5Vbu5XGSVsDyGc7yd8B/gK2qgAIAAAfiwgA +AAAAAAID7dNNSgMxGAbg7MSCOxcu5wJOv0x+OlkU7K5QoYXqVsxMMihlKMwP1Fu48QQewCN4DfEQ +egUz4sYuFKEtFN9n870hWSSQN+7P7GrsrfNV3Y9dW5Z3bNMo0FJ+zmB9EhcJ41KS1lxJpRnxbsWi +FduBtm5sFa7C/ifOo7y5Lf2QeiHar6jTaDSbnF5Mp+fzOL/x+aJuy3g+HvGhs8JY4b3yOpMZOZEo +lRW+MEoTTw3ZwqU0INNjsAe2VPk/9b/L3/s/kIKzqOtk+IbJGTtmr+bx7WoxOUoun98frk/un14O +Djfa/2q5bH4699v++uMAAAAAAAAAAAAAAAAAAAAAAHbgA/eXQh8AKAAA` + content, err := base64.StdEncoding.DecodeString(base64AlpinePackageContent) + assert.NoError(t, err) + + branches := []string{"v3.16", "v3.17"} + repositories := []string{"main", "testing"} + + rootURL := fmt.Sprintf("/api/packages/%s/alpine", user.Name) + + t.Run("RepositoryKey", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", rootURL+"/key") + resp := MakeRequest(t, req, http.StatusOK) + + assert.Equal(t, "application/x-pem-file", resp.Header().Get("Content-Type")) + assert.Contains(t, resp.Body.String(), "-----BEGIN PUBLIC KEY-----") + }) + + for _, branch := range branches { + for _, repository := range repositories { + t.Run(fmt.Sprintf("[Branch:%s,Repository:%s]", branch, repository), func(t *testing.T) { + t.Run("Upload", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + uploadURL := fmt.Sprintf("%s/%s/%s", rootURL, branch, repository) + + req := NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader([]byte{})) + MakeRequest(t, req, http.StatusUnauthorized) + + req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader([]byte{})) + AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusBadRequest) + + req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(content)) + AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusCreated) + + pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeAlpine) + assert.NoError(t, err) + assert.Len(t, pvs, 1) + + pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0]) + assert.NoError(t, err) + assert.Nil(t, pd.SemVer) + assert.IsType(t, &alpine_module.VersionMetadata{}, pd.Metadata) + assert.Equal(t, packageName, pd.Package.Name) + assert.Equal(t, packageVersion, pd.Version.Version) + + pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID) + assert.NoError(t, err) + assert.NotEmpty(t, pfs) + assert.Condition(t, func() bool { + seen := false + expectedFilename := fmt.Sprintf("%s-%s.apk", packageName, packageVersion) + expectedCompositeKey := fmt.Sprintf("%s|%s|x86_64", branch, repository) + for _, pf := range pfs { + if pf.Name == expectedFilename && pf.CompositeKey == expectedCompositeKey { + if seen { + return false + } + seen = true + + assert.True(t, pf.IsLead) + + pfps, err := packages.GetProperties(db.DefaultContext, packages.PropertyTypeFile, pf.ID) + assert.NoError(t, err) + + for _, pfp := range pfps { + switch pfp.Name { + case alpine_module.PropertyBranch: + assert.Equal(t, branch, pfp.Value) + case alpine_module.PropertyRepository: + assert.Equal(t, repository, pfp.Value) + case alpine_module.PropertyArchitecture: + assert.Equal(t, "x86_64", pfp.Value) + } + } + } + } + return seen + }) + }) + + t.Run("Index", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + url := fmt.Sprintf("%s/%s/%s/x86_64/APKINDEX.tar.gz", rootURL, branch, repository) + + req := NewRequest(t, "GET", url) + resp := MakeRequest(t, req, http.StatusOK) + + assert.Condition(t, func() bool { + br := bufio.NewReader(resp.Body) + + gzr, err := gzip.NewReader(br) + assert.NoError(t, err) + + for { + gzr.Multistream(false) + + tr := tar.NewReader(gzr) + for { + hd, err := tr.Next() + if err == io.EOF { + break + } + assert.NoError(t, err) + + if hd.Name == "APKINDEX" { + buf, err := io.ReadAll(tr) + assert.NoError(t, err) + + s := string(buf) + + assert.Contains(t, s, "C:Q1/se1PjO94hYXbfpNR1/61hVORIc=\n") + assert.Contains(t, s, "P:"+packageName+"\n") + assert.Contains(t, s, "V:"+packageVersion+"\n") + assert.Contains(t, s, "A:x86_64\n") + assert.Contains(t, s, "T:Gitea Test Package\n") + assert.Contains(t, s, "U:https://gitea.io/\n") + assert.Contains(t, s, "L:MIT\n") + assert.Contains(t, s, "S:1353\n") + assert.Contains(t, s, "I:4096\n") + assert.Contains(t, s, "o:gitea-test\n") + assert.Contains(t, s, "m:KN4CK3R \n") + assert.Contains(t, s, "t:1679498030\n") + + return true + } + } + + err = gzr.Reset(br) + if err == io.EOF { + break + } + assert.NoError(t, err) + } + + return false + }) + }) + + t.Run("Download", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/x86_64/%s-%s.apk", rootURL, branch, repository, packageName, packageVersion)) + MakeRequest(t, req, http.StatusOK) + }) + }) + } + } + + t.Run("Delete", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + for _, branch := range branches { + for _, repository := range repositories { + req := NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s/x86_64/%s-%s.apk", rootURL, branch, repository, packageName, packageVersion)) + MakeRequest(t, req, http.StatusUnauthorized) + + req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s/x86_64/%s-%s.apk", rootURL, branch, repository, packageName, packageVersion)) + AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusNoContent) + + // Deleting the last file of an architecture should remove that index + req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/x86_64/APKINDEX.tar.gz", rootURL, branch, repository)) + MakeRequest(t, req, http.StatusNotFound) + } + } + }) +} diff --git a/tests/integration/api_packages_goproxy_test.go b/tests/integration/api_packages_goproxy_test.go new file mode 100644 index 000000000000..08c1ca54f140 --- /dev/null +++ b/tests/integration/api_packages_goproxy_test.go @@ -0,0 +1,166 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "archive/zip" + "bytes" + "fmt" + "net/http" + "testing" + "time" + + "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" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func TestPackageGo(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + packageName := "gitea.com/go-gitea/gitea" + packageVersion := "v0.0.1" + packageVersion2 := "v0.0.2" + goModContent := `module "gitea.com/go-gitea/gitea"` + + createArchive := func(files map[string][]byte) []byte { + var buf bytes.Buffer + zw := zip.NewWriter(&buf) + for name, content := range files { + w, _ := zw.Create(name) + w.Write(content) + } + zw.Close() + return buf.Bytes() + } + + url := fmt.Sprintf("/api/packages/%s/go", user.Name) + + t.Run("Upload", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + content := createArchive(nil) + + req := NewRequestWithBody(t, "PUT", url+"/upload", bytes.NewReader(content)) + MakeRequest(t, req, http.StatusUnauthorized) + + req = NewRequestWithBody(t, "PUT", url+"/upload", bytes.NewReader(content)) + AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusBadRequest) + + content = createArchive(map[string][]byte{ + packageName + "@" + packageVersion + "/go.mod": []byte(goModContent), + }) + + req = NewRequestWithBody(t, "PUT", url+"/upload", bytes.NewReader(content)) + AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusCreated) + + pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeGo) + assert.NoError(t, err) + assert.Len(t, pvs, 1) + + pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0]) + assert.NoError(t, err) + assert.Nil(t, pd.Metadata) + assert.Equal(t, packageName, pd.Package.Name) + assert.Equal(t, packageVersion, pd.Version.Version) + + pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID) + assert.NoError(t, err) + assert.Len(t, pfs, 1) + assert.Equal(t, packageVersion+".zip", pfs[0].Name) + assert.True(t, pfs[0].IsLead) + + pb, err := packages.GetBlobByID(db.DefaultContext, pfs[0].BlobID) + assert.NoError(t, err) + assert.Equal(t, int64(len(content)), pb.Size) + + req = NewRequestWithBody(t, "PUT", url+"/upload", bytes.NewReader(content)) + AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusConflict) + + time.Sleep(time.Second) + + content = createArchive(map[string][]byte{ + packageName + "@" + packageVersion2 + "/go.mod": []byte(goModContent), + }) + + req = NewRequestWithBody(t, "PUT", url+"/upload", bytes.NewReader(content)) + AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusCreated) + }) + + t.Run("List", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/@v/list", url, packageName)) + resp := MakeRequest(t, req, http.StatusOK) + + assert.Equal(t, packageVersion+"\n"+packageVersion2+"\n", resp.Body.String()) + }) + + t.Run("Info", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/@v/%s.info", url, packageName, packageVersion)) + resp := MakeRequest(t, req, http.StatusOK) + + type Info struct { + Version string `json:"Version"` + Time time.Time `json:"Time"` + } + + info := &Info{} + DecodeJSON(t, resp, &info) + + assert.Equal(t, packageVersion, info.Version) + + req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/@v/latest.info", url, packageName)) + resp = MakeRequest(t, req, http.StatusOK) + + info = &Info{} + DecodeJSON(t, resp, &info) + + assert.Equal(t, packageVersion2, info.Version) + + req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/@latest", url, packageName)) + resp = MakeRequest(t, req, http.StatusOK) + + info = &Info{} + DecodeJSON(t, resp, &info) + + assert.Equal(t, packageVersion2, info.Version) + }) + + t.Run("GoMod", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/@v/%s.mod", url, packageName, packageVersion)) + resp := MakeRequest(t, req, http.StatusOK) + + assert.Equal(t, goModContent, resp.Body.String()) + + req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/@v/latest.mod", url, packageName)) + resp = MakeRequest(t, req, http.StatusOK) + + assert.Equal(t, goModContent, resp.Body.String()) + }) + + t.Run("Download", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/@v/%s.zip", url, packageName, packageVersion)) + MakeRequest(t, req, http.StatusOK) + + req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/@v/latest.zip", url, packageName)) + MakeRequest(t, req, http.StatusOK) + }) +} diff --git a/tests/integration/api_team_test.go b/tests/integration/api_team_test.go index 934e6bf23046..60c61394d453 100644 --- a/tests/integration/api_team_test.go +++ b/tests/integration/api_team_test.go @@ -29,6 +29,7 @@ func TestAPITeam(t *testing.T) { teamUser := unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{ID: 1}) team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamUser.TeamID}) + org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: teamUser.OrgID}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: teamUser.UID}) session := loginUser(t, user.Name) @@ -40,6 +41,7 @@ func TestAPITeam(t *testing.T) { DecodeJSON(t, resp, &apiTeam) assert.EqualValues(t, team.ID, apiTeam.ID) assert.Equal(t, team.Name, apiTeam.Name) + assert.EqualValues(t, convert.ToOrganization(db.DefaultContext, org), apiTeam.Organization) // non team member user will not access the teams details teamUser2 := unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{ID: 3}) @@ -58,7 +60,7 @@ func TestAPITeam(t *testing.T) { session = loginUser(t, user.Name) token = getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAdminOrg) - org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 6}) + org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 6}) // Create team. teamToCreate := &api.CreateTeamOption{ diff --git a/tests/integration/goget_test.go b/tests/integration/goget_test.go index fab3911464ac..854f8d7a2dbe 100644 --- a/tests/integration/goget_test.go +++ b/tests/integration/goget_test.go @@ -33,3 +33,29 @@ func TestGoGet(t *testing.T) { assert.Equal(t, expected, resp.Body.String()) } + +func TestGoGetForSSH(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + old := setting.Repository.GoGetCloneURLProtocol + defer func() { + setting.Repository.GoGetCloneURLProtocol = old + }() + setting.Repository.GoGetCloneURLProtocol = "ssh" + + req := NewRequest(t, "GET", "/blah/glah/plah?go-get=1") + resp := MakeRequest(t, req, http.StatusOK) + + expected := fmt.Sprintf(` + + + + + + + go get --insecure %[1]s:%[2]s/blah/glah + +`, setting.Domain, setting.HTTPPort, setting.AppURL, setting.SSH.Domain, setting.SSH.Port) + + assert.Equal(t, expected, resp.Body.String()) +} diff --git a/web_src/css/base.css b/web_src/css/base.css index aa86d140fe4b..ed00b1a29522 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -364,12 +364,6 @@ a.label, text-decoration: none !important; } -/* for most cases, we only use our svg icon as inline icon, so we need to make them inline-block and vertical-align: middle */ -svg.svg { - display: inline-block; - vertical-align: middle; -} - .ui.red.labels .label, .ui.ui.ui.red.label, .ui.red.button, @@ -1052,62 +1046,6 @@ svg.svg { box-shadow: -1px -1px 0 0 var(--color-secondary); } -.ui.cards > .card, -.ui.card { - background: var(--color-card); - border: 1px solid var(--color-secondary); - box-shadow: none; -} - -.ui.cards > .card > .content, -.ui.card > .content { - border-color: var(--color-secondary); -} - -.ui.cards > .card > .extra, -.ui.card > .extra, -.ui.cards > .card > .extra a:not(.ui), -.ui.card > .extra a:not(.ui) { - color: var(--color-text); -} - -.ui.cards > .card > .extra a:not(.ui):hover, -.ui.card > .extra a:not(.ui):hover { - color: var(--color-primary); -} - -.ui.cards > .card > .content > .header, -.ui.card > .content > .header { - color: var(--color-text); -} - -.ui.cards > .card > .content > .description, -.ui.card > .content > .description { - color: var(--color-text); -} - -.ui.cards > .card .meta > a:not(.ui), -.ui.card .meta > a:not(.ui) { - color: var(--color-text-light-2); -} - -.ui.cards > .card .meta > a:not(.ui):hover, -.ui.card .meta > a:not(.ui):hover { - color: var(--color-text); -} - -.ui.cards a.card:hover, -a.ui.card:hover { - border: 1px solid var(--color-secondary); - background: var(--color-card); -} - -.ui.cards > .card > .extra, -.ui.card > .extra { - color: var(--color-text); - border-top-color: var(--color-secondary-light-1) !important; -} - .ui.comments .comment .text { margin: 0; } @@ -1189,12 +1127,10 @@ a.ui.card:hover { img.ui.avatar, .ui.avatar img, -.ui.avatar svg, -.ui.cards > .card img.avatar, -.ui.cards > .card .avatar img, -.ui.card img.avatar, -.ui.card .avatar img { +.ui.avatar svg { border-radius: var(--border-radius); + object-fit: contain; + aspect-ratio: 1; } .ui.divided.list > .item { @@ -1967,11 +1903,6 @@ img.ui.avatar, color: var(--color-text-light); } -.repo-title .avatar { - width: 32px !important; - height: 32px !important; -} - .repo-title .labels { margin-left: 0.5rem; } @@ -2244,13 +2175,16 @@ a.ui.active.label:hover { border-left: none; } -.ui.button.button-link { +/* a ghost button can be used as inline text, it doesn't have obvious styles */ +.button.button-ghost { background: transparent; border: none; color: inherit; + margin: 0; + padding: 0; } -.ui.button.button-link:hover { +.button.button-ghost:hover { color: var(--color-primary); } @@ -2347,19 +2281,6 @@ a.ui.active.label:hover { border-color: var(--color-secondary-dark-3) !important; } -.ui.tertiary.button { - color: var(--color-text-light); - border: none; -} - -.ui.tertiary.button:hover { - color: var(--color-text); -} - -.ui.tertiary.button:focus { - color: var(--color-text-dark); -} - .ui.primary.label, .ui.primary.labels .label, .ui.ui.ui.primary.label { @@ -2595,6 +2516,11 @@ a.ui.basic.label:hover { line-height: .67em; } +.rss-icon { + display: inline-flex; + color: var(--color-text-light-1); +} + table th[data-sortt-asc]:hover, table th[data-sortt-desc]:hover { background: rgba(0, 0, 0, 0.1) !important; diff --git a/web_src/css/code/linebutton.css b/web_src/css/code/linebutton.css index 1012b38ba92a..a475138e6bfc 100644 --- a/web_src/css/code/linebutton.css +++ b/web_src/css/code/linebutton.css @@ -4,6 +4,7 @@ .code-line-menu { width: auto !important; + border: none !important; /* the border is provided by tippy, not using the `.ui.menu` border */ } .code-line-button { diff --git a/web_src/css/index.css b/web_src/css/index.css index 6fb92f2ecb21..c7701809c00e 100644 --- a/web_src/css/index.css +++ b/web_src/css/index.css @@ -10,6 +10,8 @@ @import "./modules/tippy.css"; @import "./modules/modal.css"; @import "./modules/breadcrumb.css"; +@import "./modules/card.css"; +@import "./modules/comment.css"; @import "./code/linebutton.css"; @import "./markup/content.css"; @import "./markup/codecopy.css"; diff --git a/web_src/css/modules/card.css b/web_src/css/modules/card.css new file mode 100644 index 000000000000..c0f7e83037ca --- /dev/null +++ b/web_src/css/modules/card.css @@ -0,0 +1,134 @@ +/* Below styles are a subset of the full fomantic card styles which are */ +/* needed to get all current uses of fomantic cards working. */ +/* TODO: remove all these styles and use custom styling instead */ + +.ui.card:last-child { + margin-bottom: 0; +} +.ui.card:first-child { + margin-top: 0; +} + +.ui.cards > .card, +.ui.card { + display: flex; + flex-direction: column; + max-width: 100%; + width: 290px; + min-height: 0; + padding: 0; + background: var(--color-card); + border: 1px solid var(--color-secondary); + box-shadow: none; + word-wrap: break-word; +} + +.ui.card { + margin: 1em 0; +} + +.ui.cards { + display: flex; + margin: -0.875em -0.5em; + flex-wrap: wrap; +} + +.ui.cards > .card { + display: flex; + margin: 0.875em 0.5em; + float: none; +} + +.ui.cards > .card > .content, +.ui.card > .content { + border-top: 1px solid var(--color-secondary); + max-width: 100%; + padding: 1em; + font-size: 1em; +} + +.ui.cards > .card > .content > .meta + .description, +.ui.cards > .card > .content > .header + .description, +.ui.card > .content > .meta + .description, +.ui.card > .content > .header + .description { + margin-top: .5em; +} + +.ui.cards > .card > .content > .header:not(.ui), +.ui.card > .content > .header:not(.ui) { + font-weight: 500; + font-size: 1.28571429em; + margin-top: -.21425em; + line-height: 1.28571429em; +} + +.ui.cards > .card > .content:first-child, +.ui.card > .content:first-child { + border-top: none; + border-radius: var(--border-radius) var(--border-radius) 0 0; +} + +.ui.cards > .card > :last-child, +.ui.card > :last-child { + border-radius: 0 0 var(--border-radius) var(--border-radius); +} + +.ui.cards > .card > :only-child, +.ui.card > :only-child { + border-radius: var(--border-radius) !important; +} + +.ui.cards > .card > .extra, +.ui.card > .extra, +.ui.cards > .card > .extra a:not(.ui), +.ui.card > .extra a:not(.ui) { + color: var(--color-text); +} + +.ui.cards > .card > .extra a:not(.ui):hover, +.ui.card > .extra a:not(.ui):hover { + color: var(--color-primary); +} + +.ui.cards > .card > .content > .header, +.ui.card > .content > .header { + color: var(--color-text); +} + +.ui.cards > .card > .content > .description, +.ui.card > .content > .description { + color: var(--color-text); +} + +.ui.cards > .card .meta > a:not(.ui), +.ui.card .meta > a:not(.ui) { + color: var(--color-text-light-2); +} + +.ui.cards > .card .meta > a:not(.ui):hover, +.ui.card .meta > a:not(.ui):hover { + color: var(--color-text); +} + +.ui.cards a.card:hover, +a.ui.card:hover { + border: 1px solid var(--color-secondary); + background: var(--color-card); +} + +.ui.cards > .card > .extra, +.ui.card > .extra { + color: var(--color-text); + border-top-color: var(--color-secondary-light-1) !important; +} + +.ui.three.cards { + margin-left: -1em; + margin-right: -1em; +} + +.ui.three.cards > .card { + width: calc(33.33333333333333% - 2em); + margin-left: 1em; + margin-right: 1em; +} diff --git a/web_src/css/modules/comment.css b/web_src/css/modules/comment.css new file mode 100644 index 000000000000..5a90c0852f68 --- /dev/null +++ b/web_src/css/modules/comment.css @@ -0,0 +1,91 @@ +/* These are the remnants of the fomantic comment module */ +/* TODO: remove all of these rules */ + +.ui.comments { + margin: 1.5em 0; + max-width: 650px; +} + +.ui.comments:first-child { + margin-top: 0; +} + +.ui.comments:last-child { + margin-bottom: 0; +} + +.ui.comments .comment { + position: relative; + background: none; + margin: 0.5em 0 0; + padding: 0.5em 0 0; + border: none; + border-top: none; + line-height: 1.2; +} + +.ui.comments .comment:first-child { + margin-top: 0; + padding-top: 0; +} + +.ui.comments .comment > .comments { + margin: 0 0 0.5em 0.5em; + padding: 1em 0 1em 1em; +} + +.ui.comments .comment > .comments::before { + position: absolute; + top: 0; + left: 0; +} + +.ui.comments .comment > .comments .comment { + border: none; + border-top: none; + background: none; +} + +.ui.comments .comment .avatar { + float: left; + width: 2.5em; +} + +.ui.comments .comment > .content { + display: block; +} + +.ui.comments .comment > .avatar ~ .content { + margin-left: 3.5em; +} + +.ui.comments .comment .author { + font-size: 1em; + font-weight: 500; +} + +.ui.comments .comment a.author { + cursor: pointer; +} + +.ui.comments .comment .metadata { + display: inline-block; + margin-left: 0.5em; + font-size: 0.875em; +} + +.ui.comments .comment .metadata > * { + display: inline-block; + margin: 0 0.5em 0 0; +} + +.ui.comments .comment .metadata > :last-child { + margin-right: 0; +} + +.ui.comments .comment .text { + margin: 0.25em 0 0.5em; + font-size: 1em; + word-wrap: break-word; + line-height: 1.3; +} diff --git a/web_src/css/modules/tippy.css b/web_src/css/modules/tippy.css index a026f9c6b633..8919abfec033 100644 --- a/web_src/css/modules/tippy.css +++ b/web_src/css/modules/tippy.css @@ -29,10 +29,6 @@ color: var(--color-tooltip-text); } -.tippy-box[data-theme="menu"] .ui.menu { - border: none; -} - .tippy-content { position: relative; padding: 1rem; diff --git a/web_src/css/organization.css b/web_src/css/organization.css index 1b10ba157e3d..eb2dd8a4d960 100644 --- a/web_src/css/organization.css +++ b/web_src/css/organization.css @@ -170,7 +170,7 @@ .organization.members .list .item .ui.avatar { width: 48px; - height: auto; + height: 48px; margin-right: 1rem; align-self: flex-start; } diff --git a/web_src/css/repository.css b/web_src/css/repository.css index 0d81d6511068..eafe022cee66 100644 --- a/web_src/css/repository.css +++ b/web_src/css/repository.css @@ -691,11 +691,6 @@ margin-right: 5px; } -.repository.view.issue .merge.box .timeline-avatar { - margin-top: 3px; - margin-left: 4px; -} - .repository.view.issue .merge.box .branch-update.grid .row { padding-bottom: 1rem; } @@ -788,23 +783,11 @@ left: -68px; } -.repository.view.issue .comment-list .timeline-item .timeline-avatar img { - width: 40px !important; - height: 40px !important; -} - /* Don't show the mobile oriented avatar ".inline-timeline-avatar" on desktop. Desktop uses the avatar with class ".timeline-avatar" */ .repository.view.issue .comment-list .timeline-item .inline-timeline-avatar { display: none; } -.repository.view.issue .comment-list .timeline-item img.avatar, -.repository.view.issue .comment-list .timeline-item .avatar img { - width: 20px; - height: 20px; - vertical-align: middle; -} - .repository.view.issue .comment-list .timeline-item:first-child:not(.commit) { padding-top: 0 !important; } @@ -1063,12 +1046,12 @@ .repository.view.issue .comment-list .code-comment { border: 1px solid transparent; - padding: 0.25rem 0.5rem; margin: 0; } -.repository.view.issue .comment-list .code-comment .content { - border: none !important; +/* fix fomantic's border-radius via :first-child with hidden elements */ +.collapsible-comment-box:has(.gt-hidden) { + border-radius: var(--border-radius) !important; } .repository.view.issue .comment-list .code-comment .comment-header { @@ -1091,13 +1074,7 @@ } .repository.view.issue .comment-list .comment > .avatar ~ .content { - margin-left: 3em; -} - -.repository.view.issue .comment-code-cloud .comment-list .code-comment img.avatar, -.repository.view.issue .comment-code-cloud .comment-list .comment img.avatar { - width: 28px; - height: 28px; + margin-left: 42px; } .repository.view.issue .comment-list .comment-code-cloud .segment.reactions { @@ -3085,6 +3062,7 @@ td.blob-excerpt { } .sidebar-item-link { + display: inline-flex; align-items: center; word-break: break-all; } @@ -3260,10 +3238,6 @@ td.blob-excerpt { margin-left: 6px; margin-right: 2px; } - .repository.view.issue .comment-list .timeline .inline-timeline-avatar img.avatar { - height: 24px; - width: 24px; - } .repository.view.issue .comment-list .timeline .comment-header { padding-left: 4px; } diff --git a/web_src/css/shared/issuelist.css b/web_src/css/shared/issuelist.css index c214406752cc..db0d4cfbb9ad 100644 --- a/web_src/css/shared/issuelist.css +++ b/web_src/css/shared/issuelist.css @@ -69,8 +69,6 @@ } .issue.list > .item .assignee img { - width: 20px; - height: 20px; margin-right: 2px; } diff --git a/web_src/css/user.css b/web_src/css/user.css index 0a8b49b0387d..a7106599dfe7 100644 --- a/web_src/css/user.css +++ b/web_src/css/user.css @@ -39,16 +39,13 @@ } .user.profile .ui.card #profile-avatar { - background: none; padding: 1rem 1rem 0.25rem; justify-content: center; } .user.profile .ui.card #profile-avatar img { - width: 100%; + max-width: 100%; height: auto; - object-fit: contain; - margin: 0; } @media (max-width: 767px) { @@ -124,10 +121,6 @@ object-fit: contain; } -.user.notification table button { - padding: 3px 3px 3px 5px; -} - #notification_div .tab.segment { overflow-x: auto; } diff --git a/web_src/fomantic/build/semantic.css b/web_src/fomantic/build/semantic.css index f48201b46a81..ef864dadf2fd 100644 --- a/web_src/fomantic/build/semantic.css +++ b/web_src/fomantic/build/semantic.css @@ -4623,1384 +4623,6 @@ /******************************* Site Overrides *******************************/ -/*! - * # Fomantic-UI - Card - * http://github.com/fomantic/Fomantic-UI/ - * - * - * Released under the MIT license - * http://opensource.org/licenses/MIT - * - */ - -/******************************* - Standard -*******************************/ - -/*-------------- - Card ----------------*/ - -.ui.cards > .card, -.ui.card { - max-width: 100%; - position: relative; - display: flex; - flex-direction: column; - width: 290px; - min-height: 0; - background: #FFFFFF; - padding: 0; - border: none; - border-radius: 0.28571429rem; - box-shadow: 0 1px 3px 0 #D4D4D5, 0 0 0 1px #D4D4D5; - transition: box-shadow 0.1s ease, transform 0.1s ease; - z-index: ''; - word-wrap: break-word; -} - -.ui.card { - margin: 1em 0; -} - -.ui.cards > .card a, -.ui.card a { - cursor: pointer; -} - -.ui.card:first-child { - margin-top: 0; -} - -.ui.card:last-child { - margin-bottom: 0; -} - -/*-------------- - Cards ----------------*/ - -.ui.cards { - display: flex; - margin: -0.875em -0.5em; - flex-wrap: wrap; -} - -.ui.cards > .card { - display: flex; - margin: 0.875em 0.5em; - float: none; -} - -/* Clearing */ - -.ui.cards:after, -.ui.card:after { - display: block; - content: ' '; - height: 0; - clear: both; - overflow: hidden; - visibility: hidden; -} - -/* Consecutive Card Groups Preserve Row Spacing */ - -.ui.cards ~ .ui.cards { - margin-top: 0.875em; -} - -/*-------------- - Rounded Edges ----------------*/ - -.ui.cards > .card > :first-child, -.ui.card > :first-child { - border-radius: 0.28571429rem 0.28571429rem 0 0 !important; - border-top: none !important; -} - -.ui.cards > .card > :last-child, -.ui.card > :last-child { - border-radius: 0 0 0.28571429rem 0.28571429rem !important; -} - -.ui.cards > .card > :only-child, -.ui.card > :only-child { - border-radius: 0.28571429rem !important; -} - -/*-------------- - Images ----------------*/ - -.ui.cards > .card > .image, -.ui.card > .image { - position: relative; - display: block; - flex: 0 0 auto; - padding: 0; - background: rgba(0, 0, 0, 0.05); -} - -.ui.cards > .card > .image > img, -.ui.card > .image > img { - display: block; - width: 100%; - height: auto; - border-radius: inherit; -} - -.ui.cards > .card > .image:not(.ui) > img, -.ui.card > .image:not(.ui) > img { - border: none; -} - -/*-------------- - Content ----------------*/ - -.ui.cards > .card > .content, -.ui.card > .content { - flex-grow: 1; - border: none; - border-top: 1px solid rgba(34, 36, 38, 0.1); - background: none; - margin: 0; - padding: 1em 1em; - box-shadow: none; - font-size: 1em; - border-radius: 0; -} - -.ui.cards > .card > .content:after, -.ui.card > .content:after { - display: block; - content: ' '; - height: 0; - clear: both; - overflow: hidden; - visibility: hidden; -} - -.ui.cards > .card > .content > .header, -.ui.card > .content > .header { - display: block; - margin: ''; - font-family: var(--fonts-regular); - color: rgba(0, 0, 0, 0.85); -} - -/* Default Header Size */ - -.ui.cards > .card > .content > .header:not(.ui), -.ui.card > .content > .header:not(.ui) { - font-weight: 500; - font-size: 1.28571429em; - margin-top: -0.21425em; - line-height: 1.28571429em; -} - -.ui.cards > .card > .content > .meta + .description, -.ui.cards > .card > .content > .header + .description, -.ui.card > .content > .meta + .description, -.ui.card > .content > .header + .description { - margin-top: 0.5em; -} - -/*---------------- - Floated Content ------------------*/ - -.ui.cards > .card [class*="left floated"], -.ui.card [class*="left floated"] { - float: left; -} - -.ui.cards > .card [class*="right floated"], -.ui.card [class*="right floated"] { - float: right; -} - -/*-------------- - Aligned ----------------*/ - -.ui.cards > .card [class*="left aligned"], -.ui.card [class*="left aligned"] { - text-align: left; -} - -.ui.cards > .card [class*="center aligned"], -.ui.card [class*="center aligned"] { - text-align: center; -} - -.ui.cards > .card [class*="right aligned"], -.ui.card [class*="right aligned"] { - text-align: right; -} - -/*-------------- - Content Image ----------------*/ - -.ui.cards > .card .content img, -.ui.card .content img { - display: inline-block; - vertical-align: middle; - width: ''; -} - -.ui.cards > .card img.avatar, -.ui.cards > .card .avatar img, -.ui.card img.avatar, -.ui.card .avatar img { - width: 2em; - height: 2em; - border-radius: 500rem; -} - -/*-------------- - Description ----------------*/ - -.ui.cards > .card > .content > .description, -.ui.card > .content > .description { - clear: both; - color: rgba(0, 0, 0, 0.68); -} - -/*-------------- - Paragraph ----------------*/ - -.ui.cards > .card > .content p, -.ui.card > .content p { - margin: 0 0 0.5em; -} - -.ui.cards > .card > .content p:last-child, -.ui.card > .content p:last-child { - margin-bottom: 0; -} - -/*-------------- - Meta ----------------*/ - -.ui.cards > .card .meta, -.ui.card .meta { - font-size: 1em; - color: rgba(0, 0, 0, 0.4); -} - -.ui.cards > .card .meta *, -.ui.card .meta * { - margin-right: 0.3em; -} - -.ui.cards > .card .meta :last-child, -.ui.card .meta :last-child { - margin-right: 0; -} - -.ui.cards > .card .meta [class*="right floated"], -.ui.card .meta [class*="right floated"] { - margin-right: 0; - margin-left: 0.3em; -} - -/*-------------- - Links ----------------*/ - -/* Generic */ - -.ui.cards > .card > .content a:not(.ui), -.ui.card > .content a:not(.ui) { - color: ''; - transition: color 0.1s ease; -} - -.ui.cards > .card > .content a:not(.ui):hover, -.ui.card > .content a:not(.ui):hover { - color: ''; -} - -/* Header */ - -.ui.cards > .card > .content > a.header, -.ui.card > .content > a.header { - color: rgba(0, 0, 0, 0.85); -} - -.ui.cards > .card > .content > a.header:hover, -.ui.card > .content > a.header:hover { - color: #1e70bf; -} - -/* Meta */ - -.ui.cards > .card .meta > a:not(.ui), -.ui.card .meta > a:not(.ui) { - color: rgba(0, 0, 0, 0.4); -} - -.ui.cards > .card .meta > a:not(.ui):hover, -.ui.card .meta > a:not(.ui):hover { - color: rgba(0, 0, 0, 0.87); -} - -/*-------------- - Buttons ----------------*/ - -.ui.cards > .card > .buttons, -.ui.card > .buttons, -.ui.cards > .card > .button, -.ui.card > .button { - margin: 0 -1px; - width: calc(100% + 2px); -} - -.ui.cards > .card > .buttons:last-child, -.ui.card > .buttons:last-child, -.ui.cards > .card > .button:last-child, -.ui.card > .button:last-child { - margin-bottom: -1px; -} - -/*-------------- - Dimmer ----------------*/ - -.ui.cards > .card .dimmer, -.ui.card .dimmer { - background: ''; - z-index: 10; -} - -/*-------------- - Labels ----------------*/ - -/*-----Star----- */ - -/* Icon */ - -.ui.cards > .card > .content .star.icon, -.ui.card > .content .star.icon { - cursor: pointer; - opacity: 0.75; - transition: color 0.1s ease; -} - -.ui.cards > .card > .content .star.icon:hover, -.ui.card > .content .star.icon:hover { - opacity: 1; - color: #FFB70A; -} - -.ui.cards > .card > .content .active.star.icon, -.ui.card > .content .active.star.icon { - color: #FFE623; -} - -/*-----Like----- */ - -/* Icon */ - -.ui.cards > .card > .content .like.icon, -.ui.card > .content .like.icon { - cursor: pointer; - opacity: 0.75; - transition: color 0.1s ease; -} - -.ui.cards > .card > .content .like.icon:hover, -.ui.card > .content .like.icon:hover { - opacity: 1; - color: #FF2733; -} - -.ui.cards > .card > .content .active.like.icon, -.ui.card > .content .active.like.icon { - color: #FF2733; -} - -/*---------------- - Extra Content ------------------*/ - -.ui.cards > .card > .extra, -.ui.card > .extra { - max-width: 100%; - min-height: 0 !important; - flex-grow: 0; - border-top: 1px solid rgba(0, 0, 0, 0.05) !important; - position: static; - background: none; - width: auto; - margin: 0 0; - padding: 0.75em 1em; - top: 0; - left: 0; - color: rgba(0, 0, 0, 0.4); - box-shadow: none; - transition: color 0.1s ease; -} - -.ui.cards > .card > .extra a:not(.ui), -.ui.card > .extra a:not(.ui) { - color: rgba(0, 0, 0, 0.4); -} - -.ui.cards > .card > .extra a:not(.ui):hover, -.ui.card > .extra a:not(.ui):hover { - color: #1e70bf; -} - -/******************************* - Variations -*******************************/ - -/*------------------- - Horizontal - --------------------*/ - -.ui.horizontal.cards > .card, -.ui.card.horizontal { - flex-direction: row; - flex-wrap: wrap; - min-width: 270px; - width: 400px; - max-width: 100%; -} - -.ui.horizontal.cards > .card > .image, -.ui.card.horizontal > .image { - border-radius: 0.28571429rem 0 0 0.28571429rem; - width: 150px; -} - -.ui.horizontal.cards > .card > .image > img, -.ui.card.horizontal > .image > img { - background-size: cover; - background-repeat: no-repeat; - background-position: center; - justify-content: center; - align-items: center; - display: flex; - width: 100%; - height: 100%; - border-radius: 0.28571429rem 0 0 0.28571429rem; -} - -.ui.horizontal.cards > .card > .image:last-child > img, -.ui.card.horizontal > .image:last-child > img { - border-radius: 0 0.28571429rem 0.28571429rem 0; -} - -.ui.horizontal.cards > .card > .content, -.ui.horizontal.card > .content { - flex-basis: 1px; -} - -.ui.horizontal.cards > .card > .extra, -.ui.horizontal.card > .extra { - flex-basis: 100%; -} - -/*------------------- - Raised - --------------------*/ - -.ui.raised.cards > .card, -.ui.raised.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15); -} - -.ui.raised.cards a.card:hover, -.ui.link.cards .raised.card:hover, -a.ui.raised.card:hover, -.ui.link.raised.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 4px 0 rgba(34, 36, 38, 0.15), 0 2px 10px 0 rgba(34, 36, 38, 0.25); -} - -/*------------------- - Centered - --------------------*/ - -.ui.centered.cards { - justify-content: center; -} - -.ui.centered.card { - margin-left: auto; - margin-right: auto; -} - -/*------------------- - Fluid - --------------------*/ - -.ui.fluid.card { - width: 100%; - max-width: 9999px; -} - -/*------------------- - Link - --------------------*/ - -.ui.cards a.card, -.ui.link.cards .card, -a.ui.card, -.ui.link.card { - transform: none; -} - -.ui.cards a.card:hover, -.ui.link.cards .card:not(.icon):hover, -a.ui.card:hover, -.ui.link.card:hover { - cursor: pointer; - z-index: 5; - background: #FFFFFF; - border: none; - box-shadow: 0 1px 3px 0 #BCBDBD, 0 0 0 1px #D4D4D5; - transform: translateY(-3px); -} - -/*------------------- - Colors ---------------------*/ - -.ui.primary.cards > .card, -.ui.cards > .primary.card, -.ui.primary.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #2185D0, 0 1px 3px 0 #D4D4D5; -} - -.ui.primary.cards > .card:hover, -.ui.cards > .primary.card:hover, -.ui.primary.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #1678c2, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.primary.cards > .card, -.ui.inverted.cards > .primary.card, -.ui.inverted.primary.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #54C8FF, 0 0 0 1px #555555; -} - -.ui.inverted.primary.cards > .card:hover, -.ui.inverted.cards > .primary.card:hover, -.ui.inverted.primary.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #21b8ff, 0 0 0 1px #555555; -} - -.ui.secondary.cards > .card, -.ui.cards > .secondary.card, -.ui.secondary.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #1B1C1D, 0 1px 3px 0 #D4D4D5; -} - -.ui.secondary.cards > .card:hover, -.ui.cards > .secondary.card:hover, -.ui.secondary.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #27292a, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.secondary.cards > .card, -.ui.inverted.cards > .secondary.card, -.ui.inverted.secondary.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #545454, 0 0 0 1px #555555; -} - -.ui.inverted.secondary.cards > .card:hover, -.ui.inverted.cards > .secondary.card:hover, -.ui.inverted.secondary.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #6e6e6e, 0 0 0 1px #555555; -} - -.ui.red.cards > .card, -.ui.cards > .red.card, -.ui.red.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #DB2828, 0 1px 3px 0 #D4D4D5; -} - -.ui.red.cards > .card:hover, -.ui.cards > .red.card:hover, -.ui.red.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #d01919, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.red.cards > .card, -.ui.inverted.cards > .red.card, -.ui.inverted.red.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #FF695E, 0 0 0 1px #555555; -} - -.ui.inverted.red.cards > .card:hover, -.ui.inverted.cards > .red.card:hover, -.ui.inverted.red.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #ff392b, 0 0 0 1px #555555; -} - -.ui.orange.cards > .card, -.ui.cards > .orange.card, -.ui.orange.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #F2711C, 0 1px 3px 0 #D4D4D5; -} - -.ui.orange.cards > .card:hover, -.ui.cards > .orange.card:hover, -.ui.orange.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #f26202, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.orange.cards > .card, -.ui.inverted.cards > .orange.card, -.ui.inverted.orange.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #FF851B, 0 0 0 1px #555555; -} - -.ui.inverted.orange.cards > .card:hover, -.ui.inverted.cards > .orange.card:hover, -.ui.inverted.orange.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #e76b00, 0 0 0 1px #555555; -} - -.ui.yellow.cards > .card, -.ui.cards > .yellow.card, -.ui.yellow.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #FBBD08, 0 1px 3px 0 #D4D4D5; -} - -.ui.yellow.cards > .card:hover, -.ui.cards > .yellow.card:hover, -.ui.yellow.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #eaae00, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.yellow.cards > .card, -.ui.inverted.cards > .yellow.card, -.ui.inverted.yellow.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #FFE21F, 0 0 0 1px #555555; -} - -.ui.inverted.yellow.cards > .card:hover, -.ui.inverted.cards > .yellow.card:hover, -.ui.inverted.yellow.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #ebcd00, 0 0 0 1px #555555; -} - -.ui.olive.cards > .card, -.ui.cards > .olive.card, -.ui.olive.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #B5CC18, 0 1px 3px 0 #D4D4D5; -} - -.ui.olive.cards > .card:hover, -.ui.cards > .olive.card:hover, -.ui.olive.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #a7bd0d, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.olive.cards > .card, -.ui.inverted.cards > .olive.card, -.ui.inverted.olive.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #D9E778, 0 0 0 1px #555555; -} - -.ui.inverted.olive.cards > .card:hover, -.ui.inverted.cards > .olive.card:hover, -.ui.inverted.olive.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #d2e745, 0 0 0 1px #555555; -} - -.ui.green.cards > .card, -.ui.cards > .green.card, -.ui.green.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #21BA45, 0 1px 3px 0 #D4D4D5; -} - -.ui.green.cards > .card:hover, -.ui.cards > .green.card:hover, -.ui.green.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #16ab39, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.green.cards > .card, -.ui.inverted.cards > .green.card, -.ui.inverted.green.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #2ECC40, 0 0 0 1px #555555; -} - -.ui.inverted.green.cards > .card:hover, -.ui.inverted.cards > .green.card:hover, -.ui.inverted.green.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #1ea92e, 0 0 0 1px #555555; -} - -.ui.teal.cards > .card, -.ui.cards > .teal.card, -.ui.teal.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #00B5AD, 0 1px 3px 0 #D4D4D5; -} - -.ui.teal.cards > .card:hover, -.ui.cards > .teal.card:hover, -.ui.teal.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #009c95, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.teal.cards > .card, -.ui.inverted.cards > .teal.card, -.ui.inverted.teal.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #6DFFFF, 0 0 0 1px #555555; -} - -.ui.inverted.teal.cards > .card:hover, -.ui.inverted.cards > .teal.card:hover, -.ui.inverted.teal.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #3affff, 0 0 0 1px #555555; -} - -.ui.blue.cards > .card, -.ui.cards > .blue.card, -.ui.blue.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #2185D0, 0 1px 3px 0 #D4D4D5; -} - -.ui.blue.cards > .card:hover, -.ui.cards > .blue.card:hover, -.ui.blue.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #1678c2, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.blue.cards > .card, -.ui.inverted.cards > .blue.card, -.ui.inverted.blue.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #54C8FF, 0 0 0 1px #555555; -} - -.ui.inverted.blue.cards > .card:hover, -.ui.inverted.cards > .blue.card:hover, -.ui.inverted.blue.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #21b8ff, 0 0 0 1px #555555; -} - -.ui.violet.cards > .card, -.ui.cards > .violet.card, -.ui.violet.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #6435C9, 0 1px 3px 0 #D4D4D5; -} - -.ui.violet.cards > .card:hover, -.ui.cards > .violet.card:hover, -.ui.violet.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #5829bb, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.violet.cards > .card, -.ui.inverted.cards > .violet.card, -.ui.inverted.violet.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #A291FB, 0 0 0 1px #555555; -} - -.ui.inverted.violet.cards > .card:hover, -.ui.inverted.cards > .violet.card:hover, -.ui.inverted.violet.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #745aff, 0 0 0 1px #555555; -} - -.ui.purple.cards > .card, -.ui.cards > .purple.card, -.ui.purple.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #A333C8, 0 1px 3px 0 #D4D4D5; -} - -.ui.purple.cards > .card:hover, -.ui.cards > .purple.card:hover, -.ui.purple.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #9627ba, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.purple.cards > .card, -.ui.inverted.cards > .purple.card, -.ui.inverted.purple.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #DC73FF, 0 0 0 1px #555555; -} - -.ui.inverted.purple.cards > .card:hover, -.ui.inverted.cards > .purple.card:hover, -.ui.inverted.purple.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #cf40ff, 0 0 0 1px #555555; -} - -.ui.pink.cards > .card, -.ui.cards > .pink.card, -.ui.pink.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #E03997, 0 1px 3px 0 #D4D4D5; -} - -.ui.pink.cards > .card:hover, -.ui.cards > .pink.card:hover, -.ui.pink.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #e61a8d, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.pink.cards > .card, -.ui.inverted.cards > .pink.card, -.ui.inverted.pink.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #FF8EDF, 0 0 0 1px #555555; -} - -.ui.inverted.pink.cards > .card:hover, -.ui.inverted.cards > .pink.card:hover, -.ui.inverted.pink.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #ff5bd1, 0 0 0 1px #555555; -} - -.ui.brown.cards > .card, -.ui.cards > .brown.card, -.ui.brown.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #A5673F, 0 1px 3px 0 #D4D4D5; -} - -.ui.brown.cards > .card:hover, -.ui.cards > .brown.card:hover, -.ui.brown.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #975b33, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.brown.cards > .card, -.ui.inverted.cards > .brown.card, -.ui.inverted.brown.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #D67C1C, 0 0 0 1px #555555; -} - -.ui.inverted.brown.cards > .card:hover, -.ui.inverted.cards > .brown.card:hover, -.ui.inverted.brown.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #b0620f, 0 0 0 1px #555555; -} - -.ui.grey.cards > .card, -.ui.cards > .grey.card, -.ui.grey.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #767676, 0 1px 3px 0 #D4D4D5; -} - -.ui.grey.cards > .card:hover, -.ui.cards > .grey.card:hover, -.ui.grey.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #838383, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.grey.cards > .card, -.ui.inverted.cards > .grey.card, -.ui.inverted.grey.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #DCDDDE, 0 0 0 1px #555555; -} - -.ui.inverted.grey.cards > .card:hover, -.ui.inverted.cards > .grey.card:hover, -.ui.inverted.grey.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #c2c4c5, 0 0 0 1px #555555; -} - -.ui.black.cards > .card, -.ui.cards > .black.card, -.ui.black.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #1B1C1D, 0 1px 3px 0 #D4D4D5; -} - -.ui.black.cards > .card:hover, -.ui.cards > .black.card:hover, -.ui.black.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #27292a, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.black.cards > .card, -.ui.inverted.cards > .black.card, -.ui.inverted.black.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #545454, 0 0 0 1px #555555; -} - -.ui.inverted.black.cards > .card:hover, -.ui.inverted.cards > .black.card:hover, -.ui.inverted.black.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #000000, 0 0 0 1px #555555; -} - -/*-------------- - Card Count ----------------*/ - -.ui.one.cards { - margin-left: 0; - margin-right: 0; -} - -.ui.one.cards > .card { - width: 100%; -} - -.ui.two.cards { - margin-left: -1em; - margin-right: -1em; -} - -.ui.two.cards > .card { - width: calc(50% - 2em); - margin-left: 1em; - margin-right: 1em; -} - -.ui.three.cards { - margin-left: -1em; - margin-right: -1em; -} - -.ui.three.cards > .card { - width: calc(33.33333333333333% - 2em); - margin-left: 1em; - margin-right: 1em; -} - -.ui.four.cards { - margin-left: -0.75em; - margin-right: -0.75em; -} - -.ui.four.cards > .card { - width: calc(25% - 1.5em); - margin-left: 0.75em; - margin-right: 0.75em; -} - -.ui.five.cards { - margin-left: -0.75em; - margin-right: -0.75em; -} - -.ui.five.cards > .card { - width: calc(20% - 1.5em); - margin-left: 0.75em; - margin-right: 0.75em; -} - -.ui.six.cards { - margin-left: -0.75em; - margin-right: -0.75em; -} - -.ui.six.cards > .card { - width: calc(16.666666666666664% - 1.5em); - margin-left: 0.75em; - margin-right: 0.75em; -} - -.ui.seven.cards { - margin-left: -0.5em; - margin-right: -0.5em; -} - -.ui.seven.cards > .card { - width: calc(14.285714285714285% - 1em); - margin-left: 0.5em; - margin-right: 0.5em; -} - -.ui.eight.cards { - margin-left: -0.5em; - margin-right: -0.5em; -} - -.ui.eight.cards > .card { - width: calc(12.5% - 1em); - margin-left: 0.5em; - margin-right: 0.5em; - font-size: 11px; -} - -.ui.nine.cards { - margin-left: -0.5em; - margin-right: -0.5em; -} - -.ui.nine.cards > .card { - width: calc(11.11111111111111% - 1em); - margin-left: 0.5em; - margin-right: 0.5em; - font-size: 10px; -} - -.ui.ten.cards { - margin-left: -0.5em; - margin-right: -0.5em; -} - -.ui.ten.cards > .card { - width: calc(10% - 1em); - margin-left: 0.5em; - margin-right: 0.5em; -} - -/*------------------- - Doubling - --------------------*/ - -/* Mobile Only */ - -@media only screen and (max-width: 767.98px) { - .ui.two.doubling.cards { - margin-left: 0; - margin-right: 0; - } - - .ui.two.doubling.cards > .card { - width: 100%; - margin-left: 0; - margin-right: 0; - } - - .ui.three.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.three.doubling.cards > .card { - width: calc(50% - 2em); - margin-left: 1em; - margin-right: 1em; - } - - .ui.four.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.four.doubling.cards > .card { - width: calc(50% - 2em); - margin-left: 1em; - margin-right: 1em; - } - - .ui.five.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.five.doubling.cards > .card { - width: calc(50% - 2em); - margin-left: 1em; - margin-right: 1em; - } - - .ui.six.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.six.doubling.cards > .card { - width: calc(50% - 2em); - margin-left: 1em; - margin-right: 1em; - } - - .ui.seven.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.seven.doubling.cards > .card { - width: calc(33.33333333333333% - 2em); - margin-left: 1em; - margin-right: 1em; - } - - .ui.eight.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.eight.doubling.cards > .card { - width: calc(33.33333333333333% - 2em); - margin-left: 1em; - margin-right: 1em; - } - - .ui.nine.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.nine.doubling.cards > .card { - width: calc(33.33333333333333% - 2em); - margin-left: 1em; - margin-right: 1em; - } - - .ui.ten.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.ten.doubling.cards > .card { - width: calc(33.33333333333333% - 2em); - margin-left: 1em; - margin-right: 1em; - } -} - -/* Tablet Only */ - -@media only screen and (min-width: 768px) and (max-width: 991.98px) { - .ui.two.doubling.cards { - margin-left: 0; - margin-right: 0; - } - - .ui.two.doubling.cards > .card { - width: 100%; - margin-left: 0; - margin-right: 0; - } - - .ui.three.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.three.doubling.cards > .card { - width: calc(50% - 2em); - margin-left: 1em; - margin-right: 1em; - } - - .ui.four.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.four.doubling.cards > .card { - width: calc(50% - 2em); - margin-left: 1em; - margin-right: 1em; - } - - .ui.five.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.five.doubling.cards > .card { - width: calc(33.33333333333333% - 2em); - margin-left: 1em; - margin-right: 1em; - } - - .ui.six.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.six.doubling.cards > .card { - width: calc(33.33333333333333% - 2em); - margin-left: 1em; - margin-right: 1em; - } - - .ui.eight.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.eight.doubling.cards > .card { - width: calc(33.33333333333333% - 2em); - margin-left: 1em; - margin-right: 1em; - } - - .ui.eight.doubling.cards { - margin-left: -0.75em; - margin-right: -0.75em; - } - - .ui.eight.doubling.cards > .card { - width: calc(25% - 1.5em); - margin-left: 0.75em; - margin-right: 0.75em; - } - - .ui.nine.doubling.cards { - margin-left: -0.75em; - margin-right: -0.75em; - } - - .ui.nine.doubling.cards > .card { - width: calc(25% - 1.5em); - margin-left: 0.75em; - margin-right: 0.75em; - } - - .ui.ten.doubling.cards { - margin-left: -0.75em; - margin-right: -0.75em; - } - - .ui.ten.doubling.cards > .card { - width: calc(20% - 1.5em); - margin-left: 0.75em; - margin-right: 0.75em; - } -} - -/*------------------- - Stackable - --------------------*/ - -@media only screen and (max-width: 767.98px) { - .ui.stackable.cards { - display: block !important; - } - - .ui.stackable.cards .card:first-child { - margin-top: 0 !important; - } - - .ui.stackable.cards > .card { - display: block !important; - height: auto !important; - margin: 1em 1em; - padding: 0 !important; - width: calc(100% - 2em) !important; - } -} - -/*-------------- - Size ----------------*/ - -.ui.cards > .card { - font-size: 1em; -} - -.ui.mini.cards .card { - font-size: 0.78571429rem; -} - -.ui.tiny.cards .card { - font-size: 0.85714286rem; -} - -.ui.small.cards .card { - font-size: 0.92857143rem; -} - -.ui.large.cards .card { - font-size: 1.14285714rem; -} - -.ui.big.cards .card { - font-size: 1.28571429rem; -} - -.ui.huge.cards .card { - font-size: 1.42857143rem; -} - -.ui.massive.cards .card { - font-size: 1.71428571rem; -} - -/*----------------- - Inverted - ------------------*/ - -.ui.inverted.cards > .card, -.ui.inverted.card { - background: #1B1C1D; - box-shadow: 0 1px 3px 0 #555555, 0 0 0 1px #555555; -} - -/* Content */ - -.ui.inverted.cards > .card > .content, -.ui.inverted.card > .content { - border-top: 1px solid rgba(255, 255, 255, 0.15); -} - -/* Header */ - -.ui.inverted.cards > .card > .content > .header, -.ui.inverted.card > .content > .header { - color: rgba(255, 255, 255, 0.9); -} - -/* Description */ - -.ui.inverted.cards > .card > .content > .description, -.ui.inverted.card > .content > .description { - color: rgba(255, 255, 255, 0.8); -} - -/* Meta */ - -.ui.inverted.cards > .card .meta, -.ui.inverted.card .meta { - color: rgba(255, 255, 255, 0.7); -} - -.ui.inverted.cards > .card .meta > a:not(.ui), -.ui.inverted.card .meta > a:not(.ui) { - color: rgba(255, 255, 255, 0.7); -} - -.ui.inverted.cards > .card .meta > a:not(.ui):hover, -.ui.inverted.card .meta > a:not(.ui):hover { - color: #ffffff; -} - -/* Extra */ - -.ui.inverted.cards > .card > .extra, -.ui.inverted.card > .extra { - border-top: 1px solid rgba(255, 255, 255, 0.15) !important; - color: rgba(255, 255, 255, 0.7); -} - -.ui.inverted.cards > .card > .extra a:not(.ui), -.ui.inverted.card > .extra a:not(.ui) { - color: rgba(255, 255, 255, 0.5); -} - -.ui.inverted.cards > .card > .extra a:not(.ui):hover, -.ui.inverted.card > .extra a:not(.ui):hover { - color: #1e70bf; -} - -/* Link card(s) */ - -.ui.inverted.cards a.card:hover, -.ui.inverted.link.cards .card:not(.icon):hover, -a.inverted.ui.card:hover, -.ui.inverted.link.card:hover { - background: #1B1C1D; -} - -/******************************* - Theme Overrides -*******************************/ - -/******************************* - User Variable Overrides -*******************************/ /*! * # Fomantic-UI - Checkbox * http://github.com/fomantic/Fomantic-UI/ @@ -6800,317 +5422,6 @@ a.inverted.ui.card:hover, /******************************* Site Overrides *******************************/ -/*! - * # Fomantic-UI - Comment - * http://github.com/fomantic/Fomantic-UI/ - * - * - * Released under the MIT license - * http://opensource.org/licenses/MIT - * - */ - -/******************************* - Standard -*******************************/ - -/*-------------- - Comments ----------------*/ - -.ui.comments { - margin: 1.5em 0; - max-width: 650px; -} - -.ui.comments:first-child { - margin-top: 0; -} - -.ui.comments:last-child { - margin-bottom: 0; -} - -/*-------------- - Comment ----------------*/ - -.ui.comments .comment { - position: relative; - background: none; - margin: 0.5em 0 0; - padding: 0.5em 0 0; - border: none; - border-top: none; - line-height: 1.2; -} - -.ui.comments .comment:first-child { - margin-top: 0; - padding-top: 0; -} - -/*-------------------- - Nested Comments ----------------------*/ - -.ui.comments .comment > .comments { - margin: 0 0 0.5em 0.5em; - padding: 1em 0 1em 1em; -} - -.ui.comments .comment > .comments:before { - position: absolute; - top: 0; - left: 0; -} - -.ui.comments .comment > .comments .comment { - border: none; - border-top: none; - background: none; -} - -/*-------------- - Avatar ----------------*/ - -.ui.comments .comment .avatar { - display: block; - width: 2.5em; - height: auto; - float: left; - margin: 0.2em 0 0; -} - -.ui.comments .comment img.avatar, -.ui.comments .comment .avatar img { - display: block; - margin: 0 auto; - width: 100%; - height: 100%; - border-radius: 0.25rem; -} - -/*-------------- - Content ----------------*/ - -.ui.comments .comment > .content { - display: block; -} - -/* If there is an avatar move content over */ - -.ui.comments .comment > .avatar ~ .content { - margin-left: 3.5em; -} - -/*-------------- - Author ----------------*/ - -.ui.comments .comment .author { - font-size: 1em; - color: rgba(0, 0, 0, 0.87); - font-weight: 500; -} - -.ui.comments .comment a.author { - cursor: pointer; -} - -.ui.comments .comment a.author:hover { - color: #1e70bf; -} - -/*-------------- - Metadata ----------------*/ - -.ui.comments .comment .metadata { - display: inline-block; - margin-left: 0.5em; - color: rgba(0, 0, 0, 0.4); - font-size: 0.875em; -} - -.ui.comments .comment .metadata > * { - display: inline-block; - margin: 0 0.5em 0 0; -} - -.ui.comments .comment .metadata > :last-child { - margin-right: 0; -} - -/*-------------------- - Comment Text ----------------------*/ - -.ui.comments .comment .text { - margin: 0.25em 0 0.5em; - font-size: 1em; - word-wrap: break-word; - color: rgba(0, 0, 0, 0.87); - line-height: 1.3; -} - -/*-------------------- - User Actions ----------------------*/ - -.ui.comments .comment .actions { - font-size: 0.875em; -} - -.ui.comments .comment .actions a { - cursor: pointer; - display: inline-block; - margin: 0 0.75em 0 0; - color: rgba(0, 0, 0, 0.4); -} - -.ui.comments .comment .actions a:last-child { - margin-right: 0; -} - -.ui.comments .comment .actions a.active, -.ui.comments .comment .actions a:hover { - color: rgba(0, 0, 0, 0.8); -} - -/*-------------------- - Reply Form ----------------------*/ - -.ui.comments > .reply.form { - margin-top: 1em; -} - -.ui.comments .comment .reply.form { - width: 100%; - margin-top: 1em; -} - -.ui.comments .reply.form textarea { - font-size: 1em; - height: 12em; -} - -/******************************* - State -*******************************/ - -.ui.collapsed.comments, -.ui.comments .collapsed.comments, -.ui.comments .collapsed.comment { - display: none; -} - -/******************************* - Variations -*******************************/ - -/*-------------------- - Threaded - ---------------------*/ - -.ui.threaded.comments .comment > .comments { - margin: -1.5em 0 -1em 1.25em; - padding: 3em 0 2em 2.25em; - box-shadow: -1px 0 0 rgba(34, 36, 38, 0.15); -} - -/*-------------------- - Minimal - ---------------------*/ - -.ui.minimal.comments .comment .actions { - opacity: 0; - position: absolute; - top: 0; - right: 0; - left: auto; - transition: opacity 0.2s ease; - transition-delay: 0.1s; -} - -.ui.minimal.comments .comment > .content:hover > .actions { - opacity: 1; -} - -/*------------------- - Sizes ---------------------*/ - -.ui.comments { - font-size: 1rem; -} - -.ui.mini.comments { - font-size: 0.78571429rem; -} - -.ui.tiny.comments { - font-size: 0.85714286rem; -} - -.ui.small.comments { - font-size: 0.92857143rem; -} - -.ui.large.comments { - font-size: 1.14285714rem; -} - -.ui.big.comments { - font-size: 1.28571429rem; -} - -.ui.huge.comments { - font-size: 1.42857143rem; -} - -.ui.massive.comments { - font-size: 1.71428571rem; -} - -/*------------------- - Inverted - --------------------*/ - -.ui.inverted.comments .comment { - background-color: #1B1C1D; -} - -.ui.inverted.comments .comment .author, -.ui.inverted.comments .comment .text { - color: rgba(255, 255, 255, 0.9); -} - -.ui.inverted.comments .comment .metadata, -.ui.inverted.comments .comment .actions a { - color: rgba(255, 255, 255, 0.7); -} - -.ui.inverted.comments .comment a.author:hover, -.ui.inverted.comments .comment .actions a.active, -.ui.inverted.comments .comment .actions a:hover { - color: #ffffff; -} - -.ui.inverted.threaded.comments .comment > .comments { - box-shadow: -1px 0 0 #555555; -} - -/******************************* - Theme Overrides -*******************************/ - -/******************************* - User Variable Overrides -*******************************/ /*! * # Fomantic-UI - Container * http://github.com/fomantic/Fomantic-UI/ diff --git a/web_src/fomantic/semantic.json b/web_src/fomantic/semantic.json index 738f53d2976e..a2f8e3872708 100644 --- a/web_src/fomantic/semantic.json +++ b/web_src/fomantic/semantic.json @@ -23,9 +23,7 @@ "components": [ "api", "button", - "card", "checkbox", - "comment", "container", "dimmer", "divider", diff --git a/web_src/js/components/ActionRunStatus.vue b/web_src/js/components/ActionRunStatus.vue index 885293189196..0786cb60a9a4 100644 --- a/web_src/js/components/ActionRunStatus.vue +++ b/web_src/js/components/ActionRunStatus.vue @@ -2,7 +2,7 @@ Please also update the template file above if this vue is modified. -->