diff --git a/.github/workflows/cron-lock.yml b/.github/workflows/cron-lock.yml index d4172687d5..665313135b 100644 --- a/.github/workflows/cron-lock.yml +++ b/.github/workflows/cron-lock.yml @@ -20,8 +20,4 @@ jobs: - uses: dessant/lock-threads@v5 with: issue-inactive-days: 10 - issue-comment: | - Automatically locked because of our [CONTRIBUTING guidelines](https://github.com/go-gitea/gitea/blob/main/CONTRIBUTING.md#issue-locking) pr-inactive-days: 7 - pr-comment: | - Automatically locked because of our [CONTRIBUTING guidelines](https://github.com/go-gitea/gitea/blob/main/CONTRIBUTING.md#issue-locking) diff --git a/.github/workflows/pull-compliance.yml b/.github/workflows/pull-compliance.yml index 391137f015..02a265b1ff 100644 --- a/.github/workflows/pull-compliance.yml +++ b/.github/workflows/pull-compliance.yml @@ -32,9 +32,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: "3.12" - run: pip install poetry - run: make deps-py - run: make lint-templates @@ -45,9 +45,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: "3.12" - run: pip install poetry - run: make deps-py - run: make lint-yaml diff --git a/.gitignore b/.gitignore index 814d910315..8f2544866a 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,7 @@ _test # MS VSCode .vscode -__debug_bin +__debug_bin* *.cgo1.go *.cgo2.c diff --git a/.stylelintrc.yaml b/.stylelintrc.yaml index 7dd0a566f2..c7725159f1 100644 --- a/.stylelintrc.yaml +++ b/.stylelintrc.yaml @@ -64,6 +64,7 @@ rules: "@stylistic/media-query-list-comma-newline-before": null "@stylistic/media-query-list-comma-space-after": null "@stylistic/media-query-list-comma-space-before": null + "@stylistic/named-grid-areas-alignment": null "@stylistic/no-empty-first-line": null "@stylistic/no-eol-whitespace": true "@stylistic/no-extra-semicolons": true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dc90c6905b..5d20bc2589 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -464,7 +464,7 @@ We assume in good faith that the information you provide is legally binding. We adopted a release schedule to streamline the process of working on, finishing, and issuing releases. \ The overall goal is to make a major release every three or four months, which breaks down into two or three months of general development followed by one month of testing and polishing known as the release freeze. \ All the feature pull requests should be -merged before feature freeze. And, during the frozen period, a corresponding +merged before feature freeze. All feature pull requests haven't been merged before this feature freeze will be moved to next milestone, please notice our feature freeze announcement on discord. And, during the frozen period, a corresponding release branch is open for fixes backported from main branch. Release candidates are made during this period for user testing to obtain a final version that is maintained in this branch. diff --git a/Makefile b/Makefile index 0e9e792053..9bbc56451b 100644 --- a/Makefile +++ b/Makefile @@ -908,6 +908,7 @@ fomantic: cd $(FOMANTIC_WORK_DIR) && npm install --no-save cp -f $(FOMANTIC_WORK_DIR)/theme.config.less $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/src/theme.config cp -rf $(FOMANTIC_WORK_DIR)/_site $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/src/ + $(SED_INPLACE) -e 's/ overrideBrowserslist\r/ overrideBrowserslist: ["defaults"]\r/g' $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/tasks/config/tasks.js cd $(FOMANTIC_WORK_DIR) && npx gulp -f node_modules/fomantic-ui/gulpfile.js build # fomantic uses "touchstart" as click event for some browsers, it's not ideal, so we force fomantic to always use "click" as click event $(SED_INPLACE) -e 's/clickEvent[ \t]*=/clickEvent = "click", unstableClickEvent =/g' $(FOMANTIC_WORK_DIR)/build/semantic.js diff --git a/cmd/admin_regenerate.go b/cmd/admin_regenerate.go index 0db505ff9c..ab769f6d0c 100644 --- a/cmd/admin_regenerate.go +++ b/cmd/admin_regenerate.go @@ -4,8 +4,8 @@ package cmd import ( - asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/modules/graceful" + asymkey_service "code.gitea.io/gitea/services/asymkey" repo_service "code.gitea.io/gitea/services/repository" "github.com/urfave/cli/v2" @@ -42,5 +42,5 @@ func runRegenerateKeys(_ *cli.Context) error { if err := initDB(ctx); err != nil { return err } - return asymkey_model.RewriteAllPublicKeys(ctx) + return asymkey_service.RewriteAllPublicKeys(ctx) } diff --git a/contrib/systemd/gitea.service b/contrib/systemd/gitea.service index d205c6ee8b..c091722a74 100644 --- a/contrib/systemd/gitea.service +++ b/contrib/systemd/gitea.service @@ -1,6 +1,5 @@ [Unit] Description=Gitea (Git with a cup of tea) -After=syslog.target After=network.target ### # Don't forget to add the database service dependencies diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 423feb0cac..08c3fcff56 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -956,6 +956,12 @@ LEVEL = Info ;GO_GET_CLONE_URL_PROTOCOL = https ;; ;; Close issues as long as a commit on any branch marks it as fixed +;DEFAULT_CLOSE_ISSUES_VIA_COMMITS_IN_ANY_BRANCH = false +;; +;; Allow users to push local repositories to Gitea and have them automatically created for a user or an org +;ENABLE_PUSH_CREATE_USER = false +;ENABLE_PUSH_CREATE_ORG = false +;; ;; 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 = ;; @@ -1483,8 +1489,10 @@ LEVEL = Info ;; ;; Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled ;DEFAULT_EMAIL_NOTIFICATIONS = enabled -;; Disabled features for users, could be "deletion", more features can be disabled in future +;; Disabled features for users, could be "deletion", "manage_ssh_keys","manage_gpg_keys" more features can be disabled in future ;; - deletion: a user cannot delete their own account +;; - manage_ssh_keys: a user cannot configure ssh keys +;; - manage_gpg_keys: a user cannot configure gpg keys ;USER_DISABLED_FEATURES = ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md index 81509d7d4d..241b73ec50 100644 --- a/docs/content/administration/config-cheat-sheet.en-us.md +++ b/docs/content/administration/config-cheat-sheet.en-us.md @@ -521,8 +521,10 @@ And the following unique queues: - `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**: Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled - `DISABLE_REGULAR_ORG_CREATION`: **false**: Disallow regular (non-admin) users from creating organizations. -- `USER_DISABLED_FEATURES`: **_empty_** Disabled features for users, could be `deletion` and more features can be added in future. +- `USER_DISABLED_FEATURES`: **_empty_** Disabled features for users, could be `deletion`, `manage_ssh_keys`, `manage_gpg_keys` and more features can be added in future. - `deletion`: User cannot delete their own account. + - `manage_ssh_keys`: User cannot configure ssh keys. + - `manage_gpg_keys`: User cannot configure gpg keys. ## Security (`security`) diff --git a/docs/content/administration/config-cheat-sheet.zh-cn.md b/docs/content/administration/config-cheat-sheet.zh-cn.md index 5fe0a62215..7b102eda8e 100644 --- a/docs/content/administration/config-cheat-sheet.zh-cn.md +++ b/docs/content/administration/config-cheat-sheet.zh-cn.md @@ -497,8 +497,10 @@ Gitea 创建以下非唯一队列: - `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**:用户电子邮件通知的默认配置(用户可配置)。选项:enabled、onmention、disabled - `DISABLE_REGULAR_ORG_CREATION`: **false**:禁止普通(非管理员)用户创建组织。 -- `USER_DISABLED_FEATURES`:**_empty_** 禁用的用户特性,当前允许为空或者 `deletion`, 未来可以增加更多设置。 +- `USER_DISABLED_FEATURES`:**_empty_** 禁用的用户特性,当前允许为空或者 `deletion`,`manage_ssh_keys`, `manage_gpg_keys` 未来可以增加更多设置。 - `deletion`: 用户不能通过界面或者API删除他自己。 + - `manage_ssh_keys`: 用户不能通过界面或者API配置SSH Keys。 + - `manage_gpg_keys`: 用户不能配置 GPG 密钥。 ## 安全性 (`security`) diff --git a/docs/content/administration/mail-templates.en-us.md b/docs/content/administration/mail-templates.en-us.md index b642ff4aa7..4026b89975 100644 --- a/docs/content/administration/mail-templates.en-us.md +++ b/docs/content/administration/mail-templates.en-us.md @@ -224,7 +224,7 @@ Please check [Gitea's logs](administration/logging-config.md) for error messages {{if not (eq .Body "")}}

Message content


- {{.Body | Str2html}} + {{.Body}} {{end}}


@@ -259,20 +259,20 @@ This template produces something along these lines: The template system contains several functions that can be used to further process and format the messages. Here's a list of some of them: -| Name | Parameters | Available | Usage | -| ---------------- | ----------- | --------- | --------------------------------------------------------------------------- | -| `AppUrl` | - | Any | Gitea's URL | -| `AppName` | - | Any | Set from `app.ini`, usually "Gitea" | -| `AppDomain` | - | Any | Gitea's host name | -| `EllipsisString` | string, int | Any | Truncates a string to the specified length; adds ellipsis as needed | -| `Str2html` | string | Body only | Sanitizes text by removing any HTML tags from it. | -| `SafeHTML` | string | Body only | Takes the input as HTML; can be used for `.ReviewComments.RenderedContent`. | +| Name | Parameters | Available | Usage | +| ---------------- | ----------- | --------- | ------------------------------------------------------------------- | +| `AppUrl` | - | Any | Gitea's URL | +| `AppName` | - | Any | Set from `app.ini`, usually "Gitea" | +| `AppDomain` | - | Any | Gitea's host name | +| `EllipsisString` | string, int | Any | Truncates a string to the specified length; adds ellipsis as needed | +| `SanitizeHTML` | string | Body only | Sanitizes text by removing any dangerous HTML tags from it | +| `SafeHTML` | string | Body only | Takes the input as HTML, can be used for outputing raw HTML content | These are _functions_, not metadata, so they have to be used: ```html -Like this: {{Str2html "Escapetext"}} -Or this: {{"Escapetext" | Str2html}} +Like this: {{SanitizeHTML "Escapetext"}} +Or this: {{"Escapetext" | SanitizeHTML}} Or this: {{AppUrl}} But not like this: {{.AppUrl}} ``` diff --git a/docs/content/administration/mail-templates.zh-cn.md b/docs/content/administration/mail-templates.zh-cn.md index fd455ef3a8..3c7c2a9397 100644 --- a/docs/content/administration/mail-templates.zh-cn.md +++ b/docs/content/administration/mail-templates.zh-cn.md @@ -207,7 +207,7 @@ _主题_ 和 _邮件正文_ 由 [Golang的模板引擎](https://go.dev/pkg/text/ {{if not (eq .Body "")}}

消息内容:


- {{.Body | Str2html}} + {{.Body}} {{end}}


@@ -242,20 +242,20 @@ _主题_ 和 _邮件正文_ 由 [Golang的模板引擎](https://go.dev/pkg/text/ 模板系统包含一些函数,可用于进一步处理和格式化消息。以下是其中一些函数的列表: -| 函数名 | 参数 | 可用于 | 用法 | -|------------------| ----------- | ------------ | --------------------------------------------------------------------------------- | -| `AppUrl` | - | 任何地方 | Gitea 的 URL | -| `AppName` | - | 任何地方 | 从 `app.ini` 中设置,通常为 "Gitea" | -| `AppDomain` | - | 任何地方 | Gitea 的主机名 | -| `EllipsisString` | string, int | 任何地方 | 将字符串截断为指定长度;根据需要添加省略号 | -| `Str2html` | string | 仅正文部分 | 通过删除其中的 HTML 标签对文本进行清理 | -| `SafeHTML` | string | 仅正文部分 | 将输入作为 HTML 处理;可用于 `.ReviewComments.RenderedContent` 等字段 | +| 函数名 | 参数 | 可用于 | 用法 | +|------------------| ----------- | ------------ | ------------------------------ | +| `AppUrl` | - | 任何地方 | Gitea 的 URL | +| `AppName` | - | 任何地方 | 从 `app.ini` 中设置,通常为 "Gitea" | +| `AppDomain` | - | 任何地方 | Gitea 的主机名 | +| `EllipsisString` | string, int | 任何地方 | 将字符串截断为指定长度;根据需要添加省略号 | +| `SanitizeHTML` | string | 仅正文部分 | 通过删除其中的危险 HTML 标签对文本进行清理 | +| `SafeHTML` | string | 仅正文部分 | 将输入作为 HTML 处理;可用于输出原始的 HTML 内容 | 这些都是 _函数_,而不是元数据,因此必须按以下方式使用: ```html -像这样使用: {{Str2html "Escapetext"}} -或者这样使用: {{"Escapetext" | Str2html}} +像这样使用: {{SanitizeHTML "Escapetext"}} +或者这样使用: {{"Escapetext" | SanitizeHTML}} 或者这样使用: {{AppUrl}} 但不要像这样使用: {{.AppUrl}} ``` diff --git a/docs/content/contributing/guidelines-frontend.en-us.md b/docs/content/contributing/guidelines-frontend.en-us.md index a33a38a6f9..2c0aaaed4a 100644 --- a/docs/content/contributing/guidelines-frontend.en-us.md +++ b/docs/content/contributing/guidelines-frontend.en-us.md @@ -47,7 +47,7 @@ We recommend [Google HTML/CSS Style Guide](https://google.github.io/styleguide/h 9. Avoid unnecessary `!important` in CSS, add comments to explain why it's necessary if it can't be avoided. 10. Avoid mixing different events in one event listener, prefer to use individual event listeners for every event. 11. Custom event names are recommended to use `ce-` prefix. -12. Gitea's tailwind-style CSS classes use `gt-` prefix (`gt-relative`), while Gitea's own private framework-level CSS classes use `g-` prefix (`g-modal-confirm`). +12. Prefer using Tailwind CSS which is available via `tw-` prefix, e.g. `tw-relative`. Gitea's helper CSS classes use `gt-` prefix (`gt-df`), while Gitea's own private framework-level CSS classes use `g-` prefix (`g-modal-confirm`). 13. Avoid inline scripts & styles as much as possible, it's recommended to put JS code into JS files and use CSS classes. If inline scripts & styles are unavoidable, explain the reason why it can't be avoided. ### Accessibility / ARIA diff --git a/docs/content/contributing/guidelines-frontend.zh-cn.md b/docs/content/contributing/guidelines-frontend.zh-cn.md index 43f72b4808..ace0d97f49 100644 --- a/docs/content/contributing/guidelines-frontend.zh-cn.md +++ b/docs/content/contributing/guidelines-frontend.zh-cn.md @@ -47,7 +47,7 @@ HTML 页面由[Go HTML Template](https://pkg.go.dev/html/template)渲染。 9. 避免在 CSS 中使用不必要的`!important`,如果无法避免,添加注释解释为什么需要它。 10. 避免在一个事件监听器中混合不同的事件,优先为每个事件使用独立的事件监听器。 11. 推荐使用自定义事件名称前缀`ce-`。 -12. Gitea 的 tailwind-style CSS 类使用`gt-`前缀(`gt-relative`),而 Gitea 自身的私有框架级 CSS 类使用`g-`前缀(`g-modal-confirm`)。 +12. 建议使用 Tailwind CSS,它可以通过 `tw-` 前缀获得,例如 `tw-relative`. Gitea 自身的助手类 CSS 使用 `gt-` 前缀(`gt-df`),Gitea 自身的私有框架级 CSS 类使用 `g-` 前缀(`g-modal-confirm`)。 13. 尽量避免内联脚本和样式,建议将JS代码放入JS文件中并使用CSS类。如果内联脚本和样式不可避免,请解释无法避免的原因。 ### 可访问性 / ARIA diff --git a/docs/content/help/faq.en-us.md b/docs/content/help/faq.en-us.md index 5ea2c10f5e..b3b0980125 100644 --- a/docs/content/help/faq.en-us.md +++ b/docs/content/help/faq.en-us.md @@ -221,9 +221,11 @@ Our translations are currently crowd-sourced on our [Crowdin project](https://cr Whether you want to change a translation or add a new one, it will need to be there as all translations are overwritten in our CI via the Crowdin integration. -## Push Hook / Webhook aren't running +## Push Hook / Webhook / Actions aren't running -If you can push but can't see push activities on the home dashboard, or the push doesn't trigger webhook, there are a few possibilities: +If you can push but can't see push activities on the home dashboard, or the push doesn't trigger webhook and Actions workflows, it's likely that the git hooks are not working. + +There are a few possibilities: 1. The git hooks are out of sync: run "Resynchronize pre-receive, update and post-receive hooks of all repositories" on the site admin panel 2. The git repositories (and hooks) are stored on some filesystems (ex: mounted by NAS) which don't support script execution, make sure the filesystem supports `chmod a+x any-script` diff --git a/docs/content/help/faq.zh-cn.md b/docs/content/help/faq.zh-cn.md index b8dd3cd180..25230df70b 100644 --- a/docs/content/help/faq.zh-cn.md +++ b/docs/content/help/faq.zh-cn.md @@ -225,9 +225,11 @@ Gitea还提供了自己的SSH服务器,用于在SSHD不可用时使用。 无论您想要更改翻译还是添加新的翻译,都需要在Crowdin集成中进行,因为所有翻译都会被CI覆盖。 -## 推送钩子/ Webhook未运行 +## 推送钩子/ Webhook / Actions 未运行 -如果您可以推送但无法在主页仪表板上看到推送活动,或者推送不触发Webhook,有几种可能性: +如果您可以推送但无法在主页仪表板上看到推送活动,或者推送不触发 Webhook 和 Actions,可能是 git 钩子不工作而导致的。 + +这可能是由于以下原因: 1. Git钩子不同步:在站点管理面板上运行“重新同步所有仓库的pre-receive、update和post-receive钩子” 2. Git仓库(和钩子)存储在一些不支持脚本执行的文件系统上(例如由NAS挂载),请确保文件系统支持`chmod a+x any-script` diff --git a/docs/content/usage/blocking-users.en-us.md b/docs/content/usage/blocking-users.en-us.md new file mode 100644 index 0000000000..b59bbe4d62 --- /dev/null +++ b/docs/content/usage/blocking-users.en-us.md @@ -0,0 +1,56 @@ +--- +date: "2024-01-31T00:00:00+00:00" +title: "Blocking a user" +slug: "blocking-user" +sidebar_position: 25 +toc: false +draft: false +aliases: + - /en-us/webhooks +menu: + sidebar: + parent: "usage" + name: "Blocking a user" + sidebar_position: 30 + identifier: "blocking-user" +--- + +# Blocking a user + +Gitea supports blocking of users to restrict how they can interact with you and your content. + +You can block a user in your account settings, from the user's profile or from comments created by the user. +The user is not directly notified about the block, but they can notice they are blocked when they attempt to interact with you. +Organization owners can block anyone who is not a member of the organization too. +If a blocked user has admin permissions, they can still perform all actions even if blocked. + +### When you block a user + +- the user stops following you +- you stop following the user +- the user's stars are removed from your repositories +- your stars are removed from their repositories +- the user stops watching your repositories +- you stop watching their repositories +- the user's issue assignments are removed from your repositories +- your issue assignments are removed from their repositories +- the user is removed as a collaborator on your repositories +- you are removed as a collaborator on their repositories +- any pending repository transfers to or from the blocked user are canceled + +### When you block a user, the user cannot + +- follow you +- watch your repositories +- star your repositories +- fork your repositories +- transfer repositories to you +- open issues or pull requests on your repositories +- comment on issues or pull requests you've created +- comment on issues or pull requests on your repositories +- react to your comments on issues or pull requests +- react to comments on issues or pull requests on your repositories +- assign you to issues or pull requests +- add you as a collaborator on their repositories +- send you notifications by @mentioning your username +- be added as team member (if blocked by an organization) diff --git a/docs/content/usage/issue-pull-request-templates.en-us.md b/docs/content/usage/issue-pull-request-templates.en-us.md index b031b262fb..e203c0d379 100644 --- a/docs/content/usage/issue-pull-request-templates.en-us.md +++ b/docs/content/usage/issue-pull-request-templates.en-us.md @@ -136,6 +136,12 @@ body: attributes: value: | Thanks for taking the time to fill out this bug report! + # some markdown that will only be visible once the issue has been created + - type: markdown + attributes: + value: | + This issue was created by an issue **template** :) + visible: [content] - type: input id: contact attributes: @@ -187,11 +193,16 @@ body: options: - label: I agree to follow this project's Code of Conduct required: true + - label: I have also read the CONTRIBUTION.MD + required: true + visible: [form] + - label: This is a TODO only visible after issue creation + visible: [content] ``` ### Markdown -You can use a `markdown` element to display Markdown in your form that provides extra context to the user, but is not submitted. +You can use a `markdown` element to display Markdown in your form that provides extra context to the user, but is not submitted by default. Attributes: @@ -199,6 +210,8 @@ Attributes: |-------|--------------------------------------------------------------|----------|--------|---------|--------------| | value | The text that is rendered. Markdown formatting is supported. | Required | String | - | - | +visible: Default is **[form]** + ### Textarea You can use a `textarea` element to add a multi-line text field to your form. Contributors can also attach files in `textarea` fields. @@ -219,6 +232,8 @@ Validations: |----------|------------------------------------------------------|----------|---------|---------|--------------| | required | Prevents form submission until element is completed. | Optional | Boolean | false | - | +visible: Default is **[form, content]** + ### Input You can use an `input` element to add a single-line text field to your form. @@ -240,6 +255,8 @@ Validations: | is_number | Prevents form submission until element is filled with a number. | Optional | Boolean | false | - | | regex | Prevents form submission until element is filled with a value that match the regular expression. | Optional | String | - | a [regular expression](https://en.wikipedia.org/wiki/Regular_expression) | +visible: Default is **[form, content]** + ### Dropdown You can use a `dropdown` element to add a dropdown menu in your form. @@ -259,6 +276,8 @@ Validations: |----------|------------------------------------------------------|----------|---------|---------|--------------| | required | Prevents form submission until element is completed. | Optional | Boolean | false | - | +visible: Default is **[form, content]** + ### Checkboxes You can use the `checkboxes` element to add a set of checkboxes to your form. @@ -266,17 +285,20 @@ You can use the `checkboxes` element to add a set of checkboxes to your form. Attributes: | Key | Description | Required | Type | Default | Valid values | -|-------------|-------------------------------------------------------------------------------------------------------|----------|--------|--------------|--------------| +| ----------- | ----------------------------------------------------------------------------------------------------- | -------- | ------ | ------------ | ------------ | | label | A brief description of the expected user input, which is displayed in the form. | Required | String | - | - | | description | A description of the set of checkboxes, which is displayed in the form. Supports Markdown formatting. | Optional | String | Empty String | - | | options | An array of checkboxes that the user can select. For syntax, see below. | Required | Array | - | - | For each value in the options array, you can set the following keys. -| Key | Description | Required | Type | Default | Options | -|----------|------------------------------------------------------------------------------------------------------------------------------------------|----------|---------|---------|---------| -| label | The identifier for the option, which is displayed in the form. Markdown is supported for bold or italic text formatting, and hyperlinks. | Required | String | - | - | -| required | Prevents form submission until element is completed. | Optional | Boolean | false | - | +| Key | Description | Required | Type | Default | Options | +|--------------|------------------------------------------------------------------------------------------------------------------------------------------|----------|--------------|---------|---------| +| label | The identifier for the option, which is displayed in the form. Markdown is supported for bold or italic text formatting, and hyperlinks. | Required | String | - | - | +| required | Prevents form submission until element is completed. | Optional | Boolean | false | - | +| visible | Whether a specific checkbox appears in the form only, in the created issue only, or both. Valid options are "form" and "content". | Optional | String array | false | - | + +visible: Default is **[form, content]** ## Syntax for issue config @@ -292,15 +314,15 @@ contact_links: ### Possible Options -| Key | Description | Type | Default | -|----------------------|-------------------------------------------------------------------------------------------------------|--------------------|----------------| -| blank_issues_enabled | If set to false, the User is forced to use a Template | Boolean | true | -| contact_links | Custom Links to show in the Choose Box | Contact Link Array | Empty Array | +| Key | Description | Type | Default | +|----------------------|-------------------------------------------------------|--------------------|-------------| +| blank_issues_enabled | If set to false, the User is forced to use a Template | Boolean | true | +| contact_links | Custom Links to show in the Choose Box | Contact Link Array | Empty Array | ### Contact Link -| Key | Description | Type | Required | -|----------------------|-------------------------------------------------------------------------------------------------------|---------|----------| -| name | the name of your link | String | true | -| url | The URL of your Link | String | true | -| about | A short description of your Link | String | true | +| Key | Description | Type | Required | +|-------|----------------------------------|--------|----------| +| name | the name of your link | String | true | +| url | The URL of your Link | String | true | +| about | A short description of your Link | String | true | diff --git a/models/actions/runner.go b/models/actions/runner.go index b646146ee6..67f003387b 100644 --- a/models/actions/runner.go +++ b/models/actions/runner.go @@ -13,6 +13,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/shared/types" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/util" @@ -159,7 +160,7 @@ type FindRunnerOptions struct { OwnerID int64 Sort string Filter string - IsOnline util.OptionalBool + IsOnline optional.Option[bool] WithAvailable bool // not only runners belong to, but also runners can be used } @@ -186,10 +187,12 @@ func (opts FindRunnerOptions) ToConds() builder.Cond { cond = cond.And(builder.Like{"name", opts.Filter}) } - if opts.IsOnline.IsTrue() { - cond = cond.And(builder.Gt{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()}) - } else if opts.IsOnline.IsFalse() { - cond = cond.And(builder.Lte{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()}) + if opts.IsOnline.Has() { + if opts.IsOnline.Value() { + cond = cond.And(builder.Gt{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()}) + } else { + cond = cond.And(builder.Lte{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()}) + } } return cond } diff --git a/models/activities/action.go b/models/activities/action.go index 15bd9a52ac..fcc97e3872 100644 --- a/models/activities/action.go +++ b/models/activities/action.go @@ -225,8 +225,8 @@ func (a *Action) ShortActUserName(ctx context.Context) string { return base.EllipsisString(a.GetActUserName(ctx), 20) } -// GetDisplayName gets the action's display name based on DEFAULT_SHOW_FULL_NAME, or falls back to the username if it is blank. -func (a *Action) GetDisplayName(ctx context.Context) string { +// GetActDisplayName gets the action's display name based on DEFAULT_SHOW_FULL_NAME, or falls back to the username if it is blank. +func (a *Action) GetActDisplayName(ctx context.Context) string { if setting.UI.DefaultShowFullName { trimmedFullName := strings.TrimSpace(a.GetActFullName(ctx)) if len(trimmedFullName) > 0 { @@ -236,8 +236,8 @@ func (a *Action) GetDisplayName(ctx context.Context) string { return a.ShortActUserName(ctx) } -// GetDisplayNameTitle gets the action's display name used for the title (tooltip) based on DEFAULT_SHOW_FULL_NAME -func (a *Action) GetDisplayNameTitle(ctx context.Context) string { +// GetActDisplayNameTitle gets the action's display name used for the title (tooltip) based on DEFAULT_SHOW_FULL_NAME +func (a *Action) GetActDisplayNameTitle(ctx context.Context) string { if setting.UI.DefaultShowFullName { return a.ShortActUserName(ctx) } diff --git a/models/asymkey/ssh_key_authorized_keys.go b/models/asymkey/ssh_key_authorized_keys.go index 267ab252c8..9279db2020 100644 --- a/models/asymkey/ssh_key_authorized_keys.go +++ b/models/asymkey/ssh_key_authorized_keys.go @@ -12,7 +12,6 @@ import ( "path/filepath" "strings" "sync" - "time" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/log" @@ -44,6 +43,12 @@ const ( var sshOpLocker sync.Mutex +func WithSSHOpLocker(f func() error) error { + sshOpLocker.Lock() + defer sshOpLocker.Unlock() + return f() +} + // AuthorizedStringForKey creates the authorized keys string appropriate for the provided key func AuthorizedStringForKey(key *PublicKey) string { sb := &strings.Builder{} @@ -114,65 +119,6 @@ func appendAuthorizedKeysToFile(keys ...*PublicKey) error { return nil } -// RewriteAllPublicKeys removes any authorized key and rewrite all keys from database again. -// Note: db.GetEngine(ctx).Iterate does not get latest data after insert/delete, so we have to call this function -// outside any session scope independently. -func RewriteAllPublicKeys(ctx context.Context) error { - // Don't rewrite key if internal server - if setting.SSH.StartBuiltinServer || !setting.SSH.CreateAuthorizedKeysFile { - return nil - } - - sshOpLocker.Lock() - defer sshOpLocker.Unlock() - - if setting.SSH.RootPath != "" { - // First of ensure that the RootPath is present, and if not make it with 0700 permissions - // This of course doesn't guarantee that this is the right directory for authorized_keys - // but at least if it's supposed to be this directory and it doesn't exist and we're the - // right user it will at least be created properly. - err := os.MkdirAll(setting.SSH.RootPath, 0o700) - if err != nil { - log.Error("Unable to MkdirAll(%s): %v", setting.SSH.RootPath, err) - return err - } - } - - fPath := filepath.Join(setting.SSH.RootPath, "authorized_keys") - tmpPath := fPath + ".tmp" - t, err := os.OpenFile(tmpPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600) - if err != nil { - return err - } - defer func() { - t.Close() - if err := util.Remove(tmpPath); err != nil { - log.Warn("Unable to remove temporary authorized keys file: %s: Error: %v", tmpPath, err) - } - }() - - if setting.SSH.AuthorizedKeysBackup { - isExist, err := util.IsExist(fPath) - if err != nil { - log.Error("Unable to check if %s exists. Error: %v", fPath, err) - return err - } - if isExist { - bakPath := fmt.Sprintf("%s_%d.gitea_bak", fPath, time.Now().Unix()) - if err = util.CopyFile(fPath, bakPath); err != nil { - return err - } - } - } - - if err := RegeneratePublicKeys(ctx, t); err != nil { - return err - } - - t.Close() - return util.Rename(tmpPath, fPath) -} - // RegeneratePublicKeys regenerates the authorized_keys file func RegeneratePublicKeys(ctx context.Context, t io.StringWriter) error { if err := db.GetEngine(ctx).Where("type != ?", KeyTypePrincipal).Iterate(new(PublicKey), func(idx int, bean any) (err error) { diff --git a/models/asymkey/ssh_key_principals.go b/models/asymkey/ssh_key_principals.go index 4e7dee2c91..e8b97d306e 100644 --- a/models/asymkey/ssh_key_principals.go +++ b/models/asymkey/ssh_key_principals.go @@ -9,51 +9,11 @@ import ( "strings" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/perm" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" ) -// AddPrincipalKey adds new principal to database and authorized_principals file. -func AddPrincipalKey(ctx context.Context, ownerID int64, content string, authSourceID int64) (*PublicKey, error) { - dbCtx, committer, err := db.TxContext(ctx) - if err != nil { - return nil, err - } - defer committer.Close() - - // Principals cannot be duplicated. - has, err := db.GetEngine(dbCtx). - Where("content = ? AND type = ?", content, KeyTypePrincipal). - Get(new(PublicKey)) - if err != nil { - return nil, err - } else if has { - return nil, ErrKeyAlreadyExist{0, "", content} - } - - key := &PublicKey{ - OwnerID: ownerID, - Name: content, - Content: content, - Mode: perm.AccessModeWrite, - Type: KeyTypePrincipal, - LoginSourceID: authSourceID, - } - if err = db.Insert(dbCtx, key); err != nil { - return nil, fmt.Errorf("addKey: %w", err) - } - - if err = committer.Commit(); err != nil { - return nil, err - } - - committer.Close() - - return key, RewriteAllPrincipalKeys(ctx) -} - // CheckPrincipalKeyString strips spaces and returns an error if the given principal contains newlines func CheckPrincipalKeyString(ctx context.Context, user *user_model.User, content string) (_ string, err error) { if setting.SSH.Disabled { diff --git a/models/auth/source.go b/models/auth/source.go index 1bdde8235c..f360ca9801 100644 --- a/models/auth/source.go +++ b/models/auth/source.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -243,14 +244,14 @@ func CreateSource(ctx context.Context, source *Source) error { type FindSourcesOptions struct { db.ListOptions - IsActive util.OptionalBool + IsActive optional.Option[bool] LoginType Type } func (opts FindSourcesOptions) ToConds() builder.Cond { conds := builder.NewCond() - if !opts.IsActive.IsNone() { - conds = conds.And(builder.Eq{"is_active": opts.IsActive.IsTrue()}) + if opts.IsActive.Has() { + conds = conds.And(builder.Eq{"is_active": opts.IsActive.Value()}) } if opts.LoginType != NoType { conds = conds.And(builder.Eq{"`type`": opts.LoginType}) @@ -262,7 +263,7 @@ func (opts FindSourcesOptions) ToConds() builder.Cond { // source of type LoginSSPI func IsSSPIEnabled(ctx context.Context) bool { exist, err := db.Exist[Source](ctx, FindSourcesOptions{ - IsActive: util.OptionalBoolTrue, + IsActive: optional.Some(true), LoginType: SSPI, }.ToConds()) if err != nil { diff --git a/models/fixtures/access.yml b/models/fixtures/access.yml index 641c453eb7..4171e31fef 100644 --- a/models/fixtures/access.yml +++ b/models/fixtures/access.yml @@ -42,120 +42,132 @@ - id: 8 - user_id: 15 + user_id: 10 repo_id: 21 mode: 2 - id: 9 - user_id: 15 - repo_id: 22 + user_id: 10 + repo_id: 32 mode: 2 - id: 10 user_id: 15 + repo_id: 21 + mode: 2 + +- + id: 11 + user_id: 15 + repo_id: 22 + mode: 2 + +- + id: 12 + user_id: 15 repo_id: 23 mode: 4 - - id: 11 + id: 13 user_id: 15 repo_id: 24 mode: 4 - - id: 12 + id: 14 user_id: 15 repo_id: 32 mode: 2 - - id: 13 + id: 15 user_id: 18 repo_id: 21 mode: 2 - - id: 14 + id: 16 user_id: 18 repo_id: 22 mode: 2 - - id: 15 + id: 17 user_id: 18 repo_id: 23 mode: 4 - - id: 16 + id: 18 user_id: 18 repo_id: 24 mode: 4 - - id: 17 + id: 19 user_id: 20 repo_id: 24 mode: 1 - - id: 18 + id: 20 user_id: 20 repo_id: 27 mode: 4 - - id: 19 + id: 21 user_id: 20 repo_id: 28 mode: 4 - - id: 20 + id: 22 user_id: 29 repo_id: 4 mode: 2 - - id: 21 + id: 23 user_id: 29 repo_id: 24 mode: 1 - - id: 22 + id: 24 user_id: 31 repo_id: 27 mode: 4 - - id: 23 + id: 25 user_id: 31 repo_id: 28 mode: 4 - - id: 24 + id: 26 user_id: 38 repo_id: 60 mode: 2 - - id: 25 + id: 27 user_id: 38 repo_id: 61 mode: 1 - - id: 26 + id: 28 user_id: 39 repo_id: 61 mode: 1 - - id: 27 + id: 29 user_id: 40 repo_id: 61 mode: 4 diff --git a/models/fixtures/action_run.yml b/models/fixtures/action_run.yml index 2c2151f354..a42ab77ca5 100644 --- a/models/fixtures/action_run.yml +++ b/models/fixtures/action_run.yml @@ -17,3 +17,22 @@ updated: 1683636626 need_approval: 0 approved_by: 0 +- + id: 792 + title: "update actions" + repo_id: 4 + owner_id: 1 + workflow_id: "artifact.yaml" + index: 188 + trigger_user_id: 1 + ref: "refs/heads/master" + commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" + event: "push" + is_fork_pull_request: 0 + status: 1 + started: 1683636528 + stopped: 1683636626 + created: 1683636108 + updated: 1683636626 + need_approval: 0 + approved_by: 0 diff --git a/models/fixtures/action_run_job.yml b/models/fixtures/action_run_job.yml index 071998b979..fd90f4fd5d 100644 --- a/models/fixtures/action_run_job.yml +++ b/models/fixtures/action_run_job.yml @@ -12,3 +12,17 @@ status: 1 started: 1683636528 stopped: 1683636626 +- + id: 193 + run_id: 792 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + name: job_2 + attempt: 1 + job_id: job_2 + task_id: 48 + status: 1 + started: 1683636528 + stopped: 1683636626 diff --git a/models/fixtures/action_task.yml b/models/fixtures/action_task.yml index c78fb3c5d6..443effe08c 100644 --- a/models/fixtures/action_task.yml +++ b/models/fixtures/action_task.yml @@ -18,3 +18,23 @@ log_length: 707 log_size: 90179 log_expired: 0 +- + id: 48 + job_id: 193 + attempt: 1 + runner_id: 1 + status: 6 # 6 is the status code for "running", running task can upload artifacts + started: 1683636528 + stopped: 1683636626 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + token_hash: ffffcfffffffbffffffffffffffffefffffffafffffffffffffffffffffffffffffdffffffffffffffffffffffffffffffff + token_salt: ffffffffff + token_last_eight: ffffffff + log_filename: artifact-test2/2f/47.log + log_in_storage: 1 + log_length: 707 + log_size: 90179 + log_expired: 0 diff --git a/models/fixtures/collaboration.yml b/models/fixtures/collaboration.yml index 7603bdad32..4c3ac367f6 100644 --- a/models/fixtures/collaboration.yml +++ b/models/fixtures/collaboration.yml @@ -51,3 +51,15 @@ repo_id: 60 user_id: 38 mode: 2 # write + +- + id: 10 + repo_id: 21 + user_id: 10 + mode: 2 # write + +- + id: 11 + repo_id: 32 + user_id: 10 + mode: 2 # write diff --git a/models/fixtures/issue_assignees.yml b/models/fixtures/issue_assignees.yml index e5d36f921a..c40ecad676 100644 --- a/models/fixtures/issue_assignees.yml +++ b/models/fixtures/issue_assignees.yml @@ -14,3 +14,7 @@ id: 4 assignee_id: 2 issue_id: 17 +- + id: 5 + assignee_id: 10 + issue_id: 6 diff --git a/models/fixtures/repo_transfer.yml b/models/fixtures/repo_transfer.yml index b841b5e983..db92c95248 100644 --- a/models/fixtures/repo_transfer.yml +++ b/models/fixtures/repo_transfer.yml @@ -5,3 +5,19 @@ repo_id: 3 created_unix: 1553610671 updated_unix: 1553610671 + +- + id: 2 + doer_id: 16 + recipient_id: 10 + repo_id: 21 + created_unix: 1553610671 + updated_unix: 1553610671 + +- + id: 3 + doer_id: 3 + recipient_id: 10 + repo_id: 32 + created_unix: 1553610671 + updated_unix: 1553610671 diff --git a/models/fixtures/repo_unit.yml b/models/fixtures/repo_unit.yml index 4b26674990..8a22db0445 100644 --- a/models/fixtures/repo_unit.yml +++ b/models/fixtures/repo_unit.yml @@ -650,12 +650,6 @@ type: 2 created_unix: 946684810 -- - id: 98 - repo_id: 1 - type: 8 - created_unix: 946684810 - - id: 99 repo_id: 1 diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml index d094fe82d8..e5c6224c96 100644 --- a/models/fixtures/repository.yml +++ b/models/fixtures/repository.yml @@ -614,8 +614,8 @@ owner_name: user16 lower_name: big_test_public_3 name: big_test_public_3 - num_watches: 0 - num_stars: 0 + num_watches: 1 + num_stars: 1 num_forks: 0 num_issues: 0 num_closed_issues: 0 @@ -945,8 +945,8 @@ owner_name: org3 lower_name: repo21 name: repo21 - num_watches: 0 - num_stars: 0 + num_watches: 1 + num_stars: 1 num_forks: 0 num_issues: 2 num_closed_issues: 0 diff --git a/models/fixtures/star.yml b/models/fixtures/star.yml index 860f26b8e2..39b51b3736 100644 --- a/models/fixtures/star.yml +++ b/models/fixtures/star.yml @@ -7,3 +7,13 @@ id: 2 uid: 2 repo_id: 4 + +- + id: 3 + uid: 10 + repo_id: 21 + +- + id: 4 + uid: 10 + repo_id: 32 diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml index 16b687ae04..a3de535508 100644 --- a/models/fixtures/user.yml +++ b/models/fixtures/user.yml @@ -361,7 +361,7 @@ use_custom_avatar: false num_followers: 0 num_following: 0 - num_stars: 0 + num_stars: 2 num_repos: 3 num_teams: 0 num_members: 0 diff --git a/models/fixtures/user_blocking.yml b/models/fixtures/user_blocking.yml new file mode 100644 index 0000000000..2ec9d99df5 --- /dev/null +++ b/models/fixtures/user_blocking.yml @@ -0,0 +1,19 @@ +- + id: 1 + blocker_id: 2 + blockee_id: 29 + +- + id: 2 + blocker_id: 17 + blockee_id: 28 + +- + id: 3 + blocker_id: 2 + blockee_id: 34 + +- + id: 4 + blocker_id: 50 + blockee_id: 34 diff --git a/models/fixtures/watch.yml b/models/fixtures/watch.yml index 1950ac99e7..18bcd2ed2b 100644 --- a/models/fixtures/watch.yml +++ b/models/fixtures/watch.yml @@ -27,3 +27,15 @@ user_id: 11 repo_id: 1 mode: 3 # auto + +- + id: 6 + user_id: 10 + repo_id: 21 + mode: 1 # normal + +- + id: 7 + user_id: 10 + repo_id: 32 + mode: 1 # normal diff --git a/models/issues/assignees.go b/models/issues/assignees.go index 60f32d9557..30234be07a 100644 --- a/models/issues/assignees.go +++ b/models/issues/assignees.go @@ -64,6 +64,27 @@ func IsUserAssignedToIssue(ctx context.Context, issue *Issue, user *user_model.U return db.Exist[IssueAssignees](ctx, builder.Eq{"assignee_id": user.ID, "issue_id": issue.ID}) } +type AssignedIssuesOptions struct { + db.ListOptions + AssigneeID int64 + RepoOwnerID int64 +} + +func (opts *AssignedIssuesOptions) ToConds() builder.Cond { + cond := builder.NewCond() + if opts.AssigneeID != 0 { + cond = cond.And(builder.In("issue.id", builder.Select("issue_id").From("issue_assignees").Where(builder.Eq{"assignee_id": opts.AssigneeID}))) + } + if opts.RepoOwnerID != 0 { + cond = cond.And(builder.In("issue.repo_id", builder.Select("id").From("repository").Where(builder.Eq{"owner_id": opts.RepoOwnerID}))) + } + return cond +} + +func GetAssignedIssues(ctx context.Context, opts *AssignedIssuesOptions) ([]*Issue, int64, error) { + return db.FindAndCount[Issue](ctx, opts) +} + // ToggleIssueAssignee changes a user between assigned and not assigned for this issue, and make issue comment for it. func ToggleIssueAssignee(ctx context.Context, issue *Issue, doer *user_model.User, assigneeID int64) (removed bool, comment *Comment, err error) { ctx, committer, err := db.TxContext(ctx) diff --git a/models/issues/comment.go b/models/issues/comment.go index c7b22f3cca..e37f844b5c 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -8,6 +8,7 @@ package issues import ( "context" "fmt" + "html/template" "strconv" "unicode/utf8" @@ -21,6 +22,7 @@ import ( "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/references" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" @@ -259,8 +261,8 @@ type Comment struct { CommitID int64 Line int64 // - previous line / + proposed line TreePath string - Content string `xorm:"LONGTEXT"` - RenderedContent string `xorm:"-"` + Content string `xorm:"LONGTEXT"` + RenderedContent template.HTML `xorm:"-"` // Path represents the 4 lines of code cemented by this comment Patch string `xorm:"-"` @@ -1035,8 +1037,8 @@ type FindCommentsOptions struct { TreePath string Type CommentType IssueIDs []int64 - Invalidated util.OptionalBool - IsPull util.OptionalBool + Invalidated optional.Option[bool] + IsPull optional.Option[bool] } // ToConds implements FindOptions interface @@ -1068,11 +1070,11 @@ func (opts FindCommentsOptions) ToConds() builder.Cond { if len(opts.TreePath) > 0 { cond = cond.And(builder.Eq{"comment.tree_path": opts.TreePath}) } - if !opts.Invalidated.IsNone() { - cond = cond.And(builder.Eq{"comment.invalidated": opts.Invalidated.IsTrue()}) + if opts.Invalidated.Has() { + cond = cond.And(builder.Eq{"comment.invalidated": opts.Invalidated.Value()}) } - if opts.IsPull != util.OptionalBoolNone { - cond = cond.And(builder.Eq{"issue.is_pull": opts.IsPull.IsTrue()}) + if opts.IsPull.Has() { + cond = cond.And(builder.Eq{"issue.is_pull": opts.IsPull.Value()}) } return cond } @@ -1081,7 +1083,7 @@ func (opts FindCommentsOptions) ToConds() builder.Cond { func FindComments(ctx context.Context, opts *FindCommentsOptions) (CommentList, error) { comments := make([]*Comment, 0, 10) sess := db.GetEngine(ctx).Where(opts.ToConds()) - if opts.RepoID > 0 || opts.IsPull != util.OptionalBoolNone { + if opts.RepoID > 0 || opts.IsPull.Has() { sess.Join("INNER", "issue", "issue.id = comment.issue_id") } diff --git a/models/issues/content_history.go b/models/issues/content_history.go index 8b00adda99..31c80d2cea 100644 --- a/models/issues/content_history.go +++ b/models/issues/content_history.go @@ -172,13 +172,9 @@ func FetchIssueContentHistoryList(dbCtx context.Context, issueID, commentID int6 // HasIssueContentHistory check if a ContentHistory entry exists func HasIssueContentHistory(dbCtx context.Context, issueID, commentID int64) (bool, error) { - exists, err := db.GetEngine(dbCtx).Cols("id").Exist(&ContentHistory{ - IssueID: issueID, - CommentID: commentID, - }) + exists, err := db.GetEngine(dbCtx).Where(builder.Eq{"issue_id": issueID, "comment_id": commentID}).Exist(&ContentHistory{}) if err != nil { - log.Error("can not fetch issue content history. err=%v", err) - return false, err + return false, fmt.Errorf("can not check issue content history. err: %w", err) } return exists, err } diff --git a/models/issues/content_history_test.go b/models/issues/content_history_test.go index 0ea1d0f7b2..1caa73a948 100644 --- a/models/issues/content_history_test.go +++ b/models/issues/content_history_test.go @@ -78,3 +78,22 @@ func TestContentHistory(t *testing.T) { assert.EqualValues(t, 7, list2[1].HistoryID) assert.EqualValues(t, 4, list2[2].HistoryID) } + +func TestHasIssueContentHistoryForCommentOnly(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + _ = db.TruncateBeans(db.DefaultContext, &issues_model.ContentHistory{}) + + hasHistory1, _ := issues_model.HasIssueContentHistory(db.DefaultContext, 10, 0) + assert.False(t, hasHistory1) + hasHistory2, _ := issues_model.HasIssueContentHistory(db.DefaultContext, 10, 100) + assert.False(t, hasHistory2) + + _ = issues_model.SaveIssueContentHistory(db.DefaultContext, 1, 10, 100, timeutil.TimeStampNow(), "c-a", true) + _ = issues_model.SaveIssueContentHistory(db.DefaultContext, 1, 10, 100, timeutil.TimeStampNow().Add(5), "c-b", false) + + hasHistory1, _ = issues_model.HasIssueContentHistory(db.DefaultContext, 10, 0) + assert.False(t, hasHistory1) + hasHistory2, _ = issues_model.HasIssueContentHistory(db.DefaultContext, 10, 100) + assert.True(t, hasHistory2) +} diff --git a/models/issues/issue.go b/models/issues/issue.go index 90aad10bb9..563a780dcb 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -7,6 +7,7 @@ package issues import ( "context" "fmt" + "html/template" "regexp" "slices" @@ -105,7 +106,7 @@ type Issue struct { OriginalAuthorID int64 `xorm:"index"` Title string `xorm:"name"` Content string `xorm:"LONGTEXT"` - RenderedContent string `xorm:"-"` + RenderedContent template.HTML `xorm:"-"` Labels []*Label `xorm:"-"` MilestoneID int64 `xorm:"INDEX"` Milestone *Milestone `xorm:"-"` diff --git a/models/issues/issue_search.go b/models/issues/issue_search.go index 7dc277327a..c5c9cecdb9 100644 --- a/models/issues/issue_search.go +++ b/models/issues/issue_search.go @@ -13,7 +13,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/optional" "xorm.io/builder" "xorm.io/xorm" @@ -34,8 +34,8 @@ type IssuesOptions struct { //nolint MilestoneIDs []int64 ProjectID int64 ProjectBoardID int64 - IsClosed util.OptionalBool - IsPull util.OptionalBool + IsClosed optional.Option[bool] + IsPull optional.Option[bool] LabelIDs []int64 IncludedLabelNames []string ExcludedLabelNames []string @@ -46,7 +46,7 @@ type IssuesOptions struct { //nolint UpdatedBeforeUnix int64 // prioritize issues from this repo PriorityRepoID int64 - IsArchived util.OptionalBool + IsArchived optional.Option[bool] Org *organization.Organization // issues permission scope Team *organization.Team // issues permission scope User *user_model.User // issues permission scope @@ -217,8 +217,8 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session { applyRepoConditions(sess, opts) - if !opts.IsClosed.IsNone() { - sess.And("issue.is_closed=?", opts.IsClosed.IsTrue()) + if opts.IsClosed.Has() { + sess.And("issue.is_closed=?", opts.IsClosed.Value()) } if opts.AssigneeID > 0 { @@ -260,21 +260,18 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session { applyProjectBoardCondition(sess, opts) - switch opts.IsPull { - case util.OptionalBoolTrue: - sess.And("issue.is_pull=?", true) - case util.OptionalBoolFalse: - sess.And("issue.is_pull=?", false) + if opts.IsPull.Has() { + sess.And("issue.is_pull=?", opts.IsPull.Value()) } - if opts.IsArchived != util.OptionalBoolNone { - sess.And(builder.Eq{"repository.is_archived": opts.IsArchived.IsTrue()}) + if opts.IsArchived.Has() { + sess.And(builder.Eq{"repository.is_archived": opts.IsArchived.Value()}) } applyLabelsCondition(sess, opts) if opts.User != nil { - sess.And(issuePullAccessibleRepoCond("issue.repo_id", opts.User.ID, opts.Org, opts.Team, opts.IsPull.IsTrue())) + sess.And(issuePullAccessibleRepoCond("issue.repo_id", opts.User.ID, opts.Org, opts.Team, opts.IsPull.Value())) } return sess diff --git a/models/issues/issue_stats.go b/models/issues/issue_stats.go index 99ca19f804..32c5674fc9 100644 --- a/models/issues/issue_stats.go +++ b/models/issues/issue_stats.go @@ -8,7 +8,6 @@ import ( "fmt" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/modules/util" "xorm.io/builder" "xorm.io/xorm" @@ -170,11 +169,8 @@ func applyIssuesOptions(sess *xorm.Session, opts *IssuesOptions, issueIDs []int6 applyReviewedCondition(sess, opts.ReviewedID) } - switch opts.IsPull { - case util.OptionalBoolTrue: - sess.And("issue.is_pull=?", true) - case util.OptionalBoolFalse: - sess.And("issue.is_pull=?", false) + if opts.IsPull.Has() { + sess.And("issue.is_pull=?", opts.IsPull.Value()) } return sess diff --git a/models/issues/issue_update.go b/models/issues/issue_update.go index b258dc882c..ef96e1ee50 100644 --- a/models/issues/issue_update.go +++ b/models/issues/issue_update.go @@ -517,6 +517,15 @@ func FindAndUpdateIssueMentions(ctx context.Context, issue *Issue, doer *user_mo if err != nil { return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err) } + + notBlocked := make([]*user_model.User, 0, len(mentions)) + for _, user := range mentions { + if !user_model.IsUserBlockedBy(ctx, doer, user.ID) { + notBlocked = append(notBlocked, user) + } + } + mentions = notBlocked + if err = UpdateIssueMentions(ctx, issue.ID, mentions); err != nil { return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err) } diff --git a/models/issues/issue_xref.go b/models/issues/issue_xref.go index cfc3c1683c..e2e35859df 100644 --- a/models/issues/issue_xref.go +++ b/models/issues/issue_xref.go @@ -214,6 +214,10 @@ func (issue *Issue) verifyReferencedIssue(stdCtx context.Context, ctx *crossRefe if !perm.CanReadIssuesOrPulls(refIssue.IsPull) { return nil, references.XRefActionNone, nil } + if user_model.IsUserBlockedBy(stdCtx, ctx.Doer, refIssue.PosterID, refIssue.Repo.OwnerID) { + return nil, references.XRefActionNone, nil + } + // Accept close/reopening actions only if the poster is able to close the // referenced issue manually at this moment. The only exception is // the poster of a new PR referencing an issue on the same repo: then the merger diff --git a/models/issues/label.go b/models/issues/label.go index 527d8d7853..f6ecc68cd1 100644 --- a/models/issues/label.go +++ b/models/issues/label.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/label" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -126,7 +127,7 @@ func (l *Label) CalOpenOrgIssues(ctx context.Context, repoID, labelID int64) { counts, _ := CountIssuesByRepo(ctx, &IssuesOptions{ RepoIDs: []int64{repoID}, LabelIDs: []int64{labelID}, - IsClosed: util.OptionalBoolFalse, + IsClosed: optional.Some(false), }) for _, count := range counts { diff --git a/models/issues/milestone.go b/models/issues/milestone.go index f663d42fe9..db0312adf0 100644 --- a/models/issues/milestone.go +++ b/models/issues/milestone.go @@ -6,10 +6,12 @@ package issues import ( "context" "fmt" + "html/template" "strings" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/optional" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -47,8 +49,8 @@ type Milestone struct { RepoID int64 `xorm:"INDEX"` Repo *repo_model.Repository `xorm:"-"` Name string - Content string `xorm:"TEXT"` - RenderedContent string `xorm:"-"` + Content string `xorm:"TEXT"` + RenderedContent template.HTML `xorm:"-"` IsClosed bool NumIssues int NumClosedIssues int @@ -301,7 +303,7 @@ func DeleteMilestoneByRepoID(ctx context.Context, repoID, id int64) error { } numClosedMilestones, err := db.Count[Milestone](ctx, FindMilestoneOptions{ RepoID: repo.ID, - IsClosed: util.OptionalBoolTrue, + IsClosed: optional.Some(true), }) if err != nil { return err diff --git a/models/issues/milestone_list.go b/models/issues/milestone_list.go index a73bf73c17..d1b3f0301b 100644 --- a/models/issues/milestone_list.go +++ b/models/issues/milestone_list.go @@ -8,7 +8,7 @@ import ( "strings" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/optional" "xorm.io/builder" ) @@ -28,7 +28,7 @@ func (milestones MilestoneList) getMilestoneIDs() []int64 { type FindMilestoneOptions struct { db.ListOptions RepoID int64 - IsClosed util.OptionalBool + IsClosed optional.Option[bool] Name string SortType string RepoCond builder.Cond @@ -40,8 +40,8 @@ func (opts FindMilestoneOptions) ToConds() builder.Cond { if opts.RepoID != 0 { cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) } - if opts.IsClosed != util.OptionalBoolNone { - cond = cond.And(builder.Eq{"is_closed": opts.IsClosed.IsTrue()}) + if opts.IsClosed.Has() { + cond = cond.And(builder.Eq{"is_closed": opts.IsClosed.Value()}) } if opts.RepoCond != nil && opts.RepoCond.IsValid() { cond = cond.And(builder.In("repo_id", builder.Select("id").From("repository").Where(opts.RepoCond))) diff --git a/models/issues/milestone_test.go b/models/issues/milestone_test.go index 7477af92c8..e5f6f15ca2 100644 --- a/models/issues/milestone_test.go +++ b/models/issues/milestone_test.go @@ -11,10 +11,10 @@ import ( issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" ) @@ -39,10 +39,10 @@ func TestGetMilestoneByRepoID(t *testing.T) { func TestGetMilestonesByRepoID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) test := func(repoID int64, state api.StateType) { - var isClosed util.OptionalBool + var isClosed optional.Option[bool] switch state { case api.StateClosed, api.StateOpen: - isClosed = util.OptionalBoolOf(state == api.StateClosed) + isClosed = optional.Some(state == api.StateClosed) } repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}) milestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{ @@ -84,7 +84,7 @@ func TestGetMilestonesByRepoID(t *testing.T) { milestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{ RepoID: unittest.NonexistentID, - IsClosed: util.OptionalBoolFalse, + IsClosed: optional.Some(false), }) assert.NoError(t, err) assert.Len(t, milestones, 0) @@ -101,7 +101,7 @@ func TestGetMilestones(t *testing.T) { PageSize: setting.UI.IssuePagingNum, }, RepoID: repo.ID, - IsClosed: util.OptionalBoolFalse, + IsClosed: optional.Some(false), SortType: sortType, }) assert.NoError(t, err) @@ -118,7 +118,7 @@ func TestGetMilestones(t *testing.T) { PageSize: setting.UI.IssuePagingNum, }, RepoID: repo.ID, - IsClosed: util.OptionalBoolTrue, + IsClosed: optional.Some(true), Name: "", SortType: sortType, }) @@ -178,7 +178,7 @@ func TestCountRepoClosedMilestones(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}) count, err := db.Count[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{ RepoID: repoID, - IsClosed: util.OptionalBoolTrue, + IsClosed: optional.Some(true), }) assert.NoError(t, err) assert.EqualValues(t, repo.NumClosedMilestones, count) @@ -189,7 +189,7 @@ func TestCountRepoClosedMilestones(t *testing.T) { count, err := db.Count[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{ RepoID: unittest.NonexistentID, - IsClosed: util.OptionalBoolTrue, + IsClosed: optional.Some(true), }) assert.NoError(t, err) assert.EqualValues(t, 0, count) @@ -206,7 +206,7 @@ func TestCountMilestonesByRepoIDs(t *testing.T) { openCounts, err := issues_model.CountMilestonesMap(db.DefaultContext, issues_model.FindMilestoneOptions{ RepoIDs: []int64{1, 2}, - IsClosed: util.OptionalBoolFalse, + IsClosed: optional.Some(false), }) assert.NoError(t, err) assert.EqualValues(t, repo1OpenCount, openCounts[1]) @@ -215,7 +215,7 @@ func TestCountMilestonesByRepoIDs(t *testing.T) { closedCounts, err := issues_model.CountMilestonesMap(db.DefaultContext, issues_model.FindMilestoneOptions{ RepoIDs: []int64{1, 2}, - IsClosed: util.OptionalBoolTrue, + IsClosed: optional.Some(true), }) assert.NoError(t, err) assert.EqualValues(t, repo1ClosedCount, closedCounts[1]) @@ -234,7 +234,7 @@ func TestGetMilestonesByRepoIDs(t *testing.T) { PageSize: setting.UI.IssuePagingNum, }, RepoIDs: []int64{repo1.ID, repo2.ID}, - IsClosed: util.OptionalBoolFalse, + IsClosed: optional.Some(false), SortType: sortType, }) assert.NoError(t, err) @@ -252,7 +252,7 @@ func TestGetMilestonesByRepoIDs(t *testing.T) { PageSize: setting.UI.IssuePagingNum, }, RepoIDs: []int64{repo1.ID, repo2.ID}, - IsClosed: util.OptionalBoolTrue, + IsClosed: optional.Some(true), SortType: sortType, }) assert.NoError(t, err) diff --git a/models/issues/reaction.go b/models/issues/reaction.go index bb47cf24ca..d5448636fe 100644 --- a/models/issues/reaction.go +++ b/models/issues/reaction.go @@ -240,25 +240,6 @@ func CreateReaction(ctx context.Context, opts *ReactionOptions) (*Reaction, erro return reaction, nil } -// CreateIssueReaction creates a reaction on issue. -func CreateIssueReaction(ctx context.Context, doerID, issueID int64, content string) (*Reaction, error) { - return CreateReaction(ctx, &ReactionOptions{ - Type: content, - DoerID: doerID, - IssueID: issueID, - }) -} - -// CreateCommentReaction creates a reaction on comment. -func CreateCommentReaction(ctx context.Context, doerID, issueID, commentID int64, content string) (*Reaction, error) { - return CreateReaction(ctx, &ReactionOptions{ - Type: content, - DoerID: doerID, - IssueID: issueID, - CommentID: commentID, - }) -} - // DeleteReaction deletes reaction for issue or comment. func DeleteReaction(ctx context.Context, opts *ReactionOptions) error { reaction := &Reaction{ diff --git a/models/issues/review_list.go b/models/issues/review_list.go index 282f18b4f7..ec6cb07988 100644 --- a/models/issues/review_list.go +++ b/models/issues/review_list.go @@ -9,7 +9,7 @@ import ( "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/container" - "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/optional" "xorm.io/builder" ) @@ -68,7 +68,7 @@ type FindReviewOptions struct { IssueID int64 ReviewerID int64 OfficialOnly bool - Dismissed util.OptionalBool + Dismissed optional.Option[bool] } func (opts *FindReviewOptions) toCond() builder.Cond { @@ -85,8 +85,8 @@ func (opts *FindReviewOptions) toCond() builder.Cond { if opts.OfficialOnly { cond = cond.And(builder.Eq{"official": true}) } - if !opts.Dismissed.IsNone() { - cond = cond.And(builder.Eq{"dismissed": opts.Dismissed.IsTrue()}) + if opts.Dismissed.Has() { + cond = cond.And(builder.Eq{"dismissed": opts.Dismissed.Value()}) } return cond } diff --git a/models/issues/tracked_time.go b/models/issues/tracked_time.go index 91c4832e49..4063ca043b 100644 --- a/models/issues/tracked_time.go +++ b/models/issues/tracked_time.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" @@ -340,7 +341,7 @@ func GetTrackedTimeByID(ctx context.Context, id int64) (*TrackedTime, error) { } // GetIssueTotalTrackedTime returns the total tracked time for issues by given conditions. -func GetIssueTotalTrackedTime(ctx context.Context, opts *IssuesOptions, isClosed util.OptionalBool) (int64, error) { +func GetIssueTotalTrackedTime(ctx context.Context, opts *IssuesOptions, isClosed optional.Option[bool]) (int64, error) { if len(opts.IssueIDs) <= MaxQueryParameters { return getIssueTotalTrackedTimeChunk(ctx, opts, isClosed, opts.IssueIDs) } @@ -363,7 +364,7 @@ func GetIssueTotalTrackedTime(ctx context.Context, opts *IssuesOptions, isClosed return accum, nil } -func getIssueTotalTrackedTimeChunk(ctx context.Context, opts *IssuesOptions, isClosed util.OptionalBool, issueIDs []int64) (int64, error) { +func getIssueTotalTrackedTimeChunk(ctx context.Context, opts *IssuesOptions, isClosed optional.Option[bool], issueIDs []int64) (int64, error) { sumSession := func(opts *IssuesOptions, issueIDs []int64) *xorm.Session { sess := db.GetEngine(ctx). Table("tracked_time"). @@ -378,8 +379,8 @@ func getIssueTotalTrackedTimeChunk(ctx context.Context, opts *IssuesOptions, isC } session := sumSession(opts, issueIDs) - if !isClosed.IsNone() { - session = session.And("issue.is_closed = ?", isClosed.IsTrue()) + if isClosed.Has() { + session = session.And("issue.is_closed = ?", isClosed.Value()) } return session.SumInt(new(trackedTime), "tracked_time.time") } diff --git a/models/issues/tracked_time_test.go b/models/issues/tracked_time_test.go index 9beb862ffb..d82bff967a 100644 --- a/models/issues/tracked_time_test.go +++ b/models/issues/tracked_time_test.go @@ -11,7 +11,7 @@ import ( issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/optional" "github.com/stretchr/testify/assert" ) @@ -120,15 +120,15 @@ func TestTotalTimesForEachUser(t *testing.T) { func TestGetIssueTotalTrackedTime(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - ttt, err := issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, util.OptionalBoolFalse) + ttt, err := issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, optional.Some(false)) assert.NoError(t, err) assert.EqualValues(t, 3682, ttt) - ttt, err = issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, util.OptionalBoolTrue) + ttt, err = issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, optional.Some(true)) assert.NoError(t, err) assert.EqualValues(t, 0, ttt) - ttt, err = issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, util.OptionalBoolNone) + ttt, err = issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, optional.None[bool]()) assert.NoError(t, err) assert.EqualValues(t, 3682, ttt) } diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index c27537b305..9c78c0a380 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -558,6 +558,10 @@ var migrations = []Migration{ NewMigration("Add PreviousDuration to ActionRun", v1_22.AddPreviousDurationToActionRun), // v286 -> v287 NewMigration("Add support for SHA256 git repositories", v1_22.AdjustDBForSha256), + // v287 -> v288 + NewMigration("Use Slug instead of ID for Badges", v1_22.UseSlugInsteadOfIDForBadges), + // v288 -> v289 + NewMigration("Add user_blocking table", v1_22.AddUserBlockingTable), // to modify later NewMigration("Add size limit on repository", v1_22.AddSizeLimitOnRepo), } diff --git a/models/migrations/v1_22/v287.go b/models/migrations/v1_22/v287.go new file mode 100644 index 0000000000..c8b1593286 --- /dev/null +++ b/models/migrations/v1_22/v287.go @@ -0,0 +1,46 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_22 //nolint + +import ( + "xorm.io/xorm" +) + +type BadgeUnique struct { + ID int64 `xorm:"pk autoincr"` + Slug string `xorm:"UNIQUE"` +} + +func (BadgeUnique) TableName() string { + return "badge" +} + +func UseSlugInsteadOfIDForBadges(x *xorm.Engine) error { + type Badge struct { + Slug string + } + + err := x.Sync(new(Badge)) + if err != nil { + return err + } + + sess := x.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { + return err + } + + _, err = sess.Exec("UPDATE `badge` SET `slug` = `id` Where `slug` IS NULL") + if err != nil { + return err + } + + err = sess.Sync(new(BadgeUnique)) + if err != nil { + return err + } + + return sess.Commit() +} diff --git a/models/migrations/v1_22/v287_test.go b/models/migrations/v1_22/v287_test.go new file mode 100644 index 0000000000..19c7ae3b91 --- /dev/null +++ b/models/migrations/v1_22/v287_test.go @@ -0,0 +1,57 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_22 //nolint + +import ( + "fmt" + "testing" + + "code.gitea.io/gitea/models/migrations/base" + + "github.com/stretchr/testify/assert" +) + +func Test_UpdateBadgeColName(t *testing.T) { + type Badge struct { + ID int64 `xorm:"pk autoincr"` + Description string + ImageURL string + } + + // Prepare and load the testing database + x, deferable := base.PrepareTestEnv(t, 0, new(BadgeUnique), new(Badge)) + defer deferable() + if x == nil || t.Failed() { + return + } + + oldBadges := []Badge{ + {ID: 1, Description: "Test Badge 1", ImageURL: "https://example.com/badge1.png"}, + {ID: 2, Description: "Test Badge 2", ImageURL: "https://example.com/badge2.png"}, + {ID: 3, Description: "Test Badge 3", ImageURL: "https://example.com/badge3.png"}, + } + + for _, badge := range oldBadges { + _, err := x.Insert(&badge) + assert.NoError(t, err) + } + + if err := UseSlugInsteadOfIDForBadges(x); err != nil { + assert.NoError(t, err) + return + } + + got := []BadgeUnique{} + if err := x.Table("badge").Asc("id").Find(&got); !assert.NoError(t, err) { + return + } + + for i, e := range oldBadges { + got := got[i] + assert.Equal(t, e.ID, got.ID) + assert.Equal(t, fmt.Sprintf("%d", e.ID), got.Slug) + } + + // TODO: check if badges have been updated +} diff --git a/models/migrations/v1_22/v288.go b/models/migrations/v1_22/v288.go new file mode 100644 index 0000000000..7c93bfcc66 --- /dev/null +++ b/models/migrations/v1_22/v288.go @@ -0,0 +1,26 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_22 //nolint + +import ( + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" +) + +type Blocking struct { + ID int64 `xorm:"pk autoincr"` + BlockerID int64 `xorm:"UNIQUE(block)"` + BlockeeID int64 `xorm:"UNIQUE(block)"` + Note string + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` +} + +func (*Blocking) TableName() string { + return "user_blocking" +} + +func AddUserBlockingTable(x *xorm.Engine) error { + return x.Sync(&Blocking{}) +} diff --git a/models/org.go b/models/org.go index 5f61f05b16..69cc47137e 100644 --- a/models/org.go +++ b/models/org.go @@ -12,15 +12,16 @@ import ( "code.gitea.io/gitea/models/organization" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" ) // RemoveOrgUser removes user from given organization. -func RemoveOrgUser(ctx context.Context, orgID, userID int64) error { +func RemoveOrgUser(ctx context.Context, org *organization.Organization, user *user_model.User) error { ou := new(organization.OrgUser) has, err := db.GetEngine(ctx). - Where("uid=?", userID). - And("org_id=?", orgID). + Where("uid=?", user.ID). + And("org_id=?", org.ID). Get(ou) if err != nil { return fmt.Errorf("get org-user: %w", err) @@ -28,13 +29,8 @@ func RemoveOrgUser(ctx context.Context, orgID, userID int64) error { return nil } - org, err := organization.GetOrgByID(ctx, orgID) - if err != nil { - return fmt.Errorf("GetUserByID [%d]: %w", orgID, err) - } - // Check if the user to delete is the last member in owner team. - if isOwner, err := organization.IsOrganizationOwner(ctx, orgID, userID); err != nil { + if isOwner, err := organization.IsOrganizationOwner(ctx, org.ID, user.ID); err != nil { return err } else if isOwner { t, err := organization.GetOwnerTeam(ctx, org.ID) @@ -45,8 +41,8 @@ func RemoveOrgUser(ctx context.Context, orgID, userID int64) error { if err := t.LoadMembers(ctx); err != nil { return err } - if t.Members[0].ID == userID { - return organization.ErrLastOrgOwner{UID: userID} + if t.Members[0].ID == user.ID { + return organization.ErrLastOrgOwner{UID: user.ID} } } } @@ -59,28 +55,32 @@ func RemoveOrgUser(ctx context.Context, orgID, userID int64) error { if _, err := db.DeleteByID[organization.OrgUser](ctx, ou.ID); err != nil { return err - } else if _, err = db.Exec(ctx, "UPDATE `user` SET num_members=num_members-1 WHERE id=?", orgID); err != nil { + } else if _, err = db.Exec(ctx, "UPDATE `user` SET num_members=num_members-1 WHERE id=?", org.ID); err != nil { return err } // Delete all repository accesses and unwatch them. - env, err := organization.AccessibleReposEnv(ctx, org, userID) + env, err := organization.AccessibleReposEnv(ctx, org, user.ID) if err != nil { return fmt.Errorf("AccessibleReposEnv: %w", err) } repoIDs, err := env.RepoIDs(1, org.NumRepos) if err != nil { - return fmt.Errorf("GetUserRepositories [%d]: %w", userID, err) + return fmt.Errorf("GetUserRepositories [%d]: %w", user.ID, err) } for _, repoID := range repoIDs { - if err = repo_model.WatchRepo(ctx, userID, repoID, false); err != nil { + repo, err := repo_model.GetRepositoryByID(ctx, repoID) + if err != nil { + return err + } + if err = repo_model.WatchRepo(ctx, user, repo, false); err != nil { return err } } if len(repoIDs) > 0 { if _, err = db.GetEngine(ctx). - Where("user_id = ?", userID). + Where("user_id = ?", user.ID). In("repo_id", repoIDs). Delete(new(access_model.Access)); err != nil { return err @@ -88,12 +88,12 @@ func RemoveOrgUser(ctx context.Context, orgID, userID int64) error { } // Delete member in their teams. - teams, err := organization.GetUserOrgTeams(ctx, org.ID, userID) + teams, err := organization.GetUserOrgTeams(ctx, org.ID, user.ID) if err != nil { return err } for _, t := range teams { - if err = removeTeamMember(ctx, t, userID); err != nil { + if err = removeTeamMember(ctx, t, user); err != nil { return err } } diff --git a/models/org_team.go b/models/org_team.go index 1a452436c3..aecf0d80fd 100644 --- a/models/org_team.go +++ b/models/org_team.go @@ -44,7 +44,7 @@ func AddRepository(ctx context.Context, t *organization.Team, repo *repo_model.R return fmt.Errorf("getMembers: %w", err) } for _, u := range t.Members { - if err = repo_model.WatchRepo(ctx, u.ID, repo.ID, true); err != nil { + if err = repo_model.WatchRepo(ctx, u, repo, true); err != nil { return fmt.Errorf("watchRepo: %w", err) } } @@ -125,7 +125,7 @@ func removeAllRepositories(ctx context.Context, t *organization.Team) (err error continue } - if err = repo_model.WatchRepo(ctx, user.ID, repo.ID, false); err != nil { + if err = repo_model.WatchRepo(ctx, user, repo, false); err != nil { return err } @@ -341,7 +341,7 @@ func DeleteTeam(ctx context.Context, t *organization.Team) error { } for _, tm := range t.Members { - if err := removeInvalidOrgUser(ctx, tm.ID, t.OrgID); err != nil { + if err := removeInvalidOrgUser(ctx, t.OrgID, tm); err != nil { return err } } @@ -356,19 +356,23 @@ func DeleteTeam(ctx context.Context, t *organization.Team) error { // AddTeamMember adds new membership of given team to given organization, // the user will have membership to given organization automatically when needed. -func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) error { - isAlreadyMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, userID) +func AddTeamMember(ctx context.Context, team *organization.Team, user *user_model.User) error { + if user_model.IsUserBlockedBy(ctx, user, team.OrgID) { + return user_model.ErrBlockedUser + } + + isAlreadyMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, user.ID) if err != nil || isAlreadyMember { return err } - if err := organization.AddOrgUser(ctx, team.OrgID, userID); err != nil { + if err := organization.AddOrgUser(ctx, team.OrgID, user.ID); err != nil { return err } err = db.WithTx(ctx, func(ctx context.Context) error { // check in transaction - isAlreadyMember, err = organization.IsTeamMember(ctx, team.OrgID, team.ID, userID) + isAlreadyMember, err = organization.IsTeamMember(ctx, team.OrgID, team.ID, user.ID) if err != nil || isAlreadyMember { return err } @@ -376,7 +380,7 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e sess := db.GetEngine(ctx) if err := db.Insert(ctx, &organization.TeamUser{ - UID: userID, + UID: user.ID, OrgID: team.OrgID, TeamID: team.ID, }); err != nil { @@ -392,7 +396,7 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e subQuery := builder.Select("repo_id").From("team_repo"). Where(builder.Eq{"team_id": team.ID}) - if _, err := sess.Where("user_id=?", userID). + if _, err := sess.Where("user_id=?", user.ID). In("repo_id", subQuery). And("mode < ?", team.AccessMode). SetExpr("mode", team.AccessMode). @@ -402,14 +406,14 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e // for not exist access var repoIDs []int64 - accessSubQuery := builder.Select("repo_id").From("access").Where(builder.Eq{"user_id": userID}) + accessSubQuery := builder.Select("repo_id").From("access").Where(builder.Eq{"user_id": user.ID}) if err := sess.SQL(subQuery.And(builder.NotIn("repo_id", accessSubQuery))).Find(&repoIDs); err != nil { return fmt.Errorf("select id accesses: %w", err) } accesses := make([]*access_model.Access, 0, 100) for i, repoID := range repoIDs { - accesses = append(accesses, &access_model.Access{RepoID: repoID, UserID: userID, Mode: team.AccessMode}) + accesses = append(accesses, &access_model.Access{RepoID: repoID, UserID: user.ID, Mode: team.AccessMode}) if (i%100 == 0 || i == len(repoIDs)-1) && len(accesses) > 0 { if err = db.Insert(ctx, accesses); err != nil { return fmt.Errorf("insert new user accesses: %w", err) @@ -430,10 +434,11 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e if err := team.LoadRepositories(ctx); err != nil { log.Error("team.LoadRepositories failed: %v", err) } + // FIXME: in the goroutine, it can't access the "ctx", it could only use db.DefaultContext at the moment go func(repos []*repo_model.Repository) { for _, repo := range repos { - if err = repo_model.WatchRepo(db.DefaultContext, userID, repo.ID, true); err != nil { + if err = repo_model.WatchRepo(db.DefaultContext, user, repo, true); err != nil { log.Error("watch repo failed: %v", err) } } @@ -443,16 +448,16 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e return nil } -func removeTeamMember(ctx context.Context, team *organization.Team, userID int64) error { +func removeTeamMember(ctx context.Context, team *organization.Team, user *user_model.User) error { e := db.GetEngine(ctx) - isMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, userID) + isMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, user.ID) if err != nil || !isMember { return err } // Check if the user to delete is the last member in owner team. if team.IsOwnerTeam() && team.NumMembers == 1 { - return organization.ErrLastOrgOwner{UID: userID} + return organization.ErrLastOrgOwner{UID: user.ID} } team.NumMembers-- @@ -462,7 +467,7 @@ func removeTeamMember(ctx context.Context, team *organization.Team, userID int64 } if _, err := e.Delete(&organization.TeamUser{ - UID: userID, + UID: user.ID, OrgID: team.OrgID, TeamID: team.ID, }); err != nil { @@ -476,76 +481,76 @@ func removeTeamMember(ctx context.Context, team *organization.Team, userID int64 // Delete access to team repositories. for _, repo := range team.Repos { - if err := access_model.RecalculateUserAccess(ctx, repo, userID); err != nil { + if err := access_model.RecalculateUserAccess(ctx, repo, user.ID); err != nil { return err } // Remove watches from now unaccessible - if err := ReconsiderWatches(ctx, repo, userID); err != nil { + if err := ReconsiderWatches(ctx, repo, user); err != nil { return err } // Remove issue assignments from now unaccessible - if err := ReconsiderRepoIssuesAssignee(ctx, repo, userID); err != nil { + if err := ReconsiderRepoIssuesAssignee(ctx, repo, user); err != nil { return err } } - return removeInvalidOrgUser(ctx, userID, team.OrgID) + return removeInvalidOrgUser(ctx, team.OrgID, user) } -func removeInvalidOrgUser(ctx context.Context, userID, orgID int64) error { +func removeInvalidOrgUser(ctx context.Context, orgID int64, user *user_model.User) error { // Check if the user is a member of any team in the organization. if count, err := db.GetEngine(ctx).Count(&organization.TeamUser{ - UID: userID, + UID: user.ID, OrgID: orgID, }); err != nil { return err } else if count == 0 { - return RemoveOrgUser(ctx, orgID, userID) + org, err := organization.GetOrgByID(ctx, orgID) + if err != nil { + return err + } + + return RemoveOrgUser(ctx, org, user) } return nil } // RemoveTeamMember removes member from given team of given organization. -func RemoveTeamMember(ctx context.Context, team *organization.Team, userID int64) error { +func RemoveTeamMember(ctx context.Context, team *organization.Team, user *user_model.User) error { ctx, committer, err := db.TxContext(ctx) if err != nil { return err } defer committer.Close() - if err := removeTeamMember(ctx, team, userID); err != nil { + if err := removeTeamMember(ctx, team, user); err != nil { return err } return committer.Commit() } -func ReconsiderRepoIssuesAssignee(ctx context.Context, repo *repo_model.Repository, uid int64) error { - user, err := user_model.GetUserByID(ctx, uid) - if err != nil { - return err - } - +func ReconsiderRepoIssuesAssignee(ctx context.Context, repo *repo_model.Repository, user *user_model.User) error { if canAssigned, err := access_model.CanBeAssigned(ctx, user, repo, true); err != nil || canAssigned { return err } - if _, err := db.GetEngine(ctx).Where(builder.Eq{"assignee_id": uid}). + if _, err := db.GetEngine(ctx).Where(builder.Eq{"assignee_id": user.ID}). In("issue_id", builder.Select("id").From("issue").Where(builder.Eq{"repo_id": repo.ID})). Delete(&issues_model.IssueAssignees{}); err != nil { - return fmt.Errorf("Could not delete assignee[%d] %w", uid, err) + return fmt.Errorf("Could not delete assignee[%d] %w", user.ID, err) } return nil } -func ReconsiderWatches(ctx context.Context, repo *repo_model.Repository, uid int64) error { - if has, err := access_model.HasAccess(ctx, uid, repo); err != nil || has { +func ReconsiderWatches(ctx context.Context, repo *repo_model.Repository, user *user_model.User) error { + if has, err := access_model.HasAccess(ctx, user.ID, repo); err != nil || has { return err } - if err := repo_model.WatchRepo(ctx, uid, repo.ID, false); err != nil { + if err := repo_model.WatchRepo(ctx, user, repo, false); err != nil { return err } // Remove all IssueWatches a user has subscribed to in the repository - return issues_model.RemoveIssueWatchersByRepoID(ctx, uid, repo.ID) + return issues_model.RemoveIssueWatchersByRepoID(ctx, user.ID, repo.ID) } diff --git a/models/org_team_test.go b/models/org_team_test.go index e4b7b917e8..cf2c8be536 100644 --- a/models/org_team_test.go +++ b/models/org_team_test.go @@ -21,33 +21,42 @@ import ( func TestTeam_AddMember(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - test := func(teamID, userID int64) { - team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID}) - assert.NoError(t, AddTeamMember(db.DefaultContext, team, userID)) - unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: userID, TeamID: teamID}) - unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID}, &user_model.User{ID: team.OrgID}) + test := func(team *organization.Team, user *user_model.User) { + assert.NoError(t, AddTeamMember(db.DefaultContext, team, user)) + unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID}) + unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID}, &user_model.User{ID: team.OrgID}) } - test(1, 2) - test(1, 4) - test(3, 2) + + team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1}) + team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3}) + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + + test(team1, user2) + test(team1, user4) + test(team3, user2) } func TestTeam_RemoveMember(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - testSuccess := func(teamID, userID int64) { - team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID}) - assert.NoError(t, RemoveTeamMember(db.DefaultContext, team, userID)) - unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: userID, TeamID: teamID}) - unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID}) + testSuccess := func(team *organization.Team, user *user_model.User) { + assert.NoError(t, RemoveTeamMember(db.DefaultContext, team, user)) + unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID}) + unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID}) } - testSuccess(1, 4) - testSuccess(2, 2) - testSuccess(3, 2) - testSuccess(3, unittest.NonexistentID) - team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1}) - err := RemoveTeamMember(db.DefaultContext, team, 2) + team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1}) + team2 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2}) + team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3}) + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + + testSuccess(team1, user4) + testSuccess(team2, user2) + testSuccess(team3, user2) + + err := RemoveTeamMember(db.DefaultContext, team1, user2) assert.True(t, organization.IsErrLastOrgOwner(err)) } @@ -120,33 +129,42 @@ func TestDeleteTeam(t *testing.T) { func TestAddTeamMember(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - test := func(teamID, userID int64) { - team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID}) - assert.NoError(t, AddTeamMember(db.DefaultContext, team, userID)) - unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: userID, TeamID: teamID}) - unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID}, &user_model.User{ID: team.OrgID}) + test := func(team *organization.Team, user *user_model.User) { + assert.NoError(t, AddTeamMember(db.DefaultContext, team, user)) + unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID}) + unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID}, &user_model.User{ID: team.OrgID}) } - test(1, 2) - test(1, 4) - test(3, 2) + + team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1}) + team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3}) + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + + test(team1, user2) + test(team1, user4) + test(team3, user2) } func TestRemoveTeamMember(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - testSuccess := func(teamID, userID int64) { - team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID}) - assert.NoError(t, RemoveTeamMember(db.DefaultContext, team, userID)) - unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: userID, TeamID: teamID}) - unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID}) + testSuccess := func(team *organization.Team, user *user_model.User) { + assert.NoError(t, RemoveTeamMember(db.DefaultContext, team, user)) + unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID}) + unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID}) } - testSuccess(1, 4) - testSuccess(2, 2) - testSuccess(3, 2) - testSuccess(3, unittest.NonexistentID) - team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1}) - err := RemoveTeamMember(db.DefaultContext, team, 2) + team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1}) + team2 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2}) + team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3}) + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + + testSuccess(team1, user4) + testSuccess(team2, user2) + testSuccess(team3, user2) + + err := RemoveTeamMember(db.DefaultContext, team1, user2) assert.True(t, organization.IsErrLastOrgOwner(err)) } @@ -155,15 +173,15 @@ func TestRepository_RecalculateAccesses3(t *testing.T) { team5 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 5}) user29 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29}) - has, err := db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: 29, RepoID: 23}) + has, err := db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: user29.ID, RepoID: 23}) assert.NoError(t, err) assert.False(t, has) // adding user29 to team5 should add an explicit access row for repo 23 // even though repo 23 is public - assert.NoError(t, AddTeamMember(db.DefaultContext, team5, user29.ID)) + assert.NoError(t, AddTeamMember(db.DefaultContext, team5, user29)) - has, err = db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: 29, RepoID: 23}) + has, err = db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: user29.ID, RepoID: 23}) assert.NoError(t, err) assert.True(t, has) } diff --git a/models/org_test.go b/models/org_test.go index d10a1dc218..247530406d 100644 --- a/models/org_test.go +++ b/models/org_test.go @@ -16,22 +16,27 @@ import ( func TestUser_RemoveMember(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) + org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) + user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) // remove a user that is a member - unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{UID: 4, OrgID: 3}) + unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{UID: user4.ID, OrgID: org.ID}) prevNumMembers := org.NumMembers - assert.NoError(t, RemoveOrgUser(db.DefaultContext, org.ID, 4)) - unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: 4, OrgID: 3}) - org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) + assert.NoError(t, RemoveOrgUser(db.DefaultContext, org, user4)) + unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: user4.ID, OrgID: org.ID}) + + org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: org.ID}) assert.Equal(t, prevNumMembers-1, org.NumMembers) // remove a user that is not a member - unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: 5, OrgID: 3}) + unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: user5.ID, OrgID: org.ID}) prevNumMembers = org.NumMembers - assert.NoError(t, RemoveOrgUser(db.DefaultContext, org.ID, 5)) - unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: 5, OrgID: 3}) - org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) + assert.NoError(t, RemoveOrgUser(db.DefaultContext, org, user5)) + unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: user5.ID, OrgID: org.ID}) + + org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: org.ID}) assert.Equal(t, prevNumMembers, org.NumMembers) unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{}) @@ -39,23 +44,31 @@ func TestUser_RemoveMember(t *testing.T) { func TestRemoveOrgUser(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - testSuccess := func(orgID, userID int64) { - org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: orgID}) + + testSuccess := func(org *organization.Organization, user *user_model.User) { expectedNumMembers := org.NumMembers - if unittest.BeanExists(t, &organization.OrgUser{OrgID: orgID, UID: userID}) { + if unittest.BeanExists(t, &organization.OrgUser{OrgID: org.ID, UID: user.ID}) { expectedNumMembers-- } - assert.NoError(t, RemoveOrgUser(db.DefaultContext, orgID, userID)) - unittest.AssertNotExistsBean(t, &organization.OrgUser{OrgID: orgID, UID: userID}) - org = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: orgID}) + assert.NoError(t, RemoveOrgUser(db.DefaultContext, org, user)) + unittest.AssertNotExistsBean(t, &organization.OrgUser{OrgID: org.ID, UID: user.ID}) + org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: org.ID}) assert.EqualValues(t, expectedNumMembers, org.NumMembers) } - testSuccess(3, 4) - testSuccess(3, 4) - err := RemoveOrgUser(db.DefaultContext, 7, 5) + org3 := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) + org7 := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 7}) + user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) + + testSuccess(org3, user4) + + org3 = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) + testSuccess(org3, user4) + + err := RemoveOrgUser(db.DefaultContext, org7, user5) assert.Error(t, err) assert.True(t, organization.IsErrLastOrgOwner(err)) - unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{OrgID: 7, UID: 5}) + unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{OrgID: org7.ID, UID: user5.ID}) unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{}) } diff --git a/models/organization/org.go b/models/organization/org.go index b4919defb4..a3082e9ac7 100644 --- a/models/organization/org.go +++ b/models/organization/org.go @@ -400,6 +400,7 @@ func DeleteOrganization(ctx context.Context, org *Organization) error { &TeamUnit{OrgID: org.ID}, &TeamInvite{OrgID: org.ID}, &secret_model.Secret{OwnerID: org.ID}, + &user_model.Blocking{BlockerID: org.ID}, ); err != nil { return fmt.Errorf("DeleteBeans: %w", err) } diff --git a/models/organization/team_user.go b/models/organization/team_user.go index ab767db200..d6d0a5054d 100644 --- a/models/organization/team_user.go +++ b/models/organization/team_user.go @@ -30,14 +30,6 @@ func IsTeamMember(ctx context.Context, orgID, teamID, userID int64) (bool, error Exist() } -// GetTeamUsersByTeamID returns team users for a team -func GetTeamUsersByTeamID(ctx context.Context, teamID int64) ([]*TeamUser, error) { - teamUsers := make([]*TeamUser, 0, 10) - return teamUsers, db.GetEngine(ctx). - Where("team_id=?", teamID). - Find(&teamUsers) -} - // SearchMembersOptions holds the search options type SearchMembersOptions struct { db.ListOptions diff --git a/models/packages/descriptor.go b/models/packages/descriptor.go index f849ab5c04..b8ef698d38 100644 --- a/models/packages/descriptor.go +++ b/models/packages/descriptor.go @@ -70,16 +70,26 @@ type PackageFileDescriptor struct { Properties PackagePropertyList } -// PackageWebLink returns the package web link +// PackageWebLink returns the relative package web link func (pd *PackageDescriptor) PackageWebLink() string { return fmt.Sprintf("%s/-/packages/%s/%s", pd.Owner.HomeLink(), string(pd.Package.Type), url.PathEscape(pd.Package.LowerName)) } -// FullWebLink returns the package version web link -func (pd *PackageDescriptor) FullWebLink() string { +// VersionWebLink returns the relative package version web link +func (pd *PackageDescriptor) VersionWebLink() string { return fmt.Sprintf("%s/%s", pd.PackageWebLink(), url.PathEscape(pd.Version.LowerVersion)) } +// PackageHTMLURL returns the absolute package HTML URL +func (pd *PackageDescriptor) PackageHTMLURL() string { + return fmt.Sprintf("%s/-/packages/%s/%s", pd.Owner.HTMLURL(), string(pd.Package.Type), url.PathEscape(pd.Package.LowerName)) +} + +// VersionHTMLURL returns the absolute package version HTML URL +func (pd *PackageDescriptor) VersionHTMLURL() string { + return fmt.Sprintf("%s/%s", pd.PackageHTMLURL(), url.PathEscape(pd.Version.LowerVersion)) +} + // CalculateBlobSize returns the total blobs size in bytes func (pd *PackageDescriptor) CalculateBlobSize() int64 { size := int64(0) diff --git a/models/packages/nuget/search.go b/models/packages/nuget/search.go index 53cdf2d4ad..7a505ff08f 100644 --- a/models/packages/nuget/search.go +++ b/models/packages/nuget/search.go @@ -55,7 +55,7 @@ func CountPackages(ctx context.Context, opts *packages_model.PackageSearchOption func toConds(opts *packages_model.PackageSearchOptions) builder.Cond { var cond builder.Cond = builder.Eq{ - "package.is_internal": opts.IsInternal.IsTrue(), + "package.is_internal": opts.IsInternal.Value(), "package.owner_id": opts.OwnerID, "package.type": packages_model.TypeNuGet, } diff --git a/models/packages/package_version.go b/models/packages/package_version.go index 8fc475691b..505dbaa0a5 100644 --- a/models/packages/package_version.go +++ b/models/packages/package_version.go @@ -9,6 +9,7 @@ import ( "strings" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -105,7 +106,7 @@ func getVersionByNameAndVersion(ctx context.Context, ownerID int64, packageType ExactMatch: true, Value: version, }, - IsInternal: util.OptionalBoolOf(isInternal), + IsInternal: optional.Some(isInternal), Paginator: db.NewAbsoluteListOptions(0, 1), }) if err != nil { @@ -122,7 +123,7 @@ func GetVersionsByPackageType(ctx context.Context, ownerID int64, packageType Ty pvs, _, err := SearchVersions(ctx, &PackageSearchOptions{ OwnerID: ownerID, Type: packageType, - IsInternal: util.OptionalBoolFalse, + IsInternal: optional.Some(false), }) return pvs, err } @@ -136,7 +137,7 @@ func GetVersionsByPackageName(ctx context.Context, ownerID int64, packageType Ty ExactMatch: true, Value: name, }, - IsInternal: util.OptionalBoolFalse, + IsInternal: optional.Some(false), }) return pvs, err } @@ -182,18 +183,18 @@ type PackageSearchOptions struct { Name SearchValue // only results with the specific name are found Version SearchValue // only results with the specific version are found Properties map[string]string // only results are found which contain all listed version properties with the specific value - IsInternal util.OptionalBool - HasFileWithName string // only results are found which are associated with a file with the specific name - HasFiles util.OptionalBool // only results are found which have associated files + IsInternal optional.Option[bool] + HasFileWithName string // only results are found which are associated with a file with the specific name + HasFiles optional.Option[bool] // only results are found which have associated files Sort VersionSort db.Paginator } func (opts *PackageSearchOptions) ToConds() builder.Cond { cond := builder.NewCond() - if !opts.IsInternal.IsNone() { + if opts.IsInternal.Has() { cond = builder.Eq{ - "package_version.is_internal": opts.IsInternal.IsTrue(), + "package_version.is_internal": opts.IsInternal.Value(), } } @@ -250,10 +251,10 @@ func (opts *PackageSearchOptions) ToConds() builder.Cond { cond = cond.And(builder.Exists(builder.Select("package_file.id").From("package_file").Where(fileCond))) } - if !opts.HasFiles.IsNone() { + if opts.HasFiles.Has() { filesCond := builder.Exists(builder.Select("package_file.id").From("package_file").Where(builder.Expr("package_file.version_id = package_version.id"))) - if opts.HasFiles.IsFalse() { + if !opts.HasFiles.Value() { filesCond = builder.Not{filesCond} } @@ -307,8 +308,8 @@ func SearchLatestVersions(ctx context.Context, opts *PackageSearchOptions) ([]*P And(builder.Expr("pv2.id IS NULL")) joinCond := builder.Expr("package_version.package_id = pv2.package_id AND (package_version.created_unix < pv2.created_unix OR (package_version.created_unix = pv2.created_unix AND package_version.id < pv2.id))") - if !opts.IsInternal.IsNone() { - joinCond = joinCond.And(builder.Eq{"pv2.is_internal": opts.IsInternal.IsTrue()}) + if opts.IsInternal.Has() { + joinCond = joinCond.And(builder.Eq{"pv2.is_internal": opts.IsInternal.Value()}) } sess := db.GetEngine(ctx). diff --git a/models/perm/access/access.go b/models/perm/access/access.go index 3e2568b4b4..b422a08614 100644 --- a/models/perm/access/access.go +++ b/models/perm/access/access.go @@ -128,9 +128,9 @@ func refreshAccesses(ctx context.Context, repo *repo_model.Repository, accessMap // refreshCollaboratorAccesses retrieves repository collaborations with their access modes. func refreshCollaboratorAccesses(ctx context.Context, repoID int64, accessMap map[int64]*userAccess) error { - collaborators, err := repo_model.GetCollaborators(ctx, repoID, db.ListOptions{}) + collaborators, _, err := repo_model.GetCollaborators(ctx, &repo_model.FindCollaborationOptions{RepoID: repoID}) if err != nil { - return fmt.Errorf("getCollaborations: %w", err) + return fmt.Errorf("GetCollaborators: %w", err) } for _, c := range collaborators { if c.User.IsGhost() { diff --git a/models/project/project.go b/models/project/project.go index d2fca6cdc8..8f9ee2a99e 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -6,11 +6,13 @@ package project import ( "context" "fmt" + "html/template" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -100,7 +102,7 @@ type Project struct { CardType CardType Type Type - RenderedContent string `xorm:"-"` + RenderedContent template.HTML `xorm:"-"` CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` @@ -195,7 +197,7 @@ type SearchOptions struct { db.ListOptions OwnerID int64 RepoID int64 - IsClosed util.OptionalBool + IsClosed optional.Option[bool] OrderBy db.SearchOrderBy Type Type Title string @@ -206,11 +208,8 @@ func (opts SearchOptions) ToConds() builder.Cond { if opts.RepoID > 0 { cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) } - switch opts.IsClosed { - case util.OptionalBoolTrue: - cond = cond.And(builder.Eq{"is_closed": true}) - case util.OptionalBoolFalse: - cond = cond.And(builder.Eq{"is_closed": false}) + if opts.IsClosed.Has() { + cond = cond.And(builder.Eq{"is_closed": opts.IsClosed.Value()}) } if opts.Type > 0 { diff --git a/models/repo/collaboration.go b/models/repo/collaboration.go index 7288082614..272c6ac05b 100644 --- a/models/repo/collaboration.go +++ b/models/repo/collaboration.go @@ -36,14 +36,44 @@ type Collaborator struct { Collaboration *Collaboration } +type FindCollaborationOptions struct { + db.ListOptions + RepoID int64 + RepoOwnerID int64 + CollaboratorID int64 +} + +func (opts *FindCollaborationOptions) ToConds() builder.Cond { + cond := builder.NewCond() + if opts.RepoID != 0 { + cond = cond.And(builder.Eq{"collaboration.repo_id": opts.RepoID}) + } + if opts.RepoOwnerID != 0 { + cond = cond.And(builder.Eq{"repository.owner_id": opts.RepoOwnerID}) + } + if opts.CollaboratorID != 0 { + cond = cond.And(builder.Eq{"collaboration.user_id": opts.CollaboratorID}) + } + return cond +} + +func (opts *FindCollaborationOptions) ToJoins() []db.JoinFunc { + if opts.RepoOwnerID != 0 { + return []db.JoinFunc{ + func(e db.Engine) error { + e.Join("INNER", "repository", "repository.id = collaboration.repo_id") + return nil + }, + } + } + return nil +} + // GetCollaborators returns the collaborators for a repository -func GetCollaborators(ctx context.Context, repoID int64, listOptions db.ListOptions) ([]*Collaborator, error) { - collaborations, err := db.Find[Collaboration](ctx, FindCollaborationOptions{ - ListOptions: listOptions, - RepoID: repoID, - }) +func GetCollaborators(ctx context.Context, opts *FindCollaborationOptions) ([]*Collaborator, int64, error) { + collaborations, total, err := db.FindAndCount[Collaboration](ctx, opts) if err != nil { - return nil, fmt.Errorf("db.Find[Collaboration]: %w", err) + return nil, 0, fmt.Errorf("db.FindAndCount[Collaboration]: %w", err) } collaborators := make([]*Collaborator, 0, len(collaborations)) @@ -54,7 +84,7 @@ func GetCollaborators(ctx context.Context, repoID int64, listOptions db.ListOpti usersMap := make(map[int64]*user_model.User) if err := db.GetEngine(ctx).In("id", userIDs).Find(&usersMap); err != nil { - return nil, fmt.Errorf("Find users map by user ids: %w", err) + return nil, 0, fmt.Errorf("Find users map by user ids: %w", err) } for _, c := range collaborations { @@ -67,7 +97,7 @@ func GetCollaborators(ctx context.Context, repoID int64, listOptions db.ListOpti Collaboration: c, }) } - return collaborators, nil + return collaborators, total, nil } // GetCollaboration get collaboration for a repository id with a user id @@ -88,15 +118,6 @@ func IsCollaborator(ctx context.Context, repoID, userID int64) (bool, error) { return db.GetEngine(ctx).Get(&Collaboration{RepoID: repoID, UserID: userID}) } -type FindCollaborationOptions struct { - db.ListOptions - RepoID int64 -} - -func (opts FindCollaborationOptions) ToConds() builder.Cond { - return builder.And(builder.Eq{"repo_id": opts.RepoID}) -} - // ChangeCollaborationAccessMode sets new access mode for the collaboration. func ChangeCollaborationAccessMode(ctx context.Context, repo *Repository, uid int64, mode perm.AccessMode) error { // Discard invalid input diff --git a/models/repo/collaboration_test.go b/models/repo/collaboration_test.go index 21a99dd557..639050f5fd 100644 --- a/models/repo/collaboration_test.go +++ b/models/repo/collaboration_test.go @@ -19,7 +19,7 @@ func TestRepository_GetCollaborators(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) test := func(repoID int64) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}) - collaborators, err := repo_model.GetCollaborators(db.DefaultContext, repo.ID, db.ListOptions{}) + collaborators, _, err := repo_model.GetCollaborators(db.DefaultContext, &repo_model.FindCollaborationOptions{RepoID: repo.ID}) assert.NoError(t, err) expectedLen, err := db.GetEngine(db.DefaultContext).Count(&repo_model.Collaboration{RepoID: repoID}) assert.NoError(t, err) @@ -37,11 +37,17 @@ func TestRepository_GetCollaborators(t *testing.T) { // Test db.ListOptions repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 22}) - collaborators1, err := repo_model.GetCollaborators(db.DefaultContext, repo.ID, db.ListOptions{PageSize: 1, Page: 1}) + collaborators1, _, err := repo_model.GetCollaborators(db.DefaultContext, &repo_model.FindCollaborationOptions{ + ListOptions: db.ListOptions{PageSize: 1, Page: 1}, + RepoID: repo.ID, + }) assert.NoError(t, err) assert.Len(t, collaborators1, 1) - collaborators2, err := repo_model.GetCollaborators(db.DefaultContext, repo.ID, db.ListOptions{PageSize: 1, Page: 2}) + collaborators2, _, err := repo_model.GetCollaborators(db.DefaultContext, &repo_model.FindCollaborationOptions{ + ListOptions: db.ListOptions{PageSize: 1, Page: 2}, + RepoID: repo.ID, + }) assert.NoError(t, err) assert.Len(t, collaborators2, 1) @@ -85,31 +91,6 @@ func TestRepository_ChangeCollaborationAccessMode(t *testing.T) { unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID}) } -func TestRepository_CountCollaborators(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) - count, err := db.Count[repo_model.Collaboration](db.DefaultContext, repo_model.FindCollaborationOptions{ - RepoID: repo1.ID, - }) - assert.NoError(t, err) - assert.EqualValues(t, 2, count) - - repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 22}) - count, err = db.Count[repo_model.Collaboration](db.DefaultContext, repo_model.FindCollaborationOptions{ - RepoID: repo2.ID, - }) - assert.NoError(t, err) - assert.EqualValues(t, 2, count) - - // Non-existent repository. - count, err = db.Count[repo_model.Collaboration](db.DefaultContext, repo_model.FindCollaborationOptions{ - RepoID: unittest.NonexistentID, - }) - assert.NoError(t, err) - assert.EqualValues(t, 0, count) -} - func TestRepository_IsOwnerMemberCollaborator(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) diff --git a/models/repo/release.go b/models/repo/release.go index 9287931dd5..a9f65f6c3e 100644 --- a/models/repo/release.go +++ b/models/repo/release.go @@ -7,6 +7,7 @@ package repo import ( "context" "fmt" + "html/template" "net/url" "sort" "strconv" @@ -80,7 +81,7 @@ type Release struct { NumCommits int64 NumCommitsBehind int64 `xorm:"-"` Note string `xorm:"TEXT"` - RenderedNote string `xorm:"-"` + RenderedNote template.HTML `xorm:"-"` IsDraft bool `xorm:"NOT NULL DEFAULT false"` IsPrerelease bool `xorm:"NOT NULL DEFAULT false"` IsTag bool `xorm:"NOT NULL DEFAULT false"` // will be true only if the record is a tag and has no related releases diff --git a/models/repo/repo.go b/models/repo/repo.go index 5529df66e1..d3e5d1ef8a 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -20,6 +20,7 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" @@ -412,6 +413,13 @@ func (repo *Repository) MustGetUnit(ctx context.Context, tp unit.Type) *RepoUnit Type: tp, Config: new(ActionsConfig), } + } else if tp == unit.TypeProjects { + cfg := new(ProjectsConfig) + cfg.ProjectsMode = ProjectsModeNone + return &RepoUnit{ + Type: tp, + Config: cfg, + } } return &RepoUnit{ @@ -863,7 +871,7 @@ func (repo *Repository) TemplateRepo(ctx context.Context) *Repository { type CountRepositoryOptions struct { OwnerID int64 - Private util.OptionalBool + Private optional.Option[bool] } // CountRepositories returns number of repositories. @@ -875,8 +883,8 @@ func CountRepositories(ctx context.Context, opts CountRepositoryOptions) (int64, if opts.OwnerID > 0 { sess.And("owner_id = ?", opts.OwnerID) } - if !opts.Private.IsNone() { - sess.And("is_private=?", opts.Private.IsTrue()) + if opts.Private.Has() { + sess.And("is_private=?", opts.Private.Value()) } count, err := sess.Count(new(Repository)) diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go index 533ca5251f..6b452291ea 100644 --- a/models/repo/repo_list.go +++ b/models/repo/repo_list.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" @@ -125,11 +126,11 @@ type SearchRepoOptions struct { // None -> include public and private // True -> include just private // False -> include just public - IsPrivate util.OptionalBool + IsPrivate optional.Option[bool] // None -> include collaborative AND non-collaborative // True -> include just collaborative // False -> include just non-collaborative - Collaborate util.OptionalBool + Collaborate optional.Option[bool] // What type of unit the user can be collaborative in, // it is ignored if Collaborate is False. // TypeInvalid means any unit type. @@ -137,19 +138,19 @@ type SearchRepoOptions struct { // None -> include forks AND non-forks // True -> include just forks // False -> include just non-forks - Fork util.OptionalBool + Fork optional.Option[bool] // None -> include templates AND non-templates // True -> include just templates // False -> include just non-templates - Template util.OptionalBool + Template optional.Option[bool] // None -> include mirrors AND non-mirrors // True -> include just mirrors // False -> include just non-mirrors - Mirror util.OptionalBool + Mirror optional.Option[bool] // None -> include archived AND non-archived // True -> include just archived // False -> include just non-archived - Archived util.OptionalBool + Archived optional.Option[bool] // only search topic name TopicOnly bool // only search repositories with specified primary language @@ -159,7 +160,7 @@ type SearchRepoOptions struct { // None -> include has milestones AND has no milestone // True -> include just has milestones // False -> include just has no milestone - HasMilestones util.OptionalBool + HasMilestones optional.Option[bool] // LowerNames represents valid lower names to restrict to LowerNames []string // When specified true, apply some filters over the conditions: @@ -359,12 +360,12 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { ))) } - if opts.IsPrivate != util.OptionalBoolNone { - cond = cond.And(builder.Eq{"is_private": opts.IsPrivate.IsTrue()}) + if opts.IsPrivate.Has() { + cond = cond.And(builder.Eq{"is_private": opts.IsPrivate.Value()}) } - if opts.Template != util.OptionalBoolNone { - cond = cond.And(builder.Eq{"is_template": opts.Template == util.OptionalBoolTrue}) + if opts.Template.Has() { + cond = cond.And(builder.Eq{"is_template": opts.Template.Value()}) } // Restrict to starred repositories @@ -380,11 +381,11 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { // Restrict repositories to those the OwnerID owns or contributes to as per opts.Collaborate if opts.OwnerID > 0 { accessCond := builder.NewCond() - if opts.Collaborate != util.OptionalBoolTrue { + if !opts.Collaborate.Value() { accessCond = builder.Eq{"owner_id": opts.OwnerID} } - if opts.Collaborate != util.OptionalBoolFalse { + if opts.Collaborate.ValueOrDefault(true) { // A Collaboration is: collaborateCond := builder.NewCond() @@ -472,31 +473,32 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { Where(builder.Eq{"language": opts.Language}).And(builder.Eq{"is_primary": true}))) } - if opts.Fork != util.OptionalBoolNone || opts.OnlyShowRelevant { - if opts.OnlyShowRelevant && opts.Fork == util.OptionalBoolNone { + if opts.Fork.Has() || opts.OnlyShowRelevant { + if opts.OnlyShowRelevant && !opts.Fork.Has() { cond = cond.And(builder.Eq{"is_fork": false}) } else { - cond = cond.And(builder.Eq{"is_fork": opts.Fork == util.OptionalBoolTrue}) + cond = cond.And(builder.Eq{"is_fork": opts.Fork.Value()}) } } - if opts.Mirror != util.OptionalBoolNone { - cond = cond.And(builder.Eq{"is_mirror": opts.Mirror == util.OptionalBoolTrue}) + if opts.Mirror.Has() { + cond = cond.And(builder.Eq{"is_mirror": opts.Mirror.Value()}) } if opts.Actor != nil && opts.Actor.IsRestricted { cond = cond.And(AccessibleRepositoryCondition(opts.Actor, unit.TypeInvalid)) } - if opts.Archived != util.OptionalBoolNone { - cond = cond.And(builder.Eq{"is_archived": opts.Archived == util.OptionalBoolTrue}) + if opts.Archived.Has() { + cond = cond.And(builder.Eq{"is_archived": opts.Archived.Value()}) } - switch opts.HasMilestones { - case util.OptionalBoolTrue: - cond = cond.And(builder.Gt{"num_milestones": 0}) - case util.OptionalBoolFalse: - cond = cond.And(builder.Eq{"num_milestones": 0}.Or(builder.IsNull{"num_milestones"})) + if opts.HasMilestones.Has() { + if opts.HasMilestones.Value() { + cond = cond.And(builder.Gt{"num_milestones": 0}) + } else { + cond = cond.And(builder.Eq{"num_milestones": 0}.Or(builder.IsNull{"num_milestones"})) + } } if opts.OnlyShowRelevant { diff --git a/models/repo/repo_list_test.go b/models/repo/repo_list_test.go index 83e37a27fd..88cfcde620 100644 --- a/models/repo/repo_list_test.go +++ b/models/repo/repo_list_test.go @@ -10,7 +10,7 @@ import ( "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" - "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/optional" "github.com/stretchr/testify/assert" ) @@ -27,62 +27,62 @@ func getTestCases() []struct { }{ { name: "PublicRepositoriesByName", - opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{PageSize: 10}, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{PageSize: 10}, Collaborate: optional.Some(false)}, count: 7, }, { name: "PublicAndPrivateRepositoriesByName", - opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, Collaborate: optional.Some(false)}, count: 14, }, { name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitFirstPage", - opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 5}, Private: true, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 5}, Private: true, Collaborate: optional.Some(false)}, count: 14, }, { name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitSecondPage", - opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 2, PageSize: 5}, Private: true, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 2, PageSize: 5}, Private: true, Collaborate: optional.Some(false)}, count: 14, }, { name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitThirdPage", - opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 3, PageSize: 5}, Private: true, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 3, PageSize: 5}, Private: true, Collaborate: optional.Some(false)}, count: 14, }, { name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitFourthPage", - opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 3, PageSize: 5}, Private: true, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 3, PageSize: 5}, Private: true, Collaborate: optional.Some(false)}, count: 14, }, { name: "PublicRepositoriesOfUser", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Collaborate: optional.Some(false)}, count: 2, }, { name: "PublicRepositoriesOfUser2", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Collaborate: optional.Some(false)}, count: 0, }, { name: "PublicRepositoriesOfOrg3", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Collaborate: optional.Some(false)}, count: 2, }, { name: "PublicAndPrivateRepositoriesOfUser", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, Collaborate: optional.Some(false)}, count: 4, }, { name: "PublicAndPrivateRepositoriesOfUser2", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true, Collaborate: optional.Some(false)}, count: 0, }, { name: "PublicAndPrivateRepositoriesOfOrg3", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Private: true, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Private: true, Collaborate: optional.Some(false)}, count: 4, }, { @@ -117,32 +117,32 @@ func getTestCases() []struct { }, { name: "PublicRepositoriesOfOrganization", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, Collaborate: optional.Some(false)}, count: 1, }, { name: "PublicAndPrivateRepositoriesOfOrganization", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, Private: true, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, Private: true, Collaborate: optional.Some(false)}, count: 2, }, { name: "AllPublic/PublicRepositoriesByName", - opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{PageSize: 10}, AllPublic: true, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{PageSize: 10}, AllPublic: true, Collaborate: optional.Some(false)}, count: 7, }, { name: "AllPublic/PublicAndPrivateRepositoriesByName", - opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, AllPublic: true, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, AllPublic: true, Collaborate: optional.Some(false)}, count: 14, }, { name: "AllPublic/PublicRepositoriesOfUserIncludingCollaborative", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, AllPublic: true, Template: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, AllPublic: true, Template: optional.Some(false)}, count: 33, }, { name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborative", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true, AllLimited: true, Template: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true, AllLimited: true, Template: optional.Some(false)}, count: 38, }, { @@ -157,12 +157,12 @@ func getTestCases() []struct { }, { name: "AllPublic/PublicRepositoriesOfOrganization", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, AllPublic: true, Collaborate: util.OptionalBoolFalse, Template: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, AllPublic: true, Collaborate: optional.Some(false), Template: optional.Some(false)}, count: 33, }, { name: "AllTemplates", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Template: util.OptionalBoolTrue}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Template: optional.Some(true)}, count: 2, }, { @@ -190,7 +190,7 @@ func TestSearchRepository(t *testing.T) { PageSize: 10, }, Keyword: "repo_12", - Collaborate: util.OptionalBoolFalse, + Collaborate: optional.Some(false), }) assert.NoError(t, err) @@ -205,7 +205,7 @@ func TestSearchRepository(t *testing.T) { PageSize: 10, }, Keyword: "test_repo", - Collaborate: util.OptionalBoolFalse, + Collaborate: optional.Some(false), }) assert.NoError(t, err) @@ -220,7 +220,7 @@ func TestSearchRepository(t *testing.T) { }, Keyword: "repo_13", Private: true, - Collaborate: util.OptionalBoolFalse, + Collaborate: optional.Some(false), }) assert.NoError(t, err) @@ -236,7 +236,7 @@ func TestSearchRepository(t *testing.T) { }, Keyword: "test_repo", Private: true, - Collaborate: util.OptionalBoolFalse, + Collaborate: optional.Some(false), }) assert.NoError(t, err) @@ -257,7 +257,7 @@ func TestSearchRepository(t *testing.T) { PageSize: 10, }, Keyword: "description_14", - Collaborate: util.OptionalBoolFalse, + Collaborate: optional.Some(false), IncludeDescription: true, }) @@ -274,7 +274,7 @@ func TestSearchRepository(t *testing.T) { PageSize: 10, }, Keyword: "description_14", - Collaborate: util.OptionalBoolFalse, + Collaborate: optional.Some(false), IncludeDescription: false, }) @@ -327,30 +327,25 @@ func TestSearchRepository(t *testing.T) { assert.False(t, repo.IsPrivate) } - if testCase.opts.Fork == util.OptionalBoolTrue && testCase.opts.Mirror == util.OptionalBoolTrue { - assert.True(t, repo.IsFork || repo.IsMirror) + if testCase.opts.Fork.Value() && testCase.opts.Mirror.Value() { + assert.True(t, repo.IsFork && repo.IsMirror) } else { - switch testCase.opts.Fork { - case util.OptionalBoolFalse: - assert.False(t, repo.IsFork) - case util.OptionalBoolTrue: - assert.True(t, repo.IsFork) + if testCase.opts.Fork.Has() { + assert.Equal(t, testCase.opts.Fork.Value(), repo.IsFork) } - switch testCase.opts.Mirror { - case util.OptionalBoolFalse: - assert.False(t, repo.IsMirror) - case util.OptionalBoolTrue: - assert.True(t, repo.IsMirror) + if testCase.opts.Mirror.Has() { + assert.Equal(t, testCase.opts.Mirror.Value(), repo.IsMirror) } } if testCase.opts.OwnerID > 0 && !testCase.opts.AllPublic { - switch testCase.opts.Collaborate { - case util.OptionalBoolFalse: - assert.Equal(t, testCase.opts.OwnerID, repo.Owner.ID) - case util.OptionalBoolTrue: - assert.NotEqual(t, testCase.opts.OwnerID, repo.Owner.ID) + if testCase.opts.Collaborate.Has() { + if testCase.opts.Collaborate.Value() { + assert.NotEqual(t, testCase.opts.OwnerID, repo.Owner.ID) + } else { + assert.Equal(t, testCase.opts.OwnerID, repo.Owner.ID) + } } } } diff --git a/models/repo/repo_test.go b/models/repo/repo_test.go index ca9209d751..c13b698abf 100644 --- a/models/repo/repo_test.go +++ b/models/repo/repo_test.go @@ -12,17 +12,17 @@ import ( "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/test" - "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" ) var ( countRepospts = repo_model.CountRepositoryOptions{OwnerID: 10} - countReposptsPublic = repo_model.CountRepositoryOptions{OwnerID: 10, Private: util.OptionalBoolFalse} - countReposptsPrivate = repo_model.CountRepositoryOptions{OwnerID: 10, Private: util.OptionalBoolTrue} + countReposptsPublic = repo_model.CountRepositoryOptions{OwnerID: 10, Private: optional.Some(false)} + countReposptsPrivate = repo_model.CountRepositoryOptions{OwnerID: 10, Private: optional.Some(true)} ) func TestGetRepositoryCount(t *testing.T) { @@ -64,16 +64,17 @@ func TestRepoAPIURL(t *testing.T) { func TestWatchRepo(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - const repoID = 3 - const userID = 2 - assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, userID, repoID, true)) - unittest.AssertExistsAndLoadBean(t, &repo_model.Watch{RepoID: repoID, UserID: userID}) - unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repoID}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, userID, repoID, false)) - unittest.AssertNotExistsBean(t, &repo_model.Watch{RepoID: repoID, UserID: userID}) - unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repoID}) + assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, user, repo, true)) + unittest.AssertExistsAndLoadBean(t, &repo_model.Watch{RepoID: repo.ID, UserID: user.ID}) + unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID}) + + assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, user, repo, false)) + unittest.AssertNotExistsBean(t, &repo_model.Watch{RepoID: repo.ID, UserID: user.ID}) + unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID}) } func TestMetas(t *testing.T) { diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go index 31a2a2e248..5a841f4d31 100644 --- a/models/repo/repo_unit.go +++ b/models/repo/repo_unit.go @@ -202,6 +202,53 @@ func (cfg *ActionsConfig) ToDB() ([]byte, error) { return json.Marshal(cfg) } +// ProjectsMode represents the projects enabled for a repository +type ProjectsMode string + +const ( + // ProjectsModeRepo allows only repo-level projects + ProjectsModeRepo ProjectsMode = "repo" + // ProjectsModeOwner allows only owner-level projects + ProjectsModeOwner ProjectsMode = "owner" + // ProjectsModeAll allows both kinds of projects + ProjectsModeAll ProjectsMode = "all" + // ProjectsModeNone doesn't allow projects + ProjectsModeNone ProjectsMode = "none" +) + +// ProjectsConfig describes projects config +type ProjectsConfig struct { + ProjectsMode ProjectsMode +} + +// FromDB fills up a ProjectsConfig from serialized format. +func (cfg *ProjectsConfig) FromDB(bs []byte) error { + return json.UnmarshalHandleDoubleEncode(bs, &cfg) +} + +// ToDB exports a ProjectsConfig to a serialized format. +func (cfg *ProjectsConfig) ToDB() ([]byte, error) { + return json.Marshal(cfg) +} + +func (cfg *ProjectsConfig) GetProjectsMode() ProjectsMode { + if cfg.ProjectsMode != "" { + return cfg.ProjectsMode + } + + return ProjectsModeAll +} + +func (cfg *ProjectsConfig) IsProjectsAllowed(m ProjectsMode) bool { + projectsMode := cfg.GetProjectsMode() + + if m == ProjectsModeNone { + return true + } + + return projectsMode == m || projectsMode == ProjectsModeAll +} + // BeforeSet is invoked from XORM before setting the value of a field of this object. func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) { switch colName { @@ -217,7 +264,9 @@ func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) { r.Config = new(IssuesConfig) case unit.TypeActions: r.Config = new(ActionsConfig) - case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypeProjects, unit.TypePackages: + case unit.TypeProjects: + r.Config = new(ProjectsConfig) + case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypePackages: fallthrough default: r.Config = new(UnitConfig) @@ -265,6 +314,11 @@ func (r *RepoUnit) ActionsConfig() *ActionsConfig { return r.Config.(*ActionsConfig) } +// ProjectsConfig returns config for unit.ProjectsConfig +func (r *RepoUnit) ProjectsConfig() *ProjectsConfig { + return r.Config.(*ProjectsConfig) +} + func getUnitsByRepoID(ctx context.Context, repoID int64) (units []*RepoUnit, err error) { var tmpUnits []*RepoUnit if err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Find(&tmpUnits); err != nil { diff --git a/models/repo/star.go b/models/repo/star.go index 60737149da..4c66855525 100644 --- a/models/repo/star.go +++ b/models/repo/star.go @@ -24,26 +24,30 @@ func init() { } // StarRepo or unstar repository. -func StarRepo(ctx context.Context, userID, repoID int64, star bool) error { +func StarRepo(ctx context.Context, doer *user_model.User, repo *Repository, star bool) error { ctx, committer, err := db.TxContext(ctx) if err != nil { return err } defer committer.Close() - staring := IsStaring(ctx, userID, repoID) + staring := IsStaring(ctx, doer.ID, repo.ID) if star { + if user_model.IsUserBlockedBy(ctx, doer, repo.OwnerID) { + return user_model.ErrBlockedUser + } + if staring { return nil } - if err := db.Insert(ctx, &Star{UID: userID, RepoID: repoID}); err != nil { + if err := db.Insert(ctx, &Star{UID: doer.ID, RepoID: repo.ID}); err != nil { return err } - if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars + 1 WHERE id = ?", repoID); err != nil { + if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars + 1 WHERE id = ?", repo.ID); err != nil { return err } - if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars + 1 WHERE id = ?", userID); err != nil { + if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars + 1 WHERE id = ?", doer.ID); err != nil { return err } } else { @@ -51,13 +55,13 @@ func StarRepo(ctx context.Context, userID, repoID int64, star bool) error { return nil } - if _, err := db.DeleteByBean(ctx, &Star{UID: userID, RepoID: repoID}); err != nil { + if _, err := db.DeleteByBean(ctx, &Star{UID: doer.ID, RepoID: repo.ID}); err != nil { return err } - if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars - 1 WHERE id = ?", repoID); err != nil { + if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars - 1 WHERE id = ?", repo.ID); err != nil { return err } - if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars - 1 WHERE id = ?", userID); err != nil { + if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars - 1 WHERE id = ?", doer.ID); err != nil { return err } } diff --git a/models/repo/star_test.go b/models/repo/star_test.go index 62eac4e29a..aaac89d975 100644 --- a/models/repo/star_test.go +++ b/models/repo/star_test.go @@ -9,21 +9,24 @@ import ( "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" "github.com/stretchr/testify/assert" ) func TestStarRepo(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - const userID = 2 - const repoID = 1 - unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID}) - assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, true)) - unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: userID, RepoID: repoID}) - assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, true)) - unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: userID, RepoID: repoID}) - assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, false)) - unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID}) + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + + unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID}) + assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, true)) + unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID}) + assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, true)) + unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID}) + assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, false)) + unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID}) } func TestIsStaring(t *testing.T) { @@ -54,17 +57,18 @@ func TestRepository_GetStargazers2(t *testing.T) { func TestClearRepoStars(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - const userID = 2 - const repoID = 1 - unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID}) - assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, true)) - unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: userID, RepoID: repoID}) - assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, false)) - unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID}) - assert.NoError(t, repo_model.ClearRepoStars(db.DefaultContext, repoID)) - unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID}) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + + unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID}) + assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, true)) + unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID}) + assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, false)) + unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID}) + assert.NoError(t, repo_model.ClearRepoStars(db.DefaultContext, repo.ID)) + unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID}) + gazers, err := repo_model.GetStargazers(db.DefaultContext, repo, db.ListOptions{Page: 0}) assert.NoError(t, err) assert.Len(t, gazers, 0) diff --git a/models/repo/user_repo.go b/models/repo/user_repo.go index 30c9db7474..6862247657 100644 --- a/models/repo/user_repo.go +++ b/models/repo/user_repo.go @@ -16,47 +16,82 @@ import ( "xorm.io/builder" ) +type StarredReposOptions struct { + db.ListOptions + StarrerID int64 + RepoOwnerID int64 + IncludePrivate bool +} + +func (opts *StarredReposOptions) ToConds() builder.Cond { + var cond builder.Cond = builder.Eq{ + "star.uid": opts.StarrerID, + } + if opts.RepoOwnerID != 0 { + cond = cond.And(builder.Eq{ + "repository.owner_id": opts.RepoOwnerID, + }) + } + if !opts.IncludePrivate { + cond = cond.And(builder.Eq{ + "repository.is_private": false, + }) + } + return cond +} + +func (opts *StarredReposOptions) ToJoins() []db.JoinFunc { + return []db.JoinFunc{ + func(e db.Engine) error { + e.Join("INNER", "star", "`repository`.id=`star`.repo_id") + return nil + }, + } +} + // GetStarredRepos returns the repos starred by a particular user -func GetStarredRepos(ctx context.Context, userID int64, private bool, listOptions db.ListOptions) ([]*Repository, error) { - sess := db.GetEngine(ctx). - Where("star.uid=?", userID). - Join("LEFT", "star", "`repository`.id=`star`.repo_id") - if !private { - sess = sess.And("is_private=?", false) +func GetStarredRepos(ctx context.Context, opts *StarredReposOptions) ([]*Repository, error) { + return db.Find[Repository](ctx, opts) +} + +type WatchedReposOptions struct { + db.ListOptions + WatcherID int64 + RepoOwnerID int64 + IncludePrivate bool +} + +func (opts *WatchedReposOptions) ToConds() builder.Cond { + var cond builder.Cond = builder.Eq{ + "watch.user_id": opts.WatcherID, } - - if listOptions.Page != 0 { - sess = db.SetSessionPagination(sess, &listOptions) - - repos := make([]*Repository, 0, listOptions.PageSize) - return repos, sess.Find(&repos) + if opts.RepoOwnerID != 0 { + cond = cond.And(builder.Eq{ + "repository.owner_id": opts.RepoOwnerID, + }) } + if !opts.IncludePrivate { + cond = cond.And(builder.Eq{ + "repository.is_private": false, + }) + } + return cond.And(builder.Neq{ + "watch.mode": WatchModeDont, + }) +} - repos := make([]*Repository, 0, 10) - return repos, sess.Find(&repos) +func (opts *WatchedReposOptions) ToJoins() []db.JoinFunc { + return []db.JoinFunc{ + func(e db.Engine) error { + e.Join("INNER", "watch", "`repository`.id=`watch`.repo_id") + return nil + }, + } } // GetWatchedRepos returns the repos watched by a particular user -func GetWatchedRepos(ctx context.Context, userID int64, private bool, listOptions db.ListOptions) ([]*Repository, int64, error) { - sess := db.GetEngine(ctx). - Where("watch.user_id=?", userID). - And("`watch`.mode<>?", WatchModeDont). - Join("LEFT", "watch", "`repository`.id=`watch`.repo_id") - if !private { - sess = sess.And("is_private=?", false) - } - - if listOptions.Page != 0 { - sess = db.SetSessionPagination(sess, &listOptions) - - repos := make([]*Repository, 0, listOptions.PageSize) - total, err := sess.FindAndCount(&repos) - return repos, total, err - } - - repos := make([]*Repository, 0, 10) - total, err := sess.FindAndCount(&repos) - return repos, total, err +func GetWatchedRepos(ctx context.Context, opts *WatchedReposOptions) ([]*Repository, int64, error) { + return db.FindAndCount[Repository](ctx, opts) } // GetRepoAssignees returns all users that have write access and can be assigned to issues diff --git a/models/repo/user_repo_test.go b/models/repo/user_repo_test.go index 7816b0262a..591dcea5b5 100644 --- a/models/repo/user_repo_test.go +++ b/models/repo/user_repo_test.go @@ -25,10 +25,8 @@ func TestRepoAssignees(t *testing.T) { repo21 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 21}) users, err = repo_model.GetRepoAssignees(db.DefaultContext, repo21) assert.NoError(t, err) - assert.Len(t, users, 3) - assert.Equal(t, users[0].ID, int64(15)) - assert.Equal(t, users[1].ID, int64(18)) - assert.Equal(t, users[2].ID, int64(16)) + assert.Len(t, users, 4) + assert.ElementsMatch(t, []int64{10, 15, 16, 18}, []int64{users[0].ID, users[1].ID, users[2].ID, users[3].ID}) } func TestRepoGetReviewers(t *testing.T) { diff --git a/models/repo/watch.go b/models/repo/watch.go index 80da4030cb..a616544cae 100644 --- a/models/repo/watch.go +++ b/models/repo/watch.go @@ -104,29 +104,23 @@ func watchRepoMode(ctx context.Context, watch Watch, mode WatchMode) (err error) return err } -// WatchRepoMode watch repository in specific mode. -func WatchRepoMode(ctx context.Context, userID, repoID int64, mode WatchMode) (err error) { - var watch Watch - if watch, err = GetWatch(ctx, userID, repoID); err != nil { - return err - } - return watchRepoMode(ctx, watch, mode) -} - // WatchRepo watch or unwatch repository. -func WatchRepo(ctx context.Context, userID, repoID int64, doWatch bool) (err error) { - var watch Watch - if watch, err = GetWatch(ctx, userID, repoID); err != nil { +func WatchRepo(ctx context.Context, doer *user_model.User, repo *Repository, doWatch bool) error { + watch, err := GetWatch(ctx, doer.ID, repo.ID) + if err != nil { return err } if !doWatch && watch.Mode == WatchModeAuto { - err = watchRepoMode(ctx, watch, WatchModeDont) + return watchRepoMode(ctx, watch, WatchModeDont) } else if !doWatch { - err = watchRepoMode(ctx, watch, WatchModeNone) - } else { - err = watchRepoMode(ctx, watch, WatchModeNormal) + return watchRepoMode(ctx, watch, WatchModeNone) } - return err + + if user_model.IsUserBlockedBy(ctx, doer, repo.OwnerID) { + return user_model.ErrBlockedUser + } + + return watchRepoMode(ctx, watch, WatchModeNormal) } // GetWatchers returns all watchers of given repository. diff --git a/models/repo/watch_test.go b/models/repo/watch_test.go index 7aa899291c..a95a267961 100644 --- a/models/repo/watch_test.go +++ b/models/repo/watch_test.go @@ -9,6 +9,7 @@ import ( "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" "github.com/stretchr/testify/assert" @@ -64,6 +65,8 @@ func TestWatchIfAuto(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + user12 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 12}) + watchers, err := repo_model.GetRepoWatchers(db.DefaultContext, repo.ID, db.ListOptions{Page: 1}) assert.NoError(t, err) assert.Len(t, watchers, repo.NumWatches) @@ -105,7 +108,7 @@ func TestWatchIfAuto(t *testing.T) { assert.Len(t, watchers, prevCount+1) // Should remove watch, inhibit from adding auto - assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, 12, 1, false)) + assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, user12, repo, false)) watchers, err = repo_model.GetRepoWatchers(db.DefaultContext, repo.ID, db.ListOptions{Page: 1}) assert.NoError(t, err) assert.Len(t, watchers, prevCount) @@ -116,24 +119,3 @@ func TestWatchIfAuto(t *testing.T) { assert.NoError(t, err) assert.Len(t, watchers, prevCount) } - -func TestWatchRepoMode(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 0) - - assert.NoError(t, repo_model.WatchRepoMode(db.DefaultContext, 12, 1, repo_model.WatchModeAuto)) - unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 1) - unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1, Mode: repo_model.WatchModeAuto}, 1) - - assert.NoError(t, repo_model.WatchRepoMode(db.DefaultContext, 12, 1, repo_model.WatchModeNormal)) - unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 1) - unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1, Mode: repo_model.WatchModeNormal}, 1) - - assert.NoError(t, repo_model.WatchRepoMode(db.DefaultContext, 12, 1, repo_model.WatchModeDont)) - unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 1) - unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1, Mode: repo_model.WatchModeDont}, 1) - - assert.NoError(t, repo_model.WatchRepoMode(db.DefaultContext, 12, 1, repo_model.WatchModeNone)) - unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 0) -} diff --git a/models/repo_transfer.go b/models/repo_transfer.go index 676e2dbb63..747ec2f248 100644 --- a/models/repo_transfer.go +++ b/models/repo_transfer.go @@ -13,6 +13,8 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/builder" ) // RepoTransfer is used to manage repository transfers @@ -94,21 +96,46 @@ func (r *RepoTransfer) CanUserAcceptTransfer(ctx context.Context, u *user_model. return allowed } +type PendingRepositoryTransferOptions struct { + RepoID int64 + SenderID int64 + RecipientID int64 +} + +func (opts *PendingRepositoryTransferOptions) ToConds() builder.Cond { + cond := builder.NewCond() + if opts.RepoID != 0 { + cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) + } + if opts.SenderID != 0 { + cond = cond.And(builder.Eq{"doer_id": opts.SenderID}) + } + if opts.RecipientID != 0 { + cond = cond.And(builder.Eq{"recipient_id": opts.RecipientID}) + } + return cond +} + +func GetPendingRepositoryTransfers(ctx context.Context, opts *PendingRepositoryTransferOptions) ([]*RepoTransfer, error) { + transfers := make([]*RepoTransfer, 0, 10) + return transfers, db.GetEngine(ctx). + Where(opts.ToConds()). + Find(&transfers) +} + // GetPendingRepositoryTransfer fetches the most recent and ongoing transfer // process for the repository func GetPendingRepositoryTransfer(ctx context.Context, repo *repo_model.Repository) (*RepoTransfer, error) { - transfer := new(RepoTransfer) - - has, err := db.GetEngine(ctx).Where("repo_id = ? ", repo.ID).Get(transfer) + transfers, err := GetPendingRepositoryTransfers(ctx, &PendingRepositoryTransferOptions{RepoID: repo.ID}) if err != nil { return nil, err } - if !has { + if len(transfers) != 1 { return nil, ErrNoPendingRepoTransfer{RepoID: repo.ID} } - return transfer, nil + return transfers[0], nil } func DeleteRepositoryTransfer(ctx context.Context, repoID int64) error { diff --git a/models/user/badge.go b/models/user/badge.go index ee52b44cf5..3ff3530a36 100644 --- a/models/user/badge.go +++ b/models/user/badge.go @@ -5,13 +5,15 @@ package user import ( "context" + "fmt" "code.gitea.io/gitea/models/db" ) // Badge represents a user badge type Badge struct { - ID int64 `xorm:"pk autoincr"` + ID int64 `xorm:"pk autoincr"` + Slug string `xorm:"UNIQUE"` Description string ImageURL string } @@ -39,3 +41,84 @@ func GetUserBadges(ctx context.Context, u *User) ([]*Badge, int64, error) { count, err := sess.FindAndCount(&badges) return badges, count, err } + +// CreateBadge creates a new badge. +func CreateBadge(ctx context.Context, badge *Badge) error { + _, err := db.GetEngine(ctx).Insert(badge) + return err +} + +// GetBadge returns a badge +func GetBadge(ctx context.Context, slug string) (*Badge, error) { + badge := new(Badge) + has, err := db.GetEngine(ctx).Where("slug=?", slug).Get(badge) + if !has { + return nil, err + } + return badge, err +} + +// UpdateBadge updates a badge based on its slug. +func UpdateBadge(ctx context.Context, badge *Badge) error { + _, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Update(badge) + return err +} + +// DeleteBadge deletes a badge. +func DeleteBadge(ctx context.Context, badge *Badge) error { + _, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Delete(badge) + return err +} + +// AddUserBadge adds a badge to a user. +func AddUserBadge(ctx context.Context, u *User, badge *Badge) error { + return AddUserBadges(ctx, u, []*Badge{badge}) +} + +// AddUserBadges adds badges to a user. +func AddUserBadges(ctx context.Context, u *User, badges []*Badge) error { + return db.WithTx(ctx, func(ctx context.Context) error { + for _, badge := range badges { + // hydrate badge and check if it exists + has, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Get(badge) + if err != nil { + return err + } else if !has { + return fmt.Errorf("badge with slug %s doesn't exist", badge.Slug) + } + if err := db.Insert(ctx, &UserBadge{ + BadgeID: badge.ID, + UserID: u.ID, + }); err != nil { + return err + } + } + return nil + }) +} + +// RemoveUserBadge removes a badge from a user. +func RemoveUserBadge(ctx context.Context, u *User, badge *Badge) error { + return RemoveUserBadges(ctx, u, []*Badge{badge}) +} + +// RemoveUserBadges removes badges from a user. +func RemoveUserBadges(ctx context.Context, u *User, badges []*Badge) error { + return db.WithTx(ctx, func(ctx context.Context) error { + for _, badge := range badges { + if _, err := db.GetEngine(ctx). + Join("INNER", "badge", "badge.id = `user_badge`.badge_id"). + Where("`user_badge`.user_id=? AND `badge`.slug=?", u.ID, badge.Slug). + Delete(&UserBadge{}); err != nil { + return err + } + } + return nil + }) +} + +// RemoveAllUserBadges removes all badges from a user. +func RemoveAllUserBadges(ctx context.Context, u *User) error { + _, err := db.GetEngine(ctx).Where("user_id=?", u.ID).Delete(&UserBadge{}) + return err +} diff --git a/models/user/block.go b/models/user/block.go new file mode 100644 index 0000000000..5f2b65a199 --- /dev/null +++ b/models/user/block.go @@ -0,0 +1,123 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import ( + "context" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" + + "xorm.io/builder" +) + +var ( + ErrBlockOrganization = util.NewInvalidArgumentErrorf("cannot block an organization") + ErrCanNotBlock = util.NewInvalidArgumentErrorf("cannot block the user") + ErrCanNotUnblock = util.NewInvalidArgumentErrorf("cannot unblock the user") + ErrBlockedUser = util.NewPermissionDeniedErrorf("user is blocked") +) + +type Blocking struct { + ID int64 `xorm:"pk autoincr"` + BlockerID int64 `xorm:"UNIQUE(block)"` + Blocker *User `xorm:"-"` + BlockeeID int64 `xorm:"UNIQUE(block)"` + Blockee *User `xorm:"-"` + Note string + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` +} + +func (*Blocking) TableName() string { + return "user_blocking" +} + +func init() { + db.RegisterModel(new(Blocking)) +} + +func UpdateBlockingNote(ctx context.Context, id int64, note string) error { + _, err := db.GetEngine(ctx).ID(id).Cols("note").Update(&Blocking{Note: note}) + return err +} + +func IsUserBlockedBy(ctx context.Context, blockee *User, blockerIDs ...int64) bool { + if len(blockerIDs) == 0 { + return false + } + + if blockee.IsAdmin { + return false + } + + cond := builder.Eq{"user_blocking.blockee_id": blockee.ID}. + And(builder.In("user_blocking.blocker_id", blockerIDs)) + + has, _ := db.GetEngine(ctx).Where(cond).Exist(&Blocking{}) + return has +} + +type FindBlockingOptions struct { + db.ListOptions + BlockerID int64 + BlockeeID int64 +} + +func (opts *FindBlockingOptions) ToConds() builder.Cond { + cond := builder.NewCond() + if opts.BlockerID != 0 { + cond = cond.And(builder.Eq{"user_blocking.blocker_id": opts.BlockerID}) + } + if opts.BlockeeID != 0 { + cond = cond.And(builder.Eq{"user_blocking.blockee_id": opts.BlockeeID}) + } + return cond +} + +func FindBlockings(ctx context.Context, opts *FindBlockingOptions) ([]*Blocking, int64, error) { + return db.FindAndCount[Blocking](ctx, opts) +} + +func GetBlocking(ctx context.Context, blockerID, blockeeID int64) (*Blocking, error) { + blocks, _, err := FindBlockings(ctx, &FindBlockingOptions{ + BlockerID: blockerID, + BlockeeID: blockeeID, + }) + if err != nil { + return nil, err + } + if len(blocks) == 0 { + return nil, nil + } + return blocks[0], nil +} + +type BlockingList []*Blocking + +func (blocks BlockingList) LoadAttributes(ctx context.Context) error { + ids := make(container.Set[int64], len(blocks)*2) + for _, b := range blocks { + ids.Add(b.BlockerID) + ids.Add(b.BlockeeID) + } + + userList, err := GetUsersByIDs(ctx, ids.Values()) + if err != nil { + return err + } + + userMap := make(map[int64]*User, len(userList)) + for _, u := range userList { + userMap[u.ID] = u + } + + for _, b := range blocks { + b.Blocker = userMap[b.BlockerID] + b.Blockee = userMap[b.BlockeeID] + } + + return nil +} diff --git a/models/user/email_address.go b/models/user/email_address.go index 9ddd1838d5..3cb2e8268c 100644 --- a/models/user/email_address.go +++ b/models/user/email_address.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/validation" @@ -153,37 +154,18 @@ func UpdateEmailAddress(ctx context.Context, email *EmailAddress) error { var emailRegexp = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") -// ValidateEmail check if email is a allowed address +// ValidateEmail check if email is a valid & allowed address func ValidateEmail(email string) error { - if len(email) == 0 { - return ErrEmailInvalid{email} + if err := validateEmailBasic(email); err != nil { + return err } + return validateEmailDomain(email) +} - if !emailRegexp.MatchString(email) { - return ErrEmailCharIsNotSupported{email} - } - - if email[0] == '-' { - return ErrEmailInvalid{email} - } - - if _, err := mail.ParseAddress(email); err != nil { - return ErrEmailInvalid{email} - } - - // if there is no allow list, then check email against block list - if len(setting.Service.EmailDomainAllowList) == 0 && - validation.IsEmailDomainListed(setting.Service.EmailDomainBlockList, email) { - return ErrEmailInvalid{email} - } - - // if there is an allow list, then check email against allow list - if len(setting.Service.EmailDomainAllowList) > 0 && - !validation.IsEmailDomainListed(setting.Service.EmailDomainAllowList, email) { - return ErrEmailInvalid{email} - } - - return nil +// ValidateEmailForAdmin check if email is a valid address when admins manually add users +func ValidateEmailForAdmin(email string) error { + return validateEmailBasic(email) + // In this case we do not need to check the email domain } func GetEmailAddressByEmail(ctx context.Context, email string) (*EmailAddress, error) { @@ -416,8 +398,8 @@ type SearchEmailOptions struct { db.ListOptions Keyword string SortType SearchEmailOrderBy - IsPrimary util.OptionalBool - IsActivated util.OptionalBool + IsPrimary optional.Option[bool] + IsActivated optional.Option[bool] } // SearchEmailResult is an e-mail address found in the user or email_address table @@ -444,18 +426,12 @@ func SearchEmails(ctx context.Context, opts *SearchEmailOptions) ([]*SearchEmail )) } - switch { - case opts.IsPrimary.IsTrue(): - cond = cond.And(builder.Eq{"email_address.is_primary": true}) - case opts.IsPrimary.IsFalse(): - cond = cond.And(builder.Eq{"email_address.is_primary": false}) + if opts.IsPrimary.Has() { + cond = cond.And(builder.Eq{"email_address.is_primary": opts.IsPrimary.Value()}) } - switch { - case opts.IsActivated.IsTrue(): - cond = cond.And(builder.Eq{"email_address.is_activated": true}) - case opts.IsActivated.IsFalse(): - cond = cond.And(builder.Eq{"email_address.is_activated": false}) + if opts.IsActivated.Has() { + cond = cond.And(builder.Eq{"email_address.is_activated": opts.IsActivated.Value()}) } count, err := db.GetEngine(ctx).Join("INNER", "`user`", "`user`.ID = email_address.uid"). @@ -539,3 +515,41 @@ func ActivateUserEmail(ctx context.Context, userID int64, email string, activate return committer.Commit() } + +// validateEmailBasic checks whether the email complies with the rules +func validateEmailBasic(email string) error { + if len(email) == 0 { + return ErrEmailInvalid{email} + } + + if !emailRegexp.MatchString(email) { + return ErrEmailCharIsNotSupported{email} + } + + if email[0] == '-' { + return ErrEmailInvalid{email} + } + + if _, err := mail.ParseAddress(email); err != nil { + return ErrEmailInvalid{email} + } + + return nil +} + +// validateEmailDomain checks whether the email domain is allowed or blocked +func validateEmailDomain(email string) error { + // if there is no allow list, then check email against block list + if len(setting.Service.EmailDomainAllowList) == 0 && + validation.IsEmailDomainListed(setting.Service.EmailDomainBlockList, email) { + return ErrEmailInvalid{email} + } + + // if there is an allow list, then check email against allow list + if len(setting.Service.EmailDomainAllowList) > 0 && + !validation.IsEmailDomainListed(setting.Service.EmailDomainAllowList, email) { + return ErrEmailInvalid{email} + } + + return nil +} diff --git a/models/user/email_address_test.go b/models/user/email_address_test.go index dc3073f98b..c2e010d95b 100644 --- a/models/user/email_address_test.go +++ b/models/user/email_address_test.go @@ -9,7 +9,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/optional" "github.com/stretchr/testify/assert" ) @@ -128,14 +128,14 @@ func TestListEmails(t *testing.T) { assert.True(t, contains(func(s *user_model.SearchEmailResult) bool { return s.UID == 27 })) // Must find only primary addresses (i.e. from the `user` table) - opts = &user_model.SearchEmailOptions{IsPrimary: util.OptionalBoolTrue} + opts = &user_model.SearchEmailOptions{IsPrimary: optional.Some(true)} emails, _, err = user_model.SearchEmails(db.DefaultContext, opts) assert.NoError(t, err) assert.True(t, contains(func(s *user_model.SearchEmailResult) bool { return s.IsPrimary })) assert.False(t, contains(func(s *user_model.SearchEmailResult) bool { return !s.IsPrimary })) // Must find only inactive addresses (i.e. not validated) - opts = &user_model.SearchEmailOptions{IsActivated: util.OptionalBoolFalse} + opts = &user_model.SearchEmailOptions{IsActivated: optional.Some(false)} emails, _, err = user_model.SearchEmails(db.DefaultContext, opts) assert.NoError(t, err) assert.True(t, contains(func(s *user_model.SearchEmailResult) bool { return !s.IsActivated })) diff --git a/models/user/follow.go b/models/user/follow.go index f4dd2891ff..cf9672109a 100644 --- a/models/user/follow.go +++ b/models/user/follow.go @@ -29,26 +29,30 @@ func IsFollowing(ctx context.Context, userID, followID int64) bool { } // FollowUser marks someone be another's follower. -func FollowUser(ctx context.Context, userID, followID int64) (err error) { - if userID == followID || IsFollowing(ctx, userID, followID) { +func FollowUser(ctx context.Context, user, follow *User) (err error) { + if user.ID == follow.ID || IsFollowing(ctx, user.ID, follow.ID) { return nil } + if IsUserBlockedBy(ctx, user, follow.ID) || IsUserBlockedBy(ctx, follow, user.ID) { + return ErrBlockedUser + } + ctx, committer, err := db.TxContext(ctx) if err != nil { return err } defer committer.Close() - if err = db.Insert(ctx, &Follow{UserID: userID, FollowID: followID}); err != nil { + if err = db.Insert(ctx, &Follow{UserID: user.ID, FollowID: follow.ID}); err != nil { return err } - if _, err = db.Exec(ctx, "UPDATE `user` SET num_followers = num_followers + 1 WHERE id = ?", followID); err != nil { + if _, err = db.Exec(ctx, "UPDATE `user` SET num_followers = num_followers + 1 WHERE id = ?", follow.ID); err != nil { return err } - if _, err = db.Exec(ctx, "UPDATE `user` SET num_following = num_following + 1 WHERE id = ?", userID); err != nil { + if _, err = db.Exec(ctx, "UPDATE `user` SET num_following = num_following + 1 WHERE id = ?", user.ID); err != nil { return err } return committer.Commit() diff --git a/models/user/search.go b/models/user/search.go index 9484bf4425..45b051187e 100644 --- a/models/user/search.go +++ b/models/user/search.go @@ -10,8 +10,8 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/util" "xorm.io/builder" "xorm.io/xorm" @@ -33,11 +33,11 @@ type SearchUserOptions struct { SupportedSortOrders container.Set[string] // if not nil, only allow to use the sort orders in this set - IsActive util.OptionalBool - IsAdmin util.OptionalBool - IsRestricted util.OptionalBool - IsTwoFactorEnabled util.OptionalBool - IsProhibitLogin util.OptionalBool + IsActive optional.Option[bool] + IsAdmin optional.Option[bool] + IsRestricted optional.Option[bool] + IsTwoFactorEnabled optional.Option[bool] + IsProhibitLogin optional.Option[bool] IncludeReserved bool ExtraParamStrings map[string]string @@ -89,24 +89,24 @@ func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Sess cond = cond.And(builder.Eq{"login_name": opts.LoginName}) } - if !opts.IsActive.IsNone() { - cond = cond.And(builder.Eq{"is_active": opts.IsActive.IsTrue()}) + if opts.IsActive.Has() { + cond = cond.And(builder.Eq{"is_active": opts.IsActive.Value()}) } - if !opts.IsAdmin.IsNone() { - cond = cond.And(builder.Eq{"is_admin": opts.IsAdmin.IsTrue()}) + if opts.IsAdmin.Has() { + cond = cond.And(builder.Eq{"is_admin": opts.IsAdmin.Value()}) } - if !opts.IsRestricted.IsNone() { - cond = cond.And(builder.Eq{"is_restricted": opts.IsRestricted.IsTrue()}) + if opts.IsRestricted.Has() { + cond = cond.And(builder.Eq{"is_restricted": opts.IsRestricted.Value()}) } - if !opts.IsProhibitLogin.IsNone() { - cond = cond.And(builder.Eq{"prohibit_login": opts.IsProhibitLogin.IsTrue()}) + if opts.IsProhibitLogin.Has() { + cond = cond.And(builder.Eq{"prohibit_login": opts.IsProhibitLogin.Value()}) } e := db.GetEngine(ctx) - if opts.IsTwoFactorEnabled.IsNone() { + if !opts.IsTwoFactorEnabled.Has() { return e.Where(cond) } @@ -114,7 +114,7 @@ func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Sess // While using LEFT JOIN, sometimes the performance might not be good, but it won't be a problem now, such SQL is seldom executed. // There are some possible methods to refactor this SQL in future when we really need to optimize the performance (but not now): // (1) add a column in user table (2) add a setting value in user_setting table (3) use search engines (bleve/elasticsearch) - if opts.IsTwoFactorEnabled.IsTrue() { + if opts.IsTwoFactorEnabled.Value() { cond = cond.And(builder.Expr("two_factor.uid IS NOT NULL")) } else { cond = cond.And(builder.Expr("two_factor.uid IS NULL")) @@ -131,7 +131,7 @@ func SearchUsers(ctx context.Context, opts *SearchUserOptions) (users []*User, _ defer sessCount.Close() count, err := sessCount.Count(new(User)) if err != nil { - return nil, 0, fmt.Errorf("Count: %w", err) + return nil, 0, fmt.Errorf("count: %w", err) } if len(opts.OrderBy) == 0 { diff --git a/models/user/user.go b/models/user/user.go index e92bbd4d0b..0bdda8655f 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -586,6 +586,16 @@ type CreateUserOverwriteOptions struct { // CreateUser creates record of a new user. func CreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err error) { + return createUser(ctx, u, false, overwriteDefault...) +} + +// AdminCreateUser is used by admins to manually create users +func AdminCreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err error) { + return createUser(ctx, u, true, overwriteDefault...) +} + +// createUser creates record of a new user. +func createUser(ctx context.Context, u *User, createdByAdmin bool, overwriteDefault ...*CreateUserOverwriteOptions) (err error) { if err = IsUsableUsername(u.Name); err != nil { return err } @@ -639,8 +649,14 @@ func CreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOve return err } - if err := ValidateEmail(u.Email); err != nil { - return err + if createdByAdmin { + if err := ValidateEmailForAdmin(u.Email); err != nil { + return err + } + } else { + if err := ValidateEmail(u.Email); err != nil { + return err + } } ctx, committer, err := db.TxContext(ctx) @@ -715,7 +731,7 @@ func CreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOve // IsLastAdminUser check whether user is the last admin func IsLastAdminUser(ctx context.Context, user *User) bool { - if user.IsAdmin && CountUsers(ctx, &CountUserFilter{IsAdmin: util.OptionalBoolTrue}) <= 1 { + if user.IsAdmin && CountUsers(ctx, &CountUserFilter{IsAdmin: optional.Some(true)}) <= 1 { return true } return false @@ -724,7 +740,7 @@ func IsLastAdminUser(ctx context.Context, user *User) bool { // CountUserFilter represent optional filters for CountUsers type CountUserFilter struct { LastLoginSince *int64 - IsAdmin util.OptionalBool + IsAdmin optional.Option[bool] } // CountUsers returns number of users. @@ -742,8 +758,8 @@ func countUsers(ctx context.Context, opts *CountUserFilter) int64 { cond = cond.And(builder.Gte{"last_login_unix": *opts.LastLoginSince}) } - if !opts.IsAdmin.IsNone() { - cond = cond.And(builder.Eq{"is_admin": opts.IsAdmin.IsTrue()}) + if opts.IsAdmin.Has() { + cond = cond.And(builder.Eq{"is_admin": opts.IsAdmin.Value()}) } } @@ -1167,7 +1183,7 @@ func IsUserVisibleToViewer(ctx context.Context, u, viewer *User) bool { return false } - // If they follow - they see each over + // If they follow - they see each other follower := IsFollowing(ctx, u.ID, viewer.ID) if follower { return true diff --git a/models/user/user_test.go b/models/user/user_test.go index 68cee9cdbd..f4efd071ea 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -16,10 +16,10 @@ import ( "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/auth/password/hash" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" ) @@ -103,29 +103,29 @@ func TestSearchUsers(t *testing.T) { testUserSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}}, []int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37, 38, 39, 40}) - testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolFalse}, + testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(false)}, []int64{9}) - testUserSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolTrue}, + testUserSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)}, []int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37, 38, 39, 40}) - testUserSuccess(&user_model.SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolTrue}, + testUserSuccess(&user_model.SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)}, []int64{1, 10, 11, 12, 13, 14, 15, 16, 18}) // order by name asc default - testUserSuccess(&user_model.SearchUserOptions{Keyword: "user1", ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolTrue}, + testUserSuccess(&user_model.SearchUserOptions{Keyword: "user1", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)}, []int64{1, 10, 11, 12, 13, 14, 15, 16, 18}) - testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsAdmin: util.OptionalBoolTrue}, + testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsAdmin: optional.Some(true)}, []int64{1}) - testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsRestricted: util.OptionalBoolTrue}, + testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsRestricted: optional.Some(true)}, []int64{29}) - testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsProhibitLogin: util.OptionalBoolTrue}, + testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsProhibitLogin: optional.Some(true)}, []int64{37}) - testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsTwoFactorEnabled: util.OptionalBoolTrue}, + testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsTwoFactorEnabled: optional.Some(true)}, []int64{24}) } @@ -399,14 +399,19 @@ func TestGetUserByOpenID(t *testing.T) { func TestFollowUser(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - testSuccess := func(followerID, followedID int64) { - assert.NoError(t, user_model.FollowUser(db.DefaultContext, followerID, followedID)) - unittest.AssertExistsAndLoadBean(t, &user_model.Follow{UserID: followerID, FollowID: followedID}) + testSuccess := func(follower, followed *user_model.User) { + assert.NoError(t, user_model.FollowUser(db.DefaultContext, follower, followed)) + unittest.AssertExistsAndLoadBean(t, &user_model.Follow{UserID: follower.ID, FollowID: followed.ID}) } - testSuccess(4, 2) - testSuccess(5, 2) - assert.NoError(t, user_model.FollowUser(db.DefaultContext, 2, 2)) + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) + + testSuccess(user4, user2) + testSuccess(user5, user2) + + assert.NoError(t, user_model.FollowUser(db.DefaultContext, user2, user2)) unittest.CheckConsistencyFor(t, &user_model.User{}) } diff --git a/models/webhook/webhook.go b/models/webhook/webhook.go index 4a84a3d411..894357e36a 100644 --- a/models/webhook/webhook.go +++ b/models/webhook/webhook.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/secret" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" @@ -433,7 +434,7 @@ type ListWebhookOptions struct { db.ListOptions RepoID int64 OwnerID int64 - IsActive util.OptionalBool + IsActive optional.Option[bool] } func (opts ListWebhookOptions) ToConds() builder.Cond { @@ -444,8 +445,8 @@ func (opts ListWebhookOptions) ToConds() builder.Cond { if opts.OwnerID != 0 { cond = cond.And(builder.Eq{"webhook.owner_id": opts.OwnerID}) } - if !opts.IsActive.IsNone() { - cond = cond.And(builder.Eq{"webhook.is_active": opts.IsActive.IsTrue()}) + if opts.IsActive.Has() { + cond = cond.And(builder.Eq{"webhook.is_active": opts.IsActive.Value()}) } return cond } diff --git a/models/webhook/webhook_system.go b/models/webhook/webhook_system.go index 2e89f9547b..a2a9ee321a 100644 --- a/models/webhook/webhook_system.go +++ b/models/webhook/webhook_system.go @@ -8,7 +8,7 @@ import ( "fmt" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/optional" ) // GetDefaultWebhooks returns all admin-default webhooks. @@ -34,15 +34,15 @@ func GetSystemOrDefaultWebhook(ctx context.Context, id int64) (*Webhook, error) } // GetSystemWebhooks returns all admin system webhooks. -func GetSystemWebhooks(ctx context.Context, isActive util.OptionalBool) ([]*Webhook, error) { +func GetSystemWebhooks(ctx context.Context, isActive optional.Option[bool]) ([]*Webhook, error) { webhooks := make([]*Webhook, 0, 5) - if isActive.IsNone() { + if !isActive.Has() { return webhooks, db.GetEngine(ctx). Where("repo_id=? AND owner_id=? AND is_system_webhook=?", 0, 0, true). Find(&webhooks) } return webhooks, db.GetEngine(ctx). - Where("repo_id=? AND owner_id=? AND is_system_webhook=? AND is_active = ?", 0, 0, true, isActive.IsTrue()). + Where("repo_id=? AND owner_id=? AND is_system_webhook=? AND is_active = ?", 0, 0, true, isActive.Value()). Find(&webhooks) } diff --git a/models/webhook/webhook_test.go b/models/webhook/webhook_test.go index 694fd7a873..c70c8e99fc 100644 --- a/models/webhook/webhook_test.go +++ b/models/webhook/webhook_test.go @@ -11,9 +11,9 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/optional" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/util" webhook_module "code.gitea.io/gitea/modules/webhook" "github.com/stretchr/testify/assert" @@ -123,7 +123,7 @@ func TestGetWebhookByOwnerID(t *testing.T) { func TestGetActiveWebhooksByRepoID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - hooks, err := db.Find[Webhook](db.DefaultContext, ListWebhookOptions{RepoID: 1, IsActive: util.OptionalBoolTrue}) + hooks, err := db.Find[Webhook](db.DefaultContext, ListWebhookOptions{RepoID: 1, IsActive: optional.Some(true)}) assert.NoError(t, err) if assert.Len(t, hooks, 1) { assert.Equal(t, int64(1), hooks[0].ID) @@ -143,7 +143,7 @@ func TestGetWebhooksByRepoID(t *testing.T) { func TestGetActiveWebhooksByOwnerID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - hooks, err := db.Find[Webhook](db.DefaultContext, ListWebhookOptions{OwnerID: 3, IsActive: util.OptionalBoolTrue}) + hooks, err := db.Find[Webhook](db.DefaultContext, ListWebhookOptions{OwnerID: 3, IsActive: optional.Some(true)}) assert.NoError(t, err) if assert.Len(t, hooks, 1) { assert.Equal(t, int64(3), hooks[0].ID) diff --git a/modules/actions/task_state.go b/modules/actions/task_state.go index cbbc0b357d..fe925bbb5d 100644 --- a/modules/actions/task_state.go +++ b/modules/actions/task_state.go @@ -35,6 +35,9 @@ func FullSteps(task *actions_model.ActionTask) []*actions_model.ActionTaskStep { } else if task.Status.IsDone() { preStep.Stopped = task.Stopped preStep.Status = actions_model.StatusFailure + if task.Status.IsSkipped() { + preStep.Status = actions_model.StatusSkipped + } } logIndex += preStep.LogLength diff --git a/modules/git/command.go b/modules/git/command.go index 9305ef6f92..371109730a 100644 --- a/modules/git/command.go +++ b/modules/git/command.go @@ -12,6 +12,7 @@ import ( "io" "os" "os/exec" + "runtime" "strings" "time" @@ -344,6 +345,17 @@ func (c *Command) Run(opts *RunOpts) error { log.Debug("slow git.Command.Run: %s (%s)", c, elapsed) } + // We need to check if the context is canceled by the program on Windows. + // This is because Windows does not have signal checking when terminating the process. + // It always returns exit code 1, unlike Linux, which has many exit codes for signals. + if runtime.GOOS == "windows" && + err != nil && + err.Error() == "" && + cmd.ProcessState.ExitCode() == 1 && + ctx.Err() == context.Canceled { + return ctx.Err() + } + if err != nil && ctx.Err() != context.DeadlineExceeded { return err } diff --git a/modules/indexer/issues/bleve/bleve.go b/modules/indexer/issues/bleve/bleve.go index 7c82cfbb79..6a5d65cb66 100644 --- a/modules/indexer/issues/bleve/bleve.go +++ b/modules/indexer/issues/bleve/bleve.go @@ -175,11 +175,11 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( queries = append(queries, bleve.NewDisjunctionQuery(repoQueries...)) } - if !options.IsPull.IsNone() { - queries = append(queries, inner_bleve.BoolFieldQuery(options.IsPull.IsTrue(), "is_pull")) + if options.IsPull.Has() { + queries = append(queries, inner_bleve.BoolFieldQuery(options.IsPull.Value(), "is_pull")) } - if !options.IsClosed.IsNone() { - queries = append(queries, inner_bleve.BoolFieldQuery(options.IsClosed.IsTrue(), "is_closed")) + if options.IsClosed.Has() { + queries = append(queries, inner_bleve.BoolFieldQuery(options.IsClosed.Value(), "is_closed")) } if options.NoLabelOnly { diff --git a/modules/indexer/issues/db/options.go b/modules/indexer/issues/db/options.go index 5406715bbc..69146573a8 100644 --- a/modules/indexer/issues/db/options.go +++ b/modules/indexer/issues/db/options.go @@ -11,6 +11,7 @@ import ( issue_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/indexer/issues/internal" + "code.gitea.io/gitea/modules/optional" ) func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_model.IssuesOptions, error) { @@ -75,7 +76,7 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m UpdatedAfterUnix: convertInt64(options.UpdatedAfterUnix), UpdatedBeforeUnix: convertInt64(options.UpdatedBeforeUnix), PriorityRepoID: 0, - IsArchived: 0, + IsArchived: optional.None[bool](), Org: nil, Team: nil, User: nil, diff --git a/modules/indexer/issues/elasticsearch/elasticsearch.go b/modules/indexer/issues/elasticsearch/elasticsearch.go index d059f76b32..3acd3ade71 100644 --- a/modules/indexer/issues/elasticsearch/elasticsearch.go +++ b/modules/indexer/issues/elasticsearch/elasticsearch.go @@ -153,11 +153,11 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( query.Must(q) } - if !options.IsPull.IsNone() { - query.Must(elastic.NewTermQuery("is_pull", options.IsPull.IsTrue())) + if options.IsPull.Has() { + query.Must(elastic.NewTermQuery("is_pull", options.IsPull.Value())) } - if !options.IsClosed.IsNone() { - query.Must(elastic.NewTermQuery("is_closed", options.IsClosed.IsTrue())) + if options.IsClosed.Has() { + query.Must(elastic.NewTermQuery("is_closed", options.IsClosed.Value())) } if options.NoLabelOnly { diff --git a/modules/indexer/issues/indexer.go b/modules/indexer/issues/indexer.go index 57037d2947..e3bc21b49d 100644 --- a/modules/indexer/issues/indexer.go +++ b/modules/indexer/issues/indexer.go @@ -20,10 +20,10 @@ import ( "code.gitea.io/gitea/modules/indexer/issues/internal" "code.gitea.io/gitea/modules/indexer/issues/meilisearch" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/queue" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" ) // IndexerMetadata is used to send data to the queue, so it contains only the ids. @@ -220,7 +220,7 @@ func PopulateIssueIndexer(ctx context.Context) error { ListOptions: db_model.ListOptions{Page: page, PageSize: repo_model.RepositoryListDefaultPageSize}, OrderBy: db_model.SearchOrderByID, Private: true, - Collaborate: util.OptionalBoolFalse, + Collaborate: optional.Some(false), }) if err != nil { log.Error("SearchRepositoryByName: %v", err) diff --git a/modules/indexer/issues/indexer_test.go b/modules/indexer/issues/indexer_test.go index 3b96686d98..10ffa7cbe6 100644 --- a/modules/indexer/issues/indexer_test.go +++ b/modules/indexer/issues/indexer_test.go @@ -10,8 +10,8 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/indexer/issues/internal" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" _ "code.gitea.io/gitea/models" _ "code.gitea.io/gitea/models/actions" @@ -210,13 +210,13 @@ func searchIssueIsPull(t *testing.T) { }{ { SearchOptions{ - IsPull: util.OptionalBoolFalse, + IsPull: optional.Some(false), }, []int64{17, 16, 15, 14, 13, 6, 5, 18, 10, 7, 4, 1}, }, { SearchOptions{ - IsPull: util.OptionalBoolTrue, + IsPull: optional.Some(true), }, []int64{22, 21, 12, 11, 20, 19, 9, 8, 3, 2}, }, @@ -237,13 +237,13 @@ func searchIssueIsClosed(t *testing.T) { }{ { SearchOptions{ - IsClosed: util.OptionalBoolFalse, + IsClosed: optional.Some(false), }, []int64{22, 21, 17, 16, 15, 14, 13, 12, 11, 20, 6, 19, 18, 10, 7, 9, 8, 3, 2, 1}, }, { SearchOptions{ - IsClosed: util.OptionalBoolTrue, + IsClosed: optional.Some(true), }, []int64{5, 4}, }, diff --git a/modules/indexer/issues/internal/model.go b/modules/indexer/issues/internal/model.go index 031745dd2f..947335d8ce 100644 --- a/modules/indexer/issues/internal/model.go +++ b/modules/indexer/issues/internal/model.go @@ -5,8 +5,8 @@ package internal import ( "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/util" ) // IndexerData data stored in the issue indexer @@ -77,8 +77,8 @@ type SearchOptions struct { RepoIDs []int64 // repository IDs which the issues belong to AllPublic bool // if include all public repositories - IsPull util.OptionalBool // if the issues is a pull request - IsClosed util.OptionalBool // if the issues is closed + IsPull optional.Option[bool] // if the issues is a pull request + IsClosed optional.Option[bool] // if the issues is closed IncludedLabelIDs []int64 // labels the issues have ExcludedLabelIDs []int64 // labels the issues don't have diff --git a/modules/indexer/issues/internal/tests/tests.go b/modules/indexer/issues/internal/tests/tests.go index 06fddeb65b..6724471539 100644 --- a/modules/indexer/issues/internal/tests/tests.go +++ b/modules/indexer/issues/internal/tests/tests.go @@ -16,8 +16,8 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/indexer/issues/internal" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -166,7 +166,7 @@ var cases = []*testIndexerCase{ Paginator: &db.ListOptions{ PageSize: 5, }, - IsPull: util.OptionalBoolFalse, + IsPull: optional.Some(false), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Equal(t, 5, len(result.Hits)) @@ -182,7 +182,7 @@ var cases = []*testIndexerCase{ Paginator: &db.ListOptions{ PageSize: 5, }, - IsPull: util.OptionalBoolTrue, + IsPull: optional.Some(true), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Equal(t, 5, len(result.Hits)) @@ -198,7 +198,7 @@ var cases = []*testIndexerCase{ Paginator: &db.ListOptions{ PageSize: 5, }, - IsClosed: util.OptionalBoolFalse, + IsClosed: optional.Some(false), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Equal(t, 5, len(result.Hits)) @@ -214,7 +214,7 @@ var cases = []*testIndexerCase{ Paginator: &db.ListOptions{ PageSize: 5, }, - IsClosed: util.OptionalBoolTrue, + IsClosed: optional.Some(true), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Equal(t, 5, len(result.Hits)) diff --git a/modules/indexer/issues/meilisearch/meilisearch.go b/modules/indexer/issues/meilisearch/meilisearch.go index ab8dcd0af4..325883196b 100644 --- a/modules/indexer/issues/meilisearch/meilisearch.go +++ b/modules/indexer/issues/meilisearch/meilisearch.go @@ -131,11 +131,11 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( query.And(q) } - if !options.IsPull.IsNone() { - query.And(inner_meilisearch.NewFilterEq("is_pull", options.IsPull.IsTrue())) + if options.IsPull.Has() { + query.And(inner_meilisearch.NewFilterEq("is_pull", options.IsPull.Value())) } - if !options.IsClosed.IsNone() { - query.And(inner_meilisearch.NewFilterEq("is_closed", options.IsClosed.IsTrue())) + if options.IsClosed.Has() { + query.And(inner_meilisearch.NewFilterEq("is_closed", options.IsClosed.Value())) } if options.NoLabelOnly { diff --git a/modules/issue/template/template.go b/modules/issue/template/template.go index 4e813fc91f..3be48b9edc 100644 --- a/modules/issue/template/template.go +++ b/modules/issue/template/template.go @@ -122,7 +122,13 @@ func validateRequired(field *api.IssueFormField, idx int) error { // The label is not required for a markdown or checkboxes field return nil } - return validateBoolItem(newErrorPosition(idx, field.Type), field.Validations, "required") + if err := validateBoolItem(newErrorPosition(idx, field.Type), field.Validations, "required"); err != nil { + return err + } + if required, _ := field.Validations["required"].(bool); required && !field.VisibleOnForm() { + return newErrorPosition(idx, field.Type).Errorf("can not require a hidden field") + } + return nil } func validateID(field *api.IssueFormField, idx int, ids container.Set[string]) error { @@ -172,10 +178,38 @@ func validateOptions(field *api.IssueFormField, idx int) error { return position.Errorf("'label' is required and should be a string") } + if visibility, ok := opt["visible"]; ok { + visibilityList, ok := visibility.([]any) + if !ok { + return position.Errorf("'visible' should be list") + } + for _, visibleType := range visibilityList { + visibleType, ok := visibleType.(string) + if !ok || !(visibleType == "form" || visibleType == "content") { + return position.Errorf("'visible' list can only contain strings of 'form' and 'content'") + } + } + } + if required, ok := opt["required"]; ok { if _, ok := required.(bool); !ok { return position.Errorf("'required' should be a bool") } + + // validate if hidden field is required + if visibility, ok := opt["visible"]; ok { + visibilityList, _ := visibility.([]any) + isVisible := false + for _, v := range visibilityList { + if vv, _ := v.(string); vv == "form" { + isVisible = true + break + } + } + if !isVisible { + return position.Errorf("can not require a hidden checkbox") + } + } } } } @@ -238,7 +272,7 @@ func RenderToMarkdown(template *api.IssueTemplate, values url.Values) string { IssueFormField: field, Values: values, } - if f.ID == "" { + if f.ID == "" || !f.VisibleInContent() { continue } f.WriteTo(builder) @@ -253,11 +287,6 @@ type valuedField struct { } func (f *valuedField) WriteTo(builder *strings.Builder) { - if f.Type == api.IssueFormFieldTypeMarkdown { - // markdown blocks do not appear in output - return - } - // write label if !f.HideLabel() { _, _ = fmt.Fprintf(builder, "### %s\n\n", f.Label()) @@ -269,6 +298,9 @@ func (f *valuedField) WriteTo(builder *strings.Builder) { switch f.Type { case api.IssueFormFieldTypeCheckboxes: for _, option := range f.Options() { + if !option.VisibleInContent() { + continue + } checked := " " if option.IsChecked() { checked = "x" @@ -302,6 +334,10 @@ func (f *valuedField) WriteTo(builder *strings.Builder) { } else { _, _ = fmt.Fprintf(builder, "%s\n", value) } + case api.IssueFormFieldTypeMarkdown: + if value, ok := f.Attributes["value"].(string); ok { + _, _ = fmt.Fprintf(builder, "%s\n", value) + } } _, _ = fmt.Fprintln(builder) } @@ -314,6 +350,9 @@ func (f *valuedField) Label() string { } func (f *valuedField) HideLabel() bool { + if f.Type == api.IssueFormFieldTypeMarkdown { + return true + } if label, ok := f.Attributes["hide_label"].(bool); ok { return label } @@ -385,6 +424,22 @@ func (o *valuedOption) IsChecked() bool { return false } +func (o *valuedOption) VisibleInContent() bool { + if o.field.Type == api.IssueFormFieldTypeCheckboxes { + if vs, ok := o.data.(map[string]any); ok { + if vl, ok := vs["visible"].([]any); ok { + for _, v := range vl { + if vv, _ := v.(string); vv == "content" { + return true + } + } + return false + } + } + } + return true +} + var minQuotesRegex = regexp.MustCompilePOSIX("^`{3,}") // minQuotes return 3 or more back-quotes. diff --git a/modules/issue/template/template_test.go b/modules/issue/template/template_test.go index 06e6b70d35..e24b962d61 100644 --- a/modules/issue/template/template_test.go +++ b/modules/issue/template/template_test.go @@ -10,6 +10,7 @@ import ( "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -318,6 +319,42 @@ body: `, wantErr: "body[0](checkboxes), option[0]: 'required' should be a bool", }, + { + name: "field is required but hidden", + content: ` +name: "test" +about: "this is about" +body: + - type: "input" + id: "1" + attributes: + label: "a" + validations: + required: true + visible: [content] +`, + wantErr: "body[0](input): can not require a hidden field", + }, + { + name: "checkboxes is required but hidden", + content: ` +name: "test" +about: "this is about" +body: + - type: checkboxes + id: "1" + attributes: + label: Label of checkboxes + description: Description of checkboxes + options: + - label: Option 1 + required: false + - label: Required and hidden + required: true + visible: [content] +`, + wantErr: "body[0](checkboxes), option[1]: can not require a hidden checkbox", + }, { name: "valid", content: ` @@ -374,8 +411,11 @@ body: required: true - label: Option 2 of checkboxes required: false - - label: Option 3 of checkboxes + - label: Hidden Option 3 of checkboxes + visible: [content] + - label: Required but not submitted required: true + visible: [form] `, want: &api.IssueTemplate{ Name: "Name", @@ -390,6 +430,7 @@ body: Attributes: map[string]any{ "value": "Value of the markdown", }, + Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm}, }, { Type: "textarea", @@ -404,6 +445,7 @@ body: Validations: map[string]any{ "required": true, }, + Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent}, }, { Type: "input", @@ -419,6 +461,7 @@ body: "is_number": true, "regex": "[a-zA-Z0-9]+", }, + Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent}, }, { Type: "dropdown", @@ -436,6 +479,7 @@ body: Validations: map[string]any{ "required": true, }, + Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent}, }, { Type: "checkboxes", @@ -446,9 +490,11 @@ body: "options": []any{ map[string]any{"label": "Option 1 of checkboxes", "required": true}, map[string]any{"label": "Option 2 of checkboxes", "required": false}, - map[string]any{"label": "Option 3 of checkboxes", "required": true}, + map[string]any{"label": "Hidden Option 3 of checkboxes", "visible": []string{"content"}}, + map[string]any{"label": "Required but not submitted", "required": true, "visible": []string{"form"}}, }, }, + Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent}, }, }, FileName: "test.yaml", @@ -467,7 +513,12 @@ body: - type: markdown id: id1 attributes: - value: Value of the markdown + value: Value of the markdown shown in form + - type: markdown + id: id2 + attributes: + value: Value of the markdown shown in created issue + visible: [content] `, want: &api.IssueTemplate{ Name: "Name", @@ -480,8 +531,17 @@ body: Type: "markdown", ID: "id1", Attributes: map[string]any{ - "value": "Value of the markdown", + "value": "Value of the markdown shown in form", }, + Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm}, + }, + { + Type: "markdown", + ID: "id2", + Attributes: map[string]any{ + "value": "Value of the markdown shown in created issue", + }, + Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleContent}, }, }, FileName: "test.yaml", @@ -515,6 +575,7 @@ body: Attributes: map[string]any{ "value": "Value of the markdown", }, + Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm}, }, }, FileName: "test.yaml", @@ -548,6 +609,7 @@ body: Attributes: map[string]any{ "value": "Value of the markdown", }, + Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm}, }, }, FileName: "test.yaml", @@ -622,9 +684,14 @@ body: - type: markdown id: id1 attributes: - value: Value of the markdown - - type: textarea + value: Value of the markdown shown in form + - type: markdown id: id2 + attributes: + value: Value of the markdown shown in created issue + visible: [content] + - type: textarea + id: id3 attributes: label: Label of textarea description: Description of textarea @@ -634,7 +701,7 @@ body: validations: required: true - type: input - id: id3 + id: id4 attributes: label: Label of input description: Description of input @@ -646,7 +713,7 @@ body: is_number: true regex: "[a-zA-Z0-9]+" - type: dropdown - id: id4 + id: id5 attributes: label: Label of dropdown description: Description of dropdown @@ -658,7 +725,7 @@ body: validations: required: true - type: checkboxes - id: id5 + id: id6 attributes: label: Label of checkboxes description: Description of checkboxes @@ -669,20 +736,26 @@ body: required: false - label: Option 3 of checkboxes required: true + visible: [form] + - label: Hidden Option of checkboxes + visible: [content] `, values: map[string][]string{ - "form-field-id2": {"Value of id2"}, "form-field-id3": {"Value of id3"}, - "form-field-id4": {"0,1"}, - "form-field-id5-0": {"on"}, - "form-field-id5-2": {"on"}, + "form-field-id4": {"Value of id4"}, + "form-field-id5": {"0,1"}, + "form-field-id6-0": {"on"}, + "form-field-id6-2": {"on"}, }, }, - want: `### Label of textarea -` + "```bash\nValue of id2\n```" + ` + want: `Value of the markdown shown in created issue -Value of id3 +### Label of textarea + +` + "```bash\nValue of id3\n```" + ` + +Value of id4 ### Label of dropdown @@ -692,7 +765,7 @@ Option 1 of dropdown, Option 2 of dropdown - [x] Option 1 of checkboxes - [ ] Option 2 of checkboxes -- [x] Option 3 of checkboxes +- [ ] Hidden Option of checkboxes `, }, @@ -704,7 +777,7 @@ Option 1 of dropdown, Option 2 of dropdown t.Fatal(err) } if got := RenderToMarkdown(template, tt.args.values); got != tt.want { - t.Errorf("RenderToMarkdown() = %v, want %v", got, tt.want) + assert.EqualValues(t, tt.want, got) } }) } diff --git a/modules/issue/template/unmarshal.go b/modules/issue/template/unmarshal.go index 8cae8d4c42..0fc13d7ddf 100644 --- a/modules/issue/template/unmarshal.go +++ b/modules/issue/template/unmarshal.go @@ -128,9 +128,18 @@ func unmarshal(filename string, content []byte) (*api.IssueTemplate, error) { } } for i, v := range it.Fields { + // set default id value if v.ID == "" { v.ID = strconv.Itoa(i) } + // set default visibility + if v.Visible == nil { + v.Visible = []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm} + // markdown is not submitted by default + if v.Type != api.IssueFormFieldTypeMarkdown { + v.Visible = append(v.Visible, api.IssueFormFieldVisibleContent) + } + } } } diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go index cb29431d4b..ccb63c6bab 100644 --- a/modules/markup/html_test.go +++ b/modules/markup/html_test.go @@ -397,7 +397,7 @@ func TestRender_ShortLinks(t *testing.T) { }, }, input) assert.NoError(t, err) - assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer))) buffer, err = markdown.RenderString(&markup.RenderContext{ Ctx: git.DefaultContext, Links: markup.Links{ @@ -407,7 +407,7 @@ func TestRender_ShortLinks(t *testing.T) { IsWiki: true, }, input) assert.NoError(t, err) - assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(buffer)) + assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer))) } mediatree := util.URLJoin(markup.TestRepoURL, "media", "master") @@ -510,7 +510,7 @@ func TestRender_RelativeImages(t *testing.T) { Metas: localMetas, }, input) assert.NoError(t, err) - assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer))) buffer, err = markdown.RenderString(&markup.RenderContext{ Ctx: git.DefaultContext, Links: markup.Links{ @@ -520,7 +520,7 @@ func TestRender_RelativeImages(t *testing.T) { IsWiki: true, }, input) assert.NoError(t, err) - assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(buffer)) + assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer))) } rawwiki := util.URLJoin(markup.TestRepoURL, "wiki", "raw") diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go index 771162b9a3..f0b1afa27e 100644 --- a/modules/markup/markdown/markdown.go +++ b/modules/markup/markdown/markdown.go @@ -6,6 +6,7 @@ package markdown import ( "fmt" + "html/template" "io" "strings" "sync" @@ -262,12 +263,12 @@ func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error } // RenderString renders Markdown string to HTML with all specific handling stuff and return string -func RenderString(ctx *markup.RenderContext, content string) (string, error) { +func RenderString(ctx *markup.RenderContext, content string) (template.HTML, error) { var buf strings.Builder if err := Render(ctx, strings.NewReader(content), &buf); err != nil { return "", err } - return buf.String(), nil + return template.HTML(buf.String()), nil } // RenderRaw renders Markdown to HTML without handling special links. diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go index bdf4011fa2..dbf95e5e62 100644 --- a/modules/markup/markdown/markdown_test.go +++ b/modules/markup/markdown/markdown_test.go @@ -5,6 +5,7 @@ package markdown_test import ( "context" + "html/template" "os" "strings" "testing" @@ -21,12 +22,11 @@ import ( ) const ( - AppURL = "http://localhost:3000/" - Repo = "gogits/gogs" - AppSubURL = AppURL + Repo + "/" + AppURL = "http://localhost:3000/" + FullURL = AppURL + "gogits/gogs/" ) -// these values should match the Repo const above +// these values should match the const above var localMetas = map[string]string{ "user": "gogits", "repo": "gogs", @@ -48,34 +48,33 @@ func TestMain(m *testing.M) { func TestRender_StandardLinks(t *testing.T) { setting.AppURL = AppURL - setting.AppSubURL = AppSubURL test := func(input, expected, expectedWiki string) { buffer, err := markdown.RenderString(&markup.RenderContext{ Ctx: git.DefaultContext, Links: markup.Links{ - Base: setting.AppSubURL, + Base: FullURL, }, }, input) assert.NoError(t, err) - assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer))) buffer, err = markdown.RenderString(&markup.RenderContext{ Ctx: git.DefaultContext, Links: markup.Links{ - Base: setting.AppSubURL, + Base: FullURL, }, IsWiki: true, }, input) assert.NoError(t, err) - assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(buffer)) + assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer))) } googleRendered := `

https://google.com/

` test("", googleRendered, googleRendered) - lnk := util.URLJoin(AppSubURL, "WikiPage") - lnkWiki := util.URLJoin(AppSubURL, "wiki", "WikiPage") + lnk := util.URLJoin(FullURL, "WikiPage") + lnkWiki := util.URLJoin(FullURL, "wiki", "WikiPage") test("[WikiPage](WikiPage)", `

WikiPage

`, `

WikiPage

`) @@ -83,23 +82,22 @@ func TestRender_StandardLinks(t *testing.T) { func TestRender_Images(t *testing.T) { setting.AppURL = AppURL - setting.AppSubURL = AppSubURL test := func(input, expected string) { buffer, err := markdown.RenderString(&markup.RenderContext{ Ctx: git.DefaultContext, Links: markup.Links{ - Base: setting.AppSubURL, + Base: FullURL, }, }, input) assert.NoError(t, err) - assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer))) } url := "../../.images/src/02/train.jpg" title := "Train" href := "https://gitea.io" - result := util.URLJoin(AppSubURL, url) + result := util.URLJoin(FullURL, url) // hint: With Markdown v2.5.2, there is a new syntax: [link](URL){:target="_blank"} , but we do not support it now test( @@ -289,33 +287,32 @@ This PR has been generated by [Renovate Bot](https://github.com/renovatebot/reno func TestTotal_RenderWiki(t *testing.T) { setting.AppURL = AppURL - setting.AppSubURL = AppSubURL - answers := testAnswers(util.URLJoin(AppSubURL, "wiki"), util.URLJoin(AppSubURL, "wiki", "raw")) + answers := testAnswers(util.URLJoin(FullURL, "wiki"), util.URLJoin(FullURL, "wiki", "raw")) for i := 0; i < len(sameCases); i++ { line, err := markdown.RenderString(&markup.RenderContext{ Ctx: git.DefaultContext, Links: markup.Links{ - Base: setting.AppSubURL, + Base: FullURL, }, Metas: localMetas, IsWiki: true, }, sameCases[i]) assert.NoError(t, err) - assert.Equal(t, answers[i], line) + assert.Equal(t, template.HTML(answers[i]), line) } testCases := []string{ // Guard wiki sidebar: special syntax `[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`, // rendered - `

Guardfile-DSL / Configuring-Guard

+ `

Guardfile-DSL / Configuring-Guard

`, // special syntax `[[Name|Link]]`, // rendered - `

Name

+ `

Name

`, } @@ -323,32 +320,31 @@ func TestTotal_RenderWiki(t *testing.T) { line, err := markdown.RenderString(&markup.RenderContext{ Ctx: git.DefaultContext, Links: markup.Links{ - Base: setting.AppSubURL, + Base: FullURL, }, IsWiki: true, }, testCases[i]) assert.NoError(t, err) - assert.Equal(t, testCases[i+1], line) + assert.Equal(t, template.HTML(testCases[i+1]), line) } } func TestTotal_RenderString(t *testing.T) { setting.AppURL = AppURL - setting.AppSubURL = AppSubURL - answers := testAnswers(util.URLJoin(AppSubURL, "src", "master"), util.URLJoin(AppSubURL, "media", "master")) + answers := testAnswers(util.URLJoin(FullURL, "src", "master"), util.URLJoin(FullURL, "media", "master")) for i := 0; i < len(sameCases); i++ { line, err := markdown.RenderString(&markup.RenderContext{ Ctx: git.DefaultContext, Links: markup.Links{ - Base: AppSubURL, + Base: FullURL, BranchPath: "master", }, Metas: localMetas, }, sameCases[i]) assert.NoError(t, err) - assert.Equal(t, answers[i], line) + assert.Equal(t, template.HTML(answers[i]), line) } testCases := []string{} @@ -357,11 +353,11 @@ func TestTotal_RenderString(t *testing.T) { line, err := markdown.RenderString(&markup.RenderContext{ Ctx: git.DefaultContext, Links: markup.Links{ - Base: AppSubURL, + Base: FullURL, }, }, testCases[i]) assert.NoError(t, err) - assert.Equal(t, testCases[i+1], line) + assert.Equal(t, template.HTML(testCases[i+1]), line) } } @@ -428,7 +424,7 @@ func TestRenderEmojiInLinks_Issue12331(t *testing.T) { ` res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, testcase) assert.NoError(t, err) - assert.Equal(t, expected, res) + assert.Equal(t, template.HTML(expected), res) } func TestColorPreview(t *testing.T) { @@ -462,7 +458,7 @@ func TestColorPreview(t *testing.T) { for _, test := range positiveTests { res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase) assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase) - assert.Equal(t, test.expected, res, "Unexpected result in testcase %q", test.testcase) + assert.Equal(t, template.HTML(test.expected), res, "Unexpected result in testcase %q", test.testcase) } @@ -529,7 +525,7 @@ func TestMathBlock(t *testing.T) { for _, test := range testcases { res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase) assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase) - assert.Equal(t, test.expected, res, "Unexpected result in testcase %q", test.testcase) + assert.Equal(t, template.HTML(test.expected), res, "Unexpected result in testcase %q", test.testcase) } } @@ -567,12 +563,12 @@ foo: bar for _, test := range testcases { res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase) assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase) - assert.Equal(t, test.expected, res, "Unexpected result in testcase %q", test.testcase) + assert.Equal(t, template.HTML(test.expected), res, "Unexpected result in testcase %q", test.testcase) } } func TestRenderLinks(t *testing.T) { - input := ` space @mention-user + input := ` space @mention-user${SPACE}${SPACE} /just/a/path.bin https://example.com/file.bin [local link](file.bin) @@ -593,8 +589,9 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit mail@domain.com @mention-user test #123 - space + space${SPACE}${SPACE} ` + input = strings.ReplaceAll(input, "${SPACE}", " ") // replace ${SPACE} with " ", to avoid some editor's auto-trimming cases := []struct { Links markup.Links IsWiki bool @@ -957,6 +954,6 @@ space

for i, c := range cases { result, err := markdown.RenderString(&markup.RenderContext{Ctx: context.Background(), Links: c.Links, IsWiki: c.IsWiki}, input) assert.NoError(t, err, "Unexpected error in testcase: %v", i) - assert.Equal(t, c.Expected, result, "Unexpected result in testcase %v", i) + assert.Equal(t, template.HTML(c.Expected), result, "Unexpected result in testcase %v", i) } } diff --git a/modules/optional/option_test.go b/modules/optional/option_test.go index 410fd73577..4f55608004 100644 --- a/modules/optional/option_test.go +++ b/modules/optional/option_test.go @@ -27,6 +27,16 @@ func TestOption(t *testing.T) { assert.Equal(t, int(1), some.Value()) assert.Equal(t, int(1), some.ValueOrDefault(2)) + noneBool := optional.None[bool]() + assert.False(t, noneBool.Has()) + assert.False(t, noneBool.Value()) + assert.True(t, noneBool.ValueOrDefault(true)) + + someBool := optional.Some(true) + assert.True(t, someBool.Has()) + assert.True(t, someBool.Value()) + assert.True(t, someBool.ValueOrDefault(false)) + var ptr *int assert.False(t, optional.FromPtr(ptr).Has()) diff --git a/modules/queue/workergroup.go b/modules/queue/workergroup.go index 147a4f335e..e3801ef2b2 100644 --- a/modules/queue/workergroup.go +++ b/modules/queue/workergroup.go @@ -60,6 +60,9 @@ func (q *WorkerPoolQueue[T]) doDispatchBatchToWorker(wg *workerGroup[T], flushCh full = true } + // TODO: the logic could be improved in the future, to avoid a data-race between "doStartNewWorker" and "workerNum" + // The root problem is that if we skip "doStartNewWorker" here, the "workerNum" might be decreased by other workers later + // So ideally, it should check whether there are enough workers by some approaches, and start new workers if necessary. q.workerNumMu.Lock() noWorker := q.workerNum == 0 if full || noWorker { @@ -143,7 +146,11 @@ func (q *WorkerPoolQueue[T]) doStartNewWorker(wp *workerGroup[T]) { log.Debug("Queue %q starts new worker", q.GetName()) defer log.Debug("Queue %q stops idle worker", q.GetName()) + atomic.AddInt32(&q.workerStartedCounter, 1) // Only increase counter, used for debugging + t := time.NewTicker(workerIdleDuration) + defer t.Stop() + keepWorking := true stopWorking := func() { q.workerNumMu.Lock() @@ -158,13 +165,18 @@ func (q *WorkerPoolQueue[T]) doStartNewWorker(wp *workerGroup[T]) { case batch, ok := <-q.batchChan: if !ok { stopWorking() - } else { - q.doWorkerHandle(batch) - t.Reset(workerIdleDuration) + continue + } + q.doWorkerHandle(batch) + // reset the idle ticker, and drain the tick after reset in case a tick is already triggered + t.Reset(workerIdleDuration) + select { + case <-t.C: + default: } case <-t.C: q.workerNumMu.Lock() - keepWorking = q.workerNum <= 1 + keepWorking = q.workerNum <= 1 // keep the last worker running if !keepWorking { q.workerNum-- } diff --git a/modules/queue/workerqueue.go b/modules/queue/workerqueue.go index b28fd88027..4160622d81 100644 --- a/modules/queue/workerqueue.go +++ b/modules/queue/workerqueue.go @@ -40,6 +40,8 @@ type WorkerPoolQueue[T any] struct { workerMaxNum int workerActiveNum int workerNumMu sync.Mutex + + workerStartedCounter int32 } type flushType chan struct{} diff --git a/modules/queue/workerqueue_test.go b/modules/queue/workerqueue_test.go index e60120162a..e09669c542 100644 --- a/modules/queue/workerqueue_test.go +++ b/modules/queue/workerqueue_test.go @@ -11,6 +11,7 @@ import ( "time" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" "github.com/stretchr/testify/assert" ) @@ -175,11 +176,7 @@ func testWorkerPoolQueuePersistence(t *testing.T, queueSetting setting.QueueSett } func TestWorkerPoolQueueActiveWorkers(t *testing.T) { - oldWorkerIdleDuration := workerIdleDuration - workerIdleDuration = 300 * time.Millisecond - defer func() { - workerIdleDuration = oldWorkerIdleDuration - }() + defer test.MockVariableValue(&workerIdleDuration, 300*time.Millisecond)() handler := func(items ...int) (unhandled []int) { time.Sleep(100 * time.Millisecond) @@ -250,3 +247,25 @@ func TestWorkerPoolQueueShutdown(t *testing.T) { q, _ = newWorkerPoolQueueForTest("test-workpoolqueue", qs, handler, false) assert.EqualValues(t, 20, q.GetQueueItemNumber()) } + +func TestWorkerPoolQueueWorkerIdleReset(t *testing.T) { + defer test.MockVariableValue(&workerIdleDuration, 10*time.Millisecond)() + + handler := func(items ...int) (unhandled []int) { + time.Sleep(50 * time.Millisecond) + return nil + } + + q, _ := newWorkerPoolQueueForTest("test-workpoolqueue", setting.QueueSettings{Type: "channel", BatchLength: 1, MaxWorkers: 2, Length: 100}, handler, false) + stop := runWorkerPoolQueue(q) + for i := 0; i < 20; i++ { + assert.NoError(t, q.Push(i)) + } + + time.Sleep(500 * time.Millisecond) + assert.EqualValues(t, 2, q.GetWorkerNumber()) + assert.EqualValues(t, 2, q.GetWorkerActiveNumber()) + // when the queue never becomes empty, the existing workers should keep working + assert.EqualValues(t, 2, q.workerStartedCounter) + stop() +} diff --git a/modules/references/references.go b/modules/references/references.go index 7758312564..761d6ee3d1 100644 --- a/modules/references/references.go +++ b/modules/references/references.go @@ -31,9 +31,9 @@ var ( // mentionPattern matches all mentions in the form of "@user" or "@org/team" mentionPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(@[0-9a-zA-Z-_]+|@[0-9a-zA-Z-_]+\/?[0-9a-zA-Z-_]+|@[0-9a-zA-Z-_][0-9a-zA-Z-_.]+\/?[0-9a-zA-Z-_.]+[0-9a-zA-Z-_])(?:\s|[:,;.?!]\s|[:,;.?!]?$|\)|\])`) // issueNumericPattern matches string that references to a numeric issue, e.g. #1287 - issueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[|\')([#!][0-9]+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`) + issueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[|\'|\")([#!][0-9]+)(?:\s|$|\)|\]|\'|\"|[:;,.?!]\s|[:;,.?!]$)`) // issueAlphanumericPattern matches string that references to an alphanumeric issue, e.g. ABC-1234 - issueAlphanumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([A-Z]{1,10}-[1-9][0-9]*)(?:\s|$|\)|\]|:|\.(\s|$))`) + issueAlphanumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[|\"|\')([A-Z]{1,10}-[1-9][0-9]*)(?:\s|$|\)|\]|:|\.(\s|$)|\"|\')`) // crossReferenceIssueNumericPattern matches string that references a numeric issue in a different repository // e.g. org/repo#12345 crossReferenceIssueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-zA-Z-_\.]+/[0-9a-zA-Z-_\.]+[#!][0-9]+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`) diff --git a/modules/references/references_test.go b/modules/references/references_test.go index ba7dda80cc..0c32933619 100644 --- a/modules/references/references_test.go +++ b/modules/references/references_test.go @@ -429,6 +429,8 @@ func TestRegExp_issueNumericPattern(t *testing.T) { " #12", "#12:", "ref: #12: msg", + "\"#1234\"", + "'#1234'", } falseTestCases := []string{ "# 1234", @@ -459,6 +461,8 @@ func TestRegExp_issueAlphanumericPattern(t *testing.T) { "(ABC-123)", "[ABC-123]", "ABC-123:", + "\"ABC-123\"", + "'ABC-123'", } falseTestCases := []string{ "RC-08", diff --git a/modules/repository/collaborator.go b/modules/repository/collaborator.go index ebe14e3a4c..f71c58fbdf 100644 --- a/modules/repository/collaborator.go +++ b/modules/repository/collaborator.go @@ -16,6 +16,14 @@ import ( ) func AddCollaborator(ctx context.Context, repo *repo_model.Repository, u *user_model.User) error { + if err := repo.LoadOwner(ctx); err != nil { + return err + } + + if user_model.IsUserBlockedBy(ctx, u, repo.OwnerID) || user_model.IsUserBlockedBy(ctx, repo.Owner, u.ID) { + return user_model.ErrBlockedUser + } + return db.WithTx(ctx, func(ctx context.Context) error { has, err := db.Exist[repo_model.Collaboration](ctx, builder.Eq{ "repo_id": repo.ID, diff --git a/modules/repository/create.go b/modules/repository/create.go index ca2150b972..4f18b9b3fa 100644 --- a/modules/repository/create.go +++ b/modules/repository/create.go @@ -93,6 +93,12 @@ func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, re AllowRebaseUpdate: true, }, }) + } else if tp == unit.TypeProjects { + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: tp, + Config: &repo_model.ProjectsConfig{ProjectsMode: repo_model.ProjectsModeAll}, + }) } else { units = append(units, repo_model.RepoUnit{ RepoID: repo.ID, @@ -147,7 +153,7 @@ func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, re } if setting.Service.AutoWatchNewRepos { - if err = repo_model.WatchRepo(ctx, doer.ID, repo.ID, true); err != nil { + if err = repo_model.WatchRepo(ctx, doer, repo, true); err != nil { return fmt.Errorf("WatchRepo: %w", err) } } diff --git a/modules/repository/repo.go b/modules/repository/repo.go index 39bdc6adcf..cb926084ba 100644 --- a/modules/repository/repo.go +++ b/modules/repository/repo.go @@ -5,16 +5,13 @@ package repository import ( "context" - "errors" "fmt" "io" - "net/http" "strings" "time" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" - "code.gitea.io/gitea/models/organization" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/container" @@ -22,10 +19,8 @@ import ( "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/migration" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/util" ) /* @@ -47,244 +42,6 @@ func WikiRemoteURL(ctx context.Context, remote string) string { return "" } -// MigrateRepositoryGitData starts migrating git related data after created migrating repository -func MigrateRepositoryGitData(ctx context.Context, u *user_model.User, - repo *repo_model.Repository, opts migration.MigrateOptions, - httpTransport *http.Transport, -) (*repo_model.Repository, error) { - repoPath := repo_model.RepoPath(u.Name, opts.RepoName) - - if u.IsOrganization() { - t, err := organization.OrgFromUser(u).GetOwnerTeam(ctx) - if err != nil { - return nil, err - } - repo.NumWatches = t.NumMembers - } else { - repo.NumWatches = 1 - } - - migrateTimeout := time.Duration(setting.Git.Timeout.Migrate) * time.Second - - var err error - if err = util.RemoveAll(repoPath); err != nil { - return repo, fmt.Errorf("Failed to remove %s: %w", repoPath, err) - } - - if err = git.Clone(ctx, opts.CloneAddr, repoPath, git.CloneRepoOptions{ - Mirror: true, - Quiet: true, - Timeout: migrateTimeout, - SkipTLSVerify: setting.Migrations.SkipTLSVerify, - }); err != nil { - if errors.Is(err, context.DeadlineExceeded) { - return repo, fmt.Errorf("Clone timed out. Consider increasing [git.timeout] MIGRATE in app.ini. Underlying Error: %w", err) - } - return repo, fmt.Errorf("Clone: %w", err) - } - - if err := git.WriteCommitGraph(ctx, repoPath); err != nil { - return repo, err - } - - if opts.Wiki { - wikiPath := repo_model.WikiPath(u.Name, opts.RepoName) - wikiRemotePath := WikiRemoteURL(ctx, opts.CloneAddr) - if len(wikiRemotePath) > 0 { - if err := util.RemoveAll(wikiPath); err != nil { - return repo, fmt.Errorf("Failed to remove %s: %w", wikiPath, err) - } - - if err := git.Clone(ctx, wikiRemotePath, wikiPath, git.CloneRepoOptions{ - Mirror: true, - Quiet: true, - Timeout: migrateTimeout, - Branch: "master", - SkipTLSVerify: setting.Migrations.SkipTLSVerify, - }); err != nil { - log.Warn("Clone wiki: %v", err) - if err := util.RemoveAll(wikiPath); err != nil { - return repo, fmt.Errorf("Failed to remove %s: %w", wikiPath, err) - } - } else { - if err := git.WriteCommitGraph(ctx, wikiPath); err != nil { - return repo, err - } - } - } - } - - if repo.OwnerID == u.ID { - repo.Owner = u - } - - if err = CheckDaemonExportOK(ctx, repo); err != nil { - return repo, fmt.Errorf("checkDaemonExportOK: %w", err) - } - - if stdout, _, err := git.NewCommand(ctx, "update-server-info"). - SetDescription(fmt.Sprintf("MigrateRepositoryGitData(git update-server-info): %s", repoPath)). - RunStdString(&git.RunOpts{Dir: repoPath}); err != nil { - log.Error("MigrateRepositoryGitData(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err) - return repo, fmt.Errorf("error in MigrateRepositoryGitData(git update-server-info): %w", err) - } - - gitRepo, err := git.OpenRepository(ctx, repoPath) - if err != nil { - return repo, fmt.Errorf("OpenRepository: %w", err) - } - defer gitRepo.Close() - - repo.IsEmpty, err = gitRepo.IsEmpty() - if err != nil { - return repo, fmt.Errorf("git.IsEmpty: %w", err) - } - - if !repo.IsEmpty { - if len(repo.DefaultBranch) == 0 { - // Try to get HEAD branch and set it as default branch. - headBranch, err := gitRepo.GetHEADBranch() - if err != nil { - return repo, fmt.Errorf("GetHEADBranch: %w", err) - } - if headBranch != nil { - repo.DefaultBranch = headBranch.Name - } - } - - if _, err := SyncRepoBranchesWithRepo(ctx, repo, gitRepo, u.ID); err != nil { - return repo, fmt.Errorf("SyncRepoBranchesWithRepo: %v", err) - } - - if !opts.Releases { - // note: this will greatly improve release (tag) sync - // for pull-mirrors with many tags - repo.IsMirror = opts.Mirror - if err = SyncReleasesWithTags(ctx, repo, gitRepo); err != nil { - log.Error("Failed to synchronize tags to releases for repository: %v", err) - } - } - - if opts.LFS { - endpoint := lfs.DetermineEndpoint(opts.CloneAddr, opts.LFSEndpoint) - lfsClient := lfs.NewClient(endpoint, httpTransport) - if err = StoreMissingLfsObjectsInRepository(ctx, repo, gitRepo, lfsClient); err != nil { - log.Error("Failed to store missing LFS objects for repository: %v", err) - } - } - } - - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return nil, err - } - defer committer.Close() - - if opts.Mirror { - remoteAddress, err := util.SanitizeURL(opts.CloneAddr) - if err != nil { - return repo, err - } - mirrorModel := repo_model.Mirror{ - RepoID: repo.ID, - Interval: setting.Mirror.DefaultInterval, - EnablePrune: true, - NextUpdateUnix: timeutil.TimeStampNow().AddDuration(setting.Mirror.DefaultInterval), - LFS: opts.LFS, - RemoteAddress: remoteAddress, - } - if opts.LFS { - mirrorModel.LFSEndpoint = opts.LFSEndpoint - } - - if opts.MirrorInterval != "" { - parsedInterval, err := time.ParseDuration(opts.MirrorInterval) - if err != nil { - log.Error("Failed to set Interval: %v", err) - return repo, err - } - if parsedInterval == 0 { - mirrorModel.Interval = 0 - mirrorModel.NextUpdateUnix = 0 - } else if parsedInterval < setting.Mirror.MinInterval { - err := fmt.Errorf("interval %s is set below Minimum Interval of %s", parsedInterval, setting.Mirror.MinInterval) - log.Error("Interval: %s is too frequent", opts.MirrorInterval) - return repo, err - } else { - mirrorModel.Interval = parsedInterval - mirrorModel.NextUpdateUnix = timeutil.TimeStampNow().AddDuration(parsedInterval) - } - } - - if err = repo_model.InsertMirror(ctx, &mirrorModel); err != nil { - return repo, fmt.Errorf("InsertOne: %w", err) - } - - repo.IsMirror = true - if err = UpdateRepository(ctx, repo, false); err != nil { - return nil, err - } - - // this is necessary for sync local tags from remote - configName := fmt.Sprintf("remote.%s.fetch", mirrorModel.GetRemoteName()) - if stdout, _, err := git.NewCommand(ctx, "config"). - AddOptionValues("--add", configName, `+refs/tags/*:refs/tags/*`). - RunStdString(&git.RunOpts{Dir: repoPath}); err != nil { - log.Error("MigrateRepositoryGitData(git config --add +refs/tags/*:refs/tags/*) in %v: Stdout: %s\nError: %v", repo, stdout, err) - return repo, fmt.Errorf("error in MigrateRepositoryGitData(git config --add +refs/tags/*:refs/tags/*): %w", err) - } - } else { - if err = UpdateRepoSize(ctx, repo); err != nil { - log.Error("Failed to update size for repository: %v", err) - } - if repo, err = CleanUpMigrateInfo(ctx, repo); err != nil { - return nil, err - } - } - - return repo, committer.Commit() -} - -// cleanUpMigrateGitConfig removes mirror info which prevents "push --all". -// This also removes possible user credentials. -func cleanUpMigrateGitConfig(ctx context.Context, repoPath string) error { - cmd := git.NewCommand(ctx, "remote", "rm", "origin") - // if the origin does not exist - _, stderr, err := cmd.RunStdString(&git.RunOpts{ - Dir: repoPath, - }) - if err != nil && !strings.HasPrefix(stderr, "fatal: No such remote") { - return err - } - return nil -} - -// CleanUpMigrateInfo finishes migrating repository and/or wiki with things that don't need to be done for mirrors. -func CleanUpMigrateInfo(ctx context.Context, repo *repo_model.Repository) (*repo_model.Repository, error) { - repoPath := repo.RepoPath() - if err := CreateDelegateHooks(repoPath); err != nil { - return repo, fmt.Errorf("createDelegateHooks: %w", err) - } - if repo.HasWiki() { - if err := CreateDelegateHooks(repo.WikiPath()); err != nil { - return repo, fmt.Errorf("createDelegateHooks.(wiki): %w", err) - } - } - - _, _, err := git.NewCommand(ctx, "remote", "rm", "origin").RunStdString(&git.RunOpts{Dir: repoPath}) - if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") { - return repo, fmt.Errorf("CleanUpMigrateInfo: %w", err) - } - - if repo.HasWiki() { - if err := cleanUpMigrateGitConfig(ctx, repo.WikiPath()); err != nil { - return repo, fmt.Errorf("cleanUpMigrateGitConfig (wiki): %w", err) - } - } - - return repo, UpdateRepository(ctx, repo, false) -} - // SyncRepoTags synchronizes releases table with repository tags func SyncRepoTags(ctx context.Context, repoID int64) error { repo, err := repo_model.GetRepositoryByID(ctx, repoID) diff --git a/modules/setting/admin.go b/modules/setting/admin.go index 48a2ea9744..be214a58ce 100644 --- a/modules/setting/admin.go +++ b/modules/setting/admin.go @@ -20,5 +20,7 @@ func loadAdminFrom(rootCfg ConfigProvider) { } const ( - UserFeatureDeletion = "deletion" + UserFeatureDeletion = "deletion" + UserFeatureManageSSHKeys = "manage_ssh_keys" + UserFeatureManageGPGKeys = "manage_gpg_keys" ) diff --git a/modules/setting/session.go b/modules/setting/session.go index 8b9b754b38..70497e5eaa 100644 --- a/modules/setting/session.go +++ b/modules/setting/session.go @@ -20,7 +20,7 @@ var SessionConfig = struct { ProviderConfig string // Cookie name to save session ID. Default is "MacaronSession". CookieName string - // Cookie path to store. Default is "/". HINT: there was a bug, the old value doesn't have trailing slash, and could be empty "". + // Cookie path to store. Default is "/". CookiePath string // GC interval time in seconds. Default is 3600. Gclifetime int64 @@ -49,7 +49,10 @@ func loadSessionFrom(rootCfg ConfigProvider) { fatalDuplicatedPath("session", SessionConfig.ProviderConfig) } SessionConfig.CookieName = sec.Key("COOKIE_NAME").MustString("i_like_gitea") - SessionConfig.CookiePath = AppSubURL + "/" // there was a bug, old code only set CookePath=AppSubURL, no trailing slash + SessionConfig.CookiePath = AppSubURL + if SessionConfig.CookiePath == "" { + SessionConfig.CookiePath = "/" + } SessionConfig.Secure = sec.Key("COOKIE_SECURE").MustBool(strings.HasPrefix(strings.ToLower(AppURL), "https://")) SessionConfig.Gclifetime = sec.Key("GC_INTERVAL_TIME").MustInt64(86400) SessionConfig.Maxlifetime = sec.Key("SESSION_LIFE_TIME").MustInt64(86400) diff --git a/modules/structs/issue.go b/modules/structs/issue.go index 34eae69329..16242d18ad 100644 --- a/modules/structs/issue.go +++ b/modules/structs/issue.go @@ -6,6 +6,7 @@ package structs import ( "fmt" "path" + "slices" "strings" "time" @@ -141,12 +142,37 @@ const ( // IssueFormField represents a form field // swagger:model type IssueFormField struct { - Type IssueFormFieldType `json:"type" yaml:"type"` - ID string `json:"id" yaml:"id"` - Attributes map[string]any `json:"attributes" yaml:"attributes"` - Validations map[string]any `json:"validations" yaml:"validations"` + Type IssueFormFieldType `json:"type" yaml:"type"` + ID string `json:"id" yaml:"id"` + Attributes map[string]any `json:"attributes" yaml:"attributes"` + Validations map[string]any `json:"validations" yaml:"validations"` + Visible []IssueFormFieldVisible `json:"visible,omitempty"` } +func (iff IssueFormField) VisibleOnForm() bool { + if len(iff.Visible) == 0 { + return true + } + return slices.Contains(iff.Visible, IssueFormFieldVisibleForm) +} + +func (iff IssueFormField) VisibleInContent() bool { + if len(iff.Visible) == 0 { + // we have our markdown exception + return iff.Type != IssueFormFieldTypeMarkdown + } + return slices.Contains(iff.Visible, IssueFormFieldVisibleContent) +} + +// IssueFormFieldVisible defines issue form field visible +// swagger:model +type IssueFormFieldVisible string + +const ( + IssueFormFieldVisibleForm IssueFormFieldVisible = "form" + IssueFormFieldVisibleContent IssueFormFieldVisible = "content" +) + // IssueTemplate represents an issue template for a repository // swagger:model type IssueTemplate struct { diff --git a/modules/structs/repo.go b/modules/structs/repo.go index 86f683f00a..ee7143df3a 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -90,6 +90,7 @@ type Repository struct { ExternalWiki *ExternalWiki `json:"external_wiki,omitempty"` HasPullRequests bool `json:"has_pull_requests"` HasProjects bool `json:"has_projects"` + ProjectsMode string `json:"projects_mode"` HasReleases bool `json:"has_releases"` HasPackages bool `json:"has_packages"` HasActions bool `json:"has_actions"` @@ -182,6 +183,8 @@ type EditRepoOption struct { HasPullRequests *bool `json:"has_pull_requests,omitempty"` // either `true` to enable project unit, or `false` to disable them. HasProjects *bool `json:"has_projects,omitempty"` + // `repo` to only allow repo-level projects, `owner` to only allow owner projects, `all` to allow both. + ProjectsMode *string `json:"projects_mode,omitempty" binding:"In(repo,owner,all)"` // either `true` to enable releases unit, or `false` to disable them. HasReleases *bool `json:"has_releases,omitempty"` // either `true` to enable packages unit, or `false` to disable them. diff --git a/modules/structs/user.go b/modules/structs/user.go index 0df67894b0..c43558be5d 100644 --- a/modules/structs/user.go +++ b/modules/structs/user.go @@ -1,4 +1,5 @@ // Copyright 2014 The Gogs Authors. All rights reserved. +// Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package structs @@ -108,3 +109,33 @@ type UpdateUserAvatarOption struct { // image must be base64 encoded Image string `json:"image" binding:"Required"` } + +// Badge represents a user badge +// swagger:model +type Badge struct { + ID int64 `json:"id"` + Slug string `json:"slug"` + Description string `json:"description"` + ImageURL string `json:"image_url"` +} + +// UserBadge represents a user badge +// swagger:model +type UserBadge struct { + ID int64 `json:"id"` + BadgeID int64 `json:"badge_id"` + UserID int64 `json:"user_id"` +} + +// UserBadgeOption options for link between users and badges +type UserBadgeOption struct { + // example: ["badge1","badge2"] + BadgeSlugs []string `json:"badge_slugs" binding:"Required"` +} + +// BadgeList +// swagger:response BadgeList +type BadgeList struct { + // in:body + Body []Badge `json:"body"` +} diff --git a/modules/templates/helper.go b/modules/templates/helper.go index 0f39767586..0997239a55 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -33,16 +33,16 @@ func NewFuncMap() template.FuncMap { // ----------------------------------------------------------------- // html/template related functions - "dict": dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names. - "Eval": Eval, - "SafeHTML": SafeHTML, - "HTMLFormat": HTMLFormat, - "HTMLEscape": HTMLEscape, - "QueryEscape": url.QueryEscape, - "JSEscape": JSEscapeSafe, - "Str2html": Str2html, // TODO: rename it to SanitizeHTML - "URLJoin": util.URLJoin, - "DotEscape": DotEscape, + "dict": dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names. + "Eval": Eval, + "SafeHTML": SafeHTML, + "HTMLFormat": HTMLFormat, + "HTMLEscape": HTMLEscape, + "QueryEscape": url.QueryEscape, + "JSEscape": JSEscapeSafe, + "SanitizeHTML": SanitizeHTML, + "URLJoin": util.URLJoin, + "DotEscape": DotEscape, "PathEscape": url.PathEscape, "PathEscapeSegments": util.PathEscapeSegments, @@ -207,15 +207,9 @@ func SafeHTML(s any) template.HTML { panic(fmt.Sprintf("unexpected type %T", s)) } -// Str2html sanitizes the input by pre-defined markdown rules -func Str2html(s any) template.HTML { - switch v := s.(type) { - case string: - return template.HTML(markup.Sanitize(v)) - case template.HTML: - return template.HTML(markup.Sanitize(string(v))) - } - panic(fmt.Sprintf("unexpected type %T", s)) +// SanitizeHTML sanitizes the input by pre-defined markdown rules +func SanitizeHTML(s string) template.HTML { + return template.HTML(markup.Sanitize(s)) } func HTMLEscape(s any) template.HTML { diff --git a/modules/templates/helper_test.go b/modules/templates/helper_test.go index 8f5d633d4f..64f29d033e 100644 --- a/modules/templates/helper_test.go +++ b/modules/templates/helper_test.go @@ -61,3 +61,7 @@ func TestJSEscapeSafe(t *testing.T) { func TestHTMLFormat(t *testing.T) { assert.Equal(t, template.HTML("< < 1"), HTMLFormat("%s %s %d", "<", template.HTML("<"), 1)) } + +func TestSanitizeHTML(t *testing.T) { + assert.Equal(t, template.HTML(`link xss
inline
`), SanitizeHTML(`link xss
inline
`)) +} diff --git a/modules/templates/mailer.go b/modules/templates/mailer.go index 04032e3982..f1832cba0e 100644 --- a/modules/templates/mailer.go +++ b/modules/templates/mailer.go @@ -5,6 +5,7 @@ package templates import ( "context" + "fmt" "html/template" "regexp" "strings" @@ -33,7 +34,7 @@ func mailSubjectTextFuncMap() texttmpl.FuncMap { } } -func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template, name string, content []byte) { +func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template, name string, content []byte) error { // Split template into subject and body var subjectContent []byte bodyContent := content @@ -42,20 +43,13 @@ func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template, subjectContent = content[0:loc[0]] bodyContent = content[loc[1]:] } - if _, err := stpl.New(name). - Parse(string(subjectContent)); err != nil { - log.Error("Failed to parse template [%s/subject]: %v", name, err) - if !setting.IsProd { - log.Fatal("Please fix the mail template error") - } + if _, err := stpl.New(name).Parse(string(subjectContent)); err != nil { + return fmt.Errorf("failed to parse template [%s/subject]: %w", name, err) } - if _, err := btpl.New(name). - Parse(string(bodyContent)); err != nil { - log.Error("Failed to parse template [%s/body]: %v", name, err) - if !setting.IsProd { - log.Fatal("Please fix the mail template error") - } + if _, err := btpl.New(name).Parse(string(bodyContent)); err != nil { + return fmt.Errorf("failed to parse template [%s/body]: %w", name, err) } + return nil } // Mailer provides the templates required for sending notification mails. @@ -87,7 +81,13 @@ func Mailer(ctx context.Context) (*texttmpl.Template, *template.Template) { if firstRun { log.Trace("Adding mail template %s: %s by %s", tmplName, assetPath, layerName) } - buildSubjectBodyTemplate(subjectTemplates, bodyTemplates, tmplName, content) + if err = buildSubjectBodyTemplate(subjectTemplates, bodyTemplates, tmplName, content); err != nil { + if firstRun { + log.Fatal("Failed to parse mail template, err: %v", err) + } else { + log.Error("Failed to parse mail template, err: %v", err) + } + } } } diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go index 1d9635410b..cdff31698c 100644 --- a/modules/templates/util_render.go +++ b/modules/templates/util_render.go @@ -208,7 +208,7 @@ func RenderMarkdownToHtml(ctx context.Context, input string) template.HTML { //n if err != nil { log.Error("RenderString: %v", err) } - return template.HTML(output) + return output } func RenderLabels(ctx context.Context, labels []*issues_model.Label, repoLink string) template.HTML { diff --git a/modules/templates/util_string.go b/modules/templates/util_string.go index 2771b1e223..479b755da1 100644 --- a/modules/templates/util_string.go +++ b/modules/templates/util_string.go @@ -4,6 +4,8 @@ package templates import ( + "fmt" + "html/template" "strings" "code.gitea.io/gitea/modules/base" @@ -17,6 +19,19 @@ func NewStringUtils() *StringUtils { return &stringUtils } +func (su *StringUtils) ToString(v any) string { + switch v := v.(type) { + case string: + return v + case template.HTML: + return string(v) + case fmt.Stringer: + return v.String() + default: + return fmt.Sprint(v) + } +} + func (su *StringUtils) HasPrefix(s, prefix string) bool { return strings.HasPrefix(s, prefix) } diff --git a/modules/translation/mock.go b/modules/translation/mock.go index 1f0559f38d..18fbc1044a 100644 --- a/modules/translation/mock.go +++ b/modules/translation/mock.go @@ -9,7 +9,9 @@ import ( ) // MockLocale provides a mocked locale without any translations -type MockLocale struct{} +type MockLocale struct { + Lang, LangName string // these fields are used directly in templates: ctx.Locale.Lang +} var _ Locale = (*MockLocale)(nil) diff --git a/modules/translation/translation.go b/modules/translation/translation.go index b7c18f610a..36ae58a9f1 100644 --- a/modules/translation/translation.go +++ b/modules/translation/translation.go @@ -144,7 +144,7 @@ func Match(tags ...language.Tag) language.Tag { // locale represents the information of localization. type locale struct { i18n.Locale - Lang, LangName string // these fields are used directly in templates: .i18n.Lang + Lang, LangName string // these fields are used directly in templates: ctx.Locale.Lang msgPrinter *message.Printer } diff --git a/modules/util/util.go b/modules/util/util.go index 28b549f405..5c75158196 100644 --- a/modules/util/util.go +++ b/modules/util/util.go @@ -17,64 +17,13 @@ import ( "golang.org/x/text/language" ) -// OptionalBool a boolean that can be "null" -type OptionalBool byte - -const ( - // OptionalBoolNone a "null" boolean value - OptionalBoolNone OptionalBool = iota - // OptionalBoolTrue a "true" boolean value - OptionalBoolTrue - // OptionalBoolFalse a "false" boolean value - OptionalBoolFalse -) - -// IsTrue return true if equal to OptionalBoolTrue -func (o OptionalBool) IsTrue() bool { - return o == OptionalBoolTrue -} - -// IsFalse return true if equal to OptionalBoolFalse -func (o OptionalBool) IsFalse() bool { - return o == OptionalBoolFalse -} - -// IsNone return true if equal to OptionalBoolNone -func (o OptionalBool) IsNone() bool { - return o == OptionalBoolNone -} - -// ToGeneric converts OptionalBool to optional.Option[bool] -func (o OptionalBool) ToGeneric() optional.Option[bool] { - if o.IsNone() { +// OptionalBoolParse get the corresponding optional.Option[bool] of a string using strconv.ParseBool +func OptionalBoolParse(s string) optional.Option[bool] { + v, e := strconv.ParseBool(s) + if e != nil { return optional.None[bool]() } - return optional.Some[bool](o.IsTrue()) -} - -// OptionalBoolFromGeneric converts optional.Option[bool] to OptionalBool -func OptionalBoolFromGeneric(o optional.Option[bool]) OptionalBool { - if o.Has() { - return OptionalBoolOf(o.Value()) - } - return OptionalBoolNone -} - -// OptionalBoolOf get the corresponding OptionalBool of a bool -func OptionalBoolOf(b bool) OptionalBool { - if b { - return OptionalBoolTrue - } - return OptionalBoolFalse -} - -// OptionalBoolParse get the corresponding OptionalBool of a string using strconv.ParseBool -func OptionalBoolParse(s string) OptionalBool { - b, e := strconv.ParseBool(s) - if e != nil { - return OptionalBoolNone - } - return OptionalBoolOf(b) + return optional.Some(v) } // IsEmptyString checks if the provided string is empty diff --git a/modules/util/util_test.go b/modules/util/util_test.go index c5830ce01c..819e12ee91 100644 --- a/modules/util/util_test.go +++ b/modules/util/util_test.go @@ -8,6 +8,8 @@ import ( "strings" "testing" + "code.gitea.io/gitea/modules/optional" + "github.com/stretchr/testify/assert" ) @@ -173,17 +175,17 @@ func Test_RandomBytes(t *testing.T) { assert.NotEqual(t, bytes3, bytes4) } -func Test_OptionalBool(t *testing.T) { - assert.Equal(t, OptionalBoolNone, OptionalBoolParse("")) - assert.Equal(t, OptionalBoolNone, OptionalBoolParse("x")) +func TestOptionalBoolParse(t *testing.T) { + assert.Equal(t, optional.None[bool](), OptionalBoolParse("")) + assert.Equal(t, optional.None[bool](), OptionalBoolParse("x")) - assert.Equal(t, OptionalBoolFalse, OptionalBoolParse("0")) - assert.Equal(t, OptionalBoolFalse, OptionalBoolParse("f")) - assert.Equal(t, OptionalBoolFalse, OptionalBoolParse("False")) + assert.Equal(t, optional.Some(false), OptionalBoolParse("0")) + assert.Equal(t, optional.Some(false), OptionalBoolParse("f")) + assert.Equal(t, optional.Some(false), OptionalBoolParse("False")) - assert.Equal(t, OptionalBoolTrue, OptionalBoolParse("1")) - assert.Equal(t, OptionalBoolTrue, OptionalBoolParse("t")) - assert.Equal(t, OptionalBoolTrue, OptionalBoolParse("True")) + assert.Equal(t, optional.Some(true), OptionalBoolParse("1")) + assert.Equal(t, optional.Some(true), OptionalBoolParse("t")) + assert.Equal(t, optional.Some(true), OptionalBoolParse("True")) } // Test case for any function which accepts and returns a single string. diff --git a/options/license/MIT-Khronos-old b/options/license/MIT-Khronos-old new file mode 100644 index 0000000000..430863bc98 --- /dev/null +++ b/options/license/MIT-Khronos-old @@ -0,0 +1,23 @@ +Copyright (c) 2014-2020 The Khronos Group Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and/or associated documentation files (the "Materials"), +to deal in the Materials without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Materials, and to permit persons to whom the +Materials are furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Materials. + +MODIFICATIONS TO THIS FILE MAY MEAN IT NO LONGER ACCURATELY REFLECTS KHRONOS +STANDARDS. THE UNMODIFIED, NORMATIVE VERSIONS OF KHRONOS SPECIFICATIONS AND +HEADER INFORMATION ARE LOCATED AT https://www.khronos.org/registry/ + +THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM,OUT OF OR IN CONNECTION WITH THE MATERIALS OR THE USE OR OTHER DEALINGS +IN THE MATERIALS. diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index d30103a8eb..2a421b1172 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -123,6 +123,7 @@ pin=Připnout unpin=Odepnout artifacts=Artefakty +confirm_delete_artifact=Jste si jisti, že chcete odstranit artefakt „%s“? archived=Archivováno @@ -423,6 +424,7 @@ authorization_failed_desc=Autorizace selhala, protože jsme detekovali neplatný sspi_auth_failed=SSPI autentizace selhala password_pwned=Heslo, které jste zvolili, je na seznamu odcizených hesel, která byla dříve odhalena při narušení veřejných dat. Zkuste to prosím znovu s jiným heslem. password_pwned_err=Nelze dokončit požadavek na HaveIBeenPwned +last_admin=Nelze odstranit posledního správce. Musí existovat alespoň jeden správce. [mail] view_it_on=Zobrazit na %s @@ -588,6 +590,7 @@ org_still_own_packages=Organizace stále vlastní jeden nebo více balíčků. N target_branch_not_exist=Cílová větev neexistuje. +admin_cannot_delete_self=Nemůžete se smazat, dokud jste správce. Nejdříve prosím odeberte svá administrátorská oprávnění. [user] change_avatar=Změnit váš avatar… @@ -967,6 +970,8 @@ issue_labels_helper=Vyberte sadu štítků úkolů. license=Licence license_helper=Vyberte licenční soubor. license_helper_desc=Licence řídí, co ostatní mohou a nemohou dělat s vaším kódem. Nejste si jisti, která je pro váš projekt správná? Podívejte se na Zvolte licenci +object_format=Formát objektu +object_format_helper=Objektový formát repozitáře. Nelze později změnit. SHA1 je nejvíce kompatibilní. readme=README readme_helper=Vyberte šablonu souboru README. readme_helper_desc=Toto je místo, kde můžete napsat úplný popis vašeho projektu. @@ -1033,6 +1038,7 @@ desc.public=Veřejný desc.template=Šablona desc.internal=Interní desc.archived=Archivováno +desc.sha256=SHA256 template.items=Položky šablony template.git_content=Obsah gitu (výchozí větev) @@ -1183,6 +1189,8 @@ audio_not_supported_in_browser=Váš prohlížeč nepodporuje značku pro HTML5 stored_lfs=Uloženo pomocí Git LFS symbolic_link=Symbolický odkaz executable_file=Spustitelný soubor +vendored=Vendorováno +generated=Generováno commit_graph=Graf commitů commit_graph.select=Vybrat větve commit_graph.hide_pr_refs=Skrýt požadavky na natažení @@ -1518,7 +1526,11 @@ issues.label_title=Název štítku issues.label_description=Popis štítku issues.label_color=Barva štítku issues.label_exclusive=Exkluzivní +issues.label_archive=Archivovat štítek issues.label_archived_filter=Zobrazit archivované popisky +issues.label_archive_tooltip=Archivované štítky jsou ve výchozím nastavení vyloučeny z návrhů při hledání podle popisku. +issues.label_exclusive_desc=Pojmenujte štítek rozsah/položka, aby se stal vzájemně exkluzivním s jinými štítky rozsah/. +issues.label_exclusive_warning=Jakékoliv protichůdné rozsahy štítků budou odstraněny při úpravě štítků u úkolů nebo u požadavku na natažení. issues.label_count=%d štítků issues.label_open_issues=%d otevřených úkolů issues.label_edit=Upravit @@ -1619,6 +1631,7 @@ issues.dependency.issue_closing_blockedby=Uzavření tohoto úkolu je blokováno issues.dependency.issue_close_blocks=Tento úkol blokuje uzavření následujících úkolů issues.dependency.pr_close_blocks=Tento požadavek na natažení blokuje uzavření následujících úkolů issues.dependency.issue_close_blocked=Musíte zavřít všechny úkoly, které blokují tento úkol, aby jej bylo možné zavřít. +issues.dependency.issue_batch_close_blocked=Nelze uzavřít úkoly, které jste vybrali, protože úkol #%d má stále otevřené závislosti issues.dependency.pr_close_blocked=Musíte zavřít všechny úkoly, které blokují tento požadavek na natažení, aby jej bylo možné sloučit. issues.dependency.blocks_short=Blokuje issues.dependency.blocked_by_short=Závisí na @@ -1700,6 +1713,7 @@ pulls.select_commit_hold_shift_for_range=Vyberte commit. Podržte klávesu shift pulls.review_only_possible_for_full_diff=Posouzení je možné pouze při zobrazení plného rozlišení pulls.filter_changes_by_commit=Filtrovat podle commitu pulls.nothing_to_compare=Tyto větve jsou stejné. Není potřeba vytvářet požadavek na natažení. +pulls.nothing_to_compare_have_tag=Vybraná větev/značka je stejná. pulls.nothing_to_compare_and_allow_empty_pr=Tyto větve jsou stejné. Tento požadavek na natažení bude prázdný. pulls.has_pull_request=`Požadavek na natažení mezi těmito větvemi již existuje: %[2]s#%[3]d` pulls.create=Vytvořit požadavek na natažení @@ -1822,6 +1836,7 @@ milestones.update_ago=Aktualizováno %s milestones.no_due_date=Bez lhůty dokončení milestones.open=Otevřít milestones.close=Zavřít +milestones.new_subheader=Milníky vám pomohou organizovat úkoly a sledovat jejich pokrok. milestones.completeness=%d%% Dokončeno milestones.create=Vytvořit milník milestones.title=Název @@ -1955,6 +1970,7 @@ activity.git_stats_and_deletions=a activity.git_stats_deletion_1=%d odebrání activity.git_stats_deletion_n=%d odebrání +contributors.contribution_type.filter_label=Typ příspěvku: contributors.contribution_type.commits=Commity search=Vyhledat @@ -2341,6 +2357,7 @@ settings.matrix.room_id=ID místnosti settings.matrix.message_type=Typ zprávy settings.archive.button=Archivovat repozitář settings.archive.header=Archivovat tento repozitář +settings.archive.text=Archivace repozitáře způsobí, že bude zcela určen pouze pro čtení. Bude skryt z ovládacího panelu. Nikdo (ani vy!) nebude moci vytvářet nové revize ani otevírat nové úkoly nebo žádosti o natažení. settings.archive.success=Repozitář byl úspěšně archivován. settings.archive.error=Nastala chyba při archivování repozitáře. Prohlédněte si záznam pro více detailů. settings.archive.error_ismirror=Nemůžete archivovat zrcadlený repozitář. @@ -2545,6 +2562,11 @@ error.csv.unexpected=Tento soubor nelze vykreslit, protože obsahuje neočekáva error.csv.invalid_field_count=Soubor nelze vykreslit, protože má nesprávný počet polí na řádku %d. [graphs] +component_loading=Načítání %s... +component_loading_failed=Nelze načíst %s +component_loading_info=Může to chvíli trvat… +component_failed_to_load=Došlo k neočekávané chybě. +contributors.what=příspěvky [org] org_name_holder=Název organizace @@ -2715,6 +2737,7 @@ dashboard.delete_repo_archives.started=Spuštěna úloha smazání všech archiv dashboard.delete_missing_repos=Smazat všechny repozitáře, které nemají Git soubory dashboard.delete_missing_repos.started=Spuštěna úloha mazání všech repozitářů, které nemají Git soubory. dashboard.delete_generated_repository_avatars=Odstranit vygenerované avatary repozitářů +dashboard.sync_repo_tags=Synchronizovat značky z git dat do databáze dashboard.update_mirrors=Aktualizovat zrcadla dashboard.repo_health_check=Kontrola stavu všech repozitářů dashboard.check_repo_stats=Zkontrolovat všechny statistiky repositáře @@ -2762,11 +2785,14 @@ dashboard.delete_old_actions=Odstranit všechny staré akce z databáze dashboard.delete_old_actions.started=Začalo odstraňování všech starých akcí z databáze. dashboard.update_checker=Kontrola aktualizací dashboard.delete_old_system_notices=Odstranit všechna stará systémová upozornění z databáze +dashboard.gc_lfs=Úklid LFS meta objektů dashboard.stop_zombie_tasks=Zastavit zombie úlohy dashboard.stop_endless_tasks=Zastavit nekonečné úlohy dashboard.cancel_abandoned_jobs=Zrušit opuštěné úlohy dashboard.start_schedule_tasks=Spustit naplánované úlohy dashboard.sync_branch.started=Synchronizace větví byla spuštěna +dashboard.sync_tag.started=Synchronizace značek spuštěna +dashboard.rebuild_issue_indexer=Znovu sestavit index úkolů users.user_manage_panel=Správa uživatelských účtů users.new_account=Vytvořit uživatelský účet @@ -3184,6 +3210,12 @@ notices.desc=Popis notices.op=Akce notices.delete_success=Systémové upozornění bylo smazáno. +self_check.no_problem_found=Zatím nebyl nalezen žádný problém. +self_check.database_collation_mismatch=Očekávejte, že databáze použije collation: %s +self_check.database_collation_case_insensitive=Databáze používá collation %s, což je collation nerozlišující velká a malá písmena. Ačkoli s ní Gitea může pracovat, mohou se vyskytnout vzácné případy, kdy nebude fungovat podle očekávání. +self_check.database_inconsistent_collation_columns=Databáze používá collation %s, ale tyto sloupce používají chybné collation. To může způsobit neočekávané problémy. +self_check.database_fix_mysql=Pro uživatele MySQL/MariaDB můžete použít příkaz "gitea doctor convert", který opraví problémy s collation, nebo můžete také problém vyřešit příkazem "ALTER ... COLLATE ..." SQL ručně. +self_check.database_fix_mssql=Uživatelé MSSQL mohou problém vyřešit pouze pomocí příkazu "ALTER ... COLLATE ..." SQL ručně. [action] create_repo=vytvořil/a repozitář %s @@ -3371,6 +3403,7 @@ rpm.distros.suse=na distribuce založené na SUSE rpm.install=Pro instalaci balíčku spusťte následující příkaz: rpm.repository=Informace o repozitáři rpm.repository.architectures=Architektury +rpm.repository.multiple_groups=Tento balíček je k dispozici ve více skupinách. rubygems.install=Pro instalaci balíčku pomocí gem spusťte následující příkaz: rubygems.install2=nebo ho přidejte do Gemfie: rubygems.dependencies.runtime=Běhové závislosti @@ -3498,6 +3531,8 @@ runs.actors_no_select=Všichni aktéři runs.status_no_select=Všechny stavy runs.no_results=Nebyly nalezeny žádné výsledky. runs.no_workflows=Zatím neexistují žádné pracovní postupy. +runs.no_workflows.quick_start=Nevíte jak začít s Gitea Actions? Podívejte se na průvodce rychlým startem. +runs.no_workflows.documentation=Další informace o Gitea Actions naleznete v dokumentaci. runs.no_runs=Pracovní postup zatím nebyl spuštěn. runs.empty_commit_message=(prázdná zpráva commitu) @@ -3515,6 +3550,7 @@ variables.none=Zatím nejsou žádné proměnné. variables.deletion=Odstranit proměnnou variables.deletion.description=Odstranění proměnné je trvalé a nelze jej vrátit zpět. Pokračovat? variables.description=Proměnné budou předány určitým akcím a nelze je přečíst jinak. +variables.id_not_exist=Proměnná s ID %d neexistuje. variables.edit=Upravit proměnnou variables.deletion.failed=Nepodařilo se odstranit proměnnou. variables.deletion.success=Proměnná byla odstraněna. diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 5fb6b7c011..651caadcde 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -142,6 +142,19 @@ confirm_delete_selected = Confirm to delete all selected items? name = Name value = Value +filter = Filter +filter.clear = Clear Filter +filter.is_archived = Archived +filter.not_archived = Not Archived +filter.is_fork = Forked +filter.not_fork = Not Forked +filter.is_mirror = Mirrored +filter.not_mirror = Not Mirrored +filter.is_template = Template +filter.not_template = Not Template +filter.public = Public +filter.private = Private + [aria] navbar = Navigation Bar footer = Footer @@ -307,6 +320,7 @@ env_config_keys = Environment Configuration env_config_keys_prompt = The following environment variables will also be applied to your configuration file: [home] +nav_menu = Navigation Menu uname_holder = Username or Email Address password_holder = Password switch_dashboard_context = Switch Dashboard Context @@ -619,6 +633,30 @@ form.name_reserved = The username "%s" is reserved. form.name_pattern_not_allowed = The pattern "%s" is not allowed in a username. form.name_chars_not_allowed = User name "%s" contains invalid characters. +block.block = Block +block.block.user = Block user +block.block.org = Block user for organization +block.block.failure = Failed to block user: %s +block.unblock = Unblock +block.unblock.failure = Failed to unblock user: %s +block.blocked = You have blocked this user. +block.title = Block a user +block.info = Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user. +block.info_1 = Blocking a user prevents the following actions on your account and your repositories: +block.info_2 = following your account +block.info_3 = send you notifications by @mentioning your username +block.info_4 = inviting you as a collaborator to their repositories +block.info_5 = starring, forking or watching on repositories +block.info_6 = opening and commenting on issues or pull requests +block.info_7 = reacting on your comments in issues or pull requests +block.user_to_block = User to block +block.note = Note +block.note.title = Optional note: +block.note.info = The note is not visible to the blocked user. +block.note.edit = Edit note +block.list = Blocked users +block.list.none = You have not blocked any users. + [settings] profile = Profile account = Account @@ -957,6 +995,7 @@ fork_visibility_helper = The visibility of a forked repository cannot be changed fork_branch = Branch to be cloned to the fork all_branches = All branches fork_no_valid_owners = This repository can not be forked because there are no valid owners. +fork.blocked_user = Cannot fork the repository because you are blocked by the repository owner. use_template = Use this template open_with_editor = Open with %s download_zip = Download ZIP @@ -1134,6 +1173,7 @@ watch = Watch unstar = Unstar star = Star fork = Fork +action.blocked_user = Cannot perform action because you are blocked by the repository owner. download_archive = Download Repository more_operations = More Operations @@ -1384,6 +1424,8 @@ issues.new.assignees = Assignees issues.new.clear_assignees = Clear assignees issues.new.no_assignees = No Assignees issues.new.no_reviewers = No reviewers +issues.new.blocked_user = Cannot create issue because you are blocked by the repository owner. +issues.edit.blocked_user = Cannot edit content because you are blocked by the poster or repository owner. issues.choose.get_started = Get Started issues.choose.open_external_link = Open issues.choose.blank = Default @@ -1499,6 +1541,7 @@ issues.close_comment_issue = Comment and Close issues.reopen_issue = Reopen issues.reopen_comment_issue = Comment and Reopen issues.create_comment = Comment +issues.comment.blocked_user = Cannot create or edit comment because you are blocked by the poster or repository owner. issues.closed_at = `closed this issue %[2]s` issues.reopened_at = `reopened this issue %[2]s` issues.commit_ref_at = `referenced this issue from a commit %[2]s` @@ -1697,6 +1740,7 @@ compare.compare_head = compare pulls.desc = Enable pull requests and code reviews. pulls.new = New Pull Request +pulls.new.blocked_user = Cannot create pull request because you are blocked by the repository owner. pulls.view = View Pull Request pulls.compare_changes = New Pull Request pulls.allow_edits_from_maintainers = Allow edits from maintainers @@ -1795,9 +1839,9 @@ pulls.unrelated_histories = Merge Failed: The merge head and base do not share a pulls.merge_out_of_date = Merge Failed: Whilst generating the merge, the base was updated. Hint: Try again. pulls.head_out_of_date = Merge Failed: Whilst generating the merge, the head was updated. Hint: Try again. pulls.has_merged = Failed: The pull request has been merged, you cannot merge again or change the target branch. -pulls.push_rejected = Merge Failed: The push was rejected. Review the Git Hooks for this repository. +pulls.push_rejected = Push Failed: The push was rejected. Review the Git Hooks for this repository. pulls.push_rejected_summary = Full Rejection Message -pulls.push_rejected_no_message = Merge Failed: The push was rejected but there was no remote message.
Review the Git Hooks for this repository +pulls.push_rejected_no_message = Push Failed: The push was rejected but there was no remote message. Review the Git Hooks for this repository pulls.open_unmerged_pull_exists = `You cannot perform a reopen operation because there is a pending pull request (#%d) with identical properties.` pulls.status_checking = Some checks are pending pulls.status_checks_success = All checks were successful @@ -2080,7 +2124,11 @@ settings.pulls.default_delete_branch_after_merge = Delete pull request branch af settings.pulls.default_allow_edits_from_maintainers = Allow edits from maintainers by default settings.releases_desc = Enable Repository Releases settings.packages_desc = Enable Repository Packages Registry -settings.projects_desc = Enable Repository Projects +settings.projects_desc = Enable Projects +settings.projects_mode_desc = Projects Mode (which kinds of projects to show) +settings.projects_mode_repo = Repo projects only +settings.projects_mode_owner = Only user or org projects +settings.projects_mode_all = All projects settings.actions_desc = Enable Repository Actions settings.admin_settings = Administrator Settings settings.admin_enable_health_check = Enable Repository Health Checks (git fsck) @@ -2106,6 +2154,7 @@ settings.convert_fork_succeed = The fork has been converted into a regular repos settings.transfer = Transfer Ownership settings.transfer.rejected = Repository transfer was rejected. settings.transfer.success = Repository transfer was successful. +settings.transfer.blocked_user = Cannot transfer repository because you are blocked by the new owner. settings.transfer_abort = Cancel transfer settings.transfer_abort_invalid = You cannot cancel a non existent repository transfer. settings.transfer_abort_success = The repository transfer to %s was successfully canceled. @@ -2151,6 +2200,7 @@ settings.add_collaborator_success = The collaborator has been added. settings.add_collaborator_inactive_user = Cannot add an inactive user as a collaborator. settings.add_collaborator_owner = Cannot add an owner as a collaborator. settings.add_collaborator_duplicate = The collaborator is already added to this repository. +settings.add_collaborator.blocked_user = The collaborator is blocked by the repository owner or vice versa. settings.delete_collaborator = Remove settings.collaborator_deletion = Remove Collaborator settings.collaborator_deletion_desc = Removing a collaborator will revoke their access to this repository. Continue? @@ -2717,6 +2767,7 @@ teams.add_nonexistent_repo = "The repository you're trying to add doesn't exist, teams.add_duplicate_users = User is already a team member. teams.repos.none = No repositories could be accessed by this team. teams.members.none = No members on this team. +teams.members.blocked_user = Cannot add the user because it is blocked by the organization. teams.specific_repositories = Specific repositories teams.specific_repositories_helper = Members will only have access to repositories explicitly added to the team. Selecting this will not automatically remove repositories already added with All repositories. teams.all_repositories = All repositories diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index 7fd79446cc..20ef954cd2 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -424,6 +424,7 @@ authorization_failed_desc=L'autorisation a échoué car nous avons détecté une sspi_auth_failed=Échec de l'authentification SSPI password_pwned=Le mot de passe que vous avez choisi se trouve sur la liste des mots de passe ayant fuité sur internet. Veuillez réessayer avec un mot de passe différent et considérer remplacer ce mot de passe si vous l'utilisez ailleurs. password_pwned_err=Impossible d'envoyer la demande à HaveIBeenPwned +last_admin=Vous ne pouvez pas supprimer ce compte car au moins un administrateur est requis. [mail] view_it_on=Voir sur %s @@ -1714,6 +1715,7 @@ pulls.select_commit_hold_shift_for_range=Maintenir Maj et cliquer sur des révis pulls.review_only_possible_for_full_diff=Une évaluation n'est possible que lorsque vous affichez le différentiel complet. pulls.filter_changes_by_commit=Filtrer par révision pulls.nothing_to_compare=Ces branches sont identiques. Il n’y a pas besoin de créer une demande d'ajout. +pulls.nothing_to_compare_have_tag=Les branches/étiquettes sélectionnées sont équivalentes. pulls.nothing_to_compare_and_allow_empty_pr=Ces branches sont égales. Cette demande d'ajout sera vide. pulls.has_pull_request='Il existe déjà une demande d'ajout entre ces deux branches : %[2]s#%[3]d' pulls.create=Créer une demande d'ajout diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index 5d9e21703e..af06a78642 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -123,6 +123,7 @@ pin=ピン留め unpin=ピン留め解除 artifacts=成果物 +confirm_delete_artifact=アーティファクト %s を削除してよろしいですか? archived=アーカイブ @@ -148,8 +149,8 @@ footer.software=ソフトウェアについて footer.links=リンク [heatmap] -number_of_contributions_in_the_last_12_months=過去 12 か月間で %s 個の貢献 -no_contributions=貢献なし +number_of_contributions_in_the_last_12_months=過去 12 か月間で %s 件の実績 +no_contributions=実績なし less=少 more=多 @@ -423,6 +424,7 @@ authorization_failed_desc=無効なリクエストを検出したため認可が sspi_auth_failed=SSPI認証に失敗しました password_pwned=あなたが選択したパスワードは、過去の情報漏洩事件で流出した盗まれたパスワードのリストに含まれています。 別のパスワードでもう一度試してください。 また他の登録でもこのパスワードからの変更を検討してください。 password_pwned_err=HaveIBeenPwnedへのリクエストを完了できませんでした +last_admin=最後の管理者は削除できません。少なくとも一人の管理者が必要です。 [mail] view_it_on=%s で見る @@ -588,6 +590,7 @@ org_still_own_packages=組織はまだ1つ以上のパッケージを所有し target_branch_not_exist=ターゲットのブランチが存在していません。 +admin_cannot_delete_self=あなたが管理者である場合、自分自身を削除することはできません。最初に管理者権限を削除してください。 [user] change_avatar=アバターを変更… @@ -968,6 +971,8 @@ issue_labels_helper=イシューのラベルセットを選択 license=ライセンス license_helper=ライセンス ファイルを選択してください。 license_helper_desc=ライセンスにより、他人があなたのコードに対して何ができて何ができないのかを規定します。 どれがプロジェクトにふさわしいか迷っていますか? ライセンス選択サイト も確認してみてください。 +object_format=オブジェクトのフォーマット +object_format_helper=リポジトリのオブジェクトフォーマット。後で変更することはできません。SHA1 は最も互換性があります。 readme=README readme_helper=READMEファイル テンプレートを選択してください。 readme_helper_desc=プロジェクトについての説明をひととおり書く場所です。 @@ -1035,6 +1040,7 @@ desc.public=公開 desc.template=テンプレート desc.internal=組織内 desc.archived=アーカイブ +desc.sha256=SHA256 template.items=テンプレート項目 template.git_content=Gitコンテンツ (デフォルトブランチ) @@ -1504,7 +1510,7 @@ issues.role.member_helper=このユーザーはこのリポジトリを所有し issues.role.collaborator=共同作業者 issues.role.collaborator_helper=このユーザーはリポジトリ上で共同作業するように招待されています。 issues.role.first_time_contributor=初めての貢献者 -issues.role.first_time_contributor_helper=これは、このユーザーのリポジトリへの最初の貢献です。 +issues.role.first_time_contributor_helper=これは、このユーザーによるリポジトリへの最初の貢献です。 issues.role.contributor=貢献者 issues.role.contributor_helper=このユーザーは以前にリポジトリにコミットしています。 issues.re_request_review=レビューを再依頼 @@ -2005,7 +2011,8 @@ settings.mirror_settings.docs.more_information_if_disabled=プッシュミラー settings.mirror_settings.docs.doc_link_title=リポジトリをミラーリングするには? settings.mirror_settings.docs.doc_link_pull_section=ドキュメントの「リモートリポジトリからのプル」セクション。 settings.mirror_settings.docs.pulling_remote_title=リモートリポジトリからのプル -settings.mirror_settings.mirrored_repository=同期するリポジトリ +settings.mirror_settings.mirrored_repository=ミラー元のリポジトリ +settings.mirror_settings.pushed_repository=プッシュ先のリポジトリ settings.mirror_settings.direction=方向 settings.mirror_settings.direction.pull=プル settings.mirror_settings.direction.push=プッシュ @@ -3540,6 +3547,8 @@ runs.actors_no_select=すべてのアクター runs.status_no_select=すべてのステータス runs.no_results=一致する結果はありません。 runs.no_workflows=ワークフローはまだありません。 +runs.no_workflows.quick_start=Gitea Actions の始め方がわからない? ではクイックスタートガイドをご覧ください。 +runs.no_workflows.documentation=Gitea Actions の詳細については、ドキュメントを参照してください。 runs.no_runs=ワークフローはまだ実行されていません。 runs.empty_commit_message=(空のコミットメッセージ) diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 7c8153cbb1..89f237a117 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -2119,7 +2119,7 @@ settings.trust_model.collaborator.long=协作者:信任协作者的签名 settings.trust_model.collaborator.desc=此仓库中协作者的有效签名将被标记为「可信」(无论它们是否是提交者),签名只符合提交者时将标记为「不可信」,都不匹配时标记为「不匹配」。 settings.trust_model.committer=提交者 settings.trust_model.committer.long=提交者: 信任与提交者相符的签名 (此特性类似 GitHub,这会强制采用 Gitea 作为提交者和签名者) -settings.trust_model.committer.desc=有效签名只有和提交者相匹配才会被标记为“受信任”,否则它们将被标记为“不匹配”。这强制 Gitea 成为签名提交的提交者,而实际提交者被加上 Co-authored-by: 和 Co-committed-by: 的标记。 默认的 Gitea 密钥必须撇撇数据库种的一名用户。 +settings.trust_model.committer.desc=有效签名只有和提交者相匹配才会被标记为“受信任”,否则它们将被标记为“不匹配”。这强制 Gitea 成为签名提交的提交者,而实际提交者被加上 Co-authored-by: 和 Co-committed-by: 的标记。 默认的 Gitea 密钥必须匹配数据库中的一名用户。 settings.trust_model.collaboratorcommitter=协作者+提交者 settings.trust_model.collaboratorcommitter.long=协作者+提交者:信任协作者同时是提交者的签名 settings.trust_model.collaboratorcommitter.desc=此仓库中协作者的有效签名在他同时是提交者时将被标记为「可信」,签名只匹配了提交者时将标记为「不可信」,都不匹配时标记为「不匹配」。这会强制 Gitea 成为签名者和提交者,实际的提交者将被标记于提交消息结尾处的「Co-Authored-By:」和「Co-Committed-By:」。默认的 Gitea 签名密钥必须匹配数据库中的一个用户密钥。 @@ -3250,7 +3250,9 @@ notices.delete_success=系统通知已被删除。 self_check.no_problem_found=尚未发现问题。 self_check.database_collation_mismatch=期望数据库使用的校验方式:%s self_check.database_collation_case_insensitive=数据库正在使用一个校验 %s, 这是一个不敏感的校验. 虽然Gitea可以与它合作,但可能有一些罕见的情况不如预期的那样起作用。 +self_check.database_inconsistent_collation_columns=数据库正在使用%s的排序规则,但是这些列使用了不匹配的排序规则。这可能会造成一些意外问题。 self_check.database_fix_mysql=对于MySQL/MariaDB用户,您可以使用“gitea doctor convert”命令来解决校验问题。 或者您也可以通过 "ALTER ... COLLATE ..." 这样的SQL 来手动解决这个问题。 +self_check.database_fix_mssql=对于MSSQL用户,您现在只能通过"ALTER ... COLLATE ..."SQLs手动解决这个问题。 [action] create_repo=创建了仓库 %s diff --git a/package-lock.json b/package-lock.json index 8f641edb5b..6a6eb4b947 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "@citation-js/plugin-csl": "0.7.6", "@citation-js/plugin-software-formats": "0.6.1", "@claviska/jquery-minicolors": "2.3.6", - "@github/markdown-toolbar-element": "2.2.1", + "@github/markdown-toolbar-element": "2.2.3", "@github/relative-time-element": "4.3.1", "@github/text-expander-element": "2.6.1", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", @@ -18,13 +18,12 @@ "@webcomponents/custom-elements": "1.6.0", "add-asset-webpack-plugin": "2.0.1", "ansi_up": "6.0.2", - "asciinema-player": "3.6.4", - "chart.js": "4.4.1", + "asciinema-player": "3.7.0", + "chart.js": "4.4.2", "chartjs-adapter-dayjs-4": "1.0.4", "chartjs-plugin-zoom": "2.0.1", - "clippie": "4.0.6", + "clippie": "4.0.7", "css-loader": "6.10.0", - "css-variables-parser": "1.0.1", "dayjs": "1.11.10", "dropzone": "6.0.0-beta.2", "easymde": "2.18.0", @@ -37,16 +36,16 @@ "katex": "0.16.9", "license-checker-webpack-plugin": "0.2.1", "mermaid": "10.8.0", - "mini-css-extract-plugin": "2.8.0", + "mini-css-extract-plugin": "2.8.1", "minimatch": "9.0.3", "monaco-editor": "0.46.0", "monaco-editor-webpack-plugin": "7.1.0", "pdfobject": "2.3.0", "postcss": "8.4.35", - "postcss-loader": "8.1.0", + "postcss-loader": "8.1.1", "pretty-ms": "9.0.0", "sortablejs": "1.15.2", - "swagger-ui-dist": "5.11.6", + "swagger-ui-dist": "5.11.8", "tailwindcss": "3.4.1", "throttle-debounce": "5.0.0", "tinycolor2": "1.6.0", @@ -54,25 +53,25 @@ "toastify-js": "1.12.0", "tributejs": "5.1.3", "uint8-to-base64": "0.2.0", - "vue": "3.4.19", + "vue": "3.4.21", "vue-bar-graph": "2.0.0", "vue-chartjs": "5.3.0", "vue-loader": "17.4.2", "vue3-calendar-heatmap": "2.0.5", - "webpack": "5.90.2", + "webpack": "5.90.3", "webpack-cli": "5.1.4", "wrap-ansi": "9.0.0" }, "devDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "4.1.0", - "@playwright/test": "1.41.2", + "@playwright/test": "1.42.1", "@stoplight/spectral-cli": "6.11.0", - "@stylistic/eslint-plugin-js": "1.6.2", - "@stylistic/stylelint-plugin": "2.0.0", + "@stylistic/eslint-plugin-js": "1.6.3", + "@stylistic/stylelint-plugin": "2.1.0", "@vitejs/plugin-vue": "5.0.4", - "eslint": "8.56.0", + "eslint": "8.57.0", "eslint-plugin-array-func": "4.0.0", - "eslint-plugin-github": "4.10.1", + "eslint-plugin-github": "4.10.2", "eslint-plugin-i": "2.29.1", "eslint-plugin-jquery": "1.5.1", "eslint-plugin-no-jquery": "2.7.0", @@ -82,7 +81,7 @@ "eslint-plugin-unicorn": "51.0.1", "eslint-plugin-vitest": "0.3.22", "eslint-plugin-vitest-globals": "1.4.0", - "eslint-plugin-vue": "9.21.1", + "eslint-plugin-vue": "9.22.0", "eslint-plugin-vue-scoped-css": "2.7.2", "eslint-plugin-wc": "2.0.4", "jsdom": "24.0.0", @@ -94,7 +93,7 @@ "svgo": "3.2.0", "updates": "15.1.2", "vite-string-plugin": "1.1.5", - "vitest": "1.2.2" + "vitest": "1.3.1" }, "engines": { "node": ">= 18.0.0" @@ -296,9 +295,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", - "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz", + "integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==", "bin": { "parser": "bin/babel-parser.js" }, @@ -307,9 +306,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", - "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz", + "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -466,9 +465,9 @@ } }, "node_modules/@csstools/css-parser-algorithms": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.5.0.tgz", - "integrity": "sha512-abypo6m9re3clXA00eu5syw+oaPHbJTPapu9C4pzNsJ4hdZDzushT50Zhu+iIYXgEe1CxnRMn7ngsbV+MLrlpQ==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.6.0.tgz", + "integrity": "sha512-YfEHq0eRH98ffb5/EsrrDspVWAuph6gDggAE74ZtjecsmyyWpW768hOyiONa8zwWGbIWYfa2Xp4tRTrpQQ00CQ==", "dev": true, "funding": [ { @@ -507,9 +506,9 @@ } }, "node_modules/@csstools/media-query-list-parser": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.7.tgz", - "integrity": "sha512-lHPKJDkPUECsyAvD60joYfDmp8UERYxHGkFfyLJFTVK/ERJe0sVlIFLXU5XFxdjNDTerp5L4KeaKG+Z5S94qxQ==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.8.tgz", + "integrity": "sha512-DiD3vG5ciNzeuTEoh74S+JMjQDs50R3zlxHnBnfd04YYfA/kh2KiBCGhzqLxlJcNq+7yNQ3stuZZYLX6wK/U2g==", "dev": true, "funding": [ { @@ -525,14 +524,14 @@ "node": "^14 || ^16 || >=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^2.5.0", + "@csstools/css-parser-algorithms": "^2.6.0", "@csstools/css-tokenizer": "^2.2.3" } }, "node_modules/@csstools/selector-specificity": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.0.1.tgz", - "integrity": "sha512-NPljRHkq4a14YzZ3YD406uaxh7s0g6eAq3L9aLOWywoqe8PkYamAvtsh7KNX6c++ihDrJ0RiU+/z7rGnhlZ5ww==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.0.2.tgz", + "integrity": "sha512-RpHaZ1h9LE7aALeQXmXrJkRG84ZxIsctEN2biEUmFyKpzFM3zZ35eUMcIzZFsw/2olQE6v69+esEqU2f1MKycg==", "dev": true, "funding": [ { @@ -1012,9 +1011,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", - "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1032,9 +1031,9 @@ "integrity": "sha512-gwxPzLw8XKecy1nP63i9lOBritS3bWmxl02UX6G0TwMQZbMem1BCS1tEZgYd3mkrkiDrUMWaX+DbFCuDFo3K+A==" }, "node_modules/@github/markdown-toolbar-element": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@github/markdown-toolbar-element/-/markdown-toolbar-element-2.2.1.tgz", - "integrity": "sha512-ap+ulyqzG3aVqwKsKjbDdYwM75TQXZpPtmIuPwm+54OTgcC96267oX3cEqd1wSqGsH7O5PonZ//fE9jH7Q4JkA==" + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@github/markdown-toolbar-element/-/markdown-toolbar-element-2.2.3.tgz", + "integrity": "sha512-AlquKGee+IWiAMYVB0xyHFZRMnu4n3X4HTvJHu79GiVJ1ojTukCWyxMlF5NMsecoLcBKsuBhx3QPv2vkE/zQ0A==" }, "node_modules/@github/relative-time-element": { "version": "4.3.1", @@ -1142,11 +1141,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" - }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -1206,13 +1200,13 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" @@ -1227,9 +1221,9 @@ } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "engines": { "node": ">=6.0.0" } @@ -1249,9 +1243,9 @@ "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.22", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", - "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -1389,12 +1383,12 @@ } }, "node_modules/@playwright/test": { - "version": "1.41.2", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.2.tgz", - "integrity": "sha512-qQB9h7KbibJzrDpkXkYvsmiDJK14FULCCZgEcoe2AvFAS64oCirWTwzTlAYEbKaRxWs5TFesE1Na6izMv3HfGg==", + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.42.1.tgz", + "integrity": "sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ==", "dev": true, "dependencies": { - "playwright": "1.41.2" + "playwright": "1.42.1" }, "bin": { "playwright": "cli.js" @@ -1465,9 +1459,9 @@ "dev": true }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.11.0.tgz", - "integrity": "sha512-BV+u2QSfK3i1o6FucqJh5IK9cjAU6icjFFhvknzFgu472jzl0bBojfDAkJLBEsHFMo+YZg6rthBvBBt8z12IBQ==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.12.0.tgz", + "integrity": "sha512-+ac02NL/2TCKRrJu2wffk1kZ+RyqxVUlbjSagNgPm94frxtr+XDL12E5Ll1enWskLrtrZ2r8L3wED1orIibV/w==", "cpu": [ "arm" ], @@ -1478,9 +1472,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.11.0.tgz", - "integrity": "sha512-0ij3iw7sT5jbcdXofWO2NqDNjSVVsf6itcAkV2I6Xsq4+6wjW1A8rViVB67TfBEan7PV2kbLzT8rhOVWLI2YXw==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.12.0.tgz", + "integrity": "sha512-OBqcX2BMe6nvjQ0Nyp7cC90cnumt8PXmO7Dp3gfAju/6YwG0Tj74z1vKrfRz7qAv23nBcYM8BCbhrsWqO7PzQQ==", "cpu": [ "arm64" ], @@ -1491,9 +1485,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.11.0.tgz", - "integrity": "sha512-yPLs6RbbBMupArf6qv1UDk6dzZvlH66z6NLYEwqTU0VHtss1wkI4UYeeMS7TVj5QRVvaNAWYKP0TD/MOeZ76Zg==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.12.0.tgz", + "integrity": "sha512-X64tZd8dRE/QTrBIEs63kaOBG0b5GVEd3ccoLtyf6IdXtHdh8h+I56C2yC3PtC9Ucnv0CpNFJLqKFVgCYe0lOQ==", "cpu": [ "arm64" ], @@ -1504,9 +1498,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.11.0.tgz", - "integrity": "sha512-OvqIgwaGAwnASzXaZEeoJY3RltOFg+WUbdkdfoluh2iqatd090UeOG3A/h0wNZmE93dDew9tAtXgm3/+U/B6bw==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.12.0.tgz", + "integrity": "sha512-cc71KUZoVbUJmGP2cOuiZ9HSOP14AzBAThn3OU+9LcA1+IUqswJyR1cAJj3Mg55HbjZP6OLAIscbQsQLrpgTOg==", "cpu": [ "x64" ], @@ -1517,9 +1511,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.11.0.tgz", - "integrity": "sha512-X17s4hZK3QbRmdAuLd2EE+qwwxL8JxyVupEqAkxKPa/IgX49ZO+vf0ka69gIKsaYeo6c1CuwY3k8trfDtZ9dFg==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.12.0.tgz", + "integrity": "sha512-a6w/Y3hyyO6GlpKL2xJ4IOh/7d+APaqLYdMf86xnczU3nurFTaVN9s9jOXQg97BE4nYm/7Ga51rjec5nfRdrvA==", "cpu": [ "arm" ], @@ -1530,9 +1524,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.11.0.tgz", - "integrity": "sha512-673Lu9EJwxVB9NfYeA4AdNu0FOHz7g9t6N1DmT7bZPn1u6bTF+oZjj+fuxUcrfxWXE0r2jxl5QYMa9cUOj9NFg==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.12.0.tgz", + "integrity": "sha512-0fZBq27b+D7Ar5CQMofVN8sggOVhEtzFUwOwPppQt0k+VR+7UHMZZY4y+64WJ06XOhBTKXtQB/Sv0NwQMXyNAA==", "cpu": [ "arm64" ], @@ -1543,9 +1537,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.11.0.tgz", - "integrity": "sha512-yFW2msTAQNpPJaMmh2NpRalr1KXI7ZUjlN6dY/FhWlOclMrZezm5GIhy3cP4Ts2rIAC+IPLAjNibjp1BsxCVGg==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.12.0.tgz", + "integrity": "sha512-eTvzUS3hhhlgeAv6bfigekzWZjaEX9xP9HhxB0Dvrdbkk5w/b+1Sxct2ZuDxNJKzsRStSq1EaEkVSEe7A7ipgQ==", "cpu": [ "arm64" ], @@ -1556,9 +1550,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.11.0.tgz", - "integrity": "sha512-kKT9XIuhbvYgiA3cPAGntvrBgzhWkGpBMzuk1V12Xuoqg7CI41chye4HU0vLJnGf9MiZzfNh4I7StPeOzOWJfA==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.12.0.tgz", + "integrity": "sha512-ix+qAB9qmrCRiaO71VFfY8rkiAZJL8zQRXveS27HS+pKdjwUfEhqo2+YF2oI+H/22Xsiski+qqwIBxVewLK7sw==", "cpu": [ "riscv64" ], @@ -1569,9 +1563,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.11.0.tgz", - "integrity": "sha512-6q4ESWlyTO+erp1PSCmASac+ixaDv11dBk1fqyIuvIUc/CmRAX2Zk+2qK1FGo5q7kyDcjHCFVwgGFCGIZGVwCA==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.12.0.tgz", + "integrity": "sha512-TenQhZVOtw/3qKOPa7d+QgkeM6xY0LtwzR8OplmyL5LrgTWIXpTQg2Q2ycBf8jm+SFW2Wt/DTn1gf7nFp3ssVA==", "cpu": [ "x64" ], @@ -1582,9 +1576,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.11.0.tgz", - "integrity": "sha512-vIAQUmXeMLmaDN78HSE4Kh6xqof2e3TJUKr+LPqXWU4NYNON0MDN9h2+t4KHrPAQNmU3w1GxBQ/n01PaWFwa5w==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.12.0.tgz", + "integrity": "sha512-LfFdRhNnW0zdMvdCb5FNuWlls2WbbSridJvxOvYWgSBOYZtgBfW9UGNJG//rwMqTX1xQE9BAodvMH9tAusKDUw==", "cpu": [ "x64" ], @@ -1595,9 +1589,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.11.0.tgz", - "integrity": "sha512-LVXo9dDTGPr0nezMdqa1hK4JeoMZ02nstUxGYY/sMIDtTYlli1ZxTXBYAz3vzuuvKO4X6NBETciIh7N9+abT1g==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.12.0.tgz", + "integrity": "sha512-JPDxovheWNp6d7AHCgsUlkuCKvtu3RB55iNEkaQcf0ttsDU/JZF+iQnYcQJSk/7PtT4mjjVG8N1kpwnI9SLYaw==", "cpu": [ "arm64" ], @@ -1608,9 +1602,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.11.0.tgz", - "integrity": "sha512-xZVt6K70Gr3I7nUhug2dN6VRR1ibot3rXqXS3wo+8JP64t7djc3lBFyqO4GiVrhNaAIhUCJtwQ/20dr0h0thmQ==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.12.0.tgz", + "integrity": "sha512-fjtuvMWRGJn1oZacG8IPnzIV6GF2/XG+h71FKn76OYFqySXInJtseAqdprVTDTyqPxQOG9Exak5/E9Z3+EJ8ZA==", "cpu": [ "ia32" ], @@ -1621,9 +1615,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.11.0.tgz", - "integrity": "sha512-f3I7h9oTg79UitEco9/2bzwdciYkWr8pITs3meSDSlr1TdvQ7IxkQaaYN2YqZXX5uZhiYL+VuYDmHwNzhx+HOg==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.12.0.tgz", + "integrity": "sha512-ZYmr5mS2wd4Dew/JjT0Fqi2NPB/ZhZ2VvPp7SmvPZb4Y1CG/LRcS6tcRo2cYU7zLK5A7cdbhWnnWmUjoI4qapg==", "cpu": [ "x64" ], @@ -2091,9 +2085,9 @@ "dev": true }, "node_modules/@stylistic/eslint-plugin-js": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-1.6.2.tgz", - "integrity": "sha512-ndT6X2KgWGxv8101pdMOxL8pihlYIHcOv3ICd70cgaJ9exwkPn8hJj4YQwslxoAlre1TFHnXd/G1/hYXgDrjIA==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-1.6.3.tgz", + "integrity": "sha512-ckdz51oHxD2FaxgY2piJWJVJiwgp8Uu96s+as2yB3RMwavn3nHBrpliVukXY9S/DmMicPRB2+H8nBk23GDG+qA==", "dev": true, "dependencies": { "@types/eslint": "^8.56.2", @@ -2110,19 +2104,19 @@ } }, "node_modules/@stylistic/stylelint-plugin": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@stylistic/stylelint-plugin/-/stylelint-plugin-2.0.0.tgz", - "integrity": "sha512-dHKuT6PGd1WGZLOTuozAM7GdQzdmlmnFXYzvV1jYJXXpcCpV/OJ3+n8TXpMkoOeKHpJydY43EOoZTO1W/FOA4Q==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@stylistic/stylelint-plugin/-/stylelint-plugin-2.1.0.tgz", + "integrity": "sha512-mUZEW9uImHSbXeyzbFmHb8WPBv56UTaEnWL/3dGdAiJ54C+8GTfDwDVdI6gbqT9wV7zynkPu7tCXc5746H9mZQ==", "dev": true, "dependencies": { - "@csstools/css-parser-algorithms": "^2.3.2", - "@csstools/css-tokenizer": "^2.2.1", - "@csstools/media-query-list-parser": "^2.1.5", + "@csstools/css-parser-algorithms": "^2.5.0", + "@csstools/css-tokenizer": "^2.2.3", + "@csstools/media-query-list-parser": "^2.1.7", "is-plain-object": "^5.0.0", - "postcss-selector-parser": "^6.0.13", + "postcss-selector-parser": "^6.0.15", "postcss-value-parser": "^4.2.0", "style-search": "^0.1.0", - "stylelint": "^16.0.2" + "stylelint": "^16.2.1" }, "engines": { "node": "^18.12 || >=20.9" @@ -2189,9 +2183,9 @@ } }, "node_modules/@types/eslint": { - "version": "8.56.2", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.2.tgz", - "integrity": "sha512-uQDwm1wFHmbBbCZCqAlq6Do9LYwByNZHWzXppSnay9SuwJ+VRbjkbLABer54kcPnMSlG6Fdiy2yaFXm/z9Z5gw==", + "version": "8.56.5", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.5.tgz", + "integrity": "sha512-u5/YPJHo1tvkSF2CE0USEkxon82Z5DBy2xR+qfyYNszpX9qcs4sT6uq2kBbj4BXY1+DBGDPnrhMZV3pKWGNukw==", "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -2241,9 +2235,9 @@ "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" }, "node_modules/@types/node": { - "version": "20.11.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz", - "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==", + "version": "20.11.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", + "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", "dependencies": { "undici-types": "~5.26.4" } @@ -2261,9 +2255,9 @@ "dev": true }, "node_modules/@types/semver": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz", - "integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==", + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true }, "node_modules/@types/tern": { @@ -2286,16 +2280,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", - "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.1.0.tgz", + "integrity": "sha512-j6vT/kCulhG5wBmGtstKeiVr1rdXE4nk+DT1k6trYkwlrvW9eOF5ZbgKnd/YR6PcM4uTEXa0h6Fcvf6X7Dxl0w==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/type-utils": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", + "@typescript-eslint/scope-manager": "7.1.0", + "@typescript-eslint/type-utils": "7.1.0", + "@typescript-eslint/utils": "7.1.0", + "@typescript-eslint/visitor-keys": "7.1.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -2311,8 +2305,8 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -2321,15 +2315,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", - "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.1.0.tgz", + "integrity": "sha512-V1EknKUubZ1gWFjiOZhDSNToOjs63/9O0puCgGS8aDOgpZY326fzFu15QAUjwaXzRZjf/qdsdBrckYdv9YxB8w==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", + "@typescript-eslint/scope-manager": "7.1.0", + "@typescript-eslint/types": "7.1.0", + "@typescript-eslint/typescript-estree": "7.1.0", + "@typescript-eslint/visitor-keys": "7.1.0", "debug": "^4.3.4" }, "engines": { @@ -2340,7 +2334,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -2349,13 +2343,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", - "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.1.0.tgz", + "integrity": "sha512-6TmN4OJiohHfoOdGZ3huuLhpiUgOGTpgXNUPJgeZOZR3DnIpdSgtt83RS35OYNNXxM4TScVlpVKC9jyQSETR1A==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0" + "@typescript-eslint/types": "7.1.0", + "@typescript-eslint/visitor-keys": "7.1.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -2366,13 +2360,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", - "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.1.0.tgz", + "integrity": "sha512-UZIhv8G+5b5skkcuhgvxYWHjk7FW7/JP5lPASMEUoliAPwIH/rxoUSQPia2cuOj9AmDZmwUl1usKm85t5VUMew==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/typescript-estree": "7.1.0", + "@typescript-eslint/utils": "7.1.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -2384,7 +2378,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -2393,9 +2387,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", - "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.1.0.tgz", + "integrity": "sha512-qTWjWieJ1tRJkxgZYXx6WUYtWlBc48YRxgY2JN1aGeVpkhmnopq+SUC8UEVGNXIvWH7XyuTjwALfG6bFEgCkQA==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -2406,13 +2400,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", - "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.1.0.tgz", + "integrity": "sha512-k7MyrbD6E463CBbSpcOnwa8oXRdHzH1WiVzOipK3L5KSML92ZKgUBrTlehdi7PEIMT8k0bQixHUGXggPAlKnOQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", + "@typescript-eslint/types": "7.1.0", + "@typescript-eslint/visitor-keys": "7.1.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -2434,17 +2428,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", - "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.1.0.tgz", + "integrity": "sha512-WUFba6PZC5OCGEmbweGpnNJytJiLG7ZvDBJJoUcX4qZYf1mGZ97mO2Mps6O2efxJcJdRNpqweCistDbZMwIVHw==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/scope-manager": "7.1.0", + "@typescript-eslint/types": "7.1.0", + "@typescript-eslint/typescript-estree": "7.1.0", "semver": "^7.5.4" }, "engines": { @@ -2455,16 +2449,16 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^8.56.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", - "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.1.0.tgz", + "integrity": "sha512-FhUqNWluiGNzlvnDZiXad4mZRhtghdoKW6e98GoEOYSu5cND+E39rG5KwJMUzeENwm1ztYBRqof8wMLP+wNPIA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/types": "7.1.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -2495,13 +2489,13 @@ } }, "node_modules/@vitest/expect": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.2.2.tgz", - "integrity": "sha512-3jpcdPAD7LwHUUiT2pZTj2U82I2Tcgg2oVPvKxhn6mDI2On6tfvPQTjAI4628GUGDZrCm4Zna9iQHm5cEexOAg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.3.1.tgz", + "integrity": "sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==", "dev": true, "dependencies": { - "@vitest/spy": "1.2.2", - "@vitest/utils": "1.2.2", + "@vitest/spy": "1.3.1", + "@vitest/utils": "1.3.1", "chai": "^4.3.10" }, "funding": { @@ -2509,12 +2503,12 @@ } }, "node_modules/@vitest/runner": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.2.2.tgz", - "integrity": "sha512-JctG7QZ4LSDXr5CsUweFgcpEvrcxOV1Gft7uHrvkQ+fsAVylmWQvnaAr/HDp3LAH1fztGMQZugIheTWjaGzYIg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.3.1.tgz", + "integrity": "sha512-5FzF9c3jG/z5bgCnjr8j9LNq/9OxV2uEBAITOXfoe3rdZJTdO7jzThth7FXv/6b+kdY65tpRQB7WaKhNZwX+Kg==", "dev": true, "dependencies": { - "@vitest/utils": "1.2.2", + "@vitest/utils": "1.3.1", "p-limit": "^5.0.0", "pathe": "^1.1.1" }, @@ -2550,9 +2544,9 @@ } }, "node_modules/@vitest/snapshot": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.2.2.tgz", - "integrity": "sha512-SmGY4saEw1+bwE1th6S/cZmPxz/Q4JWsl7LvbQIky2tKE35US4gd0Mjzqfr84/4OD0tikGWaWdMja/nWL5NIPA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.3.1.tgz", + "integrity": "sha512-EF++BZbt6RZmOlE3SuTPu/NfwBF6q4ABS37HHXzs2LUVPBLx2QoY/K0fKpRChSo8eLiuxcbCVfqKgx/dplCDuQ==", "dev": true, "dependencies": { "magic-string": "^0.30.5", @@ -2576,9 +2570,9 @@ } }, "node_modules/@vitest/spy": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.2.2.tgz", - "integrity": "sha512-k9Gcahssw8d7X3pSLq3e3XEu/0L78mUkCjivUqCQeXJm9clfXR/Td8+AP+VC1O6fKPIDLcHDTAmBOINVuv6+7g==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.3.1.tgz", + "integrity": "sha512-xAcW+S099ylC9VLU7eZfdT9myV67Nor9w9zhf0mGCYJSO+zM2839tOeROTdikOi/8Qeusffvxb/MyBSOja1Uig==", "dev": true, "dependencies": { "tinyspy": "^2.2.0" @@ -2588,9 +2582,9 @@ } }, "node_modules/@vitest/utils": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.2.2.tgz", - "integrity": "sha512-WKITBHLsBHlpjnDQahr+XK6RE7MiAsgrIkr0pGhQ9ygoxBfUeG0lUG5iLlzqjmKSlBv3+j5EGsriBzh+C3Tq9g==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.3.1.tgz", + "integrity": "sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==", "dev": true, "dependencies": { "diff-sequences": "^29.6.3", @@ -2618,39 +2612,39 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.19.tgz", - "integrity": "sha512-gj81785z0JNzRcU0Mq98E56e4ltO1yf8k5PQ+tV/7YHnbZkrM0fyFyuttnN8ngJZjbpofWE/m4qjKBiLl8Ju4w==", + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.21.tgz", + "integrity": "sha512-MjXawxZf2SbZszLPYxaFCjxfibYrzr3eYbKxwpLR9EQN+oaziSu3qKVbwBERj1IFIB8OLUewxB5m/BFzi613og==", "dependencies": { "@babel/parser": "^7.23.9", - "@vue/shared": "3.4.19", + "@vue/shared": "3.4.21", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.0.2" } }, "node_modules/@vue/compiler-dom": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.19.tgz", - "integrity": "sha512-vm6+cogWrshjqEHTzIDCp72DKtea8Ry/QVpQRYoyTIg9k7QZDX6D8+HGURjtmatfgM8xgCFtJJaOlCaRYRK3QA==", + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.21.tgz", + "integrity": "sha512-IZC6FKowtT1sl0CR5DpXSiEB5ayw75oT2bma1BEhV7RRR1+cfwLrxc2Z8Zq/RGFzJ8w5r9QtCOvTjQgdn0IKmA==", "dependencies": { - "@vue/compiler-core": "3.4.19", - "@vue/shared": "3.4.19" + "@vue/compiler-core": "3.4.21", + "@vue/shared": "3.4.21" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.19.tgz", - "integrity": "sha512-LQ3U4SN0DlvV0xhr1lUsgLCYlwQfUfetyPxkKYu7dkfvx7g3ojrGAkw0AERLOKYXuAGnqFsEuytkdcComei3Yg==", + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.21.tgz", + "integrity": "sha512-me7epoTxYlY+2CUM7hy9PCDdpMPfIwrOvAXud2Upk10g4YLv9UBW7kL798TvMeDhPthkZ0CONNrK2GoeI1ODiQ==", "dependencies": { "@babel/parser": "^7.23.9", - "@vue/compiler-core": "3.4.19", - "@vue/compiler-dom": "3.4.19", - "@vue/compiler-ssr": "3.4.19", - "@vue/shared": "3.4.19", + "@vue/compiler-core": "3.4.21", + "@vue/compiler-dom": "3.4.21", + "@vue/compiler-ssr": "3.4.21", + "@vue/shared": "3.4.21", "estree-walker": "^2.0.2", - "magic-string": "^0.30.6", - "postcss": "^8.4.33", + "magic-string": "^0.30.7", + "postcss": "^8.4.35", "source-map-js": "^1.0.2" } }, @@ -2666,57 +2660,57 @@ } }, "node_modules/@vue/compiler-ssr": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.19.tgz", - "integrity": "sha512-P0PLKC4+u4OMJ8sinba/5Z/iDT84uMRRlrWzadgLA69opCpI1gG4N55qDSC+dedwq2fJtzmGald05LWR5TFfLw==", + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.21.tgz", + "integrity": "sha512-M5+9nI2lPpAsgXOGQobnIueVqc9sisBFexh5yMIMRAPYLa7+5wEJs8iqOZc1WAa9WQbx9GR2twgznU8LTIiZ4Q==", "dependencies": { - "@vue/compiler-dom": "3.4.19", - "@vue/shared": "3.4.19" + "@vue/compiler-dom": "3.4.21", + "@vue/shared": "3.4.21" } }, "node_modules/@vue/reactivity": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.19.tgz", - "integrity": "sha512-+VcwrQvLZgEclGZRHx4O2XhyEEcKaBi50WbxdVItEezUf4fqRh838Ix6amWTdX0CNb/b6t3Gkz3eOebfcSt+UA==", + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.21.tgz", + "integrity": "sha512-UhenImdc0L0/4ahGCyEzc/pZNwVgcglGy9HVzJ1Bq2Mm9qXOpP8RyNTjookw/gOCUlXSEtuZ2fUg5nrHcoqJcw==", "dependencies": { - "@vue/shared": "3.4.19" + "@vue/shared": "3.4.21" } }, "node_modules/@vue/runtime-core": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.19.tgz", - "integrity": "sha512-/Z3tFwOrerJB/oyutmJGoYbuoadphDcJAd5jOuJE86THNZji9pYjZroQ2NFsZkTxOq0GJbb+s2kxTYToDiyZzw==", + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.21.tgz", + "integrity": "sha512-pQthsuYzE1XcGZznTKn73G0s14eCJcjaLvp3/DKeYWoFacD9glJoqlNBxt3W2c5S40t6CCcpPf+jG01N3ULyrA==", "dependencies": { - "@vue/reactivity": "3.4.19", - "@vue/shared": "3.4.19" + "@vue/reactivity": "3.4.21", + "@vue/shared": "3.4.21" } }, "node_modules/@vue/runtime-dom": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.19.tgz", - "integrity": "sha512-IyZzIDqfNCF0OyZOauL+F4yzjMPN2rPd8nhqPP2N1lBn3kYqJpPHHru+83Rkvo2lHz5mW+rEeIMEF9qY3PB94g==", + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.21.tgz", + "integrity": "sha512-gvf+C9cFpevsQxbkRBS1NpU8CqxKw0ebqMvLwcGQrNpx6gqRDodqKqA+A2VZZpQ9RpK2f9yfg8VbW/EpdFUOJw==", "dependencies": { - "@vue/runtime-core": "3.4.19", - "@vue/shared": "3.4.19", + "@vue/runtime-core": "3.4.21", + "@vue/shared": "3.4.21", "csstype": "^3.1.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.19.tgz", - "integrity": "sha512-eAj2p0c429RZyyhtMRnttjcSToch+kTWxFPHlzGMkR28ZbF1PDlTcmGmlDxccBuqNd9iOQ7xPRPAGgPVj+YpQw==", + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.21.tgz", + "integrity": "sha512-aV1gXyKSN6Rz+6kZ6kr5+Ll14YzmIbeuWe7ryJl5muJ4uwSwY/aStXTixx76TwkZFJLm1aAlA/HSWEJ4EyiMkg==", "dependencies": { - "@vue/compiler-ssr": "3.4.19", - "@vue/shared": "3.4.19" + "@vue/compiler-ssr": "3.4.21", + "@vue/shared": "3.4.21" }, "peerDependencies": { - "vue": "3.4.19" + "vue": "3.4.21" } }, "node_modules/@vue/shared": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.19.tgz", - "integrity": "sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw==" + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.21.tgz", + "integrity": "sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g==" }, "node_modules/@webassemblyjs/ast": { "version": "1.11.6", @@ -3269,9 +3263,9 @@ } }, "node_modules/asciinema-player": { - "version": "3.6.4", - "resolved": "https://registry.npmjs.org/asciinema-player/-/asciinema-player-3.6.4.tgz", - "integrity": "sha512-yyMHTjoDuz82/BYPrc3J5GjOtlNI5t2VHTZWss8BmRcY/6nXv+Vilip+XzwIyRBa3/2SSn9FJIEg8bJXBc9o4w==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/asciinema-player/-/asciinema-player-3.7.0.tgz", + "integrity": "sha512-0RDc4j7TkjyhAwxkDe3vNqjAcizc7tubYW2VZi/06csY8iAoSC2uRvSyfNzh9ONDZu8pdf0bZJ91A84Gexb3tg==", "dependencies": { "@babel/runtime": "^7.21.0", "solid-js": "^1.3.0" @@ -3350,10 +3344,13 @@ } }, "node_modules/available-typed-arrays": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.6.tgz", - "integrity": "sha512-j1QzY8iPNPG4o4xmO3ptzpRxTciqD3MgEHtifP/YnJpIo58Xu+ne4BejlbkuaLfXn/nz6HFiw29bLpj2PNMdGg==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -3566,9 +3563,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001587", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001587.tgz", - "integrity": "sha512-HMFNotUmLXn71BQxg8cijvqxnIAofforZOwGsxyXJ0qugTdspUF4sPSJ2vhgprHCB996tIDzEq1ubumPDV8ULA==", + "version": "1.0.30001591", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001591.tgz", + "integrity": "sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ==", "funding": [ { "type": "opencollective", @@ -3627,14 +3624,14 @@ } }, "node_modules/chart.js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.1.tgz", - "integrity": "sha512-C74QN1bxwV1v2PEujhmKjOZ7iUM4w6BWs23Md/6aOZZSlwMzeCIDGuZay++rBgChYru7/+QFeoQW0fQoP534Dg==", + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.2.tgz", + "integrity": "sha512-6GD7iKwFpP5kbSD4MeRRRlTnQvxfQREy36uEtm1hzHzcOqwWx0YEHuspuoNlslu+nciLIB7fjjsHkUv/FzFcOg==", "dependencies": { "@kurkle/color": "^0.3.0" }, "engines": { - "pnpm": ">=7" + "pnpm": ">=8" } }, "node_modules/chartjs-adapter-dayjs-4": { @@ -3756,9 +3753,9 @@ } }, "node_modules/clippie": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/clippie/-/clippie-4.0.6.tgz", - "integrity": "sha512-E5EtOw8iMH0enuL3kBZJ+Po1nPnBD7O+HHpIaWpfWgHbHmdoOQoERrlNOcEEn2yMJQ98WqeKacouAcnRXn7oWA==" + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/clippie/-/clippie-4.0.7.tgz", + "integrity": "sha512-xmIARCRFQUoCR0kNNu4uIv5f/IFqM1fUts0vQwt1hQEdCPEqs3/dTaG38WenlWOgs3Fcn73PBYXbPIVSlOgFRw==" }, "node_modules/cliui": { "version": "7.0.4", @@ -4034,35 +4031,6 @@ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, - "node_modules/css-variables-parser": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/css-variables-parser/-/css-variables-parser-1.0.1.tgz", - "integrity": "sha512-GWaqrwGtAWVr/yjjE17iyvbcy+W3voe0vko1/xLCwFeYd3kTLstzUdVH+g5TTXejrtlsb1FS4L9rP6PmeTa8wQ==", - "dependencies": { - "postcss": "^7.0.36" - } - }, - "node_modules/css-variables-parser/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/css-variables-parser/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, "node_modules/css-what": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", @@ -4842,9 +4810,9 @@ } }, "node_modules/dompurify": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.8.tgz", - "integrity": "sha512-b7uwreMYL2eZhrSCRC4ahLTeZcPZxSmYfmcQGXGkXiZSNW1X85v+SDM5KsWcpivIiUBH47Ji7NtyUdpLeF5JZQ==" + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.9.tgz", + "integrity": "sha512-uyb4NDIvQ3hRn6NiC+SIFaP4mJ/MdXlvtunaqK9Bn6dD3RuB/1S/gasEjDHD8eiaqdSael2vBv+hOs7Y+jhYOQ==" }, "node_modules/domutils": { "version": "3.1.0", @@ -4887,19 +4855,19 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.671", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.671.tgz", - "integrity": "sha512-UUlE+/rWbydmp+FW8xlnnTA5WNA0ZZd2XL8CuMS72rh+k4y1f8+z6yk3UQhEwqHQWj6IBdL78DwWOdGMvYfQyA==" + "version": "1.4.690", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.690.tgz", + "integrity": "sha512-+2OAGjUx68xElQhydpcbqH50hE8Vs2K6TkAeLhICYfndb67CVH0UsZaijmRUE3rHlIxU1u0jxwhgVe6fK3YANA==" }, "node_modules/elkjs": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.1.tgz", - "integrity": "sha512-JWKDyqAdltuUcyxaECtYG6H4sqysXSLeoXuGUBfRNESMTkj+w+qdb0jya8Z/WI0jVd03WQtCGhS6FOFtlhD5FQ==" + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.2.tgz", + "integrity": "sha512-2Y/RaA1pdgSHpY0YG4TYuYCD2wh97CRvu22eLG3Kz0pgQ/6KbIFTxsTnDc4MH/6hFlg2L/9qXrDMG0nMjP63iw==" }, "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" }, "node_modules/emojis-list": { "version": "3.0.0", @@ -4910,9 +4878,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", - "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "version": "5.15.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.1.tgz", + "integrity": "sha512-3d3JRbwsCLJsYgvb6NuWEG44jjPSOMuS73L/6+7BZuoKm3W+qXnSoIYVHi8dG7Qcg4inAY4jbzkZ7MnskePeDg==", "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -4960,18 +4928,18 @@ } }, "node_modules/es-abstract": { - "version": "1.22.4", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.4.tgz", - "integrity": "sha512-vZYJlk2u6qHYxBOTjAeg7qUxHdNfih64Uu2J8QqWgXZ2cri0ZpJAkzDUK/q593+mvKwlxyaxr6F1Q+3LKoQRgg==", + "version": "1.22.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.5.tgz", + "integrity": "sha512-oW69R+4q2wG+Hc3KZePPZxOiisRIqfKBVo/HLx94QcJeWGU/8sZhCvc829rd1kS366vlJbzBfXf9yWwf0+Ko7w==", "dev": true, "dependencies": { "array-buffer-byte-length": "^1.0.1", "arraybuffer.prototype.slice": "^1.0.3", - "available-typed-arrays": "^1.0.6", + "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.7", "es-define-property": "^1.0.0", "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.0.2", + "es-set-tostringtag": "^2.0.3", "es-to-primitive": "^1.2.1", "function.prototype.name": "^1.1.6", "get-intrinsic": "^1.2.4", @@ -4979,15 +4947,15 @@ "globalthis": "^1.0.3", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.1", + "has-proto": "^1.0.3", "has-symbols": "^1.0.3", "hasown": "^2.0.1", "internal-slot": "^1.0.7", "is-array-buffer": "^3.0.4", "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", + "is-negative-zero": "^2.0.3", "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", + "is-shared-array-buffer": "^1.0.3", "is-string": "^1.0.7", "is-typed-array": "^1.1.13", "is-weakref": "^1.0.2", @@ -5000,10 +4968,10 @@ "string.prototype.trim": "^1.2.8", "string.prototype.trimend": "^1.0.7", "string.prototype.trimstart": "^1.0.7", - "typed-array-buffer": "^1.0.1", - "typed-array-byte-length": "^1.0.0", - "typed-array-byte-offset": "^1.0.0", - "typed-array-length": "^1.0.4", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.5", "unbox-primitive": "^1.0.2", "which-typed-array": "^1.1.14" }, @@ -5095,14 +5063,14 @@ "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==" }, "node_modules/es-set-tostringtag": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", - "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.2", - "has-tostringtag": "^1.0.0", - "hasown": "^2.0.0" + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" }, "engines": { "node": ">= 0.4" @@ -5220,16 +5188,16 @@ } }, "node_modules/eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", - "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.56.0", - "@humanwhocodes/config-array": "^0.11.13", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -5322,9 +5290,9 @@ } }, "node_modules/eslint-module-utils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", - "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz", + "integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==", "dev": true, "dependencies": { "debug": "^3.2.7" @@ -5415,14 +5383,14 @@ } }, "node_modules/eslint-plugin-github": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-github/-/eslint-plugin-github-4.10.1.tgz", - "integrity": "sha512-1AqQBockOM+m0ZUpwfjWtX0lWdX5cRi/hwJnSNvXoOmz/Hh+ULH6QFz6ENWueTWjoWpgPv0af3bj+snps6o4og==", + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-github/-/eslint-plugin-github-4.10.2.tgz", + "integrity": "sha512-F1F5aAFgi1Y5hYoTFzGQACBkw5W1hu2Fu5FSTrMlXqrojJnKl1S2pWO/rprlowRQpt+hzHhqSpsfnodJEVd5QA==", "dev": true, "dependencies": { "@github/browserslist-config": "^1.0.0", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", + "@typescript-eslint/eslint-plugin": "^7.0.1", + "@typescript-eslint/parser": "^7.0.1", "aria-query": "^5.3.0", "eslint-config-prettier": ">=8.0.0", "eslint-plugin-escompat": "^3.3.3", @@ -5633,12 +5601,6 @@ "concat-map": "0.0.1" } }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -5810,17 +5772,117 @@ "integrity": "sha512-WE+YlK9X9s4vf5EaYRU0Scw7WItDZStm+PapFSYlg2ABNtaQ4zIG7wEqpoUB3SlfM+SgkhgmzR0TeJOO5k3/Nw==", "dev": true }, + "node_modules/eslint-plugin-vitest/node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-vitest/node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-vitest/node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-vitest/node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint-plugin-vitest/node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/eslint-plugin-vue": { - "version": "9.21.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.21.1.tgz", - "integrity": "sha512-XVtI7z39yOVBFJyi8Ljbn7kY9yHzznKXL02qQYn+ta63Iy4A9JFBw6o4OSB9hyD2++tVT+su9kQqetUyCCwhjw==", + "version": "9.22.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.22.0.tgz", + "integrity": "sha512-7wCXv5zuVnBtZE/74z4yZ0CM8AjH6bk4MQGm7hZjUC2DBppKU5ioeOk5LGSg/s9a1ZJnIsdPLJpXnu1Rc+cVHg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "natural-compare": "^1.4.0", "nth-check": "^2.1.1", - "postcss-selector-parser": "^6.0.13", - "semver": "^7.5.4", + "postcss-selector-parser": "^6.0.15", + "semver": "^7.6.0", "vue-eslint-parser": "^9.4.2", "xml-name-validator": "^4.0.0" }, @@ -6231,9 +6293,9 @@ } }, "node_modules/flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, "node_modules/for-each": { @@ -6681,9 +6743,9 @@ } }, "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", "dev": true, "engines": { "node": ">= 0.4" @@ -7218,9 +7280,9 @@ } }, "node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "dev": true, "engines": { "node": ">= 0.4" @@ -7331,12 +7393,15 @@ } }, "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2" + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8777,9 +8842,9 @@ } }, "node_modules/mini-css-extract-plugin": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.8.0.tgz", - "integrity": "sha512-CxmUYPFcTgET1zImteG/LZOy/4T5rTojesQXkSNBiquhydn78tfbCE9sjIjnJ/UcjNjOC1bphTCCW5rrS7cXAg==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.8.1.tgz", + "integrity": "sha512-/1HDlyFRxWIZPI1ZpgqlZ8jMw/1Dp/dl3P0L1jtZ+zVcHqwPhGwaJwKL00WVgfnBy6PWCde9W65or7IIETImuA==", "dependencies": { "schema-utils": "^4.0.0", "tapable": "^2.2.1" @@ -8827,9 +8892,9 @@ } }, "node_modules/mlly": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.5.0.tgz", - "integrity": "sha512-NPVQvAY1xr1QoVeG0cy8yUYC7FQcOx6evl/RjT1wL5FvzPnzOysoqB/jmx/DhssT2dYa8nxECLAaFI/+gVLhDQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.6.1.tgz", + "integrity": "sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA==", "dev": true, "dependencies": { "acorn": "^8.11.3", @@ -9031,9 +9096,9 @@ } }, "node_modules/npm-run-path": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz", - "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", "dev": true, "dependencies": { "path-key": "^4.0.0" @@ -9516,12 +9581,12 @@ "dev": true }, "node_modules/playwright": { - "version": "1.41.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.41.2.tgz", - "integrity": "sha512-v0bOa6H2GJChDL8pAeLa/LZC4feoAMbSQm1/jF/ySsWWoaNItvrMP7GEkvEEFyCTUYKMxjQKaTSg5up7nR6/8A==", + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.42.1.tgz", + "integrity": "sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==", "dev": true, "dependencies": { - "playwright-core": "1.41.2" + "playwright-core": "1.42.1" }, "bin": { "playwright": "cli.js" @@ -9534,9 +9599,9 @@ } }, "node_modules/playwright-core": { - "version": "1.41.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.41.2.tgz", - "integrity": "sha512-VaTvwCA4Y8kxEe+kfm2+uUUw5Lubf38RxF7FpBxLPmGe5sdNkSg5e3ChEigaGrX7qdqT3pt2m/98LiyvU2x6CA==", + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.1.tgz", + "integrity": "sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==", "dev": true, "bin": { "playwright-core": "cli.js" @@ -9563,6 +9628,15 @@ "node": ">=12.0.0" } }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { "version": "8.4.35", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", @@ -9640,9 +9714,9 @@ } }, "node_modules/postcss-loader": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.0.tgz", - "integrity": "sha512-AbperNcX3rlob7Ay7A/HQcrofug1caABBkopoFeOQMspZBqcqj6giYn1Bwey/0uiOPAcR+NQD0I2HC7rXzk91w==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz", + "integrity": "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==", "dependencies": { "cosmiconfig": "^9.0.0", "jiti": "^1.20.0", @@ -10570,14 +10644,15 @@ } }, "node_modules/set-function-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", - "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dev": true, "dependencies": { - "define-data-property": "^1.0.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -10614,12 +10689,12 @@ } }, "node_modules/side-channel": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.5.tgz", - "integrity": "sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dev": true, "dependencies": { - "call-bind": "^1.0.6", + "call-bind": "^1.0.7", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.4", "object-inspect": "^1.13.1" @@ -10860,6 +10935,16 @@ "node": ">=8" } }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/string-width/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, "node_modules/string.prototype.trim": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", @@ -10974,12 +11059,12 @@ } }, "node_modules/strip-literal": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz", - "integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.0.0.tgz", + "integrity": "sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==", "dev": true, "dependencies": { - "acorn": "^8.10.0" + "js-tokens": "^8.0.2" }, "funding": { "url": "https://github.com/sponsors/antfu" @@ -11102,41 +11187,18 @@ } }, "node_modules/stylelint/node_modules/flat-cache": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.0.tgz", - "integrity": "sha512-EryKbCE/wxpxKniQlyas6PY1I9vwtF3uCBweX+N8KYTCn3Y12RTGtQAJ/bd5pl7kxUAc8v/R3Ake/N17OZiFqA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "dependencies": { "flatted": "^3.2.9", - "keyv": "^4.5.4", - "rimraf": "^5.0.5" + "keyv": "^4.5.4" }, "engines": { "node": ">=16" } }, - "node_modules/stylelint/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/stylelint/node_modules/postcss-safe-parser": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.0.tgz", @@ -11172,24 +11234,6 @@ "node": ">=8" } }, - "node_modules/stylelint/node_modules/rimraf": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", - "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", - "dev": true, - "dependencies": { - "glob": "^10.3.7" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/stylelint/node_modules/strip-ansi": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", @@ -11380,9 +11424,9 @@ } }, "node_modules/swagger-ui-dist": { - "version": "5.11.6", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.11.6.tgz", - "integrity": "sha512-K5BpYuMoPpJY7NwCHIWohH6tU9o0fs1+plNT5KJ+3BBlVEh4H1CpeKJV8o91lpscVY9oqb2jmaAassnW3wVoTg==" + "version": "5.11.8", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.11.8.tgz", + "integrity": "sha512-IfPtCPdf6opT5HXrzHO4kjL1eco0/8xJCtcs7ilhKuzatrpF2j9s+3QbOag6G3mVFKf+g+Ca5UG9DquVUs2obA==" }, "node_modules/symbol-tree": { "version": "3.2.4", @@ -11524,9 +11568,9 @@ } }, "node_modules/terser": { - "version": "5.27.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.1.tgz", - "integrity": "sha512-29wAr6UU/oQpnTw5HoadwjUZnFQXGdOfj0LjZ4sVxzqwHh/QVkvr7m8y9WoR4iN3FRitVduTc6KdjcW38Npsug==", + "version": "5.28.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.28.1.tgz", + "integrity": "sha512-wM+bZp54v/E9eRRGXb5ZFDvinrJIOaTapx3WUokyVGZu5ucVCK55zEgGd5Dl2fSr3jUo5sDiERErUWLY6QPFyA==", "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -11839,12 +11883,12 @@ } }, "node_modules/typed-array-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.1.tgz", - "integrity": "sha512-RSqu1UEuSlrBhHTWC8O9FnPjOduNs4M7rJ4pRKoEjtx1zUNOPN2sSXHLDX+Y2WPbHIxbvg4JFo2DNAEfPIKWoQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.6", + "call-bind": "^1.0.7", "es-errors": "^1.3.0", "is-typed-array": "^1.1.13" }, @@ -11853,15 +11897,16 @@ } }, "node_modules/typed-array-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", - "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -11871,16 +11916,17 @@ } }, "node_modules/typed-array-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", - "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", "dev": true, "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -11890,14 +11936,20 @@ } }, "node_modules/typed-array-length": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", - "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.5.tgz", + "integrity": "sha512-yMi0PlwuznKHxKmcpoOdeLwxBoVPkqZxd7q2FgMkmD3bNwvF5VW0+UlUQ1k1vmktTu4Yu13Q0RIxEP8+B+wloA==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -11923,9 +11975,9 @@ "integrity": "sha512-Oy/k+tFle5NAA3J/yrrYGfvEnPVrDZ8s8/WCwjUE75k331QyKIsFss7byQ/PzBmXLY6h1moRnZbnaxWBe3I3CA==" }, "node_modules/uc.micro": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.0.0.tgz", - "integrity": "sha512-DffL94LsNOccVn4hyfRe5rdKa273swqeA5DJpMOeFmEn1wCDc7nAbbB0gXlgBCL7TNzeTv6G7XVWzan7iJtfig==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", "dev": true }, "node_modules/ufo": { @@ -12108,9 +12160,9 @@ } }, "node_modules/vite": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.3.tgz", - "integrity": "sha512-UfmUD36DKkqhi/F75RrxvPpry+9+tTkrXfMNZD+SboZqBCMsxKtO52XeGzzuh7ioz+Eo/SYDBbdb0Z7vgcDJew==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.4.tgz", + "integrity": "sha512-n+MPqzq+d9nMVTKyewqw6kSt+R3CkvF9QAKY8obiQn8g1fwTscKxyfaYnC632HtBXAQGc1Yjomphwn1dtwGAHg==", "dev": true, "dependencies": { "esbuild": "^0.19.3", @@ -12163,9 +12215,9 @@ } }, "node_modules/vite-node": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.2.2.tgz", - "integrity": "sha512-1as4rDTgVWJO3n1uHmUYqq7nsFgINQ9u+mRcXpjeOMJUmviqNKjcZB7UfRZrlM7MjYXMKpuWp5oGkjaFLnjawg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.3.1.tgz", + "integrity": "sha512-azbRrqRxlWTJEVbzInZCTchx0X69M/XPTCz4H+TLvlTcR/xH/3hkRqhOakT41fMJCMzXTu4UvegkZiEoJAWvng==", "dev": true, "dependencies": { "cac": "^6.7.14", @@ -12211,9 +12263,9 @@ } }, "node_modules/vite/node_modules/rollup": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.11.0.tgz", - "integrity": "sha512-2xIbaXDXjf3u2tajvA5xROpib7eegJ9Y/uPlSFhXLNpK9ampCczXAhLEb5yLzJyG3LAdI1NWtNjDXiLyniNdjQ==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.12.0.tgz", + "integrity": "sha512-wz66wn4t1OHIJw3+XU7mJJQV/2NAfw5OAk6G6Hoo3zcvz/XOfQ52Vgi+AN4Uxoxi0KBBwk2g8zPrTDA4btSB/Q==", "dev": true, "dependencies": { "@types/estree": "1.0.5" @@ -12226,35 +12278,34 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.11.0", - "@rollup/rollup-android-arm64": "4.11.0", - "@rollup/rollup-darwin-arm64": "4.11.0", - "@rollup/rollup-darwin-x64": "4.11.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.11.0", - "@rollup/rollup-linux-arm64-gnu": "4.11.0", - "@rollup/rollup-linux-arm64-musl": "4.11.0", - "@rollup/rollup-linux-riscv64-gnu": "4.11.0", - "@rollup/rollup-linux-x64-gnu": "4.11.0", - "@rollup/rollup-linux-x64-musl": "4.11.0", - "@rollup/rollup-win32-arm64-msvc": "4.11.0", - "@rollup/rollup-win32-ia32-msvc": "4.11.0", - "@rollup/rollup-win32-x64-msvc": "4.11.0", + "@rollup/rollup-android-arm-eabi": "4.12.0", + "@rollup/rollup-android-arm64": "4.12.0", + "@rollup/rollup-darwin-arm64": "4.12.0", + "@rollup/rollup-darwin-x64": "4.12.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.12.0", + "@rollup/rollup-linux-arm64-gnu": "4.12.0", + "@rollup/rollup-linux-arm64-musl": "4.12.0", + "@rollup/rollup-linux-riscv64-gnu": "4.12.0", + "@rollup/rollup-linux-x64-gnu": "4.12.0", + "@rollup/rollup-linux-x64-musl": "4.12.0", + "@rollup/rollup-win32-arm64-msvc": "4.12.0", + "@rollup/rollup-win32-ia32-msvc": "4.12.0", + "@rollup/rollup-win32-x64-msvc": "4.12.0", "fsevents": "~2.3.2" } }, "node_modules/vitest": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.2.2.tgz", - "integrity": "sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.3.1.tgz", + "integrity": "sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==", "dev": true, "dependencies": { - "@vitest/expect": "1.2.2", - "@vitest/runner": "1.2.2", - "@vitest/snapshot": "1.2.2", - "@vitest/spy": "1.2.2", - "@vitest/utils": "1.2.2", + "@vitest/expect": "1.3.1", + "@vitest/runner": "1.3.1", + "@vitest/snapshot": "1.3.1", + "@vitest/spy": "1.3.1", + "@vitest/utils": "1.3.1", "acorn-walk": "^8.3.2", - "cac": "^6.7.14", "chai": "^4.3.10", "debug": "^4.3.4", "execa": "^8.0.1", @@ -12263,11 +12314,11 @@ "pathe": "^1.1.1", "picocolors": "^1.0.0", "std-env": "^3.5.0", - "strip-literal": "^1.3.0", + "strip-literal": "^2.0.0", "tinybench": "^2.5.1", "tinypool": "^0.8.2", "vite": "^5.0.0", - "vite-node": "1.2.2", + "vite-node": "1.3.1", "why-is-node-running": "^2.2.2" }, "bin": { @@ -12282,8 +12333,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "^1.0.0", - "@vitest/ui": "^1.0.0", + "@vitest/browser": "1.3.1", + "@vitest/ui": "1.3.1", "happy-dom": "*", "jsdom": "*" }, @@ -12321,15 +12372,15 @@ } }, "node_modules/vue": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.19.tgz", - "integrity": "sha512-W/7Fc9KUkajFU8dBeDluM4sRGc/aa4YJnOYck8dkjgZoXtVsn3OeTGni66FV1l3+nvPA7VBFYtPioaGKUmEADw==", + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.21.tgz", + "integrity": "sha512-5hjyV/jLEIKD/jYl4cavMcnzKwjMKohureP8ejn3hhEjwhWIhWeuzL2kJAjzl/WyVsgPY56Sy4Z40C3lVshxXA==", "dependencies": { - "@vue/compiler-dom": "3.4.19", - "@vue/compiler-sfc": "3.4.19", - "@vue/runtime-dom": "3.4.19", - "@vue/server-renderer": "3.4.19", - "@vue/shared": "3.4.19" + "@vue/compiler-dom": "3.4.21", + "@vue/compiler-sfc": "3.4.21", + "@vue/runtime-dom": "3.4.21", + "@vue/server-renderer": "3.4.21", + "@vue/shared": "3.4.21" }, "peerDependencies": { "typescript": "*" @@ -12463,9 +12514,9 @@ } }, "node_modules/webpack": { - "version": "5.90.2", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.2.tgz", - "integrity": "sha512-ziXu8ABGr0InCMEYFnHrYweinHK2PWrMqnwdHk2oK3rRhv/1B+2FnfwYv5oD+RrknK/Pp/Hmyvu+eAsaMYhzCw==", + "version": "5.90.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.3.tgz", + "integrity": "sha512-h6uDYlWCctQRuXBs1oYpVe6sFcWedl0dpcVaTf/YF67J9bKvwJajFulMVSYKHrksMB3I/pIagRzDxwxkebuzKA==", "dependencies": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.5", @@ -12964,9 +13015,12 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yaml": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", - "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.0.tgz", + "integrity": "sha512-j9iR8g+/t0lArF4V6NE/QCfT+CO7iLqrXAHZbJdo+LfjqP1vR8Fg5bSiaq6Q2lOD1AUEVrEVIgABvBFYojJVYQ==", + "bin": { + "yaml": "bin.mjs" + }, "engines": { "node": ">= 14" } diff --git a/package.json b/package.json index 3f0f9103cf..d5e8170228 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "@citation-js/plugin-csl": "0.7.6", "@citation-js/plugin-software-formats": "0.6.1", "@claviska/jquery-minicolors": "2.3.6", - "@github/markdown-toolbar-element": "2.2.1", + "@github/markdown-toolbar-element": "2.2.3", "@github/relative-time-element": "4.3.1", "@github/text-expander-element": "2.6.1", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", @@ -17,13 +17,12 @@ "@webcomponents/custom-elements": "1.6.0", "add-asset-webpack-plugin": "2.0.1", "ansi_up": "6.0.2", - "asciinema-player": "3.6.4", - "chart.js": "4.4.1", + "asciinema-player": "3.7.0", + "chart.js": "4.4.2", "chartjs-adapter-dayjs-4": "1.0.4", "chartjs-plugin-zoom": "2.0.1", - "clippie": "4.0.6", + "clippie": "4.0.7", "css-loader": "6.10.0", - "css-variables-parser": "1.0.1", "dayjs": "1.11.10", "dropzone": "6.0.0-beta.2", "easymde": "2.18.0", @@ -36,16 +35,16 @@ "katex": "0.16.9", "license-checker-webpack-plugin": "0.2.1", "mermaid": "10.8.0", - "mini-css-extract-plugin": "2.8.0", + "mini-css-extract-plugin": "2.8.1", "minimatch": "9.0.3", "monaco-editor": "0.46.0", "monaco-editor-webpack-plugin": "7.1.0", "pdfobject": "2.3.0", "postcss": "8.4.35", - "postcss-loader": "8.1.0", + "postcss-loader": "8.1.1", "pretty-ms": "9.0.0", "sortablejs": "1.15.2", - "swagger-ui-dist": "5.11.6", + "swagger-ui-dist": "5.11.8", "tailwindcss": "3.4.1", "throttle-debounce": "5.0.0", "tinycolor2": "1.6.0", @@ -53,25 +52,25 @@ "toastify-js": "1.12.0", "tributejs": "5.1.3", "uint8-to-base64": "0.2.0", - "vue": "3.4.19", + "vue": "3.4.21", "vue-bar-graph": "2.0.0", "vue-chartjs": "5.3.0", "vue-loader": "17.4.2", "vue3-calendar-heatmap": "2.0.5", - "webpack": "5.90.2", + "webpack": "5.90.3", "webpack-cli": "5.1.4", "wrap-ansi": "9.0.0" }, "devDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "4.1.0", - "@playwright/test": "1.41.2", + "@playwright/test": "1.42.1", "@stoplight/spectral-cli": "6.11.0", - "@stylistic/eslint-plugin-js": "1.6.2", - "@stylistic/stylelint-plugin": "2.0.0", + "@stylistic/eslint-plugin-js": "1.6.3", + "@stylistic/stylelint-plugin": "2.1.0", "@vitejs/plugin-vue": "5.0.4", - "eslint": "8.56.0", + "eslint": "8.57.0", "eslint-plugin-array-func": "4.0.0", - "eslint-plugin-github": "4.10.1", + "eslint-plugin-github": "4.10.2", "eslint-plugin-i": "2.29.1", "eslint-plugin-jquery": "1.5.1", "eslint-plugin-no-jquery": "2.7.0", @@ -81,7 +80,7 @@ "eslint-plugin-unicorn": "51.0.1", "eslint-plugin-vitest": "0.3.22", "eslint-plugin-vitest-globals": "1.4.0", - "eslint-plugin-vue": "9.21.1", + "eslint-plugin-vue": "9.22.0", "eslint-plugin-vue-scoped-css": "2.7.2", "eslint-plugin-wc": "2.0.4", "jsdom": "24.0.0", @@ -93,7 +92,7 @@ "svgo": "3.2.0", "updates": "15.1.2", "vite-string-plugin": "1.1.5", - "vitest": "1.2.2" + "vitest": "1.3.1" }, "browserslist": [ "defaults" diff --git a/poetry.lock b/poetry.lock index 4cb58c6ef2..46520fba3c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "click" @@ -27,12 +27,12 @@ files = [ [[package]] name = "cssbeautifier" -version = "1.14.11" +version = "1.15.1" description = "CSS unobfuscator and beautifier." optional = false python-versions = "*" files = [ - {file = "cssbeautifier-1.14.11.tar.gz", hash = "sha256:40544c2b62bbcb64caa5e7f37a02df95654e5ce1bcacadac4ca1f3dc89c31513"}, + {file = "cssbeautifier-1.15.1.tar.gz", hash = "sha256:9f7064362aedd559c55eeecf6b6bed65e05f33488dcbe39044f0403c26e1c006"}, ] [package.dependencies] @@ -67,13 +67,12 @@ tqdm = ">=4.62.2,<5.0.0" [[package]] name = "editorconfig" -version = "0.12.3" +version = "0.12.4" description = "EditorConfig File Locator and Interpreter for Python" optional = false python-versions = "*" files = [ - {file = "EditorConfig-0.12.3-py3-none-any.whl", hash = "sha256:6b0851425aa875b08b16789ee0eeadbd4ab59666e9ebe728e526314c4a2e52c1"}, - {file = "EditorConfig-0.12.3.tar.gz", hash = "sha256:57f8ce78afcba15c8b18d46b5170848c88d56fd38f05c2ec60dbbfcb8996e89e"}, + {file = "EditorConfig-0.12.4.tar.gz", hash = "sha256:24857fa1793917dd9ccf0c7810a07e05404ce9b823521c7dce22a4fb5d125f80"}, ] [[package]] @@ -100,12 +99,12 @@ files = [ [[package]] name = "jsbeautifier" -version = "1.14.11" +version = "1.15.1" description = "JavaScript unobfuscator and beautifier." optional = false python-versions = "*" files = [ - {file = "jsbeautifier-1.14.11.tar.gz", hash = "sha256:6b632581ea60dd1c133cd25a48ad187b4b91f526623c4b0fb5443ef805250505"}, + {file = "jsbeautifier-1.15.1.tar.gz", hash = "sha256:ebd733b560704c602d744eafc839db60a1ee9326e30a2a80c4adb8718adc1b24"}, ] [package.dependencies] @@ -114,13 +113,13 @@ six = ">=1.13.0" [[package]] name = "json5" -version = "0.9.14" +version = "0.9.18" description = "A Python implementation of the JSON5 data format." optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "json5-0.9.14-py2.py3-none-any.whl", hash = "sha256:740c7f1b9e584a468dbb2939d8d458db3427f2c93ae2139d05f47e453eae964f"}, - {file = "json5-0.9.14.tar.gz", hash = "sha256:9ed66c3a6ca3510a976a9ef9b8c0787de24802724ab1860bc0153c7fdd589b02"}, + {file = "json5-0.9.18-py2.py3-none-any.whl", hash = "sha256:3f20193ff8dfdec6ab114b344e7ac5d76fac453c8bab9bdfe1460d1d528ec393"}, + {file = "json5-0.9.18.tar.gz", hash = "sha256:ecb8ac357004e3522fb989da1bf08b146011edbd14fdffae6caad3bd68493467"}, ] [package.extras] @@ -322,13 +321,13 @@ files = [ [[package]] name = "tqdm" -version = "4.66.1" +version = "4.66.2" description = "Fast, Extensible Progress Meter" optional = false python-versions = ">=3.7" files = [ - {file = "tqdm-4.66.1-py3-none-any.whl", hash = "sha256:d302b3c5b53d47bce91fea46679d9c3c6508cf6332229aa1e7d8653723793386"}, - {file = "tqdm-4.66.1.tar.gz", hash = "sha256:d88e651f9db8d8551a62556d3cff9e3034274ca5d66e93197cf2490e2dcb69c7"}, + {file = "tqdm-4.66.2-py3-none-any.whl", hash = "sha256:1ee4f8a893eb9bef51c6e35730cebf234d5d0b6bd112b0271e10ed7c24a02bd9"}, + {file = "tqdm-4.66.2.tar.gz", hash = "sha256:6cd52cdf0fef0e0f543299cfc96fec90d7b8a7e88745f411ec33eb44d5ed3531"}, ] [package.dependencies] @@ -342,13 +341,13 @@ telegram = ["requests"] [[package]] name = "yamllint" -version = "1.35.0" +version = "1.35.1" description = "A linter for YAML files." optional = false python-versions = ">=3.8" files = [ - {file = "yamllint-1.35.0-py3-none-any.whl", hash = "sha256:601b0adaaac6d9bacb16a2e612e7ee8d23caf941ceebf9bfe2cff0f196266004"}, - {file = "yamllint-1.35.0.tar.gz", hash = "sha256:9bc99c3e9fe89b4c6ee26e17aa817cf2d14390de6577cb6e2e6ed5f72120c835"}, + {file = "yamllint-1.35.1-py3-none-any.whl", hash = "sha256:2e16e504bb129ff515b37823b472750b36b6de07963bd74b307341ef5ad8bdc3"}, + {file = "yamllint-1.35.1.tar.gz", hash = "sha256:7a003809f88324fd2c877734f2d575ee7881dd9043360657cc8049c809eba6cd"}, ] [package.dependencies] @@ -360,5 +359,5 @@ dev = ["doc8", "flake8", "flake8-import-order", "rstcheck[sphinx]", "sphinx"] [metadata] lock-version = "2.0" -python-versions = "^3.8" -content-hash = "ba1c2c4235872f67354b5f52aa5bf0cd616354961530d9dc907f9fba28cc1ece" +python-versions = "^3.10" +content-hash = "cd2ff218e9f27a464dfbc8ec2387824a90f4360e04c3f2e58cc375796b7df33a" diff --git a/pyproject.toml b/pyproject.toml index bef41d6266..bb768d5cb1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,11 +5,11 @@ description = "" authors = [] [tool.poetry.dependencies] -python = "^3.8" +python = "^3.10" [tool.poetry.group.dev.dependencies] djlint = "1.34.1" -yamllint = "1.35.0" +yamllint = "1.35.1" [tool.djlint] profile="golang" diff --git a/routers/api/actions/artifact.pb.go b/routers/api/actions/artifact.pb.go new file mode 100644 index 0000000000..590eda9fb9 --- /dev/null +++ b/routers/api/actions/artifact.pb.go @@ -0,0 +1,1058 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.32.0 +// protoc v4.25.2 +// source: artifact.proto + +package actions + +import ( + reflect "reflect" + sync "sync" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + wrapperspb "google.golang.org/protobuf/types/known/wrapperspb" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type CreateArtifactRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` + WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` + Version int32 `protobuf:"varint,5,opt,name=version,proto3" json:"version,omitempty"` +} + +func (x *CreateArtifactRequest) Reset() { + *x = CreateArtifactRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_artifact_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateArtifactRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateArtifactRequest) ProtoMessage() {} + +func (x *CreateArtifactRequest) ProtoReflect() protoreflect.Message { + mi := &file_artifact_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateArtifactRequest.ProtoReflect.Descriptor instead. +func (*CreateArtifactRequest) Descriptor() ([]byte, []int) { + return file_artifact_proto_rawDescGZIP(), []int{0} +} + +func (x *CreateArtifactRequest) GetWorkflowRunBackendId() string { + if x != nil { + return x.WorkflowRunBackendId + } + return "" +} + +func (x *CreateArtifactRequest) GetWorkflowJobRunBackendId() string { + if x != nil { + return x.WorkflowJobRunBackendId + } + return "" +} + +func (x *CreateArtifactRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CreateArtifactRequest) GetExpiresAt() *timestamppb.Timestamp { + if x != nil { + return x.ExpiresAt + } + return nil +} + +func (x *CreateArtifactRequest) GetVersion() int32 { + if x != nil { + return x.Version + } + return 0 +} + +type CreateArtifactResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` + SignedUploadUrl string `protobuf:"bytes,2,opt,name=signed_upload_url,json=signedUploadUrl,proto3" json:"signed_upload_url,omitempty"` +} + +func (x *CreateArtifactResponse) Reset() { + *x = CreateArtifactResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_artifact_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateArtifactResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateArtifactResponse) ProtoMessage() {} + +func (x *CreateArtifactResponse) ProtoReflect() protoreflect.Message { + mi := &file_artifact_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateArtifactResponse.ProtoReflect.Descriptor instead. +func (*CreateArtifactResponse) Descriptor() ([]byte, []int) { + return file_artifact_proto_rawDescGZIP(), []int{1} +} + +func (x *CreateArtifactResponse) GetOk() bool { + if x != nil { + return x.Ok + } + return false +} + +func (x *CreateArtifactResponse) GetSignedUploadUrl() string { + if x != nil { + return x.SignedUploadUrl + } + return "" +} + +type FinalizeArtifactRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` + WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + Size int64 `protobuf:"varint,4,opt,name=size,proto3" json:"size,omitempty"` + Hash *wrapperspb.StringValue `protobuf:"bytes,5,opt,name=hash,proto3" json:"hash,omitempty"` +} + +func (x *FinalizeArtifactRequest) Reset() { + *x = FinalizeArtifactRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_artifact_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FinalizeArtifactRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FinalizeArtifactRequest) ProtoMessage() {} + +func (x *FinalizeArtifactRequest) ProtoReflect() protoreflect.Message { + mi := &file_artifact_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FinalizeArtifactRequest.ProtoReflect.Descriptor instead. +func (*FinalizeArtifactRequest) Descriptor() ([]byte, []int) { + return file_artifact_proto_rawDescGZIP(), []int{2} +} + +func (x *FinalizeArtifactRequest) GetWorkflowRunBackendId() string { + if x != nil { + return x.WorkflowRunBackendId + } + return "" +} + +func (x *FinalizeArtifactRequest) GetWorkflowJobRunBackendId() string { + if x != nil { + return x.WorkflowJobRunBackendId + } + return "" +} + +func (x *FinalizeArtifactRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *FinalizeArtifactRequest) GetSize() int64 { + if x != nil { + return x.Size + } + return 0 +} + +func (x *FinalizeArtifactRequest) GetHash() *wrapperspb.StringValue { + if x != nil { + return x.Hash + } + return nil +} + +type FinalizeArtifactResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` + ArtifactId int64 `protobuf:"varint,2,opt,name=artifact_id,json=artifactId,proto3" json:"artifact_id,omitempty"` +} + +func (x *FinalizeArtifactResponse) Reset() { + *x = FinalizeArtifactResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_artifact_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FinalizeArtifactResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FinalizeArtifactResponse) ProtoMessage() {} + +func (x *FinalizeArtifactResponse) ProtoReflect() protoreflect.Message { + mi := &file_artifact_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FinalizeArtifactResponse.ProtoReflect.Descriptor instead. +func (*FinalizeArtifactResponse) Descriptor() ([]byte, []int) { + return file_artifact_proto_rawDescGZIP(), []int{3} +} + +func (x *FinalizeArtifactResponse) GetOk() bool { + if x != nil { + return x.Ok + } + return false +} + +func (x *FinalizeArtifactResponse) GetArtifactId() int64 { + if x != nil { + return x.ArtifactId + } + return 0 +} + +type ListArtifactsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` + WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` + NameFilter *wrapperspb.StringValue `protobuf:"bytes,3,opt,name=name_filter,json=nameFilter,proto3" json:"name_filter,omitempty"` + IdFilter *wrapperspb.Int64Value `protobuf:"bytes,4,opt,name=id_filter,json=idFilter,proto3" json:"id_filter,omitempty"` +} + +func (x *ListArtifactsRequest) Reset() { + *x = ListArtifactsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_artifact_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListArtifactsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListArtifactsRequest) ProtoMessage() {} + +func (x *ListArtifactsRequest) ProtoReflect() protoreflect.Message { + mi := &file_artifact_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListArtifactsRequest.ProtoReflect.Descriptor instead. +func (*ListArtifactsRequest) Descriptor() ([]byte, []int) { + return file_artifact_proto_rawDescGZIP(), []int{4} +} + +func (x *ListArtifactsRequest) GetWorkflowRunBackendId() string { + if x != nil { + return x.WorkflowRunBackendId + } + return "" +} + +func (x *ListArtifactsRequest) GetWorkflowJobRunBackendId() string { + if x != nil { + return x.WorkflowJobRunBackendId + } + return "" +} + +func (x *ListArtifactsRequest) GetNameFilter() *wrapperspb.StringValue { + if x != nil { + return x.NameFilter + } + return nil +} + +func (x *ListArtifactsRequest) GetIdFilter() *wrapperspb.Int64Value { + if x != nil { + return x.IdFilter + } + return nil +} + +type ListArtifactsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Artifacts []*ListArtifactsResponse_MonolithArtifact `protobuf:"bytes,1,rep,name=artifacts,proto3" json:"artifacts,omitempty"` +} + +func (x *ListArtifactsResponse) Reset() { + *x = ListArtifactsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_artifact_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListArtifactsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListArtifactsResponse) ProtoMessage() {} + +func (x *ListArtifactsResponse) ProtoReflect() protoreflect.Message { + mi := &file_artifact_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListArtifactsResponse.ProtoReflect.Descriptor instead. +func (*ListArtifactsResponse) Descriptor() ([]byte, []int) { + return file_artifact_proto_rawDescGZIP(), []int{5} +} + +func (x *ListArtifactsResponse) GetArtifacts() []*ListArtifactsResponse_MonolithArtifact { + if x != nil { + return x.Artifacts + } + return nil +} + +type ListArtifactsResponse_MonolithArtifact struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` + WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` + DatabaseId int64 `protobuf:"varint,3,opt,name=database_id,json=databaseId,proto3" json:"database_id,omitempty"` + Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"` + Size int64 `protobuf:"varint,5,opt,name=size,proto3" json:"size,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` +} + +func (x *ListArtifactsResponse_MonolithArtifact) Reset() { + *x = ListArtifactsResponse_MonolithArtifact{} + if protoimpl.UnsafeEnabled { + mi := &file_artifact_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListArtifactsResponse_MonolithArtifact) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListArtifactsResponse_MonolithArtifact) ProtoMessage() {} + +func (x *ListArtifactsResponse_MonolithArtifact) ProtoReflect() protoreflect.Message { + mi := &file_artifact_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListArtifactsResponse_MonolithArtifact.ProtoReflect.Descriptor instead. +func (*ListArtifactsResponse_MonolithArtifact) Descriptor() ([]byte, []int) { + return file_artifact_proto_rawDescGZIP(), []int{6} +} + +func (x *ListArtifactsResponse_MonolithArtifact) GetWorkflowRunBackendId() string { + if x != nil { + return x.WorkflowRunBackendId + } + return "" +} + +func (x *ListArtifactsResponse_MonolithArtifact) GetWorkflowJobRunBackendId() string { + if x != nil { + return x.WorkflowJobRunBackendId + } + return "" +} + +func (x *ListArtifactsResponse_MonolithArtifact) GetDatabaseId() int64 { + if x != nil { + return x.DatabaseId + } + return 0 +} + +func (x *ListArtifactsResponse_MonolithArtifact) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ListArtifactsResponse_MonolithArtifact) GetSize() int64 { + if x != nil { + return x.Size + } + return 0 +} + +func (x *ListArtifactsResponse_MonolithArtifact) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +type GetSignedArtifactURLRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` + WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` +} + +func (x *GetSignedArtifactURLRequest) Reset() { + *x = GetSignedArtifactURLRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_artifact_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetSignedArtifactURLRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSignedArtifactURLRequest) ProtoMessage() {} + +func (x *GetSignedArtifactURLRequest) ProtoReflect() protoreflect.Message { + mi := &file_artifact_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetSignedArtifactURLRequest.ProtoReflect.Descriptor instead. +func (*GetSignedArtifactURLRequest) Descriptor() ([]byte, []int) { + return file_artifact_proto_rawDescGZIP(), []int{7} +} + +func (x *GetSignedArtifactURLRequest) GetWorkflowRunBackendId() string { + if x != nil { + return x.WorkflowRunBackendId + } + return "" +} + +func (x *GetSignedArtifactURLRequest) GetWorkflowJobRunBackendId() string { + if x != nil { + return x.WorkflowJobRunBackendId + } + return "" +} + +func (x *GetSignedArtifactURLRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type GetSignedArtifactURLResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + SignedUrl string `protobuf:"bytes,1,opt,name=signed_url,json=signedUrl,proto3" json:"signed_url,omitempty"` +} + +func (x *GetSignedArtifactURLResponse) Reset() { + *x = GetSignedArtifactURLResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_artifact_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetSignedArtifactURLResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSignedArtifactURLResponse) ProtoMessage() {} + +func (x *GetSignedArtifactURLResponse) ProtoReflect() protoreflect.Message { + mi := &file_artifact_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetSignedArtifactURLResponse.ProtoReflect.Descriptor instead. +func (*GetSignedArtifactURLResponse) Descriptor() ([]byte, []int) { + return file_artifact_proto_rawDescGZIP(), []int{8} +} + +func (x *GetSignedArtifactURLResponse) GetSignedUrl() string { + if x != nil { + return x.SignedUrl + } + return "" +} + +type DeleteArtifactRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` + WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` +} + +func (x *DeleteArtifactRequest) Reset() { + *x = DeleteArtifactRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_artifact_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DeleteArtifactRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteArtifactRequest) ProtoMessage() {} + +func (x *DeleteArtifactRequest) ProtoReflect() protoreflect.Message { + mi := &file_artifact_proto_msgTypes[9] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteArtifactRequest.ProtoReflect.Descriptor instead. +func (*DeleteArtifactRequest) Descriptor() ([]byte, []int) { + return file_artifact_proto_rawDescGZIP(), []int{9} +} + +func (x *DeleteArtifactRequest) GetWorkflowRunBackendId() string { + if x != nil { + return x.WorkflowRunBackendId + } + return "" +} + +func (x *DeleteArtifactRequest) GetWorkflowJobRunBackendId() string { + if x != nil { + return x.WorkflowJobRunBackendId + } + return "" +} + +func (x *DeleteArtifactRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type DeleteArtifactResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` + ArtifactId int64 `protobuf:"varint,2,opt,name=artifact_id,json=artifactId,proto3" json:"artifact_id,omitempty"` +} + +func (x *DeleteArtifactResponse) Reset() { + *x = DeleteArtifactResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_artifact_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DeleteArtifactResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteArtifactResponse) ProtoMessage() {} + +func (x *DeleteArtifactResponse) ProtoReflect() protoreflect.Message { + mi := &file_artifact_proto_msgTypes[10] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteArtifactResponse.ProtoReflect.Descriptor instead. +func (*DeleteArtifactResponse) Descriptor() ([]byte, []int) { + return file_artifact_proto_rawDescGZIP(), []int{10} +} + +func (x *DeleteArtifactResponse) GetOk() bool { + if x != nil { + return x.Ok + } + return false +} + +func (x *DeleteArtifactResponse) GetArtifactId() int64 { + if x != nil { + return x.ArtifactId + } + return 0 +} + +var File_artifact_proto protoreflect.FileDescriptor + +var file_artifact_proto_rawDesc = []byte{ + 0x0a, 0x0e, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x12, 0x1d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x2e, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x1a, + 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x22, 0xf5, 0x01, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, + 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f, + 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, + 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72, + 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, + 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f, + 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, + 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, + 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x61, + 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x18, + 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, + 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x54, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x02, + 0x6f, 0x6b, 0x12, 0x2a, 0x0a, 0x11, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x75, 0x70, 0x6c, + 0x6f, 0x61, 0x64, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, + 0x69, 0x67, 0x6e, 0x65, 0x64, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x55, 0x72, 0x6c, 0x22, 0xe8, + 0x01, 0x0a, 0x17, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, + 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f, + 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, + 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72, + 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, + 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f, + 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, + 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, + 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x30, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x22, 0x4b, 0x0a, 0x18, 0x46, 0x69, 0x6e, + 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x02, 0x6f, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, + 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x61, 0x72, 0x74, 0x69, + 0x66, 0x61, 0x63, 0x74, 0x49, 0x64, 0x22, 0x84, 0x02, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x41, + 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, + 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, + 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, + 0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, + 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, + 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, + 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3d, 0x0a, 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x66, 0x69, 0x6c, + 0x74, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, + 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0a, 0x6e, 0x61, 0x6d, 0x65, 0x46, 0x69, 0x6c, + 0x74, 0x65, 0x72, 0x12, 0x38, 0x0a, 0x09, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x52, 0x08, 0x69, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x7c, 0x0a, + 0x15, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x63, 0x0a, 0x09, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, + 0x63, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x45, 0x2e, 0x67, 0x69, 0x74, 0x68, + 0x75, 0x62, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x72, 0x65, 0x73, 0x75, 0x6c, + 0x74, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x72, + 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, + 0x4d, 0x6f, 0x6e, 0x6f, 0x6c, 0x69, 0x74, 0x68, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, + 0x52, 0x09, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x22, 0xa1, 0x02, 0x0a, 0x26, + 0x4c, 0x69, 0x73, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x4d, 0x6f, 0x6e, 0x6f, 0x6c, 0x69, 0x74, 0x68, 0x41, 0x72, + 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, + 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, + 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a, + 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75, + 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52, + 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x64, + 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x0a, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, + 0x73, 0x69, 0x7a, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, + 0x61, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, + 0xa6, 0x01, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x41, 0x72, 0x74, + 0x69, 0x66, 0x61, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, + 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, + 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, + 0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, + 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, + 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, + 0x6e, 0x64, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x3d, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x53, + 0x69, 0x67, 0x6e, 0x65, 0x64, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x55, 0x52, 0x4c, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x69, 0x67, 0x6e, + 0x65, 0x64, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x69, + 0x67, 0x6e, 0x65, 0x64, 0x55, 0x72, 0x6c, 0x22, 0xa0, 0x01, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, + 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, + 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, + 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, + 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, + 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, + 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x49, 0x0a, 0x16, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x02, 0x6f, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, + 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x61, 0x72, 0x74, 0x69, 0x66, + 0x61, 0x63, 0x74, 0x49, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_artifact_proto_rawDescOnce sync.Once + file_artifact_proto_rawDescData = file_artifact_proto_rawDesc +) + +func file_artifact_proto_rawDescGZIP() []byte { + file_artifact_proto_rawDescOnce.Do(func() { + file_artifact_proto_rawDescData = protoimpl.X.CompressGZIP(file_artifact_proto_rawDescData) + }) + return file_artifact_proto_rawDescData +} + +var ( + file_artifact_proto_msgTypes = make([]protoimpl.MessageInfo, 11) + file_artifact_proto_goTypes = []interface{}{ + (*CreateArtifactRequest)(nil), // 0: github.actions.results.api.v1.CreateArtifactRequest + (*CreateArtifactResponse)(nil), // 1: github.actions.results.api.v1.CreateArtifactResponse + (*FinalizeArtifactRequest)(nil), // 2: github.actions.results.api.v1.FinalizeArtifactRequest + (*FinalizeArtifactResponse)(nil), // 3: github.actions.results.api.v1.FinalizeArtifactResponse + (*ListArtifactsRequest)(nil), // 4: github.actions.results.api.v1.ListArtifactsRequest + (*ListArtifactsResponse)(nil), // 5: github.actions.results.api.v1.ListArtifactsResponse + (*ListArtifactsResponse_MonolithArtifact)(nil), // 6: github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact + (*GetSignedArtifactURLRequest)(nil), // 7: github.actions.results.api.v1.GetSignedArtifactURLRequest + (*GetSignedArtifactURLResponse)(nil), // 8: github.actions.results.api.v1.GetSignedArtifactURLResponse + (*DeleteArtifactRequest)(nil), // 9: github.actions.results.api.v1.DeleteArtifactRequest + (*DeleteArtifactResponse)(nil), // 10: github.actions.results.api.v1.DeleteArtifactResponse + (*timestamppb.Timestamp)(nil), // 11: google.protobuf.Timestamp + (*wrapperspb.StringValue)(nil), // 12: google.protobuf.StringValue + (*wrapperspb.Int64Value)(nil), // 13: google.protobuf.Int64Value + } +) + +var file_artifact_proto_depIdxs = []int32{ + 11, // 0: github.actions.results.api.v1.CreateArtifactRequest.expires_at:type_name -> google.protobuf.Timestamp + 12, // 1: github.actions.results.api.v1.FinalizeArtifactRequest.hash:type_name -> google.protobuf.StringValue + 12, // 2: github.actions.results.api.v1.ListArtifactsRequest.name_filter:type_name -> google.protobuf.StringValue + 13, // 3: github.actions.results.api.v1.ListArtifactsRequest.id_filter:type_name -> google.protobuf.Int64Value + 6, // 4: github.actions.results.api.v1.ListArtifactsResponse.artifacts:type_name -> github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact + 11, // 5: github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact.created_at:type_name -> google.protobuf.Timestamp + 6, // [6:6] is the sub-list for method output_type + 6, // [6:6] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name +} + +func init() { file_artifact_proto_init() } +func file_artifact_proto_init() { + if File_artifact_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_artifact_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateArtifactRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_artifact_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateArtifactResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_artifact_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FinalizeArtifactRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_artifact_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FinalizeArtifactResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_artifact_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListArtifactsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_artifact_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListArtifactsResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_artifact_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListArtifactsResponse_MonolithArtifact); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_artifact_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetSignedArtifactURLRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_artifact_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetSignedArtifactURLResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_artifact_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DeleteArtifactRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_artifact_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DeleteArtifactResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_artifact_proto_rawDesc, + NumEnums: 0, + NumMessages: 11, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_artifact_proto_goTypes, + DependencyIndexes: file_artifact_proto_depIdxs, + MessageInfos: file_artifact_proto_msgTypes, + }.Build() + File_artifact_proto = out.File + file_artifact_proto_rawDesc = nil + file_artifact_proto_goTypes = nil + file_artifact_proto_depIdxs = nil +} diff --git a/routers/api/actions/artifact.proto b/routers/api/actions/artifact.proto new file mode 100644 index 0000000000..c68e5d030d --- /dev/null +++ b/routers/api/actions/artifact.proto @@ -0,0 +1,73 @@ +syntax = "proto3"; + +import "google/protobuf/timestamp.proto"; +import "google/protobuf/wrappers.proto"; + +package github.actions.results.api.v1; + +message CreateArtifactRequest { + string workflow_run_backend_id = 1; + string workflow_job_run_backend_id = 2; + string name = 3; + google.protobuf.Timestamp expires_at = 4; + int32 version = 5; +} + +message CreateArtifactResponse { + bool ok = 1; + string signed_upload_url = 2; +} + +message FinalizeArtifactRequest { + string workflow_run_backend_id = 1; + string workflow_job_run_backend_id = 2; + string name = 3; + int64 size = 4; + google.protobuf.StringValue hash = 5; +} + +message FinalizeArtifactResponse { + bool ok = 1; + int64 artifact_id = 2; +} + +message ListArtifactsRequest { + string workflow_run_backend_id = 1; + string workflow_job_run_backend_id = 2; + google.protobuf.StringValue name_filter = 3; + google.protobuf.Int64Value id_filter = 4; +} + +message ListArtifactsResponse { + repeated ListArtifactsResponse_MonolithArtifact artifacts = 1; +} + +message ListArtifactsResponse_MonolithArtifact { + string workflow_run_backend_id = 1; + string workflow_job_run_backend_id = 2; + int64 database_id = 3; + string name = 4; + int64 size = 5; + google.protobuf.Timestamp created_at = 6; +} + +message GetSignedArtifactURLRequest { + string workflow_run_backend_id = 1; + string workflow_job_run_backend_id = 2; + string name = 3; +} + +message GetSignedArtifactURLResponse { + string signed_url = 1; +} + +message DeleteArtifactRequest { + string workflow_run_backend_id = 1; + string workflow_job_run_backend_id = 2; + string name = 3; +} + +message DeleteArtifactResponse { + bool ok = 1; + int64 artifact_id = 2; +} diff --git a/routers/api/actions/artifacts_chunks.go b/routers/api/actions/artifacts_chunks.go index 0713c8bba8..3a81724b3a 100644 --- a/routers/api/actions/artifacts_chunks.go +++ b/routers/api/actions/artifacts_chunks.go @@ -5,11 +5,16 @@ package actions import ( "crypto/md5" + "crypto/sha256" "encoding/base64" + "encoding/hex" + "errors" "fmt" + "hash" "io" "path/filepath" "sort" + "strings" "time" "code.gitea.io/gitea/models/actions" @@ -18,6 +23,52 @@ import ( "code.gitea.io/gitea/modules/storage" ) +func saveUploadChunkBase(st storage.ObjectStorage, ctx *ArtifactContext, + artifact *actions.ActionArtifact, + contentSize, runID, start, end, length int64, checkMd5 bool, +) (int64, error) { + // build chunk store path + storagePath := fmt.Sprintf("tmp%d/%d-%d-%d-%d.chunk", runID, runID, artifact.ID, start, end) + var r io.Reader = ctx.Req.Body + var hasher hash.Hash + if checkMd5 { + // use io.TeeReader to avoid reading all body to md5 sum. + // it writes data to hasher after reading end + // if hash is not matched, delete the read-end result + hasher = md5.New() + r = io.TeeReader(r, hasher) + } + // save chunk to storage + writtenSize, err := st.Save(storagePath, r, -1) + if err != nil { + return -1, fmt.Errorf("save chunk to storage error: %v", err) + } + var checkErr error + if checkMd5 { + // check md5 + reqMd5String := ctx.Req.Header.Get(artifactXActionsResultsMD5Header) + chunkMd5String := base64.StdEncoding.EncodeToString(hasher.Sum(nil)) + log.Info("[artifact] check chunk md5, sum: %s, header: %s", chunkMd5String, reqMd5String) + // if md5 not match, delete the chunk + if reqMd5String != chunkMd5String { + checkErr = fmt.Errorf("md5 not match") + } + } + if writtenSize != contentSize { + checkErr = errors.Join(checkErr, fmt.Errorf("contentSize not match body size")) + } + if checkErr != nil { + if err := st.Delete(storagePath); err != nil { + log.Error("Error deleting chunk: %s, %v", storagePath, err) + } + return -1, checkErr + } + log.Info("[artifact] save chunk %s, size: %d, artifact id: %d, start: %d, end: %d", + storagePath, contentSize, artifact.ID, start, end) + // return chunk total size + return length, nil +} + func saveUploadChunk(st storage.ObjectStorage, ctx *ArtifactContext, artifact *actions.ActionArtifact, contentSize, runID int64, @@ -29,33 +80,15 @@ func saveUploadChunk(st storage.ObjectStorage, ctx *ArtifactContext, log.Warn("parse content range error: %v, content-range: %s", err, contentRange) return -1, fmt.Errorf("parse content range error: %v", err) } - // build chunk store path - storagePath := fmt.Sprintf("tmp%d/%d-%d-%d-%d.chunk", runID, runID, artifact.ID, start, end) - // use io.TeeReader to avoid reading all body to md5 sum. - // it writes data to hasher after reading end - // if hash is not matched, delete the read-end result - hasher := md5.New() - r := io.TeeReader(ctx.Req.Body, hasher) - // save chunk to storage - writtenSize, err := st.Save(storagePath, r, -1) - if err != nil { - return -1, fmt.Errorf("save chunk to storage error: %v", err) - } - // check md5 - reqMd5String := ctx.Req.Header.Get(artifactXActionsResultsMD5Header) - chunkMd5String := base64.StdEncoding.EncodeToString(hasher.Sum(nil)) - log.Info("[artifact] check chunk md5, sum: %s, header: %s", chunkMd5String, reqMd5String) - // if md5 not match, delete the chunk - if reqMd5String != chunkMd5String || writtenSize != contentSize { - if err := st.Delete(storagePath); err != nil { - log.Error("Error deleting chunk: %s, %v", storagePath, err) - } - return -1, fmt.Errorf("md5 not match") - } - log.Info("[artifact] save chunk %s, size: %d, artifact id: %d, start: %d, end: %d", - storagePath, contentSize, artifact.ID, start, end) - // return chunk total size - return length, nil + return saveUploadChunkBase(st, ctx, artifact, contentSize, runID, start, end, length, true) +} + +func appendUploadChunk(st storage.ObjectStorage, ctx *ArtifactContext, + artifact *actions.ActionArtifact, + start, contentSize, runID int64, +) (int64, error) { + end := start + contentSize - 1 + return saveUploadChunkBase(st, ctx, artifact, contentSize, runID, start, end, contentSize, false) } type chunkFileItem struct { @@ -111,14 +144,14 @@ func mergeChunksForRun(ctx *ArtifactContext, st storage.ObjectStorage, runID int log.Debug("artifact %d chunks not found", art.ID) continue } - if err := mergeChunksForArtifact(ctx, chunks, st, art); err != nil { + if err := mergeChunksForArtifact(ctx, chunks, st, art, ""); err != nil { return err } } return nil } -func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st storage.ObjectStorage, artifact *actions.ActionArtifact) error { +func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st storage.ObjectStorage, artifact *actions.ActionArtifact, checksum string) error { sort.Slice(chunks, func(i, j int) bool { return chunks[i].Start < chunks[j].Start }) @@ -157,6 +190,14 @@ func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st st readers = append(readers, readCloser) } mergedReader := io.MultiReader(readers...) + shaPrefix := "sha256:" + var hash hash.Hash + if strings.HasPrefix(checksum, shaPrefix) { + hash = sha256.New() + } + if hash != nil { + mergedReader = io.TeeReader(mergedReader, hash) + } // if chunk is gzip, use gz as extension // download-artifact action will use content-encoding header to decide if it should decompress the file @@ -185,6 +226,14 @@ func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st st } }() + if hash != nil { + rawChecksum := hash.Sum(nil) + actualChecksum := hex.EncodeToString(rawChecksum) + if !strings.HasSuffix(checksum, actualChecksum) { + return fmt.Errorf("update artifact error checksum is invalid") + } + } + // save storage path to artifact log.Debug("[artifact] merge chunks to artifact: %d, %s, old:%s", artifact.ID, storagePath, artifact.StoragePath) // if artifact is already uploaded, delete the old file diff --git a/routers/api/actions/artifacts_utils.go b/routers/api/actions/artifacts_utils.go index 381e7eb16e..aaf89ef40e 100644 --- a/routers/api/actions/artifacts_utils.go +++ b/routers/api/actions/artifacts_utils.go @@ -43,6 +43,17 @@ func validateRunID(ctx *ArtifactContext) (*actions.ActionTask, int64, bool) { return task, runID, true } +func validateRunIDV4(ctx *ArtifactContext, rawRunID string) (*actions.ActionTask, int64, bool) { + task := ctx.ActionTask + runID, err := strconv.ParseInt(rawRunID, 10, 64) + if err != nil || task.Job.RunID != runID { + log.Error("Error runID not match") + ctx.Error(http.StatusBadRequest, "run-id does not match") + return nil, 0, false + } + return task, runID, true +} + func validateArtifactHash(ctx *ArtifactContext, artifactName string) bool { paramHash := ctx.Params("artifact_hash") // use artifact name to create upload url diff --git a/routers/api/actions/artifactsv4.go b/routers/api/actions/artifactsv4.go new file mode 100644 index 0000000000..8300989c75 --- /dev/null +++ b/routers/api/actions/artifactsv4.go @@ -0,0 +1,512 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +// GitHub Actions Artifacts V4 API Simple Description +// +// 1. Upload artifact +// 1.1. CreateArtifact +// Post: /twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact +// Request: +// { +// "workflow_run_backend_id": "21", +// "workflow_job_run_backend_id": "49", +// "name": "test", +// "version": 4 +// } +// Response: +// { +// "ok": true, +// "signedUploadUrl": "http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75" +// } +// 1.2. Upload Zip Content to Blobstorage (unauthenticated request) +// PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=block +// 1.3. Continue Upload Zip Content to Blobstorage (unauthenticated request), repeat until everything is uploaded +// PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=appendBlock +// 1.4. Unknown xml payload to Blobstorage (unauthenticated request), ignored for now +// PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=blockList +// 1.5. FinalizeArtifact +// Post: /twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact +// Request +// { +// "workflow_run_backend_id": "21", +// "workflow_job_run_backend_id": "49", +// "name": "test", +// "size": "2097", +// "hash": "sha256:b6325614d5649338b87215d9536b3c0477729b8638994c74cdefacb020a2cad4" +// } +// Response +// { +// "ok": true, +// "artifactId": "4" +// } +// 2. Download artifact +// 2.1. ListArtifacts and optionally filter by artifact exact name or id +// Post: /twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts +// Request +// { +// "workflow_run_backend_id": "21", +// "workflow_job_run_backend_id": "49", +// "name_filter": "test" +// } +// Response +// { +// "artifacts": [ +// { +// "workflowRunBackendId": "21", +// "workflowJobRunBackendId": "49", +// "databaseId": "4", +// "name": "test", +// "size": "2093", +// "createdAt": "2024-01-23T00:13:28Z" +// } +// ] +// } +// 2.2. GetSignedArtifactURL get the URL to download the artifact zip file of a specific artifact +// Post: /twirp/github.actions.results.api.v1.ArtifactService/GetSignedArtifactURL +// Request +// { +// "workflow_run_backend_id": "21", +// "workflow_job_run_backend_id": "49", +// "name": "test" +// } +// Response +// { +// "signedUrl": "http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/DownloadArtifact?sig=wHzFOwpF-6220-5CA0CIRmAX9VbiTC2Mji89UOqo1E8=&expires=2024-01-23+21%3A51%3A56.872846295+%2B0100+CET&artifactName=test&taskID=76" +// } +// 2.3. Download Zip from Blobstorage (unauthenticated request) +// GET: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/DownloadArtifact?sig=wHzFOwpF-6220-5CA0CIRmAX9VbiTC2Mji89UOqo1E8=&expires=2024-01-23+21%3A51%3A56.872846295+%2B0100+CET&artifactName=test&taskID=76 + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/storage" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/context" + + "google.golang.org/protobuf/encoding/protojson" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/known/timestamppb" +) + +const ( + ArtifactV4RouteBase = "/twirp/github.actions.results.api.v1.ArtifactService" + ArtifactV4ContentEncoding = "application/zip" +) + +type artifactV4Routes struct { + prefix string + fs storage.ObjectStorage +} + +func ArtifactV4Contexter() func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + base, baseCleanUp := context.NewBaseContext(resp, req) + defer baseCleanUp() + + ctx := &ArtifactContext{Base: base} + ctx.AppendContextValue(artifactContextKey, ctx) + + next.ServeHTTP(ctx.Resp, ctx.Req) + }) + } +} + +func ArtifactsV4Routes(prefix string) *web.Route { + m := web.NewRoute() + + r := artifactV4Routes{ + prefix: prefix, + fs: storage.ActionsArtifacts, + } + + m.Group("", func() { + m.Post("CreateArtifact", r.createArtifact) + m.Post("FinalizeArtifact", r.finalizeArtifact) + m.Post("ListArtifacts", r.listArtifacts) + m.Post("GetSignedArtifactURL", r.getSignedArtifactURL) + m.Post("DeleteArtifact", r.deleteArtifact) + }, ArtifactContexter()) + m.Group("", func() { + m.Put("UploadArtifact", r.uploadArtifact) + m.Get("DownloadArtifact", r.downloadArtifact) + }, ArtifactV4Contexter()) + + return m +} + +func (r artifactV4Routes) buildSignature(endp, expires, artifactName string, taskID int64) []byte { + mac := hmac.New(sha256.New, setting.GetGeneralTokenSigningSecret()) + mac.Write([]byte(endp)) + mac.Write([]byte(expires)) + mac.Write([]byte(artifactName)) + mac.Write([]byte(fmt.Sprint(taskID))) + return mac.Sum(nil) +} + +func (r artifactV4Routes) buildArtifactURL(endp, artifactName string, taskID int64) string { + expires := time.Now().Add(60 * time.Minute).Format("2006-01-02 15:04:05.999999999 -0700 MST") + uploadURL := strings.TrimSuffix(setting.AppURL, "/") + strings.TrimSuffix(r.prefix, "/") + + "/" + endp + "?sig=" + base64.URLEncoding.EncodeToString(r.buildSignature(endp, expires, artifactName, taskID)) + "&expires=" + url.QueryEscape(expires) + "&artifactName=" + url.QueryEscape(artifactName) + "&taskID=" + fmt.Sprint(taskID) + return uploadURL +} + +func (r artifactV4Routes) verifySignature(ctx *ArtifactContext, endp string) (*actions.ActionTask, string, bool) { + rawTaskID := ctx.Req.URL.Query().Get("taskID") + sig := ctx.Req.URL.Query().Get("sig") + expires := ctx.Req.URL.Query().Get("expires") + artifactName := ctx.Req.URL.Query().Get("artifactName") + dsig, _ := base64.URLEncoding.DecodeString(sig) + taskID, _ := strconv.ParseInt(rawTaskID, 10, 64) + + expecedsig := r.buildSignature(endp, expires, artifactName, taskID) + if !hmac.Equal(dsig, expecedsig) { + log.Error("Error unauthorized") + ctx.Error(http.StatusUnauthorized, "Error unauthorized") + return nil, "", false + } + t, err := time.Parse("2006-01-02 15:04:05.999999999 -0700 MST", expires) + if err != nil || t.Before(time.Now()) { + log.Error("Error link expired") + ctx.Error(http.StatusUnauthorized, "Error link expired") + return nil, "", false + } + task, err := actions.GetTaskByID(ctx, taskID) + if err != nil { + log.Error("Error runner api getting task by ID: %v", err) + ctx.Error(http.StatusInternalServerError, "Error runner api getting task by ID") + return nil, "", false + } + if task.Status != actions.StatusRunning { + log.Error("Error runner api getting task: task is not running") + ctx.Error(http.StatusInternalServerError, "Error runner api getting task: task is not running") + return nil, "", false + } + if err := task.LoadJob(ctx); err != nil { + log.Error("Error runner api getting job: %v", err) + ctx.Error(http.StatusInternalServerError, "Error runner api getting job") + return nil, "", false + } + return task, artifactName, true +} + +func (r *artifactV4Routes) getArtifactByName(ctx *ArtifactContext, runID int64, name string) (*actions.ActionArtifact, error) { + var art actions.ActionArtifact + has, err := db.GetEngine(ctx).Where("run_id = ? AND artifact_name = ? AND artifact_path = ? AND content_encoding = ?", runID, name, name+".zip", ArtifactV4ContentEncoding).Get(&art) + if err != nil { + return nil, err + } else if !has { + return nil, util.ErrNotExist + } + return &art, nil +} + +func (r *artifactV4Routes) parseProtbufBody(ctx *ArtifactContext, req protoreflect.ProtoMessage) bool { + body, err := io.ReadAll(ctx.Req.Body) + if err != nil { + log.Error("Error decode request body: %v", err) + ctx.Error(http.StatusInternalServerError, "Error decode request body") + return false + } + err = protojson.Unmarshal(body, req) + if err != nil { + log.Error("Error decode request body: %v", err) + ctx.Error(http.StatusInternalServerError, "Error decode request body") + return false + } + return true +} + +func (r *artifactV4Routes) sendProtbufBody(ctx *ArtifactContext, req protoreflect.ProtoMessage) { + resp, err := protojson.Marshal(req) + if err != nil { + log.Error("Error encode response body: %v", err) + ctx.Error(http.StatusInternalServerError, "Error encode response body") + return + } + ctx.Resp.Header().Set("Content-Type", "application/json;charset=utf-8") + ctx.Resp.WriteHeader(http.StatusOK) + _, _ = ctx.Resp.Write(resp) +} + +func (r *artifactV4Routes) createArtifact(ctx *ArtifactContext) { + var req CreateArtifactRequest + + if ok := r.parseProtbufBody(ctx, &req); !ok { + return + } + _, _, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId) + if !ok { + return + } + + artifactName := req.Name + + rententionDays := setting.Actions.ArtifactRetentionDays + if req.ExpiresAt != nil { + rententionDays = int64(time.Until(req.ExpiresAt.AsTime()).Hours() / 24) + } + // create or get artifact with name and path + artifact, err := actions.CreateArtifact(ctx, ctx.ActionTask, artifactName, artifactName+".zip", rententionDays) + if err != nil { + log.Error("Error create or get artifact: %v", err) + ctx.Error(http.StatusInternalServerError, "Error create or get artifact") + return + } + artifact.ContentEncoding = ArtifactV4ContentEncoding + if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { + log.Error("Error UpdateArtifactByID: %v", err) + ctx.Error(http.StatusInternalServerError, "Error UpdateArtifactByID") + return + } + + respData := CreateArtifactResponse{ + Ok: true, + SignedUploadUrl: r.buildArtifactURL("UploadArtifact", artifactName, ctx.ActionTask.ID), + } + r.sendProtbufBody(ctx, &respData) +} + +func (r *artifactV4Routes) uploadArtifact(ctx *ArtifactContext) { + task, artifactName, ok := r.verifySignature(ctx, "UploadArtifact") + if !ok { + return + } + + comp := ctx.Req.URL.Query().Get("comp") + switch comp { + case "block", "appendBlock": + // get artifact by name + artifact, err := r.getArtifactByName(ctx, task.Job.RunID, artifactName) + if err != nil { + log.Error("Error artifact not found: %v", err) + ctx.Error(http.StatusNotFound, "Error artifact not found") + return + } + + if comp == "block" { + artifact.FileSize = 0 + artifact.FileCompressedSize = 0 + } + + _, err = appendUploadChunk(r.fs, ctx, artifact, artifact.FileSize, ctx.Req.ContentLength, artifact.RunID) + if err != nil { + log.Error("Error runner api getting task: task is not running") + ctx.Error(http.StatusInternalServerError, "Error runner api getting task: task is not running") + return + } + artifact.FileCompressedSize += ctx.Req.ContentLength + artifact.FileSize += ctx.Req.ContentLength + if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { + log.Error("Error UpdateArtifactByID: %v", err) + ctx.Error(http.StatusInternalServerError, "Error UpdateArtifactByID") + return + } + ctx.JSON(http.StatusCreated, "appended") + case "blocklist": + ctx.JSON(http.StatusCreated, "created") + } +} + +func (r *artifactV4Routes) finalizeArtifact(ctx *ArtifactContext) { + var req FinalizeArtifactRequest + + if ok := r.parseProtbufBody(ctx, &req); !ok { + return + } + _, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId) + if !ok { + return + } + + // get artifact by name + artifact, err := r.getArtifactByName(ctx, runID, req.Name) + if err != nil { + log.Error("Error artifact not found: %v", err) + ctx.Error(http.StatusNotFound, "Error artifact not found") + return + } + chunkMap, err := listChunksByRunID(r.fs, runID) + if err != nil { + log.Error("Error merge chunks: %v", err) + ctx.Error(http.StatusInternalServerError, "Error merge chunks") + return + } + chunks, ok := chunkMap[artifact.ID] + if !ok { + log.Error("Error merge chunks") + ctx.Error(http.StatusInternalServerError, "Error merge chunks") + return + } + checksum := "" + if req.Hash != nil { + checksum = req.Hash.Value + } + if err := mergeChunksForArtifact(ctx, chunks, r.fs, artifact, checksum); err != nil { + log.Error("Error merge chunks: %v", err) + ctx.Error(http.StatusInternalServerError, "Error merge chunks") + return + } + + respData := FinalizeArtifactResponse{ + Ok: true, + ArtifactId: artifact.ID, + } + r.sendProtbufBody(ctx, &respData) +} + +func (r *artifactV4Routes) listArtifacts(ctx *ArtifactContext) { + var req ListArtifactsRequest + + if ok := r.parseProtbufBody(ctx, &req); !ok { + return + } + _, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId) + if !ok { + return + } + + artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{RunID: runID}) + if err != nil { + log.Error("Error getting artifacts: %v", err) + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + if len(artifacts) == 0 { + log.Debug("[artifact] handleListArtifacts, no artifacts") + ctx.Error(http.StatusNotFound) + return + } + + list := []*ListArtifactsResponse_MonolithArtifact{} + + table := map[string]*ListArtifactsResponse_MonolithArtifact{} + for _, artifact := range artifacts { + if _, ok := table[artifact.ArtifactName]; ok || req.IdFilter != nil && artifact.ID != req.IdFilter.Value || req.NameFilter != nil && artifact.ArtifactName != req.NameFilter.Value || artifact.ArtifactName+".zip" != artifact.ArtifactPath || artifact.ContentEncoding != ArtifactV4ContentEncoding { + table[artifact.ArtifactName] = nil + continue + } + + table[artifact.ArtifactName] = &ListArtifactsResponse_MonolithArtifact{ + Name: artifact.ArtifactName, + CreatedAt: timestamppb.New(artifact.CreatedUnix.AsTime()), + DatabaseId: artifact.ID, + WorkflowRunBackendId: req.WorkflowRunBackendId, + WorkflowJobRunBackendId: req.WorkflowJobRunBackendId, + Size: artifact.FileSize, + } + } + for _, artifact := range table { + if artifact != nil { + list = append(list, artifact) + } + } + + respData := ListArtifactsResponse{ + Artifacts: list, + } + r.sendProtbufBody(ctx, &respData) +} + +func (r *artifactV4Routes) getSignedArtifactURL(ctx *ArtifactContext) { + var req GetSignedArtifactURLRequest + + if ok := r.parseProtbufBody(ctx, &req); !ok { + return + } + _, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId) + if !ok { + return + } + + artifactName := req.Name + + // get artifact by name + artifact, err := r.getArtifactByName(ctx, runID, artifactName) + if err != nil { + log.Error("Error artifact not found: %v", err) + ctx.Error(http.StatusNotFound, "Error artifact not found") + return + } + + respData := GetSignedArtifactURLResponse{} + + if setting.Actions.ArtifactStorage.MinioConfig.ServeDirect { + u, err := storage.ActionsArtifacts.URL(artifact.StoragePath, artifact.ArtifactPath) + if u != nil && err == nil { + respData.SignedUrl = u.String() + } + } + if respData.SignedUrl == "" { + respData.SignedUrl = r.buildArtifactURL("DownloadArtifact", artifactName, ctx.ActionTask.ID) + } + r.sendProtbufBody(ctx, &respData) +} + +func (r *artifactV4Routes) downloadArtifact(ctx *ArtifactContext) { + task, artifactName, ok := r.verifySignature(ctx, "DownloadArtifact") + if !ok { + return + } + + // get artifact by name + artifact, err := r.getArtifactByName(ctx, task.Job.RunID, artifactName) + if err != nil { + log.Error("Error artifact not found: %v", err) + ctx.Error(http.StatusNotFound, "Error artifact not found") + return + } + + file, _ := r.fs.Open(artifact.StoragePath) + + _, _ = io.Copy(ctx.Resp, file) +} + +func (r *artifactV4Routes) deleteArtifact(ctx *ArtifactContext) { + var req DeleteArtifactRequest + + if ok := r.parseProtbufBody(ctx, &req); !ok { + return + } + _, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId) + if !ok { + return + } + + // get artifact by name + artifact, err := r.getArtifactByName(ctx, runID, req.Name) + if err != nil { + log.Error("Error artifact not found: %v", err) + ctx.Error(http.StatusNotFound, "Error artifact not found") + return + } + + err = actions.SetArtifactNeedDelete(ctx, runID, req.Name) + if err != nil { + log.Error("Error deleting artifacts: %v", err) + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + + respData := DeleteArtifactResponse{ + Ok: true, + ArtifactId: artifact.ID, + } + r.sendProtbufBody(ctx, &respData) +} diff --git a/routers/api/packages/cargo/cargo.go b/routers/api/packages/cargo/cargo.go index d01a13d78f..140e532efd 100644 --- a/routers/api/packages/cargo/cargo.go +++ b/routers/api/packages/cargo/cargo.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/models/db" packages_model "code.gitea.io/gitea/models/packages" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" packages_module "code.gitea.io/gitea/modules/packages" cargo_module "code.gitea.io/gitea/modules/packages/cargo" "code.gitea.io/gitea/modules/setting" @@ -110,7 +111,7 @@ func SearchPackages(ctx *context.Context) { OwnerID: ctx.Package.Owner.ID, Type: packages_model.TypeCargo, Name: packages_model.SearchValue{Value: ctx.FormTrim("q")}, - IsInternal: util.OptionalBoolFalse, + IsInternal: optional.Some(false), Paginator: &paginator, }, ) diff --git a/routers/api/packages/chef/chef.go b/routers/api/packages/chef/chef.go index 720fce0a2a..b49f4e9d0a 100644 --- a/routers/api/packages/chef/chef.go +++ b/routers/api/packages/chef/chef.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/models/db" packages_model "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/modules/optional" packages_module "code.gitea.io/gitea/modules/packages" chef_module "code.gitea.io/gitea/modules/packages/chef" "code.gitea.io/gitea/modules/setting" @@ -40,7 +41,7 @@ func PackagesUniverse(ctx *context.Context) { pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ OwnerID: ctx.Package.Owner.ID, Type: packages_model.TypeChef, - IsInternal: util.OptionalBoolFalse, + IsInternal: optional.Some(false), }) if err != nil { apiError(ctx, http.StatusInternalServerError, err) @@ -85,7 +86,7 @@ func EnumeratePackages(ctx *context.Context) { OwnerID: ctx.Package.Owner.ID, Type: packages_model.TypeChef, Name: packages_model.SearchValue{Value: ctx.FormTrim("q")}, - IsInternal: util.OptionalBoolFalse, + IsInternal: optional.Some(false), Paginator: db.NewAbsoluteListOptions( ctx.FormInt("start"), ctx.FormInt("items"), diff --git a/routers/api/packages/composer/composer.go b/routers/api/packages/composer/composer.go index 346408d261..a045da40de 100644 --- a/routers/api/packages/composer/composer.go +++ b/routers/api/packages/composer/composer.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/models/db" packages_model "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/modules/optional" packages_module "code.gitea.io/gitea/modules/packages" composer_module "code.gitea.io/gitea/modules/packages/composer" "code.gitea.io/gitea/modules/setting" @@ -66,7 +67,7 @@ func SearchPackages(ctx *context.Context) { OwnerID: ctx.Package.Owner.ID, Type: packages_model.TypeComposer, Name: packages_model.SearchValue{Value: ctx.FormTrim("q")}, - IsInternal: util.OptionalBoolFalse, + IsInternal: optional.Some(false), Paginator: &paginator, } if ctx.FormTrim("type") != "" { diff --git a/routers/api/packages/goproxy/goproxy.go b/routers/api/packages/goproxy/goproxy.go index 9eb515d9a1..d658066bb4 100644 --- a/routers/api/packages/goproxy/goproxy.go +++ b/routers/api/packages/goproxy/goproxy.go @@ -12,6 +12,7 @@ import ( "time" packages_model "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/modules/optional" packages_module "code.gitea.io/gitea/modules/packages" goproxy_module "code.gitea.io/gitea/modules/packages/goproxy" "code.gitea.io/gitea/modules/util" @@ -129,7 +130,7 @@ func resolvePackage(ctx *context.Context, ownerID int64, name, version string) ( Value: name, ExactMatch: true, }, - IsInternal: util.OptionalBoolFalse, + IsInternal: optional.Some(false), Sort: packages_model.SortCreatedDesc, }) if err != nil { diff --git a/routers/api/packages/helm/helm.go b/routers/api/packages/helm/helm.go index e7a346d9ca..efdb83ec0e 100644 --- a/routers/api/packages/helm/helm.go +++ b/routers/api/packages/helm/helm.go @@ -15,6 +15,7 @@ import ( packages_model "code.gitea.io/gitea/models/packages" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" packages_module "code.gitea.io/gitea/modules/packages" helm_module "code.gitea.io/gitea/modules/packages/helm" "code.gitea.io/gitea/modules/setting" @@ -42,7 +43,7 @@ func Index(ctx *context.Context) { pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ OwnerID: ctx.Package.Owner.ID, Type: packages_model.TypeHelm, - IsInternal: util.OptionalBoolFalse, + IsInternal: optional.Some(false), }) if err != nil { apiError(ctx, http.StatusInternalServerError, err) @@ -110,7 +111,7 @@ func DownloadPackageFile(ctx *context.Context) { Value: ctx.Params("package"), }, HasFileWithName: filename, - IsInternal: util.OptionalBoolFalse, + IsInternal: optional.Some(false), }) if err != nil { apiError(ctx, http.StatusInternalServerError, err) diff --git a/routers/api/packages/npm/api.go b/routers/api/packages/npm/api.go index 8470874884..f8e839c424 100644 --- a/routers/api/packages/npm/api.go +++ b/routers/api/packages/npm/api.go @@ -12,6 +12,7 @@ import ( packages_model "code.gitea.io/gitea/models/packages" npm_module "code.gitea.io/gitea/modules/packages/npm" + "code.gitea.io/gitea/modules/setting" ) func createPackageMetadataResponse(registryURL string, pds []*packages_model.PackageDescriptor) *npm_module.PackageMetadata { @@ -98,7 +99,7 @@ func createPackageSearchResponse(pds []*packages_model.PackageDescriptor, total Maintainers: []npm_module.User{}, // npm cli needs this field Keywords: metadata.Keywords, Links: &npm_module.PackageSearchPackageLinks{ - Registry: pd.FullWebLink(), + Registry: setting.AppURL + "api/packages/" + pd.Owner.Name + "/npm", Homepage: metadata.ProjectURL, }, }, diff --git a/routers/api/packages/npm/npm.go b/routers/api/packages/npm/npm.go index 72b4305928..84acfffae2 100644 --- a/routers/api/packages/npm/npm.go +++ b/routers/api/packages/npm/npm.go @@ -17,6 +17,7 @@ import ( access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/optional" packages_module "code.gitea.io/gitea/modules/packages" npm_module "code.gitea.io/gitea/modules/packages/npm" "code.gitea.io/gitea/modules/setting" @@ -120,7 +121,7 @@ func DownloadPackageFileByName(ctx *context.Context) { Value: packageNameFromParams(ctx), }, HasFileWithName: filename, - IsInternal: util.OptionalBoolFalse, + IsInternal: optional.Some(false), }) if err != nil { apiError(ctx, http.StatusInternalServerError, err) @@ -395,7 +396,7 @@ func setPackageTag(ctx std_ctx.Context, tag string, pv *packages_model.PackageVe Properties: map[string]string{ npm_module.TagProperty: tag, }, - IsInternal: util.OptionalBoolFalse, + IsInternal: optional.Some(false), }) if err != nil { return err @@ -431,7 +432,7 @@ func PackageSearch(ctx *context.Context) { pvs, total, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{ OwnerID: ctx.Package.Owner.ID, Type: packages_model.TypeNpm, - IsInternal: util.OptionalBoolFalse, + IsInternal: optional.Some(false), Name: packages_model.SearchValue{ ExactMatch: false, Value: ctx.FormTrim("text"), diff --git a/routers/api/packages/nuget/nuget.go b/routers/api/packages/nuget/nuget.go index a0273aad5a..c28bc6c9d9 100644 --- a/routers/api/packages/nuget/nuget.go +++ b/routers/api/packages/nuget/nuget.go @@ -18,6 +18,7 @@ import ( packages_model "code.gitea.io/gitea/models/packages" nuget_model "code.gitea.io/gitea/models/packages/nuget" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" packages_module "code.gitea.io/gitea/modules/packages" nuget_module "code.gitea.io/gitea/modules/packages/nuget" "code.gitea.io/gitea/modules/setting" @@ -122,7 +123,7 @@ func SearchServiceV2(ctx *context.Context) { Name: packages_model.SearchValue{ Value: getSearchTerm(ctx), }, - IsInternal: util.OptionalBoolFalse, + IsInternal: optional.Some(false), Paginator: paginator, }) if err != nil { @@ -172,7 +173,7 @@ func SearchServiceV2Count(ctx *context.Context) { Name: packages_model.SearchValue{ Value: getSearchTerm(ctx), }, - IsInternal: util.OptionalBoolFalse, + IsInternal: optional.Some(false), }) if err != nil { apiError(ctx, http.StatusInternalServerError, err) @@ -187,7 +188,7 @@ func SearchServiceV3(ctx *context.Context) { pvs, count, err := nuget_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ OwnerID: ctx.Package.Owner.ID, Name: packages_model.SearchValue{Value: ctx.FormTrim("q")}, - IsInternal: util.OptionalBoolFalse, + IsInternal: optional.Some(false), Paginator: db.NewAbsoluteListOptions( ctx.FormInt("skip"), ctx.FormInt("take"), @@ -313,7 +314,7 @@ func EnumeratePackageVersionsV2(ctx *context.Context) { ExactMatch: true, Value: packageName, }, - IsInternal: util.OptionalBoolFalse, + IsInternal: optional.Some(false), Paginator: paginator, }) if err != nil { @@ -358,7 +359,7 @@ func EnumeratePackageVersionsV2Count(ctx *context.Context) { ExactMatch: true, Value: strings.Trim(ctx.FormTrim("id"), "'"), }, - IsInternal: util.OptionalBoolFalse, + IsInternal: optional.Some(false), }) if err != nil { apiError(ctx, http.StatusInternalServerError, err) diff --git a/routers/api/packages/rubygems/rubygems.go b/routers/api/packages/rubygems/rubygems.go index 5d05b6d524..d2fbcd01f0 100644 --- a/routers/api/packages/rubygems/rubygems.go +++ b/routers/api/packages/rubygems/rubygems.go @@ -13,6 +13,7 @@ import ( "strings" packages_model "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/modules/optional" packages_module "code.gitea.io/gitea/modules/packages" rubygems_module "code.gitea.io/gitea/modules/packages/rubygems" "code.gitea.io/gitea/modules/util" @@ -43,7 +44,7 @@ func EnumeratePackagesLatest(ctx *context.Context) { pvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{ OwnerID: ctx.Package.Owner.ID, Type: packages_model.TypeRubyGems, - IsInternal: util.OptionalBoolFalse, + IsInternal: optional.Some(false), }) if err != nil { apiError(ctx, http.StatusInternalServerError, err) @@ -304,7 +305,7 @@ func getVersionsByFilename(ctx *context.Context, filename string) ([]*packages_m OwnerID: ctx.Package.Owner.ID, Type: packages_model.TypeRubyGems, HasFileWithName: filename, - IsInternal: util.OptionalBoolFalse, + IsInternal: optional.Some(false), }) return pvs, err } diff --git a/routers/api/packages/swift/swift.go b/routers/api/packages/swift/swift.go index 1fc8baeaac..a9da3ea9c2 100644 --- a/routers/api/packages/swift/swift.go +++ b/routers/api/packages/swift/swift.go @@ -15,6 +15,7 @@ import ( packages_model "code.gitea.io/gitea/models/packages" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" packages_module "code.gitea.io/gitea/modules/packages" swift_module "code.gitea.io/gitea/modules/packages/swift" "code.gitea.io/gitea/modules/setting" @@ -433,7 +434,7 @@ func LookupPackageIdentifiers(ctx *context.Context) { Properties: map[string]string{ swift_module.PropertyRepositoryURL: url, }, - IsInternal: util.OptionalBoolFalse, + IsInternal: optional.Some(false), }) if err != nil { apiError(ctx, http.StatusInternalServerError, err) diff --git a/routers/api/v1/admin/hooks.go b/routers/api/v1/admin/hooks.go index 2217d002a0..4c168b55bf 100644 --- a/routers/api/v1/admin/hooks.go +++ b/routers/api/v1/admin/hooks.go @@ -8,6 +8,7 @@ import ( "net/http" "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" @@ -37,7 +38,7 @@ func ListHooks(ctx *context.APIContext) { // "200": // "$ref": "#/responses/HookList" - sysHooks, err := webhook.GetSystemWebhooks(ctx, util.OptionalBoolNone) + sysHooks, err := webhook.GetSystemWebhooks(ctx, optional.None[bool]()) if err != nil { ctx.Error(http.StatusInternalServerError, "GetSystemWebhooks", err) return diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go index 64315108b0..7f4200f684 100644 --- a/routers/api/v1/admin/user.go +++ b/routers/api/v1/admin/user.go @@ -133,7 +133,7 @@ func CreateUser(ctx *context.APIContext) { u.UpdatedUnix = u.CreatedUnix } - if err := user_model.CreateUser(ctx, u, overwriteDefault); err != nil { + if err := user_model.AdminCreateUser(ctx, u, overwriteDefault); err != nil { if user_model.IsErrUserAlreadyExist(err) || user_model.IsErrEmailAlreadyUsed(err) || db.IsErrNameReserved(err) || diff --git a/routers/api/v1/admin/user_badge.go b/routers/api/v1/admin/user_badge.go new file mode 100644 index 0000000000..bacd1f809b --- /dev/null +++ b/routers/api/v1/admin/user_badge.go @@ -0,0 +1,124 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package admin + +import ( + "net/http" + + user_model "code.gitea.io/gitea/models/user" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/context" +) + +// ListUserBadges lists all badges belonging to a user +func ListUserBadges(ctx *context.APIContext) { + // swagger:operation GET /admin/users/{username}/badges admin adminListUserBadges + // --- + // summary: List a user's badges + // produces: + // - application/json + // parameters: + // - name: username + // in: path + // description: username of user + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/BadgeList" + // "404": + // "$ref": "#/responses/notFound" + + badges, maxResults, err := user_model.GetUserBadges(ctx, ctx.ContextUser) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetUserBadges", err) + return + } + + ctx.SetTotalCountHeader(maxResults) + ctx.JSON(http.StatusOK, &badges) +} + +// AddUserBadges add badges to a user +func AddUserBadges(ctx *context.APIContext) { + // swagger:operation POST /admin/users/{username}/badges admin adminAddUserBadges + // --- + // summary: Add a badge to a user + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: username + // in: path + // description: username of user + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/UserBadgeOption" + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + + form := web.GetForm(ctx).(*api.UserBadgeOption) + badges := prepareBadgesForReplaceOrAdd(ctx, *form) + + if err := user_model.AddUserBadges(ctx, ctx.ContextUser, badges); err != nil { + ctx.Error(http.StatusInternalServerError, "ReplaceUserBadges", err) + return + } + + ctx.Status(http.StatusNoContent) +} + +// DeleteUserBadges delete a badge from a user +func DeleteUserBadges(ctx *context.APIContext) { + // swagger:operation DELETE /admin/users/{username}/badges admin adminDeleteUserBadges + // --- + // summary: Remove a badge from a user + // produces: + // - application/json + // parameters: + // - name: username + // in: path + // description: username of user + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/UserBadgeOption" + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + // "422": + // "$ref": "#/responses/validationError" + + form := web.GetForm(ctx).(*api.UserBadgeOption) + badges := prepareBadgesForReplaceOrAdd(ctx, *form) + + if err := user_model.RemoveUserBadges(ctx, ctx.ContextUser, badges); err != nil { + ctx.Error(http.StatusInternalServerError, "ReplaceUserBadges", err) + return + } + + ctx.Status(http.StatusNoContent) +} + +func prepareBadgesForReplaceOrAdd(ctx *context.APIContext, form api.UserBadgeOption) []*user_model.Badge { + badges := make([]*user_model.Badge, len(form.BadgeSlugs)) + for i, badge := range form.BadgeSlugs { + badges[i] = &user_model.Badge{ + Slug: badge, + } + } + return badges +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 0913571c27..c65650c388 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1027,7 +1027,16 @@ func Routes() *web.Route { m.Group("/avatar", func() { m.Post("", bind(api.UpdateUserAvatarOption{}), user.UpdateAvatar) m.Delete("", user.DeleteAvatar) - }, reqToken()) + }) + + m.Group("/blocks", func() { + m.Get("", user.ListBlocks) + m.Group("/{username}", func() { + m.Get("", user.CheckUserBlock) + m.Put("", user.BlockUser) + m.Delete("", user.UnblockUser) + }, context.UserAssignmentAPI()) + }) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken()) // Repositories (requires repo scope, org scope) @@ -1477,6 +1486,15 @@ func Routes() *web.Route { m.Delete("", org.DeleteAvatar) }, reqToken(), reqOrgOwnership()) m.Get("/activities/feeds", org.ListOrgActivityFeeds) + + m.Group("/blocks", func() { + m.Get("", org.ListBlocks) + m.Group("/{username}", func() { + m.Get("", org.CheckUserBlock) + m.Put("", org.BlockUser) + m.Delete("", org.UnblockUser) + }) + }, reqToken(), reqOrgOwnership()) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true)) m.Group("/teams/{teamid}", func() { m.Combo("").Get(reqToken(), org.GetTeam). @@ -1519,6 +1537,9 @@ func Routes() *web.Route { m.Post("/orgs", bind(api.CreateOrgOption{}), admin.CreateOrg) m.Post("/repos", bind(api.CreateRepoOption{}), admin.CreateRepo) m.Post("/rename", bind(api.RenameUserOption{}), admin.RenameUser) + m.Get("/badges", admin.ListUserBadges) + m.Post("/badges", bind(api.UserBadgeOption{}), admin.AddUserBadges) + m.Delete("/badges", bind(api.UserBadgeOption{}), admin.DeleteUserBadges) }, context.UserAssignmentAPI()) }) m.Group("/emails", func() { diff --git a/routers/api/v1/misc/markup_test.go b/routers/api/v1/misc/markup_test.go index f499501c2f..5236fd06ae 100644 --- a/routers/api/v1/misc/markup_test.go +++ b/routers/api/v1/misc/markup_test.go @@ -20,9 +20,9 @@ import ( ) const ( - AppURL = "http://localhost:3000/" - Repo = "gogits/gogs" - AppSubURL = AppURL + Repo + "/" + AppURL = "http://localhost:3000/" + Repo = "gogits/gogs" + FullURL = AppURL + Repo + "/" ) func testRenderMarkup(t *testing.T, mode, filePath, text, responseBody string, responseCode int) { @@ -74,20 +74,20 @@ func TestAPI_RenderGFM(t *testing.T) { // rendered `

Wiki! Enjoy :)

`, // Guard wiki sidebar: special syntax `[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`, // rendered - `

Guardfile-DSL / Configuring-Guard

+ `

Guardfile-DSL / Configuring-Guard

`, // special syntax `[[Name|Link]]`, // rendered - `

Name

+ `

Name

`, // empty ``, @@ -111,8 +111,8 @@ Here are some links to the most important topics. You can find the full list of

Wine Staging on website wine-staging.com.

Here are some links to the most important topics. You can find the full list of pages at the sidebar.

-

Configuration -images/icon-bug.png

+

Configuration +images/icon-bug.png

`, } diff --git a/routers/api/v1/org/block.go b/routers/api/v1/org/block.go new file mode 100644 index 0000000000..69a5222a20 --- /dev/null +++ b/routers/api/v1/org/block.go @@ -0,0 +1,116 @@ +// Copyright 2024 The Gitea Authors. +// SPDX-License-Identifier: MIT + +package org + +import ( + "code.gitea.io/gitea/routers/api/v1/shared" + "code.gitea.io/gitea/services/context" +) + +func ListBlocks(ctx *context.APIContext) { + // swagger:operation GET /orgs/{org}/blocks organization organizationListBlocks + // --- + // summary: List users blocked by the organization + // parameters: + // - name: org + // in: path + // description: name of the organization + // type: string + // required: true + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // produces: + // - application/json + // responses: + // "200": + // "$ref": "#/responses/UserList" + + shared.ListBlocks(ctx, ctx.Org.Organization.AsUser()) +} + +func CheckUserBlock(ctx *context.APIContext) { + // swagger:operation GET /orgs/{org}/blocks/{username} organization organizationCheckUserBlock + // --- + // summary: Check if a user is blocked by the organization + // parameters: + // - name: org + // in: path + // description: name of the organization + // type: string + // required: true + // - name: username + // in: path + // description: user to check + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + + shared.CheckUserBlock(ctx, ctx.Org.Organization.AsUser()) +} + +func BlockUser(ctx *context.APIContext) { + // swagger:operation PUT /orgs/{org}/blocks/{username} organization organizationBlockUser + // --- + // summary: Block a user + // parameters: + // - name: org + // in: path + // description: name of the organization + // type: string + // required: true + // - name: username + // in: path + // description: user to block + // type: string + // required: true + // - name: note + // in: query + // description: optional note for the block + // type: string + // responses: + // "204": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" + + shared.BlockUser(ctx, ctx.Org.Organization.AsUser()) +} + +func UnblockUser(ctx *context.APIContext) { + // swagger:operation DELETE /orgs/{org}/blocks/{username} organization organizationUnblockUser + // --- + // summary: Unblock a user + // parameters: + // - name: org + // in: path + // description: name of the organization + // type: string + // required: true + // - name: username + // in: path + // description: user to unblock + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" + + shared.UnblockUser(ctx, ctx.Doer, ctx.Org.Organization.AsUser()) +} diff --git a/routers/api/v1/org/member.go b/routers/api/v1/org/member.go index fb66d4c3f5..9db9ad964b 100644 --- a/routers/api/v1/org/member.go +++ b/routers/api/v1/org/member.go @@ -318,7 +318,7 @@ func DeleteMember(ctx *context.APIContext) { if ctx.Written() { return } - if err := models.RemoveOrgUser(ctx, ctx.Org.Organization.ID, member.ID); err != nil { + if err := models.RemoveOrgUser(ctx, ctx.Org.Organization, member); err != nil { ctx.Error(http.StatusInternalServerError, "RemoveOrgUser", err) } ctx.Status(http.StatusNoContent) diff --git a/routers/api/v1/org/team.go b/routers/api/v1/org/team.go index b62a386fd7..015af774e3 100644 --- a/routers/api/v1/org/team.go +++ b/routers/api/v1/org/team.go @@ -15,6 +15,7 @@ import ( access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" unit_model "code.gitea.io/gitea/models/unit" + user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web" @@ -486,6 +487,8 @@ func AddTeamMember(ctx *context.APIContext) { // responses: // "204": // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" @@ -493,8 +496,12 @@ func AddTeamMember(ctx *context.APIContext) { if ctx.Written() { return } - if err := models.AddTeamMember(ctx, ctx.Org.Team, u.ID); err != nil { - ctx.Error(http.StatusInternalServerError, "AddMember", err) + if err := models.AddTeamMember(ctx, ctx.Org.Team, u); err != nil { + if errors.Is(err, user_model.ErrBlockedUser) { + ctx.Error(http.StatusForbidden, "AddTeamMember", err) + } else { + ctx.Error(http.StatusInternalServerError, "AddTeamMember", err) + } return } ctx.Status(http.StatusNoContent) @@ -530,7 +537,7 @@ func RemoveTeamMember(ctx *context.APIContext) { return } - if err := models.RemoveTeamMember(ctx, ctx.Org.Team, u.ID); err != nil { + if err := models.RemoveTeamMember(ctx, ctx.Org.Team, u); err != nil { ctx.Error(http.StatusInternalServerError, "RemoveTeamMember", err) return } diff --git a/routers/api/v1/packages/package.go b/routers/api/v1/packages/package.go index 3be31b13ae..b38aa13167 100644 --- a/routers/api/v1/packages/package.go +++ b/routers/api/v1/packages/package.go @@ -7,8 +7,8 @@ import ( "net/http" "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/modules/optional" api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" @@ -60,7 +60,7 @@ func ListPackages(ctx *context.APIContext) { OwnerID: ctx.Package.Owner.ID, Type: packages.Type(packageType), Name: packages.SearchValue{Value: query}, - IsInternal: util.OptionalBoolFalse, + IsInternal: optional.Some(false), Paginator: &listOptions, }) if err != nil { diff --git a/routers/api/v1/repo/collaborators.go b/routers/api/v1/repo/collaborators.go index 7d48d71516..4ce14f7d01 100644 --- a/routers/api/v1/repo/collaborators.go +++ b/routers/api/v1/repo/collaborators.go @@ -8,7 +8,6 @@ import ( "errors" "net/http" - "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" @@ -54,15 +53,10 @@ func ListCollaborators(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - count, err := db.Count[repo_model.Collaboration](ctx, repo_model.FindCollaborationOptions{ - RepoID: ctx.Repo.Repository.ID, + collaborators, total, err := repo_model.GetCollaborators(ctx, &repo_model.FindCollaborationOptions{ + ListOptions: utils.GetListOptions(ctx), + RepoID: ctx.Repo.Repository.ID, }) - if err != nil { - ctx.InternalServerError(err) - return - } - - collaborators, err := repo_model.GetCollaborators(ctx, ctx.Repo.Repository.ID, utils.GetListOptions(ctx)) if err != nil { ctx.Error(http.StatusInternalServerError, "ListCollaborators", err) return @@ -73,7 +67,7 @@ func ListCollaborators(ctx *context.APIContext) { users[i] = convert.ToUser(ctx, collaborator.User, ctx.Doer) } - ctx.SetTotalCountHeader(count) + ctx.SetTotalCountHeader(total) ctx.JSON(http.StatusOK, users) } @@ -159,6 +153,8 @@ func AddCollaborator(ctx *context.APIContext) { // responses: // "204": // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" // "422": @@ -182,7 +178,11 @@ func AddCollaborator(ctx *context.APIContext) { } if err := repo_module.AddCollaborator(ctx, ctx.Repo.Repository, collaborator); err != nil { - ctx.Error(http.StatusInternalServerError, "AddCollaborator", err) + if errors.Is(err, user_model.ErrBlockedUser) { + ctx.Error(http.StatusForbidden, "AddCollaborator", err) + } else { + ctx.Error(http.StatusInternalServerError, "AddCollaborator", err) + } return } @@ -237,7 +237,7 @@ func DeleteCollaborator(ctx *context.APIContext) { return } - if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, collaborator.ID); err != nil { + if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, collaborator); err != nil { ctx.Error(http.StatusInternalServerError, "DeleteCollaboration", err) return } diff --git a/routers/api/v1/repo/fork.go b/routers/api/v1/repo/fork.go index 212cc7a93b..a1e3c9804b 100644 --- a/routers/api/v1/repo/fork.go +++ b/routers/api/v1/repo/fork.go @@ -149,6 +149,8 @@ func CreateFork(ctx *context.APIContext) { if err != nil { if errors.Is(err, util.ErrAlreadyExist) || repo_model.IsErrReachLimitOfRepo(err) { ctx.Error(http.StatusConflict, "ForkRepository", err) + } else if errors.Is(err, user_model.ErrBlockedUser) { + ctx.Error(http.StatusForbidden, "ForkRepository", err) } else { ctx.Error(http.StatusInternalServerError, "ForkRepository", err) } diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index efc1a08a05..b63e7ab662 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -5,6 +5,7 @@ package repo import ( + "errors" "fmt" "net/http" "strconv" @@ -19,10 +20,10 @@ import ( "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" issue_indexer "code.gitea.io/gitea/modules/indexer/issues" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/services/context" @@ -122,14 +123,14 @@ func SearchIssues(ctx *context.APIContext) { return } - var isClosed util.OptionalBool + var isClosed optional.Option[bool] switch ctx.FormString("state") { case "closed": - isClosed = util.OptionalBoolTrue + isClosed = optional.Some(true) case "all": - isClosed = util.OptionalBoolNone + isClosed = optional.None[bool]() default: - isClosed = util.OptionalBoolFalse + isClosed = optional.Some(false) } var ( @@ -142,7 +143,7 @@ func SearchIssues(ctx *context.APIContext) { Private: false, AllPublic: true, TopicOnly: false, - Collaborate: util.OptionalBoolNone, + Collaborate: optional.None[bool](), // This needs to be a column that is not nil in fixtures or // MySQL will return different results when sorting by null in some cases OrderBy: db.SearchOrderByAlphabetically, @@ -165,7 +166,7 @@ func SearchIssues(ctx *context.APIContext) { opts.OwnerID = owner.ID opts.AllLimited = false opts.AllPublic = false - opts.Collaborate = util.OptionalBoolFalse + opts.Collaborate = optional.Some(false) } if ctx.FormString("team") != "" { if ctx.FormString("owner") == "" { @@ -204,14 +205,14 @@ func SearchIssues(ctx *context.APIContext) { keyword = "" } - var isPull util.OptionalBool + var isPull optional.Option[bool] switch ctx.FormString("type") { case "pulls": - isPull = util.OptionalBoolTrue + isPull = optional.Some(true) case "issues": - isPull = util.OptionalBoolFalse + isPull = optional.Some(false) default: - isPull = util.OptionalBoolNone + isPull = optional.None[bool]() } var includedAnyLabels []int64 @@ -396,14 +397,14 @@ func ListIssues(ctx *context.APIContext) { return } - var isClosed util.OptionalBool + var isClosed optional.Option[bool] switch ctx.FormString("state") { case "closed": - isClosed = util.OptionalBoolTrue + isClosed = optional.Some(true) case "all": - isClosed = util.OptionalBoolNone + isClosed = optional.None[bool]() default: - isClosed = util.OptionalBoolFalse + isClosed = optional.Some(false) } keyword := ctx.FormTrim("q") @@ -452,31 +453,29 @@ func ListIssues(ctx *context.APIContext) { listOptions := utils.GetListOptions(ctx) - var isPull util.OptionalBool + isPull := optional.None[bool]() switch ctx.FormString("type") { case "pulls": - isPull = util.OptionalBoolTrue + isPull = optional.Some(true) case "issues": - isPull = util.OptionalBoolFalse - default: - isPull = util.OptionalBoolNone + isPull = optional.Some(false) } - if isPull != util.OptionalBoolNone && !ctx.Repo.CanReadIssuesOrPulls(isPull.IsTrue()) { + if isPull.Has() && !ctx.Repo.CanReadIssuesOrPulls(isPull.Value()) { ctx.NotFound() return } - if isPull == util.OptionalBoolNone { + if !isPull.Has() { canReadIssues := ctx.Repo.CanRead(unit.TypeIssues) canReadPulls := ctx.Repo.CanRead(unit.TypePullRequests) if !canReadIssues && !canReadPulls { ctx.NotFound() return } else if !canReadIssues { - isPull = util.OptionalBoolTrue + isPull = optional.Some(true) } else if !canReadPulls { - isPull = util.OptionalBoolFalse + isPull = optional.Some(false) } } @@ -655,6 +654,7 @@ func CreateIssue(ctx *context.APIContext) { // "$ref": "#/responses/validationError" // "423": // "$ref": "#/responses/repoArchivedError" + form := web.GetForm(ctx).(*api.CreateIssueOption) var deadlineUnix timeutil.TimeStamp if form.Deadline != nil && ctx.Repo.CanWrite(unit.TypeIssues) { @@ -709,12 +709,14 @@ func CreateIssue(ctx *context.APIContext) { form.Labels = make([]int64, 0) } - if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs); err != nil { + if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs, 0); err != nil { if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) { ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err) - return + } else if errors.Is(err, user_model.ErrBlockedUser) { + ctx.Error(http.StatusForbidden, "NewIssue", err) + } else { + ctx.Error(http.StatusInternalServerError, "NewIssue", err) } - ctx.Error(http.StatusInternalServerError, "NewIssue", err) return } @@ -850,7 +852,11 @@ func EditIssue(ctx *context.APIContext) { err = issue_service.UpdateAssignees(ctx, issue, oneAssignee, form.Assignees, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err) + if errors.Is(err, user_model.ErrBlockedUser) { + ctx.Error(http.StatusForbidden, "UpdateAssignees", err) + } else { + ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err) + } return } } diff --git a/routers/api/v1/repo/issue_comment.go b/routers/api/v1/repo/issue_comment.go index 763419b7a2..21aabadf3d 100644 --- a/routers/api/v1/repo/issue_comment.go +++ b/routers/api/v1/repo/issue_comment.go @@ -14,8 +14,8 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/optional" api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/services/context" @@ -278,15 +278,15 @@ func ListRepoIssueComments(ctx *context.APIContext) { return } - var isPull util.OptionalBool + var isPull optional.Option[bool] canReadIssue := ctx.Repo.CanRead(unit.TypeIssues) canReadPull := ctx.Repo.CanRead(unit.TypePullRequests) if canReadIssue && canReadPull { - isPull = util.OptionalBoolNone + isPull = optional.None[bool]() } else if canReadIssue { - isPull = util.OptionalBoolFalse + isPull = optional.Some(false) } else if canReadPull { - isPull = util.OptionalBoolTrue + isPull = optional.Some(true) } else { ctx.NotFound() return @@ -382,6 +382,7 @@ func CreateIssueComment(ctx *context.APIContext) { // "$ref": "#/responses/notFound" // "423": // "$ref": "#/responses/repoArchivedError" + form := web.GetForm(ctx).(*api.CreateIssueCommentOption) issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { @@ -401,7 +402,11 @@ func CreateIssueComment(ctx *context.APIContext) { comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Body, nil) if err != nil { - ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err) + if errors.Is(err, user_model.ErrBlockedUser) { + ctx.Error(http.StatusForbidden, "CreateIssueComment", err) + } else { + ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err) + } return } @@ -522,6 +527,7 @@ func EditIssueComment(ctx *context.APIContext) { // "$ref": "#/responses/notFound" // "423": // "$ref": "#/responses/repoArchivedError" + form := web.GetForm(ctx).(*api.EditIssueCommentOption) editIssueComment(ctx, *form) } @@ -610,7 +616,11 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption) oldContent := comment.Content comment.Content = form.Body if err := issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil { - ctx.Error(http.StatusInternalServerError, "UpdateComment", err) + if errors.Is(err, user_model.ErrBlockedUser) { + ctx.Error(http.StatusForbidden, "UpdateComment", err) + } else { + ctx.Error(http.StatusInternalServerError, "UpdateComment", err) + } return } diff --git a/routers/api/v1/repo/issue_comment_attachment.go b/routers/api/v1/repo/issue_comment_attachment.go index e7436db798..4096cbf07b 100644 --- a/routers/api/v1/repo/issue_comment_attachment.go +++ b/routers/api/v1/repo/issue_comment_attachment.go @@ -4,10 +4,12 @@ package repo import ( + "errors" "net/http" issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" @@ -154,6 +156,8 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) { // "$ref": "#/responses/Attachment" // "400": // "$ref": "#/responses/error" + // "403": + // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/error" // "423": @@ -199,7 +203,11 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) { } if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, comment.Content); err != nil { - ctx.ServerError("UpdateComment", err) + if errors.Is(err, user_model.ErrBlockedUser) { + ctx.Error(http.StatusForbidden, "UpdateComment", err) + } else { + ctx.ServerError("UpdateComment", err) + } return } diff --git a/routers/api/v1/repo/issue_reaction.go b/routers/api/v1/repo/issue_reaction.go index 799c687812..3ff3d19f13 100644 --- a/routers/api/v1/repo/issue_reaction.go +++ b/routers/api/v1/repo/issue_reaction.go @@ -8,11 +8,13 @@ import ( "net/http" issues_model "code.gitea.io/gitea/models/issues" + user_model "code.gitea.io/gitea/models/user" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" + issue_service "code.gitea.io/gitea/services/issue" ) // GetIssueCommentReactions list reactions of a comment from an issue @@ -218,9 +220,9 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp if isCreateType { // PostIssueCommentReaction part - reaction, err := issues_model.CreateCommentReaction(ctx, ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Reaction) + reaction, err := issue_service.CreateCommentReaction(ctx, ctx.Doer, comment, form.Reaction) if err != nil { - if issues_model.IsErrForbiddenIssueReaction(err) { + if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) { ctx.Error(http.StatusForbidden, err.Error(), err) } else if issues_model.IsErrReactionAlreadyExist(err) { ctx.JSON(http.StatusOK, api.Reaction{ @@ -434,9 +436,9 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i if isCreateType { // PostIssueReaction part - reaction, err := issues_model.CreateIssueReaction(ctx, ctx.Doer.ID, issue.ID, form.Reaction) + reaction, err := issue_service.CreateIssueReaction(ctx, ctx.Doer, issue, form.Reaction) if err != nil { - if issues_model.IsErrForbiddenIssueReaction(err) { + if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) { ctx.Error(http.StatusForbidden, err.Error(), err) } else if issues_model.IsErrReactionAlreadyExist(err) { ctx.JSON(http.StatusOK, api.Reaction{ @@ -445,7 +447,7 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i Created: reaction.CreatedUnix.AsTime(), }) } else { - ctx.Error(http.StatusInternalServerError, "CreateCommentReaction", err) + ctx.Error(http.StatusInternalServerError, "CreateIssueReaction", err) } return } diff --git a/routers/api/v1/repo/milestone.go b/routers/api/v1/repo/milestone.go index d4c828fe8b..b9534016e4 100644 --- a/routers/api/v1/repo/milestone.go +++ b/routers/api/v1/repo/milestone.go @@ -11,9 +11,9 @@ import ( "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/modules/optional" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/services/context" @@ -61,10 +61,10 @@ func ListMilestones(ctx *context.APIContext) { // "$ref": "#/responses/notFound" state := api.StateType(ctx.FormString("state")) - var isClosed util.OptionalBool + var isClosed optional.Option[bool] switch state { case api.StateClosed, api.StateOpen: - isClosed = util.OptionalBoolOf(state == api.StateClosed) + isClosed = optional.Some(state == api.StateClosed) } milestones, total, err := db.FindAndCount[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{ diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 8f9848f71d..4cb94b11a2 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -362,6 +362,8 @@ func CreatePullRequest(ctx *context.APIContext) { // responses: // "201": // "$ref": "#/responses/PullRequest" + // "403": + // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" // "409": @@ -510,9 +512,11 @@ func CreatePullRequest(ctx *context.APIContext) { if err := pull_service.NewPullRequest(ctx, repo, prIssue, labelIDs, []string{}, pr, assigneeIDs); err != nil { if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) { ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err) - return + } else if errors.Is(err, user_model.ErrBlockedUser) { + ctx.Error(http.StatusForbidden, "BlockedUser", err) + } else { + ctx.Error(http.StatusInternalServerError, "NewPullRequest", err) } - ctx.Error(http.StatusInternalServerError, "NewPullRequest", err) return } @@ -630,6 +634,8 @@ func EditPullRequest(ctx *context.APIContext) { if err != nil { if user_model.IsErrUserNotExist(err) { ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err)) + } else if errors.Is(err, user_model.ErrBlockedUser) { + ctx.Error(http.StatusForbidden, "UpdateAssignees", err) } else { ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err) } diff --git a/routers/api/v1/repo/release.go b/routers/api/v1/repo/release.go index a47fc1cc59..f0f3c0bbc7 100644 --- a/routers/api/v1/repo/release.go +++ b/routers/api/v1/repo/release.go @@ -4,6 +4,7 @@ package repo import ( + "fmt" "net/http" "code.gitea.io/gitea/models" @@ -215,6 +216,10 @@ func CreateRelease(ctx *context.APIContext) { // "409": // "$ref": "#/responses/error" form := web.GetForm(ctx).(*api.CreateReleaseOption) + if ctx.Repo.Repository.IsEmpty { + ctx.Error(http.StatusUnprocessableEntity, "RepoIsEmpty", fmt.Errorf("repo is empty")) + return + } rel, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, form.TagName) if err != nil { if !repo_model.IsErrReleaseNotExist(err) { diff --git a/routers/api/v1/repo/release_attachment.go b/routers/api/v1/repo/release_attachment.go index a29bce66a4..59fd83e3a2 100644 --- a/routers/api/v1/repo/release_attachment.go +++ b/routers/api/v1/repo/release_attachment.go @@ -4,7 +4,9 @@ package repo import ( + "io" "net/http" + "strings" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/log" @@ -154,6 +156,7 @@ func CreateReleaseAttachment(ctx *context.APIContext) { // - application/json // consumes: // - multipart/form-data + // - application/octet-stream // parameters: // - name: owner // in: path @@ -180,7 +183,7 @@ func CreateReleaseAttachment(ctx *context.APIContext) { // in: formData // description: attachment to upload // type: file - // required: true + // required: false // responses: // "201": // "$ref": "#/responses/Attachment" @@ -202,20 +205,36 @@ func CreateReleaseAttachment(ctx *context.APIContext) { } // Get uploaded file from request - file, header, err := ctx.Req.FormFile("attachment") - if err != nil { - ctx.Error(http.StatusInternalServerError, "GetFile", err) - return - } - defer file.Close() + var content io.ReadCloser + var filename string + var size int64 = -1 - filename := header.Filename - if query := ctx.FormString("name"); query != "" { - filename = query + if strings.HasPrefix(strings.ToLower(ctx.Req.Header.Get("Content-Type")), "multipart/form-data") { + file, header, err := ctx.Req.FormFile("attachment") + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetFile", err) + return + } + defer file.Close() + + content = file + size = header.Size + filename = header.Filename + if name := ctx.FormString("name"); name != "" { + filename = name + } + } else { + content = ctx.Req.Body + filename = ctx.FormString("name") + } + + if filename == "" { + ctx.Error(http.StatusBadRequest, "CreateReleaseAttachment", "Could not determine name of attachment.") + return } // Create a new attachment and save the file - attach, err := attachment.UploadAttachment(ctx, file, setting.Repository.Release.AllowedTypes, header.Size, &repo_model.Attachment{ + attach, err := attachment.UploadAttachment(ctx, content, setting.Repository.Release.AllowedTypes, size, &repo_model.Attachment{ Name: filename, UploaderID: ctx.Doer.ID, RepoID: ctx.Repo.Repository.ID, diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index ff4e0ddfba..eed8c9f302 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -24,10 +24,10 @@ import ( "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/label" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/validation" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/v1/utils" @@ -135,33 +135,33 @@ func Search(ctx *context.APIContext) { PriorityOwnerID: ctx.FormInt64("priority_owner_id"), TeamID: ctx.FormInt64("team_id"), TopicOnly: ctx.FormBool("topic"), - Collaborate: util.OptionalBoolNone, + Collaborate: optional.None[bool](), Private: ctx.IsSigned && (ctx.FormString("private") == "" || ctx.FormBool("private")), - Template: util.OptionalBoolNone, + Template: optional.None[bool](), StarredByID: ctx.FormInt64("starredBy"), IncludeDescription: ctx.FormBool("includeDesc"), } if ctx.FormString("template") != "" { - opts.Template = util.OptionalBoolOf(ctx.FormBool("template")) + opts.Template = optional.Some(ctx.FormBool("template")) } if ctx.FormBool("exclusive") { - opts.Collaborate = util.OptionalBoolFalse + opts.Collaborate = optional.Some(false) } mode := ctx.FormString("mode") switch mode { case "source": - opts.Fork = util.OptionalBoolFalse - opts.Mirror = util.OptionalBoolFalse + opts.Fork = optional.Some(false) + opts.Mirror = optional.Some(false) case "fork": - opts.Fork = util.OptionalBoolTrue + opts.Fork = optional.Some(true) case "mirror": - opts.Mirror = util.OptionalBoolTrue + opts.Mirror = optional.Some(true) case "collaborative": - opts.Mirror = util.OptionalBoolFalse - opts.Collaborate = util.OptionalBoolTrue + opts.Mirror = optional.Some(false) + opts.Collaborate = optional.Some(true) case "": default: ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid search mode: \"%s\"", mode)) @@ -169,11 +169,11 @@ func Search(ctx *context.APIContext) { } if ctx.FormString("archived") != "" { - opts.Archived = util.OptionalBoolOf(ctx.FormBool("archived")) + opts.Archived = optional.Some(ctx.FormBool("archived")) } if ctx.FormString("is_private") != "" { - opts.IsPrivate = util.OptionalBoolOf(ctx.FormBool("is_private")) + opts.IsPrivate = optional.Some(ctx.FormBool("is_private")) } sortMode := ctx.FormString("sort") @@ -949,13 +949,33 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { } } - if opts.HasProjects != nil && !unit_model.TypeProjects.UnitGlobalDisabled() { - if *opts.HasProjects { + currHasProjects := repo.UnitEnabled(ctx, unit_model.TypeProjects) + newHasProjects := currHasProjects + if opts.HasProjects != nil { + newHasProjects = *opts.HasProjects + } + if currHasProjects || newHasProjects { + if newHasProjects && !unit_model.TypeProjects.UnitGlobalDisabled() { + unit, err := repo.GetUnit(ctx, unit_model.TypeProjects) + var config *repo_model.ProjectsConfig + if err != nil { + config = &repo_model.ProjectsConfig{ + ProjectsMode: repo_model.ProjectsModeAll, + } + } else { + config = unit.ProjectsConfig() + } + + if opts.ProjectsMode != nil { + config.ProjectsMode = repo_model.ProjectsMode(*opts.ProjectsMode) + } + units = append(units, repo_model.RepoUnit{ RepoID: repo.ID, Type: unit_model.TypeProjects, + Config: config, }) - } else { + } else if !newHasProjects && !unit_model.TypeProjects.UnitGlobalDisabled() { deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects) } } diff --git a/routers/api/v1/repo/transfer.go b/routers/api/v1/repo/transfer.go index 4f05c0df51..776b336761 100644 --- a/routers/api/v1/repo/transfer.go +++ b/routers/api/v1/repo/transfer.go @@ -4,6 +4,7 @@ package repo import ( + "errors" "fmt" "net/http" @@ -117,7 +118,11 @@ func Transfer(ctx *context.APIContext) { return } - ctx.InternalServerError(err) + if errors.Is(err, user_model.ErrBlockedUser) { + ctx.Error(http.StatusForbidden, "BlockedUser", err) + } else { + ctx.InternalServerError(err) + } return } diff --git a/routers/api/v1/shared/block.go b/routers/api/v1/shared/block.go new file mode 100644 index 0000000000..a1e65625ed --- /dev/null +++ b/routers/api/v1/shared/block.go @@ -0,0 +1,98 @@ +// Copyright 2024 The Gitea Authors. +// SPDX-License-Identifier: MIT + +package shared + +import ( + "errors" + "net/http" + + user_model "code.gitea.io/gitea/models/user" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/routers/api/v1/utils" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/convert" + user_service "code.gitea.io/gitea/services/user" +) + +func ListBlocks(ctx *context.APIContext, blocker *user_model.User) { + blocks, total, err := user_model.FindBlockings(ctx, &user_model.FindBlockingOptions{ + ListOptions: utils.GetListOptions(ctx), + BlockerID: blocker.ID, + }) + if err != nil { + ctx.Error(http.StatusInternalServerError, "FindBlockings", err) + return + } + + if err := user_model.BlockingList(blocks).LoadAttributes(ctx); err != nil { + ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) + return + } + + users := make([]*api.User, 0, len(blocks)) + for _, b := range blocks { + users = append(users, convert.ToUser(ctx, b.Blockee, blocker)) + } + + ctx.SetTotalCountHeader(total) + ctx.JSON(http.StatusOK, &users) +} + +func CheckUserBlock(ctx *context.APIContext, blocker *user_model.User) { + blockee, err := user_model.GetUserByName(ctx, ctx.Params("username")) + if err != nil { + ctx.NotFound("GetUserByName", err) + return + } + + status := http.StatusNotFound + blocking, err := user_model.GetBlocking(ctx, blocker.ID, blockee.ID) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetBlocking", err) + return + } + if blocking != nil { + status = http.StatusNoContent + } + + ctx.Status(status) +} + +func BlockUser(ctx *context.APIContext, blocker *user_model.User) { + blockee, err := user_model.GetUserByName(ctx, ctx.Params("username")) + if err != nil { + ctx.NotFound("GetUserByName", err) + return + } + + if err := user_service.BlockUser(ctx, ctx.Doer, blocker, blockee, ctx.FormString("note")); err != nil { + if errors.Is(err, user_model.ErrCanNotBlock) || errors.Is(err, user_model.ErrBlockOrganization) { + ctx.Error(http.StatusBadRequest, "BlockUser", err) + } else { + ctx.Error(http.StatusInternalServerError, "BlockUser", err) + } + return + } + + ctx.Status(http.StatusNoContent) +} + +func UnblockUser(ctx *context.APIContext, doer, blocker *user_model.User) { + blockee, err := user_model.GetUserByName(ctx, ctx.Params("username")) + if err != nil { + ctx.NotFound("GetUserByName", err) + return + } + + if err := user_service.UnblockUser(ctx, doer, blocker, blockee); err != nil { + if errors.Is(err, user_model.ErrCanNotUnblock) || errors.Is(err, user_model.ErrBlockOrganization) { + ctx.Error(http.StatusBadRequest, "UnblockUser", err) + } else { + ctx.Error(http.StatusInternalServerError, "UnblockUser", err) + } + return + } + + ctx.Status(http.StatusNoContent) +} diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index 6f7859df62..e03862d7b9 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -190,4 +190,10 @@ type swaggerParameterBodies struct { // in:body CreateOrUpdateSecretOption api.CreateOrUpdateSecretOption + + // in:body + UserBadgeOption api.UserBadgeOption + + // in:body + UserBadgeList api.BadgeList } diff --git a/routers/api/v1/user/block.go b/routers/api/v1/user/block.go new file mode 100644 index 0000000000..7231e9add7 --- /dev/null +++ b/routers/api/v1/user/block.go @@ -0,0 +1,96 @@ +// Copyright 2024 The Gitea Authors. +// SPDX-License-Identifier: MIT + +package user + +import ( + "code.gitea.io/gitea/routers/api/v1/shared" + "code.gitea.io/gitea/services/context" +) + +func ListBlocks(ctx *context.APIContext) { + // swagger:operation GET /user/blocks user userListBlocks + // --- + // summary: List users blocked by the authenticated user + // parameters: + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // produces: + // - application/json + // responses: + // "200": + // "$ref": "#/responses/UserList" + + shared.ListBlocks(ctx, ctx.Doer) +} + +func CheckUserBlock(ctx *context.APIContext) { + // swagger:operation GET /user/blocks/{username} user userCheckUserBlock + // --- + // summary: Check if a user is blocked by the authenticated user + // parameters: + // - name: username + // in: path + // description: user to check + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + + shared.CheckUserBlock(ctx, ctx.Doer) +} + +func BlockUser(ctx *context.APIContext) { + // swagger:operation PUT /user/blocks/{username} user userBlockUser + // --- + // summary: Block a user + // parameters: + // - name: username + // in: path + // description: user to block + // type: string + // required: true + // - name: note + // in: query + // description: optional note for the block + // type: string + // responses: + // "204": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" + + shared.BlockUser(ctx, ctx.Doer) +} + +func UnblockUser(ctx *context.APIContext) { + // swagger:operation DELETE /user/blocks/{username} user userUnblockUser + // --- + // summary: Unblock a user + // parameters: + // - name: username + // in: path + // description: user to unblock + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" + + shared.UnblockUser(ctx, ctx.Doer, ctx.Doer) +} diff --git a/routers/api/v1/user/follower.go b/routers/api/v1/user/follower.go index 398c6b2567..6abb70de19 100644 --- a/routers/api/v1/user/follower.go +++ b/routers/api/v1/user/follower.go @@ -5,6 +5,7 @@ package user import ( + "errors" "net/http" user_model "code.gitea.io/gitea/models/user" @@ -221,11 +222,17 @@ func Follow(ctx *context.APIContext) { // responses: // "204": // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" - if err := user_model.FollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID); err != nil { - ctx.Error(http.StatusInternalServerError, "FollowUser", err) + if err := user_model.FollowUser(ctx, ctx.Doer, ctx.ContextUser); err != nil { + if errors.Is(err, user_model.ErrBlockedUser) { + ctx.Error(http.StatusForbidden, "FollowUser", err) + } else { + ctx.Error(http.StatusInternalServerError, "FollowUser", err) + } return } ctx.Status(http.StatusNoContent) diff --git a/routers/api/v1/user/gpg_key.go b/routers/api/v1/user/gpg_key.go index b8438cd2aa..dcf5da0b2e 100644 --- a/routers/api/v1/user/gpg_key.go +++ b/routers/api/v1/user/gpg_key.go @@ -10,6 +10,7 @@ import ( asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/v1/utils" @@ -132,6 +133,11 @@ func GetGPGKey(ctx *context.APIContext) { // CreateUserGPGKey creates new GPG key to given user by ID. func CreateUserGPGKey(ctx *context.APIContext, form api.CreateGPGKeyOption, uid int64) { + if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageGPGKeys) { + ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited")) + return + } + token := asymkey_model.VerificationToken(ctx.Doer, 1) lastToken := asymkey_model.VerificationToken(ctx.Doer, 0) @@ -268,6 +274,11 @@ func DeleteGPGKey(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" + if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageGPGKeys) { + ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited")) + return + } + if err := asymkey_model.DeleteGPGKey(ctx, ctx.Doer, ctx.ParamsInt64(":id")); err != nil { if asymkey_model.IsErrGPGKeyAccessDenied(err) { ctx.Error(http.StatusForbidden, "", "You do not have access to this key") diff --git a/routers/api/v1/user/key.go b/routers/api/v1/user/key.go index ada6759f8e..bcbfd93bd3 100644 --- a/routers/api/v1/user/key.go +++ b/routers/api/v1/user/key.go @@ -5,6 +5,7 @@ package user import ( std_ctx "context" + "fmt" "net/http" asymkey_model "code.gitea.io/gitea/models/asymkey" @@ -198,6 +199,11 @@ func GetPublicKey(ctx *context.APIContext) { // CreateUserPublicKey creates new public key to given user by ID. func CreateUserPublicKey(ctx *context.APIContext, form api.CreateKeyOption, uid int64) { + if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageSSHKeys) { + ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited")) + return + } + content, err := asymkey_model.CheckPublicKeyString(form.Key) if err != nil { repo.HandleCheckKeyStringError(ctx, err) @@ -263,6 +269,11 @@ func DeletePublicKey(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" + if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageSSHKeys) { + ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited")) + return + } + id := ctx.ParamsInt64(":id") externallyManaged, err := asymkey_model.PublicKeyIsExternallyManaged(ctx, id) if err != nil { diff --git a/routers/api/v1/user/star.go b/routers/api/v1/user/star.go index e624884db3..ad9ed9548d 100644 --- a/routers/api/v1/user/star.go +++ b/routers/api/v1/user/star.go @@ -5,10 +5,9 @@ package user import ( - std_context "context" + "errors" "net/http" - "code.gitea.io/gitea/models/db" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" @@ -20,8 +19,12 @@ import ( // getStarredRepos returns the repos that the user with the specified userID has // starred -func getStarredRepos(ctx std_context.Context, user *user_model.User, private bool, listOptions db.ListOptions) ([]*api.Repository, error) { - starredRepos, err := repo_model.GetStarredRepos(ctx, user.ID, private, listOptions) +func getStarredRepos(ctx *context.APIContext, user *user_model.User, private bool) ([]*api.Repository, error) { + starredRepos, err := repo_model.GetStarredRepos(ctx, &repo_model.StarredReposOptions{ + ListOptions: utils.GetListOptions(ctx), + StarrerID: user.ID, + IncludePrivate: private, + }) if err != nil { return nil, err } @@ -65,7 +68,7 @@ func GetStarredRepos(ctx *context.APIContext) { // "$ref": "#/responses/notFound" private := ctx.ContextUser.ID == ctx.Doer.ID - repos, err := getStarredRepos(ctx, ctx.ContextUser, private, utils.GetListOptions(ctx)) + repos, err := getStarredRepos(ctx, ctx.ContextUser, private) if err != nil { ctx.Error(http.StatusInternalServerError, "getStarredRepos", err) return @@ -95,7 +98,7 @@ func GetMyStarredRepos(ctx *context.APIContext) { // "200": // "$ref": "#/responses/RepositoryList" - repos, err := getStarredRepos(ctx, ctx.Doer, true, utils.GetListOptions(ctx)) + repos, err := getStarredRepos(ctx, ctx.Doer, true) if err != nil { ctx.Error(http.StatusInternalServerError, "getStarredRepos", err) } @@ -152,12 +155,18 @@ func Star(ctx *context.APIContext) { // responses: // "204": // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" - err := repo_model.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true) + err := repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, true) if err != nil { - ctx.Error(http.StatusInternalServerError, "StarRepo", err) + if errors.Is(err, user_model.ErrBlockedUser) { + ctx.Error(http.StatusForbidden, "BlockedUser", err) + } else { + ctx.Error(http.StatusInternalServerError, "StarRepo", err) + } return } ctx.Status(http.StatusNoContent) @@ -185,7 +194,7 @@ func Unstar(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - err := repo_model.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false) + err := repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, false) if err != nil { ctx.Error(http.StatusInternalServerError, "StarRepo", err) return diff --git a/routers/api/v1/user/watch.go b/routers/api/v1/user/watch.go index 706f4cc66b..2cc23ae476 100644 --- a/routers/api/v1/user/watch.go +++ b/routers/api/v1/user/watch.go @@ -4,10 +4,9 @@ package user import ( - std_context "context" + "errors" "net/http" - "code.gitea.io/gitea/models/db" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" @@ -18,8 +17,12 @@ import ( ) // getWatchedRepos returns the repos that the user with the specified userID is watching -func getWatchedRepos(ctx std_context.Context, user *user_model.User, private bool, listOptions db.ListOptions) ([]*api.Repository, int64, error) { - watchedRepos, total, err := repo_model.GetWatchedRepos(ctx, user.ID, private, listOptions) +func getWatchedRepos(ctx *context.APIContext, user *user_model.User, private bool) ([]*api.Repository, int64, error) { + watchedRepos, total, err := repo_model.GetWatchedRepos(ctx, &repo_model.WatchedReposOptions{ + ListOptions: utils.GetListOptions(ctx), + WatcherID: user.ID, + IncludePrivate: private, + }) if err != nil { return nil, 0, err } @@ -63,7 +66,7 @@ func GetWatchedRepos(ctx *context.APIContext) { // "$ref": "#/responses/notFound" private := ctx.ContextUser.ID == ctx.Doer.ID - repos, total, err := getWatchedRepos(ctx, ctx.ContextUser, private, utils.GetListOptions(ctx)) + repos, total, err := getWatchedRepos(ctx, ctx.ContextUser, private) if err != nil { ctx.Error(http.StatusInternalServerError, "getWatchedRepos", err) } @@ -92,7 +95,7 @@ func GetMyWatchedRepos(ctx *context.APIContext) { // "200": // "$ref": "#/responses/RepositoryList" - repos, total, err := getWatchedRepos(ctx, ctx.Doer, true, utils.GetListOptions(ctx)) + repos, total, err := getWatchedRepos(ctx, ctx.Doer, true) if err != nil { ctx.Error(http.StatusInternalServerError, "getWatchedRepos", err) } @@ -157,12 +160,18 @@ func Watch(ctx *context.APIContext) { // responses: // "200": // "$ref": "#/responses/WatchInfo" + // "403": + // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" - err := repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true) + err := repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, true) if err != nil { - ctx.Error(http.StatusInternalServerError, "WatchRepo", err) + if errors.Is(err, user_model.ErrBlockedUser) { + ctx.Error(http.StatusForbidden, "BlockedUser", err) + } else { + ctx.Error(http.StatusInternalServerError, "WatchRepo", err) + } return } ctx.JSON(http.StatusOK, api.WatchInfo{ @@ -197,7 +206,7 @@ func Unwatch(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - err := repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false) + err := repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, false) if err != nil { ctx.Error(http.StatusInternalServerError, "UnwatchRepo", err) return diff --git a/routers/common/middleware.go b/routers/common/middleware.go index 1ee4c629ad..c7c75fb099 100644 --- a/routers/common/middleware.go +++ b/routers/common/middleware.go @@ -38,6 +38,7 @@ func ProtocolMiddlewares() (handlers []any) { }) }) + // wrap the request and response, use the process context and add it to the process manager handlers = append(handlers, func(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { ctx, _, finished := process.GetManager().AddTypedContext(req.Context(), fmt.Sprintf("%s: %s", req.Method, req.RequestURI), process.RequestProcessType, true) diff --git a/routers/init.go b/routers/init.go index e0a7150ba3..aaf95920c2 100644 --- a/routers/init.go +++ b/routers/init.go @@ -9,7 +9,6 @@ import ( "runtime" "code.gitea.io/gitea/models" - asymkey_model "code.gitea.io/gitea/models/asymkey" authmodel "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/eventsource" @@ -33,6 +32,7 @@ import ( "code.gitea.io/gitea/routers/private" web_routers "code.gitea.io/gitea/routers/web" actions_service "code.gitea.io/gitea/services/actions" + asymkey_service "code.gitea.io/gitea/services/asymkey" "code.gitea.io/gitea/services/auth" "code.gitea.io/gitea/services/auth/source/oauth2" "code.gitea.io/gitea/services/automerge" @@ -94,7 +94,7 @@ func syncAppConfForGit(ctx context.Context) error { mustInitCtx(ctx, repo_service.SyncRepositoryHooks) log.Info("re-write ssh public keys ...") - mustInitCtx(ctx, asymkey_model.RewriteAllPublicKeys) + mustInitCtx(ctx, asymkey_service.RewriteAllPublicKeys) return system.AppState.Set(ctx, runtimeState) } @@ -198,6 +198,8 @@ func NormalRoutes() *web.Route { // TODO: this prefix should be generated with a token string with runner ? prefix = "/api/actions_pipeline" r.Mount(prefix, actions_router.ArtifactsRoutes(prefix)) + prefix = actions_router.ArtifactV4RouteBase + r.Mount(prefix, actions_router.ArtifactsV4Routes(prefix)) } return r diff --git a/routers/web/admin/emails.go b/routers/web/admin/emails.go index 4296d70aee..2cf4035c6a 100644 --- a/routers/web/admin/emails.go +++ b/routers/web/admin/emails.go @@ -12,8 +12,8 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" ) @@ -68,10 +68,10 @@ func Emails(ctx *context.Context) { opts.Keyword = ctx.FormTrim("q") opts.SortType = orderBy if len(ctx.FormString("is_activated")) != 0 { - opts.IsActivated = util.OptionalBoolOf(ctx.FormBool("activated")) + opts.IsActivated = optional.Some(ctx.FormBool("activated")) } if len(ctx.FormString("is_primary")) != 0 { - opts.IsPrimary = util.OptionalBoolOf(ctx.FormBool("primary")) + opts.IsPrimary = optional.Some(ctx.FormBool("primary")) } if len(opts.Keyword) == 0 || isKeywordValid(opts.Keyword) { diff --git a/routers/web/admin/hooks.go b/routers/web/admin/hooks.go index 8d4c66fdb2..8d59fbb858 100644 --- a/routers/web/admin/hooks.go +++ b/routers/web/admin/hooks.go @@ -8,8 +8,8 @@ import ( "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" ) @@ -35,7 +35,7 @@ func DefaultOrSystemWebhooks(ctx *context.Context) { sys["Title"] = ctx.Tr("admin.systemhooks") sys["Description"] = ctx.Tr("admin.systemhooks.desc") - sys["Webhooks"], err = webhook.GetSystemWebhooks(ctx, util.OptionalBoolNone) + sys["Webhooks"], err = webhook.GetSystemWebhooks(ctx, optional.None[bool]()) sys["BaseLink"] = setting.AppSubURL + "/admin/hooks" sys["BaseLinkNew"] = setting.AppSubURL + "/admin/system-hooks" if err != nil { diff --git a/routers/web/admin/packages.go b/routers/web/admin/packages.go index 7c16b69a85..39f064a1be 100644 --- a/routers/web/admin/packages.go +++ b/routers/web/admin/packages.go @@ -11,8 +11,8 @@ import ( "code.gitea.io/gitea/models/db" packages_model "code.gitea.io/gitea/models/packages" "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" packages_service "code.gitea.io/gitea/services/packages" packages_cleanup_service "code.gitea.io/gitea/services/packages/cleanup" @@ -36,7 +36,7 @@ func Packages(ctx *context.Context) { Type: packages_model.Type(packageType), Name: packages_model.SearchValue{Value: query}, Sort: sort, - IsInternal: util.OptionalBoolFalse, + IsInternal: optional.Some(false), Paginator: &db.ListOptions{ PageSize: setting.UI.PackagesPagingNum, Page: page, diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go index cbca26eba8..dfb103c8ab 100644 --- a/routers/web/admin/users.go +++ b/routers/web/admin/users.go @@ -96,7 +96,7 @@ func NewUser(ctx *context.Context) { ctx.Data["login_type"] = "0-0" sources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{ - IsActive: util.OptionalBoolTrue, + IsActive: optional.Some(true), }) if err != nil { ctx.ServerError("auth.Sources", err) @@ -117,7 +117,7 @@ func NewUserPost(ctx *context.Context) { ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice() sources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{ - IsActive: util.OptionalBoolTrue, + IsActive: optional.Some(true), }) if err != nil { ctx.ServerError("auth.Sources", err) @@ -177,7 +177,7 @@ func NewUserPost(ctx *context.Context) { u.MustChangePassword = form.MustChangePassword } - if err := user_model.CreateUser(ctx, u, overwriteDefault); err != nil { + if err := user_model.AdminCreateUser(ctx, u, overwriteDefault); err != nil { switch { case user_model.IsErrUserAlreadyExist(err): ctx.Data["Err_UserName"] = true @@ -276,7 +276,7 @@ func ViewUser(ctx *context.Context) { OwnerID: u.ID, OrderBy: db.SearchOrderByAlphabetically, Private: true, - Collaborate: util.OptionalBoolFalse, + Collaborate: optional.Some(false), }) if err != nil { ctx.ServerError("SearchRepository", err) @@ -439,6 +439,7 @@ func EditUserPost(ctx *context.Context) { AllowCreateOrganization: optional.Some(form.AllowCreateOrganization), IsRestricted: optional.Some(form.Restricted), Visibility: optional.Some(form.Visibility), + Language: optional.Some(form.Language), } if err := user_service.UpdateUser(ctx, u, opts); err != nil { diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 7704a110a6..da6bef207a 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -123,9 +123,21 @@ func resetLocale(ctx *context.Context, u *user_model.User) error { return nil } +func RedirectAfterLogin(ctx *context.Context) { + redirectTo := ctx.FormString("redirect_to") + if redirectTo == "" { + redirectTo = ctx.GetSiteCookie("redirect_to") + } + middleware.DeleteRedirectToCookie(ctx.Resp) + nextRedirectTo := setting.AppSubURL + string(setting.LandingPageURL) + if setting.LandingPageURL == setting.LandingPageLogin { + nextRedirectTo = setting.AppSubURL + "/" // do not cycle-redirect to the login page + } + ctx.RedirectToFirst(redirectTo, nextRedirectTo) +} + func CheckAutoLogin(ctx *context.Context) bool { - // Check auto-login - isSucceed, err := autoSignIn(ctx) + isSucceed, err := autoSignIn(ctx) // try to auto-login if err != nil { if errors.Is(err, auth_service.ErrAuthTokenInvalidHash) { ctx.Flash.Error(ctx.Tr("auth.remember_me.compromised"), true) @@ -138,17 +150,10 @@ func CheckAutoLogin(ctx *context.Context) bool { redirectTo := ctx.FormString("redirect_to") if len(redirectTo) > 0 { middleware.SetRedirectToCookie(ctx.Resp, redirectTo) - } else { - redirectTo = ctx.GetSiteCookie("redirect_to") } if isSucceed { - middleware.DeleteRedirectToCookie(ctx.Resp) - nextRedirectTo := setting.AppSubURL + string(setting.LandingPageURL) - if setting.LandingPageURL == setting.LandingPageLogin { - nextRedirectTo = setting.AppSubURL + "/" // do not cycle-redirect to the login page - } - ctx.RedirectToFirst(redirectTo, nextRedirectTo) + RedirectAfterLogin(ctx) return true } @@ -163,7 +168,12 @@ func SignIn(ctx *context.Context) { return } - oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, util.OptionalBoolTrue) + if ctx.IsSigned { + RedirectAfterLogin(ctx) + return + } + + oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true)) if err != nil { ctx.ServerError("UserSignIn", err) return @@ -186,7 +196,7 @@ func SignIn(ctx *context.Context) { func SignInPost(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("sign_in") - oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, util.OptionalBoolTrue) + oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true)) if err != nil { ctx.ServerError("UserSignIn", err) return @@ -410,7 +420,7 @@ func SignUp(ctx *context.Context) { ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up" - oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, util.OptionalBoolTrue) + oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true)) if err != nil { ctx.ServerError("UserSignUp", err) return @@ -439,7 +449,7 @@ func SignUpPost(ctx *context.Context) { ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up" - oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, util.OptionalBoolTrue) + oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true)) if err != nil { ctx.ServerError("UserSignUp", err) return diff --git a/routers/web/auth/auth_test.go b/routers/web/auth/auth_test.go new file mode 100644 index 0000000000..c6afbf877c --- /dev/null +++ b/routers/web/auth/auth_test.go @@ -0,0 +1,43 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package auth + +import ( + "net/http" + "net/url" + "testing" + + "code.gitea.io/gitea/modules/test" + "code.gitea.io/gitea/services/contexttest" + + "github.com/stretchr/testify/assert" +) + +func TestUserLogin(t *testing.T) { + ctx, resp := contexttest.MockContext(t, "/user/login") + SignIn(ctx) + assert.Equal(t, http.StatusOK, resp.Code) + + ctx, resp = contexttest.MockContext(t, "/user/login") + ctx.IsSigned = true + SignIn(ctx) + assert.Equal(t, http.StatusSeeOther, resp.Code) + assert.Equal(t, "/", test.RedirectURL(resp)) + + ctx, resp = contexttest.MockContext(t, "/user/login?redirect_to=/other") + ctx.IsSigned = true + SignIn(ctx) + assert.Equal(t, "/other", test.RedirectURL(resp)) + + ctx, resp = contexttest.MockContext(t, "/user/login") + ctx.Req.AddCookie(&http.Cookie{Name: "redirect_to", Value: "/other-cookie"}) + ctx.IsSigned = true + SignIn(ctx) + assert.Equal(t, "/other-cookie", test.RedirectURL(resp)) + + ctx, resp = contexttest.MockContext(t, "/user/login?redirect_to="+url.QueryEscape("https://example.com")) + ctx.IsSigned = true + SignIn(ctx) + assert.Equal(t, "/", test.RedirectURL(resp)) +} diff --git a/routers/web/explore/repo.go b/routers/web/explore/repo.go index bb3fca3aea..a9baf4c73a 100644 --- a/routers/web/explore/repo.go +++ b/routers/web/explore/repo.go @@ -109,6 +109,21 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) { language := ctx.FormTrim("language") ctx.Data["Language"] = language + archived := ctx.FormOptionalBool("archived") + ctx.Data["IsArchived"] = archived + + fork := ctx.FormOptionalBool("fork") + ctx.Data["IsFork"] = fork + + mirror := ctx.FormOptionalBool("mirror") + ctx.Data["IsMirror"] = mirror + + template := ctx.FormOptionalBool("template") + ctx.Data["IsTemplate"] = template + + private := ctx.FormOptionalBool("private") + ctx.Data["IsPrivate"] = private + repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ Page: page, @@ -125,6 +140,11 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) { Language: language, IncludeDescription: setting.UI.SearchRepoDescription, OnlyShowRelevant: opts.OnlyShowRelevant, + Archived: archived, + Fork: fork, + Mirror: mirror, + Template: template, + IsPrivate: private, }) if err != nil { ctx.ServerError("SearchRepository", err) diff --git a/routers/web/explore/user.go b/routers/web/explore/user.go index 41f440f9d9..b79a79fb2c 100644 --- a/routers/web/explore/user.go +++ b/routers/web/explore/user.go @@ -12,10 +12,10 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/sitemap" "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" ) @@ -155,7 +155,7 @@ func Users(ctx *context.Context) { Actor: ctx.Doer, Type: user_model.UserTypeIndividual, ListOptions: db.ListOptions{PageSize: setting.UI.ExplorePagingNum}, - IsActive: util.OptionalBoolTrue, + IsActive: optional.Some(true), Visible: []structs.VisibleType{structs.VisibleTypePublic, structs.VisibleTypeLimited, structs.VisibleTypePrivate}, SupportedSortOrders: supportedSortOrders, diff --git a/routers/web/feed/convert.go b/routers/web/feed/convert.go index 298fe0bb39..3defa436a7 100644 --- a/routers/web/feed/convert.go +++ b/routers/web/feed/convert.go @@ -50,7 +50,7 @@ func toReleaseLink(ctx *context.Context, act *activities_model.Action) string { // renderMarkdown creates a minimal markdown render context from an action. // If rendering fails, the original markdown text is returned -func renderMarkdown(ctx *context.Context, act *activities_model.Action, content string) string { +func renderMarkdown(ctx *context.Context, act *activities_model.Action, content string) template.HTML { markdownCtx := &markup.RenderContext{ Ctx: ctx, Links: markup.Links{ @@ -64,7 +64,7 @@ func renderMarkdown(ctx *context.Context, act *activities_model.Action, content } markdown, err := markdown.RenderString(markdownCtx, content) if err != nil { - return content + return templates.SanitizeHTML(content) // old code did so: use SanitizeHTML to render in tmpl } return markdown } @@ -74,7 +74,11 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio for _, act := range actions { act.LoadActUser(ctx) - var content, desc, title string + // TODO: the code seems quite strange (maybe not right) + // sometimes it uses text content but sometimes it uses HTML content + // it should clearly defines which kind of content it should use for the feed items: plan text or rich HTML + var title, desc string + var content template.HTML link := &feeds.Link{Href: act.GetCommentHTMLURL(ctx)} @@ -228,7 +232,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio desc = act.GetIssueTitle(ctx) comment := act.GetIssueInfos()[1] if len(comment) != 0 { - desc += "\n\n" + renderMarkdown(ctx, act, comment) + desc += "\n\n" + string(renderMarkdown(ctx, act, comment)) } case activities_model.ActionMergePullRequest, activities_model.ActionAutoMergePullRequest: desc = act.GetIssueInfos()[1] @@ -239,7 +243,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio } } if len(content) == 0 { - content = desc + content = templates.SanitizeHTML(desc) } items = append(items, &feeds.Item{ @@ -253,7 +257,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio }, Id: fmt.Sprintf("%v: %v", strconv.FormatInt(act.ID, 10), link.Href), Created: act.CreatedUnix.AsTime(), - Content: content, + Content: string(content), }) } return items, err @@ -282,7 +286,8 @@ func releasesToFeedItems(ctx *context.Context, releases []*repo_model.Release, i return nil, err } - var title, content string + var title string + var content template.HTML if rel.IsTag { title = rel.TagName @@ -311,7 +316,7 @@ func releasesToFeedItems(ctx *context.Context, releases []*repo_model.Release, i Email: rel.Publisher.GetEmail(), }, Id: fmt.Sprintf("%v: %v", strconv.FormatInt(rel.ID, 10), link.Href), - Content: content, + Content: string(content), }) } diff --git a/routers/web/feed/profile.go b/routers/web/feed/profile.go index 2b70aad17b..08cbcd9e12 100644 --- a/routers/web/feed/profile.go +++ b/routers/web/feed/profile.go @@ -58,7 +58,7 @@ func showUserFeed(ctx *context.Context, formatType string) { feed := &feeds.Feed{ Title: ctx.Locale.TrString("home.feed_of", ctx.ContextUser.DisplayName()), Link: &feeds.Link{Href: ctx.ContextUser.HTMLURL()}, - Description: ctxUserDescription, + Description: string(ctxUserDescription), Created: time.Now(), } diff --git a/routers/web/home.go b/routers/web/home.go index 555f94c983..d4be0931e8 100644 --- a/routers/web/home.go +++ b/routers/web/home.go @@ -13,10 +13,10 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/sitemap" "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/routers/web/auth" "code.gitea.io/gitea/routers/web/user" @@ -71,7 +71,7 @@ func HomeSitemap(ctx *context.Context) { _, cnt, err := user_model.SearchUsers(ctx, &user_model.SearchUserOptions{ Type: user_model.UserTypeIndividual, ListOptions: db.ListOptions{PageSize: 1}, - IsActive: util.OptionalBoolTrue, + IsActive: optional.Some(true), Visible: []structs.VisibleType{structs.VisibleTypePublic}, }) if err != nil { diff --git a/routers/web/org/block.go b/routers/web/org/block.go new file mode 100644 index 0000000000..d40458e250 --- /dev/null +++ b/routers/web/org/block.go @@ -0,0 +1,38 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package org + +import ( + "net/http" + + "code.gitea.io/gitea/modules/base" + shared_user "code.gitea.io/gitea/routers/web/shared/user" + "code.gitea.io/gitea/services/context" +) + +const ( + tplSettingsBlockedUsers base.TplName = "org/settings/blocked_users" +) + +func BlockedUsers(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("user.block.list") + ctx.Data["PageIsOrgSettings"] = true + ctx.Data["PageIsSettingsBlockedUsers"] = true + + shared_user.BlockedUsers(ctx, ctx.ContextUser) + if ctx.Written() { + return + } + + ctx.HTML(http.StatusOK, tplSettingsBlockedUsers) +} + +func BlockedUsersPost(ctx *context.Context) { + shared_user.BlockedUsersPost(ctx, ctx.ContextUser) + if ctx.Written() { + return + } + + ctx.Redirect(ctx.ContextUser.OrganisationLink() + "/settings/blocked_users") +} diff --git a/routers/web/org/home.go b/routers/web/org/home.go index 4a7378689a..71d10f3a43 100644 --- a/routers/web/org/home.go +++ b/routers/web/org/home.go @@ -85,6 +85,21 @@ func Home(ctx *context.Context) { page = 1 } + archived := ctx.FormOptionalBool("archived") + ctx.Data["IsArchived"] = archived + + fork := ctx.FormOptionalBool("fork") + ctx.Data["IsFork"] = fork + + mirror := ctx.FormOptionalBool("mirror") + ctx.Data["IsMirror"] = mirror + + template := ctx.FormOptionalBool("template") + ctx.Data["IsTemplate"] = template + + private := ctx.FormOptionalBool("private") + ctx.Data["IsPrivate"] = private + var ( repos []*repo_model.Repository count int64 @@ -102,6 +117,11 @@ func Home(ctx *context.Context) { Actor: ctx.Doer, Language: language, IncludeDescription: setting.UI.SearchRepoDescription, + Archived: archived, + Fork: fork, + Mirror: mirror, + Template: template, + IsPrivate: private, }) if err != nil { ctx.ServerError("SearchRepository", err) diff --git a/routers/web/org/members.go b/routers/web/org/members.go index 9a3d60e122..63ac57cf0d 100644 --- a/routers/web/org/members.go +++ b/routers/web/org/members.go @@ -9,6 +9,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/organization" + user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -78,40 +79,43 @@ func Members(ctx *context.Context) { // MembersAction response for operation to a member of organization func MembersAction(ctx *context.Context) { - uid := ctx.FormInt64("uid") - if uid == 0 { + member, err := user_model.GetUserByID(ctx, ctx.FormInt64("uid")) + if err != nil { + log.Error("GetUserByID: %v", err) + } + if member == nil { ctx.Redirect(ctx.Org.OrgLink + "/members") return } org := ctx.Org.Organization - var err error + switch ctx.Params(":action") { case "private": - if ctx.Doer.ID != uid && !ctx.Org.IsOwner { + if ctx.Doer.ID != member.ID && !ctx.Org.IsOwner { ctx.Error(http.StatusNotFound) return } - err = organization.ChangeOrgUserStatus(ctx, org.ID, uid, false) + err = organization.ChangeOrgUserStatus(ctx, org.ID, member.ID, false) case "public": - if ctx.Doer.ID != uid && !ctx.Org.IsOwner { + if ctx.Doer.ID != member.ID && !ctx.Org.IsOwner { ctx.Error(http.StatusNotFound) return } - err = organization.ChangeOrgUserStatus(ctx, org.ID, uid, true) + err = organization.ChangeOrgUserStatus(ctx, org.ID, member.ID, true) case "remove": if !ctx.Org.IsOwner { ctx.Error(http.StatusNotFound) return } - err = models.RemoveOrgUser(ctx, org.ID, uid) + err = models.RemoveOrgUser(ctx, org, member) if organization.IsErrLastOrgOwner(err) { ctx.Flash.Error(ctx.Tr("form.last_org_owner")) ctx.JSONRedirect(ctx.Org.OrgLink + "/members") return } case "leave": - err = models.RemoveOrgUser(ctx, org.ID, ctx.Doer.ID) + err = models.RemoveOrgUser(ctx, org, ctx.Doer) if err == nil { ctx.Flash.Success(ctx.Tr("form.organization_leave_success", org.DisplayName())) ctx.JSON(http.StatusOK, map[string]any{ diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index 338558fa23..ad8bb90d9e 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -18,8 +18,9 @@ import ( "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/web" shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/context" @@ -66,7 +67,7 @@ func Projects(ctx *context.Context) { PageSize: setting.UI.IssuePagingNum, }, OwnerID: ctx.ContextUser.ID, - IsClosed: util.OptionalBoolOf(isShowClosed), + IsClosed: optional.Some(isShowClosed), OrderBy: project_model.GetSearchOrderByBySortType(sortType), Type: projectType, Title: keyword, @@ -78,7 +79,7 @@ func Projects(ctx *context.Context) { opTotal, err := db.Count[project_model.Project](ctx, project_model.SearchOptions{ OwnerID: ctx.ContextUser.ID, - IsClosed: util.OptionalBoolOf(!isShowClosed), + IsClosed: optional.Some(!isShowClosed), Type: projectType, }) if err != nil { @@ -104,7 +105,7 @@ func Projects(ctx *context.Context) { } for _, project := range projects { - project.RenderedContent = project.Description + project.RenderedContent = templates.SanitizeHTML(project.Description) // FIXME: is it right? why not render? } err = shared_user.LoadHeaderCount(ctx) @@ -387,7 +388,7 @@ func ViewProject(ctx *context.Context) { if len(referencedIDs) > 0 { if linkedPrs, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{ IssueIDs: referencedIDs, - IsPull: util.OptionalBoolTrue, + IsPull: optional.Some(true), }); err == nil { linkedPrsMap[issue.ID] = linkedPrs } @@ -395,7 +396,7 @@ func ViewProject(ctx *context.Context) { } } - project.RenderedContent = project.Description + project.RenderedContent = templates.SanitizeHTML(project.Description) // FIXME: is it right? why not render? ctx.Data["LinkedPRs"] = linkedPrsMap ctx.Data["PageIsViewProjects"] = true ctx.Data["CanWriteProjects"] = canWriteProjects(ctx) diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go index fd7486cacd..144d9b1b43 100644 --- a/routers/web/org/teams.go +++ b/routers/web/org/teams.go @@ -5,6 +5,7 @@ package org import ( + "errors" "fmt" "net/http" "net/url" @@ -77,9 +78,9 @@ func TeamsAction(ctx *context.Context) { ctx.Error(http.StatusNotFound) return } - err = models.AddTeamMember(ctx, ctx.Org.Team, ctx.Doer.ID) + err = models.AddTeamMember(ctx, ctx.Org.Team, ctx.Doer) case "leave": - err = models.RemoveTeamMember(ctx, ctx.Org.Team, ctx.Doer.ID) + err = models.RemoveTeamMember(ctx, ctx.Org.Team, ctx.Doer) if err != nil { if org_model.IsErrLastOrgOwner(err) { ctx.Flash.Error(ctx.Tr("form.last_org_owner")) @@ -100,13 +101,13 @@ func TeamsAction(ctx *context.Context) { return } - uid := ctx.FormInt64("uid") - if uid == 0 { + user, _ := user_model.GetUserByID(ctx, ctx.FormInt64("uid")) + if user == nil { ctx.Redirect(ctx.Org.OrgLink + "/teams") return } - err = models.RemoveTeamMember(ctx, ctx.Org.Team, uid) + err = models.RemoveTeamMember(ctx, ctx.Org.Team, user) if err != nil { if org_model.IsErrLastOrgOwner(err) { ctx.Flash.Error(ctx.Tr("form.last_org_owner")) @@ -161,7 +162,7 @@ func TeamsAction(ctx *context.Context) { if ctx.Org.Team.IsMember(ctx, u.ID) { ctx.Flash.Error(ctx.Tr("org.teams.add_duplicate_users")) } else { - err = models.AddTeamMember(ctx, ctx.Org.Team, u.ID) + err = models.AddTeamMember(ctx, ctx.Org.Team, u) } page = "team" @@ -189,6 +190,8 @@ func TeamsAction(ctx *context.Context) { if err != nil { if org_model.IsErrLastOrgOwner(err) { ctx.Flash.Error(ctx.Tr("form.last_org_owner")) + } else if errors.Is(err, user_model.ErrBlockedUser) { + ctx.Flash.Error(ctx.Tr("org.teams.members.blocked_user")) } else { log.Error("Action(%s): %v", ctx.Params(":action"), err) ctx.JSON(http.StatusOK, map[string]any{ @@ -590,7 +593,7 @@ func TeamInvitePost(ctx *context.Context) { return } - if err := models.AddTeamMember(ctx, team, ctx.Doer.ID); err != nil { + if err := models.AddTeamMember(ctx, team, ctx.Doer); err != nil { ctx.ServerError("AddTeamMember", err) return } diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go index e784912377..f27329aa0f 100644 --- a/routers/web/repo/actions/actions.go +++ b/routers/web/repo/actions/actions.go @@ -16,8 +16,8 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/web/repo" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" @@ -78,7 +78,7 @@ func List(ctx *context.Context) { // Get all runner labels runners, err := db.Find[actions_model.ActionRunner](ctx, actions_model.FindRunnerOptions{ RepoID: ctx.Repo.Repository.ID, - IsOnline: util.OptionalBoolTrue, + IsOnline: optional.Some(true), WithAvailable: true, }) if err != nil { diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 52c3cf1d07..3f8030e40d 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -22,6 +22,7 @@ import ( "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -602,6 +603,28 @@ func ArtifactsDownloadView(ctx *context_module.Context) { ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(artifactName), artifactName)) + // Artifacts using the v4 backend are stored as a single combined zip file per artifact on the backend + // The v4 backend enshures ContentEncoding is set to "application/zip", which is not the case for the old backend + if len(artifacts) == 1 && artifacts[0].ArtifactName+".zip" == artifacts[0].ArtifactPath && artifacts[0].ContentEncoding == "application/zip" { + art := artifacts[0] + if setting.Actions.ArtifactStorage.MinioConfig.ServeDirect { + u, err := storage.ActionsArtifacts.URL(art.StoragePath, art.ArtifactPath) + if u != nil && err == nil { + ctx.Redirect(u.String()) + return + } + } + f, err := storage.ActionsArtifacts.Open(art.StoragePath) + if err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + _, _ = io.Copy(ctx.Resp, f) + return + } + + // Artifacts using the v1-v3 backend are stored as multiple individual files per artifact on the backend + // Those need to be zipped for download writer := zip.NewWriter(ctx.Resp) defer writer.Close() for _, art := range artifacts { diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go index 05f06a3ceb..ae51f0596b 100644 --- a/routers/web/repo/branch.go +++ b/routers/web/repo/branch.go @@ -18,6 +18,7 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" @@ -53,7 +54,7 @@ func Branches(ctx *context.Context) { kw := ctx.FormString("q") - defaultBranch, branches, branchesCount, err := repo_service.LoadBranches(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, util.OptionalBoolNone, kw, page, pageSize) + defaultBranch, branches, branchesCount, err := repo_service.LoadBranches(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, optional.None[bool](), kw, page, pageSize) if err != nil { ctx.ServerError("LoadBranches", err) return @@ -231,7 +232,7 @@ func CreateBranch(ctx *context.Context) { if len(e.Message) == 0 { ctx.Flash.Error(ctx.Tr("repo.editor.push_rejected_no_message")) } else { - flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{ + flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.editor.push_rejected"), "Summary": ctx.Tr("repo.editor.push_rejected_summary"), "Details": utils.SanitizeFlashErrorString(e.Message), diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index 8f3d9612ec..6146ce4ce4 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -341,7 +341,7 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b if len(errPushRej.Message) == 0 { ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplEditFile, &form) } else { - flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{ + flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.editor.push_rejected"), "Summary": ctx.Tr("repo.editor.push_rejected_summary"), "Details": utils.SanitizeFlashErrorString(errPushRej.Message), @@ -353,7 +353,7 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b ctx.RenderWithErr(flashError, tplEditFile, &form) } } else { - flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{ + flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.editor.fail_to_update_file", form.TreePath), "Summary": ctx.Tr("repo.editor.fail_to_update_file_summary"), "Details": utils.SanitizeFlashErrorString(err.Error()), @@ -542,7 +542,7 @@ func DeleteFilePost(ctx *context.Context) { if len(errPushRej.Message) == 0 { ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplDeleteFile, &form) } else { - flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{ + flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.editor.push_rejected"), "Summary": ctx.Tr("repo.editor.push_rejected_summary"), "Details": utils.SanitizeFlashErrorString(errPushRej.Message), @@ -742,7 +742,7 @@ func UploadFilePost(ctx *context.Context) { if len(errPushRej.Message) == 0 { ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplUploadFile, &form) } else { - flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{ + flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.editor.push_rejected"), "Summary": ctx.Tr("repo.editor.push_rejected_summary"), "Details": utils.SanitizeFlashErrorString(errPushRej.Message), diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 65e74a2d90..83a5b76bf1 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -9,6 +9,7 @@ import ( stdCtx "context" "errors" "fmt" + "html/template" "math/big" "net/http" "net/url" @@ -38,9 +39,11 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/markdown" + "code.gitea.io/gitea/modules/optional" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/templates/vars" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -54,6 +57,7 @@ import ( issue_service "code.gitea.io/gitea/services/issue" pull_service "code.gitea.io/gitea/services/pull" repo_service "code.gitea.io/gitea/services/repository" + user_service "code.gitea.io/gitea/services/user" ) const ( @@ -137,7 +141,7 @@ func MustAllowPulls(ctx *context.Context) { } } -func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption util.OptionalBool) { +func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption optional.Option[bool]) { var err error viewType := ctx.FormString("type") sortType := ctx.FormString("sort") @@ -238,18 +242,18 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti } } - var isShowClosed util.OptionalBool + var isShowClosed optional.Option[bool] switch ctx.FormString("state") { case "closed": - isShowClosed = util.OptionalBoolTrue + isShowClosed = optional.Some(true) case "all": - isShowClosed = util.OptionalBoolNone + isShowClosed = optional.None[bool]() default: - isShowClosed = util.OptionalBoolFalse + isShowClosed = optional.Some(false) } // if there are closed issues and no open issues, default to showing all issues if len(ctx.FormString("state")) == 0 && issueStats.OpenCount == 0 && issueStats.ClosedCount != 0 { - isShowClosed = util.OptionalBoolNone + isShowClosed = optional.None[bool]() } if repo.IsTimetrackerEnabled(ctx) { @@ -269,10 +273,10 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti } var total int - switch isShowClosed { - case util.OptionalBoolTrue: + switch { + case isShowClosed.Value(): total = int(issueStats.ClosedCount) - case util.OptionalBoolNone: + case !isShowClosed.Has(): total = int(issueStats.OpenCount + issueStats.ClosedCount) default: total = int(issueStats.OpenCount) @@ -428,7 +432,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti return } - pinned, err := issues_model.GetPinnedIssues(ctx, repo.ID, isPullOption.IsTrue()) + pinned, err := issues_model.GetPinnedIssues(ctx, repo.ID, isPullOption.Value()) if err != nil { ctx.ServerError("GetPinnedIssues", err) return @@ -458,10 +462,10 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti ctx.Data["AssigneeID"] = assigneeID ctx.Data["PosterID"] = posterID ctx.Data["Keyword"] = keyword - switch isShowClosed { - case util.OptionalBoolTrue: + switch { + case isShowClosed.Value(): ctx.Data["State"] = "closed" - case util.OptionalBoolNone: + case !isShowClosed.Has(): ctx.Data["State"] = "all" default: ctx.Data["State"] = "open" @@ -510,7 +514,7 @@ func Issues(ctx *context.Context) { ctx.Data["NewIssueChooseTemplate"] = issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo) } - issues(ctx, ctx.FormInt64("milestone"), ctx.FormInt64("project"), util.OptionalBoolOf(isPullList)) + issues(ctx, ctx.FormInt64("milestone"), ctx.FormInt64("project"), optional.Some(isPullList)) if ctx.Written() { return } @@ -552,7 +556,7 @@ func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *repo_model.R var err error ctx.Data["OpenMilestones"], err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{ RepoID: repo.ID, - IsClosed: util.OptionalBoolFalse, + IsClosed: optional.Some(false), }) if err != nil { ctx.ServerError("GetMilestones", err) @@ -560,7 +564,7 @@ func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *repo_model.R } ctx.Data["ClosedMilestones"], err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{ RepoID: repo.ID, - IsClosed: util.OptionalBoolTrue, + IsClosed: optional.Some(true), }) if err != nil { ctx.ServerError("GetMilestones", err) @@ -584,52 +588,63 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) { if repo.Owner.IsOrganization() { repoOwnerType = project_model.TypeOrganization } + + projectsUnit := repo.MustGetUnit(ctx, unit.TypeProjects) + + var openProjects []*project_model.Project + var closedProjects []*project_model.Project var err error - projects, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{ - ListOptions: db.ListOptionsAll, - RepoID: repo.ID, - IsClosed: util.OptionalBoolFalse, - Type: project_model.TypeRepository, - }) - if err != nil { - ctx.ServerError("GetProjects", err) - return - } - projects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{ - ListOptions: db.ListOptionsAll, - OwnerID: repo.OwnerID, - IsClosed: util.OptionalBoolFalse, - Type: repoOwnerType, - }) - if err != nil { - ctx.ServerError("GetProjects", err) - return + + if projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeRepo) { + openProjects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{ + ListOptions: db.ListOptionsAll, + RepoID: repo.ID, + IsClosed: optional.Some(false), + Type: project_model.TypeRepository, + }) + if err != nil { + ctx.ServerError("GetProjects", err) + return + } + closedProjects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{ + ListOptions: db.ListOptionsAll, + RepoID: repo.ID, + IsClosed: optional.Some(true), + Type: project_model.TypeRepository, + }) + if err != nil { + ctx.ServerError("GetProjects", err) + return + } } - ctx.Data["OpenProjects"] = append(projects, projects2...) - - projects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{ - ListOptions: db.ListOptionsAll, - RepoID: repo.ID, - IsClosed: util.OptionalBoolTrue, - Type: project_model.TypeRepository, - }) - if err != nil { - ctx.ServerError("GetProjects", err) - return - } - projects2, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{ - ListOptions: db.ListOptionsAll, - OwnerID: repo.OwnerID, - IsClosed: util.OptionalBoolTrue, - Type: repoOwnerType, - }) - if err != nil { - ctx.ServerError("GetProjects", err) - return + if projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeOwner) { + openProjects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{ + ListOptions: db.ListOptionsAll, + OwnerID: repo.OwnerID, + IsClosed: optional.Some(false), + Type: repoOwnerType, + }) + if err != nil { + ctx.ServerError("GetProjects", err) + return + } + openProjects = append(openProjects, openProjects2...) + closedProjects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{ + ListOptions: db.ListOptionsAll, + OwnerID: repo.OwnerID, + IsClosed: optional.Some(true), + Type: repoOwnerType, + }) + if err != nil { + ctx.ServerError("GetProjects", err) + return + } + closedProjects = append(closedProjects, closedProjects2...) } - ctx.Data["ClosedProjects"] = append(projects, projects2...) + ctx.Data["OpenProjects"] = openProjects + ctx.Data["ClosedProjects"] = closedProjects } // repoReviewerSelection items to bee shown @@ -1014,7 +1029,7 @@ func NewIssue(ctx *context.Context) { ctx.HTML(http.StatusOK, tplIssueNew) } -func renderErrorOfTemplates(ctx *context.Context, errs map[string]error) string { +func renderErrorOfTemplates(ctx *context.Context, errs map[string]error) template.HTML { var files []string for k := range errs { files = append(files, k) @@ -1026,14 +1041,14 @@ func renderErrorOfTemplates(ctx *context.Context, errs map[string]error) string lines = append(lines, fmt.Sprintf("%s: %v", file, errs[file])) } - flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{ + flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.issues.choose.ignore_invalid_templates"), "Summary": ctx.Tr("repo.issues.choose.invalid_templates", len(errs)), "Details": utils.SanitizeFlashErrorString(strings.Join(lines, "\n")), }) if err != nil { log.Debug("render flash error: %v", err) - flashError = ctx.Locale.TrString("repo.issues.choose.ignore_invalid_templates") + flashError = ctx.Locale.Tr("repo.issues.choose.ignore_invalid_templates") } return flashError } @@ -1209,6 +1224,14 @@ func NewIssuePost(ctx *context.Context) { return } + if projectID > 0 { + if !ctx.Repo.CanRead(unit.TypeProjects) { + // User must also be able to see the project. + ctx.Error(http.StatusBadRequest, "user hasn't permissions to read projects") + return + } + } + if setting.Attachment.Enabled { attachments = form.Files } @@ -1241,27 +1264,17 @@ func NewIssuePost(ctx *context.Context) { Ref: form.Ref, } - if err := issue_service.NewIssue(ctx, repo, issue, labelIDs, attachments, assigneeIDs); err != nil { + if err := issue_service.NewIssue(ctx, repo, issue, labelIDs, attachments, assigneeIDs, projectID); err != nil { if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) { ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error()) - return + } else if errors.Is(err, user_model.ErrBlockedUser) { + ctx.JSONError(ctx.Tr("repo.issues.new.blocked_user")) + } else { + ctx.ServerError("NewIssue", err) } - ctx.ServerError("NewIssue", err) return } - if projectID > 0 { - if !ctx.Repo.CanRead(unit.TypeProjects) { - // User must also be able to see the project. - ctx.Error(http.StatusBadRequest, "user hasn't permissions to read projects") - return - } - if err := issues_model.ChangeProjectAssign(ctx, issue, ctx.Doer, projectID); err != nil { - ctx.ServerError("ChangeProjectAssign", err) - return - } - } - log.Trace("Issue created: %d/%d", repo.ID, issue.ID) if ctx.FormString("redirect_after_creation") == "project" && projectID > 0 { ctx.JSONRedirect(ctx.Repo.RepoLink + "/projects/" + strconv.FormatInt(projectID, 10)) @@ -1759,7 +1772,7 @@ func ViewIssue(ctx *context.Context) { // so "|" is used as delimeter to mark the new format if comment.Content[0] != '|' { // handle old time comments that have formatted text stored - comment.RenderedContent = comment.Content + comment.RenderedContent = templates.SanitizeHTML(comment.Content) comment.Content = "" } else { // else it's just a duration in seconds to pass on to the frontend @@ -2033,6 +2046,10 @@ func ViewIssue(ctx *context.Context) { } ctx.Data["Tags"] = tags + ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool { + return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee) + } + ctx.HTML(http.StatusOK, tplIssueView) } @@ -2236,7 +2253,11 @@ func UpdateIssueContent(ctx *context.Context) { } if err := issue_service.ChangeContent(ctx, issue, ctx.Doer, ctx.Req.FormValue("content")); err != nil { - ctx.ServerError("ChangeContent", err) + if errors.Is(err, user_model.ErrBlockedUser) { + ctx.JSONError(ctx.Tr("repo.issues.edit.blocked_user")) + } else { + ctx.ServerError("ChangeContent", err) + } return } @@ -2499,14 +2520,14 @@ func SearchIssues(ctx *context.Context) { return } - var isClosed util.OptionalBool + var isClosed optional.Option[bool] switch ctx.FormString("state") { case "closed": - isClosed = util.OptionalBoolTrue + isClosed = optional.Some(true) case "all": - isClosed = util.OptionalBoolNone + isClosed = optional.None[bool]() default: - isClosed = util.OptionalBoolFalse + isClosed = optional.Some(false) } var ( @@ -2519,7 +2540,7 @@ func SearchIssues(ctx *context.Context) { Private: false, AllPublic: true, TopicOnly: false, - Collaborate: util.OptionalBoolNone, + Collaborate: optional.None[bool](), // This needs to be a column that is not nil in fixtures or // MySQL will return different results when sorting by null in some cases OrderBy: db.SearchOrderByAlphabetically, @@ -2542,7 +2563,7 @@ func SearchIssues(ctx *context.Context) { opts.OwnerID = owner.ID opts.AllLimited = false opts.AllPublic = false - opts.Collaborate = util.OptionalBoolFalse + opts.Collaborate = optional.Some(false) } if ctx.FormString("team") != "" { if ctx.FormString("owner") == "" { @@ -2581,14 +2602,12 @@ func SearchIssues(ctx *context.Context) { keyword = "" } - var isPull util.OptionalBool + isPull := optional.None[bool]() switch ctx.FormString("type") { case "pulls": - isPull = util.OptionalBoolTrue + isPull = optional.Some(true) case "issues": - isPull = util.OptionalBoolFalse - default: - isPull = util.OptionalBoolNone + isPull = optional.Some(false) } var includedAnyLabels []int64 @@ -2723,14 +2742,14 @@ func ListIssues(ctx *context.Context) { return } - var isClosed util.OptionalBool + var isClosed optional.Option[bool] switch ctx.FormString("state") { case "closed": - isClosed = util.OptionalBoolTrue + isClosed = optional.Some(true) case "all": - isClosed = util.OptionalBoolNone + isClosed = optional.None[bool]() default: - isClosed = util.OptionalBoolFalse + isClosed = optional.Some(false) } keyword := ctx.FormTrim("q") @@ -2782,14 +2801,12 @@ func ListIssues(ctx *context.Context) { projectID = &v } - var isPull util.OptionalBool + isPull := optional.None[bool]() switch ctx.FormString("type") { case "pulls": - isPull = util.OptionalBoolTrue + isPull = optional.Some(true) case "issues": - isPull = util.OptionalBoolFalse - default: - isPull = util.OptionalBoolNone + isPull = optional.Some(false) } // FIXME: we should be more efficient here @@ -3098,7 +3115,11 @@ func NewComment(ctx *context.Context) { comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Content, attachments) if err != nil { - ctx.ServerError("CreateIssueComment", err) + if errors.Is(err, user_model.ErrBlockedUser) { + ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user")) + } else { + ctx.ServerError("CreateIssueComment", err) + } return } @@ -3142,7 +3163,11 @@ func UpdateCommentContent(ctx *context.Context) { return } if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil { - ctx.ServerError("UpdateComment", err) + if errors.Is(err, user_model.ErrBlockedUser) { + ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user")) + } else { + ctx.ServerError("UpdateComment", err) + } return } @@ -3250,9 +3275,9 @@ func ChangeIssueReaction(ctx *context.Context) { switch ctx.Params(":action") { case "react": - reaction, err := issues_model.CreateIssueReaction(ctx, ctx.Doer.ID, issue.ID, form.Content) + reaction, err := issue_service.CreateIssueReaction(ctx, ctx.Doer, issue, form.Content) if err != nil { - if issues_model.IsErrForbiddenIssueReaction(err) { + if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) { ctx.ServerError("ChangeIssueReaction", err) return } @@ -3294,7 +3319,7 @@ func ChangeIssueReaction(ctx *context.Context) { return } - html, err := ctx.RenderToString(tplReactions, map[string]any{ + html, err := ctx.RenderToHTML(tplReactions, map[string]any{ "ctxData": ctx.Data, "ActionURL": fmt.Sprintf("%s/issues/%d/reactions", ctx.Repo.RepoLink, issue.Index), "Reactions": issue.Reactions.GroupByType(), @@ -3357,9 +3382,9 @@ func ChangeCommentReaction(ctx *context.Context) { switch ctx.Params(":action") { case "react": - reaction, err := issues_model.CreateCommentReaction(ctx, ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Content) + reaction, err := issue_service.CreateCommentReaction(ctx, ctx.Doer, comment, form.Content) if err != nil { - if issues_model.IsErrForbiddenIssueReaction(err) { + if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) { ctx.ServerError("ChangeIssueReaction", err) return } @@ -3401,7 +3426,7 @@ func ChangeCommentReaction(ctx *context.Context) { return } - html, err := ctx.RenderToString(tplReactions, map[string]any{ + html, err := ctx.RenderToHTML(tplReactions, map[string]any{ "ctxData": ctx.Data, "ActionURL": fmt.Sprintf("%s/comments/%d/reactions", ctx.Repo.RepoLink, comment.ID), "Reactions": comment.Reactions.GroupByType(), @@ -3544,8 +3569,8 @@ func updateAttachments(ctx *context.Context, item any, files []string) error { return err } -func attachmentsHTML(ctx *context.Context, attachments []*repo_model.Attachment, content string) string { - attachHTML, err := ctx.RenderToString(tplAttachment, map[string]any{ +func attachmentsHTML(ctx *context.Context, attachments []*repo_model.Attachment, content string) template.HTML { + attachHTML, err := ctx.RenderToHTML(tplAttachment, map[string]any{ "ctxData": ctx.Data, "Attachments": attachments, "Content": content, diff --git a/routers/web/repo/issue_content_history.go b/routers/web/repo/issue_content_history.go index fce0eccc7b..1ec497658f 100644 --- a/routers/web/repo/issue_content_history.go +++ b/routers/web/repo/issue_content_history.go @@ -94,7 +94,7 @@ func canSoftDeleteContentHistory(ctx *context.Context, issue *issues_model.Issue // CanWrite means the doer can manage the issue/PR list if ctx.Repo.IsOwner() || ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) { canSoftDelete = true - } else { + } else if ctx.Doer != nil { // for read-only users, they could still post issues or comments, // they should be able to delete the history related to their own issue/comment, a case is: // 1. the user posts some sensitive data @@ -186,6 +186,10 @@ func SoftDeleteContentHistory(ctx *context.Context) { if ctx.Written() { return } + if ctx.Doer == nil { + ctx.NotFound("Require SignIn", nil) + return + } commentID := ctx.FormInt64("comment_id") historyID := ctx.FormInt64("history_id") diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go index 49ac94aaf1..c41b844ce4 100644 --- a/routers/web/repo/milestone.go +++ b/routers/web/repo/milestone.go @@ -14,9 +14,9 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/markdown" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" @@ -51,7 +51,7 @@ func Milestones(ctx *context.Context) { PageSize: setting.UI.IssuePagingNum, }, RepoID: ctx.Repo.Repository.ID, - IsClosed: util.OptionalBoolOf(isShowClosed), + IsClosed: optional.Some(isShowClosed), SortType: sortType, Name: keyword, }) @@ -292,7 +292,7 @@ func MilestoneIssuesAndPulls(ctx *context.Context) { ctx.Data["Title"] = milestone.Name ctx.Data["Milestone"] = milestone - issues(ctx, milestoneID, projectID, util.OptionalBoolNone) + issues(ctx, milestoneID, projectID, optional.None[bool]()) ret := issue.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo) ctx.Data["NewIssueChooseTemplate"] = len(ret.IssueTemplates) > 0 diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go index 6ed5909dcf..11874ab0d0 100644 --- a/routers/web/repo/packages.go +++ b/routers/web/repo/packages.go @@ -10,8 +10,8 @@ import ( "code.gitea.io/gitea/models/packages" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" ) @@ -37,7 +37,7 @@ func Packages(ctx *context.Context) { RepoID: ctx.Repo.Repository.ID, Type: packages.Type(packageType), Name: packages.SearchValue{Value: query}, - IsInternal: util.OptionalBoolFalse, + IsInternal: optional.Some(false), }) if err != nil { ctx.ServerError("SearchLatestVersions", err) diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 1f9ee727c3..86909b5fd0 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -14,14 +14,14 @@ import ( issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/perm" project_model "code.gitea.io/gitea/models/project" - attachment_model "code.gitea.io/gitea/models/repo" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/markdown" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" @@ -33,16 +33,17 @@ const ( tplProjectsView base.TplName = "repo/projects/view" ) -// MustEnableProjects check if projects are enabled in settings -func MustEnableProjects(ctx *context.Context) { +// MustEnableRepoProjects check if repo projects are enabled in settings +func MustEnableRepoProjects(ctx *context.Context) { if unit.TypeProjects.UnitGlobalDisabled() { ctx.NotFound("EnableKanbanBoard", nil) return } if ctx.Repo.Repository != nil { - if !ctx.Repo.CanRead(unit.TypeProjects) { - ctx.NotFound("MustEnableProjects", nil) + projectsUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeProjects) + if !ctx.Repo.CanRead(unit.TypeProjects) || !projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeRepo) { + ctx.NotFound("MustEnableRepoProjects", nil) return } } @@ -78,7 +79,7 @@ func Projects(ctx *context.Context) { Page: page, }, RepoID: repo.ID, - IsClosed: util.OptionalBoolOf(isShowClosed), + IsClosed: optional.Some(isShowClosed), OrderBy: project_model.GetSearchOrderByBySortType(sortType), Type: project_model.TypeRepository, Title: keyword, @@ -325,10 +326,10 @@ func ViewProject(ctx *context.Context) { } if project.CardType != project_model.CardTypeTextOnly { - issuesAttachmentMap := make(map[int64][]*attachment_model.Attachment) + issuesAttachmentMap := make(map[int64][]*repo_model.Attachment) for _, issuesList := range issuesMap { for _, issue := range issuesList { - if issueAttachment, err := attachment_model.GetAttachmentsByIssueIDImagesLatest(ctx, issue.ID); err == nil { + if issueAttachment, err := repo_model.GetAttachmentsByIssueIDImagesLatest(ctx, issue.ID); err == nil { issuesAttachmentMap[issue.ID] = issueAttachment } } @@ -349,7 +350,7 @@ func ViewProject(ctx *context.Context) { if len(referencedIDs) > 0 { if linkedPrs, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{ IssueIDs: referencedIDs, - IsPull: util.OptionalBoolTrue, + IsPull: optional.Some(true), }); err == nil { linkedPrsMap[issue.ID] = linkedPrs } diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index b1521a2112..ed063715e5 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -47,6 +47,7 @@ import ( notify_service "code.gitea.io/gitea/services/notify" pull_service "code.gitea.io/gitea/services/pull" repo_service "code.gitea.io/gitea/services/repository" + user_service "code.gitea.io/gitea/services/user" "github.com/gobwas/glob" ) @@ -112,7 +113,7 @@ func getRepository(ctx *context.Context, repoID int64) *repo_model.Repository { } func getForkRepository(ctx *context.Context) *repo_model.Repository { - forkRepo := getRepository(ctx, ctx.ParamsInt64(":repoid")) + forkRepo := ctx.Repo.Repository if ctx.Written() { return nil } @@ -308,6 +309,8 @@ func ForkPost(ctx *context.Context) { ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplFork, &form) case db.IsErrNamePatternNotAllowed(err): ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplFork, &form) + case errors.Is(err, user_model.ErrBlockedUser): + ctx.RenderWithErr(ctx.Tr("repo.fork.blocked_user"), tplFork, form) default: ctx.ServerError("ForkPost", err) } @@ -1065,6 +1068,10 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi } upload.AddUploadContext(ctx, "comment") + ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool { + return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee) + } + ctx.HTML(http.StatusOK, tplPullFiles) } @@ -1129,7 +1136,7 @@ func UpdatePullRequest(ctx *context.Context) { if err = pull_service.Update(ctx, issue.PullRequest, ctx.Doer, message, rebase); err != nil { if models.IsErrMergeConflicts(err) { conflictError := err.(models.ErrMergeConflicts) - flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{ + flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.pulls.merge_conflict"), "Summary": ctx.Tr("repo.pulls.merge_conflict_summary"), "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "
" + utils.SanitizeFlashErrorString(conflictError.StdOut), @@ -1143,7 +1150,7 @@ func UpdatePullRequest(ctx *context.Context) { return } else if models.IsErrRebaseConflicts(err) { conflictError := err.(models.ErrRebaseConflicts) - flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{ + flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.pulls.rebase_conflict", utils.SanitizeFlashErrorString(conflictError.CommitSHA)), "Summary": ctx.Tr("repo.pulls.rebase_conflict_summary"), "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "
" + utils.SanitizeFlashErrorString(conflictError.StdOut), @@ -1275,7 +1282,7 @@ func MergePullRequest(ctx *context.Context) { ctx.JSONError(ctx.Tr("repo.pulls.invalid_merge_option")) } else if models.IsErrMergeConflicts(err) { conflictError := err.(models.ErrMergeConflicts) - flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{ + flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.editor.merge_conflict"), "Summary": ctx.Tr("repo.editor.merge_conflict_summary"), "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "
" + utils.SanitizeFlashErrorString(conflictError.StdOut), @@ -1288,7 +1295,7 @@ func MergePullRequest(ctx *context.Context) { ctx.JSONRedirect(issue.Link()) } else if models.IsErrRebaseConflicts(err) { conflictError := err.(models.ErrRebaseConflicts) - flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{ + flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.pulls.rebase_conflict", utils.SanitizeFlashErrorString(conflictError.CommitSHA)), "Summary": ctx.Tr("repo.pulls.rebase_conflict_summary"), "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "
" + utils.SanitizeFlashErrorString(conflictError.StdOut), @@ -1318,7 +1325,7 @@ func MergePullRequest(ctx *context.Context) { if len(message) == 0 { ctx.Flash.Error(ctx.Tr("repo.pulls.push_rejected_no_message")) } else { - flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{ + flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.pulls.push_rejected"), "Summary": ctx.Tr("repo.pulls.push_rejected_summary"), "Details": utils.SanitizeFlashErrorString(pushrejErr.Message), @@ -1483,7 +1490,6 @@ func CompareAndPullRequestPost(ctx *context.Context) { if err := pull_service.NewPullRequest(ctx, repo, pullIssue, labelIDs, attachments, pullRequest, assigneeIDs); err != nil { if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) { ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error()) - return } else if git.IsErrPushRejected(err) { pushrejErr := err.(*git.ErrPushRejected) message := pushrejErr.Message @@ -1491,7 +1497,7 @@ func CompareAndPullRequestPost(ctx *context.Context) { ctx.JSONError(ctx.Tr("repo.pulls.push_rejected_no_message")) return } - flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{ + flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.pulls.push_rejected"), "Summary": ctx.Tr("repo.pulls.push_rejected_summary"), "Details": utils.SanitizeFlashErrorString(pushrejErr.Message), @@ -1500,11 +1506,18 @@ func CompareAndPullRequestPost(ctx *context.Context) { ctx.ServerError("CompareAndPullRequest.HTMLString", err) return } - ctx.Flash.Error(flashError) - ctx.JSONRedirect(pullIssue.Link()) // FIXME: it's unfriendly, and will make the content lost - return + ctx.JSONError(flashError) + } else if errors.Is(err, user_model.ErrBlockedUser) { + flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ + "Message": ctx.Tr("repo.pulls.push_rejected"), + "Summary": ctx.Tr("repo.pulls.new.blocked_user"), + }) + if err != nil { + ctx.ServerError("CompareAndPullRequest.HTMLString", err) + return + } + ctx.JSONError(flashError) } - ctx.ServerError("NewPullRequest", err) return } diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go index a730c2d3b7..dbc190928f 100644 --- a/routers/web/repo/release.go +++ b/routers/web/repo/release.go @@ -113,7 +113,7 @@ func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions) cacheUsers[r.PublisherID] = r.Publisher } - r.Note, err = markdown.RenderString(&markup.RenderContext{ + r.RenderedNote, err = markdown.RenderString(&markup.RenderContext{ Links: markup.Links{ Base: ctx.Repo.RepoLink, }, @@ -185,6 +185,11 @@ func Releases(ctx *context.Context) { ctx.ServerError("getReleaseInfos", err) return } + for _, rel := range releases { + if rel.Release.IsTag && rel.Release.Title == "" { + rel.Release.Title = rel.Release.TagName + } + } ctx.Data["Releases"] = releases @@ -283,6 +288,7 @@ func SingleRelease(ctx *context.Context) { TagNames: []string{ctx.Params("*")}, // only show draft releases for users who can write, read-only users shouldn't see draft releases. IncludeDrafts: writeAccess, + IncludeTags: true, }) if err != nil { ctx.ServerError("getReleaseInfos", err) @@ -294,6 +300,9 @@ func SingleRelease(ctx *context.Context) { } release := releases[0].Release + if release.IsTag && release.Title == "" { + release.Title = release.TagName + } ctx.Data["PageIsSingleTag"] = release.IsTag if release.IsTag { diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index 9891ecee92..8abed4ef5b 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -314,13 +314,13 @@ func Action(ctx *context.Context) { var err error switch ctx.Params(":action") { case "watch": - err = repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true) + err = repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, true) case "unwatch": - err = repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false) + err = repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, false) case "star": - err = repo_model.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true) + err = repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, true) case "unstar": - err = repo_model.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false) + err = repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, false) case "accept_transfer": err = acceptOrRejectRepoTransfer(ctx, true) case "reject_transfer": @@ -337,8 +337,12 @@ func Action(ctx *context.Context) { } if err != nil { - ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.Params(":action")), err) - return + if errors.Is(err, user_model.ErrBlockedUser) { + ctx.Flash.Error(ctx.Tr("repo.action.blocked_user")) + } else { + ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.Params(":action")), err) + return + } } switch ctx.Params(":action") { @@ -554,33 +558,33 @@ func SearchRepo(ctx *context.Context) { PriorityOwnerID: ctx.FormInt64("priority_owner_id"), TeamID: ctx.FormInt64("team_id"), TopicOnly: ctx.FormBool("topic"), - Collaborate: util.OptionalBoolNone, + Collaborate: optional.None[bool](), Private: ctx.IsSigned && (ctx.FormString("private") == "" || ctx.FormBool("private")), - Template: util.OptionalBoolNone, + Template: optional.None[bool](), StarredByID: ctx.FormInt64("starredBy"), IncludeDescription: ctx.FormBool("includeDesc"), } if ctx.FormString("template") != "" { - opts.Template = util.OptionalBoolOf(ctx.FormBool("template")) + opts.Template = optional.Some(ctx.FormBool("template")) } if ctx.FormBool("exclusive") { - opts.Collaborate = util.OptionalBoolFalse + opts.Collaborate = optional.Some(false) } mode := ctx.FormString("mode") switch mode { case "source": - opts.Fork = util.OptionalBoolFalse - opts.Mirror = util.OptionalBoolFalse + opts.Fork = optional.Some(false) + opts.Mirror = optional.Some(false) case "fork": - opts.Fork = util.OptionalBoolTrue + opts.Fork = optional.Some(true) case "mirror": - opts.Mirror = util.OptionalBoolTrue + opts.Mirror = optional.Some(true) case "collaborative": - opts.Mirror = util.OptionalBoolFalse - opts.Collaborate = util.OptionalBoolTrue + opts.Mirror = optional.Some(false) + opts.Collaborate = optional.Some(true) case "": default: ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid search mode: \"%s\"", mode)) @@ -588,11 +592,11 @@ func SearchRepo(ctx *context.Context) { } if ctx.FormString("archived") != "" { - opts.Archived = util.OptionalBoolOf(ctx.FormBool("archived")) + opts.Archived = optional.Some(ctx.FormBool("archived")) } if ctx.FormString("is_private") != "" { - opts.IsPrivate = util.OptionalBoolOf(ctx.FormBool("is_private")) + opts.IsPrivate = optional.Some(ctx.FormBool("is_private")) } sortMode := ctx.FormString("sort") diff --git a/routers/web/repo/setting/collaboration.go b/routers/web/repo/setting/collaboration.go index 6bfd485566..31f9f76d0f 100644 --- a/routers/web/repo/setting/collaboration.go +++ b/routers/web/repo/setting/collaboration.go @@ -4,10 +4,10 @@ package setting import ( + "errors" "net/http" "strings" - "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" repo_model "code.gitea.io/gitea/models/repo" @@ -27,7 +27,7 @@ func Collaboration(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.settings.collaboration") ctx.Data["PageIsSettingsCollaboration"] = true - users, err := repo_model.GetCollaborators(ctx, ctx.Repo.Repository.ID, db.ListOptions{}) + users, _, err := repo_model.GetCollaborators(ctx, &repo_model.FindCollaborationOptions{RepoID: ctx.Repo.Repository.ID}) if err != nil { ctx.ServerError("GetCollaborators", err) return @@ -101,7 +101,12 @@ func CollaborationPost(ctx *context.Context) { } if err = repo_module.AddCollaborator(ctx, ctx.Repo.Repository, u); err != nil { - ctx.ServerError("AddCollaborator", err) + if errors.Is(err, user_model.ErrBlockedUser) { + ctx.Flash.Error(ctx.Tr("repo.settings.add_collaborator.blocked_user")) + ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") + } else { + ctx.ServerError("AddCollaborator", err) + } return } @@ -126,10 +131,19 @@ func ChangeCollaborationAccessMode(ctx *context.Context) { // DeleteCollaboration delete a collaboration for a repository func DeleteCollaboration(ctx *context.Context) { - if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, ctx.FormInt64("id")); err != nil { - ctx.Flash.Error("DeleteCollaboration: " + err.Error()) + if collaborator, err := user_model.GetUserByID(ctx, ctx.FormInt64("id")); err != nil { + if user_model.IsErrUserNotExist(err) { + ctx.Flash.Error(ctx.Tr("form.user_not_exist")) + } else { + ctx.ServerError("GetUserByName", err) + return + } } else { - ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success")) + if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, collaborator); err != nil { + ctx.Flash.Error("DeleteCollaboration: " + err.Error()) + } else { + ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success")) + } } ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings/collaboration") diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index 6838e1f9cd..db7ec0fc99 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -5,6 +5,7 @@ package setting import ( + "errors" "fmt" "net/http" "strconv" @@ -23,7 +24,6 @@ import ( "code.gitea.io/gitea/modules/indexer/stats" "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/log" - repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" @@ -551,6 +551,9 @@ func SettingsPost(ctx *context.Context) { units = append(units, repo_model.RepoUnit{ RepoID: repo.ID, Type: unit_model.TypeProjects, + Config: &repo_model.ProjectsConfig{ + ProjectsMode: repo_model.ProjectsMode(form.ProjectsMode), + }, }) } else if !unit_model.TypeProjects.UnitGlobalDisabled() { deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects) @@ -710,7 +713,7 @@ func SettingsPost(ctx *context.Context) { } repo.IsMirror = false - if _, err := repo_module.CleanUpMigrateInfo(ctx, repo); err != nil { + if _, err := repo_service.CleanUpMigrateInfo(ctx, repo); err != nil { ctx.ServerError("CleanUpMigrateInfo", err) return } else if err = repo_model.DeleteMirrorByRepoID(ctx, ctx.Repo.Repository.ID); err != nil { @@ -797,6 +800,8 @@ func SettingsPost(ctx *context.Context) { ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil) } else if models.IsErrRepoTransferInProgress(err) { ctx.RenderWithErr(ctx.Tr("repo.settings.transfer_in_progress"), tplSettingsOptions, nil) + } else if errors.Is(err, user_model.ErrBlockedUser) { + ctx.RenderWithErr(ctx.Tr("repo.settings.transfer.blocked_user"), tplSettingsOptions, nil) } else { ctx.ServerError("TransferOwnership", err) } diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index e89739e2fb..4df10fbea1 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -35,7 +35,6 @@ import ( "code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/charset" - "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/highlight" "code.gitea.io/gitea/modules/lfs" @@ -859,25 +858,18 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri defer cancel() } - selected := make(container.Set[string]) - selected.AddMultiple(ctx.FormStrings("f[]")...) - - entries := allEntries - if len(selected) > 0 { - entries = make(git.Entries, 0, len(selected)) - for _, entry := range allEntries { - if selected.Contains(entry.Name()) { - entries = append(entries, entry) - } - } - } - - var latestCommit *git.Commit - ctx.Data["Files"], latestCommit, err = entries.GetCommitsInfo(commitInfoCtx, ctx.Repo.Commit, ctx.Repo.TreePath) + files, latestCommit, err := allEntries.GetCommitsInfo(commitInfoCtx, ctx.Repo.Commit, ctx.Repo.TreePath) if err != nil { ctx.ServerError("GetCommitsInfo", err) return nil } + ctx.Data["Files"] = files + for _, f := range files { + if f.Commit == nil { + ctx.Data["HasFilesWithoutLatestCommit"] = true + break + } + } if !loadLatestCommitData(ctx, latestCommit) { return nil diff --git a/routers/web/repo/wiki_test.go b/routers/web/repo/wiki_test.go index 49c83cfef5..719cca3049 100644 --- a/routers/web/repo/wiki_test.go +++ b/routers/web/repo/wiki_test.go @@ -79,7 +79,7 @@ func assertPagesMetas(t *testing.T, expectedNames []string, metas any) { func TestWiki(t *testing.T) { unittest.PrepareTestEnv(t) - ctx, _ := contexttest.MockContext(t, "user2/repo1/wiki/?action=_pages") + ctx, _ := contexttest.MockContext(t, "user2/repo1/wiki") ctx.SetParams("*", "Home") contexttest.LoadRepo(t, ctx, 1) Wiki(ctx) diff --git a/routers/web/shared/packages/packages.go b/routers/web/shared/packages/packages.go index 1454396f04..57671ad8f1 100644 --- a/routers/web/shared/packages/packages.go +++ b/routers/web/shared/packages/packages.go @@ -13,7 +13,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" @@ -157,7 +157,7 @@ func SetRulePreviewContext(ctx *context.Context, owner *user_model.User) { for _, p := range packages { pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ PackageID: p.ID, - IsInternal: util.OptionalBoolFalse, + IsInternal: optional.Some(false), Sort: packages_model.SortCreatedDesc, Paginator: db.NewAbsoluteListOptions(pcr.KeepCount, 200), }) diff --git a/routers/web/shared/user/block.go b/routers/web/shared/user/block.go new file mode 100644 index 0000000000..8a2357623f --- /dev/null +++ b/routers/web/shared/user/block.go @@ -0,0 +1,76 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import ( + "errors" + + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/forms" + user_service "code.gitea.io/gitea/services/user" +) + +func BlockedUsers(ctx *context.Context, blocker *user_model.User) { + blocks, _, err := user_model.FindBlockings(ctx, &user_model.FindBlockingOptions{ + BlockerID: blocker.ID, + }) + if err != nil { + ctx.ServerError("FindBlockings", err) + return + } + if err := user_model.BlockingList(blocks).LoadAttributes(ctx); err != nil { + ctx.ServerError("LoadAttributes", err) + return + } + ctx.Data["UserBlocks"] = blocks +} + +func BlockedUsersPost(ctx *context.Context, blocker *user_model.User) { + form := web.GetForm(ctx).(*forms.BlockUserForm) + if ctx.HasError() { + ctx.ServerError("FormValidation", nil) + return + } + + blockee, err := user_model.GetUserByName(ctx, form.Blockee) + if err != nil { + ctx.ServerError("GetUserByName", nil) + return + } + + switch form.Action { + case "block": + if err := user_service.BlockUser(ctx, ctx.Doer, blocker, blockee, form.Note); err != nil { + if errors.Is(err, user_model.ErrCanNotBlock) || errors.Is(err, user_model.ErrBlockOrganization) { + ctx.Flash.Error(ctx.Tr("user.block.block.failure", err.Error())) + } else { + ctx.ServerError("BlockUser", err) + return + } + } + case "unblock": + if err := user_service.UnblockUser(ctx, ctx.Doer, blocker, blockee); err != nil { + if errors.Is(err, user_model.ErrCanNotUnblock) || errors.Is(err, user_model.ErrBlockOrganization) { + ctx.Flash.Error(ctx.Tr("user.block.unblock.failure", err.Error())) + } else { + ctx.ServerError("UnblockUser", err) + return + } + } + case "note": + block, err := user_model.GetBlocking(ctx, blocker.ID, blockee.ID) + if err != nil { + ctx.ServerError("GetBlocking", err) + return + } + if block != nil { + if err := user_model.UpdateBlockingNote(ctx, block.ID, form.Note); err != nil { + ctx.ServerError("UpdateBlockingNote", err) + return + } + } + } +} diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go index 2253b8840d..2d6d9ad98d 100644 --- a/routers/web/shared/user/header.go +++ b/routers/web/shared/user/header.go @@ -16,8 +16,8 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" ) @@ -72,6 +72,14 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) { if _, ok := ctx.Data["NumFollowing"]; !ok { _, ctx.Data["NumFollowing"], _ = user_model.GetUserFollowing(ctx, ctx.ContextUser, ctx.Doer, db.ListOptions{PageSize: 1, Page: 1}) } + + if ctx.Doer != nil { + if block, err := user_model.GetBlocking(ctx, ctx.Doer.ID, ctx.ContextUser.ID); err != nil { + ctx.ServerError("GetBlocking", err) + } else { + ctx.Data["UserBlocking"] = block + } + } } func FindUserProfileReadme(ctx *context.Context, doer *user_model.User) (profileDbRepo *repo_model.Repository, profileGitRepo *git.Repository, profileReadmeBlob *git.Blob, profileClose func()) { @@ -114,7 +122,7 @@ func LoadHeaderCount(ctx *context.Context) error { Actor: ctx.Doer, OwnerID: ctx.ContextUser.ID, Private: ctx.IsSigned, - Collaborate: util.OptionalBoolFalse, + Collaborate: optional.Some(false), IncludeDescription: setting.UI.SearchRepoDescription, }) if err != nil { @@ -130,7 +138,7 @@ func LoadHeaderCount(ctx *context.Context) error { } projectCount, err := db.Count[project_model.Project](ctx, project_model.SearchOptions{ OwnerID: ctx.ContextUser.ID, - IsClosed: util.OptionalBoolOf(false), + IsClosed: optional.Some(false), Type: projectType, }) if err != nil { diff --git a/routers/web/user/home.go b/routers/web/user/home.go index 78548e6df7..caa7115259 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -28,8 +28,8 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/markdown" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/web/feed" "code.gitea.io/gitea/services/context" issue_service "code.gitea.io/gitea/services/issue" @@ -161,8 +161,8 @@ func Milestones(ctx *context.Context) { Private: true, AllPublic: false, // Include also all public repositories of users and public organisations AllLimited: false, // Include also all public repositories of limited organisations - Archived: util.OptionalBoolFalse, - HasMilestones: util.OptionalBoolTrue, // Just needs display repos has milestones + Archived: optional.Some(false), + HasMilestones: optional.Some(true), // Just needs display repos has milestones } if ctxUser.IsOrganization() && ctx.Org.Team != nil { @@ -214,7 +214,7 @@ func Milestones(ctx *context.Context) { counts, err := issues_model.CountMilestonesMap(ctx, issues_model.FindMilestoneOptions{ RepoCond: userRepoCond, Name: keyword, - IsClosed: util.OptionalBoolOf(isShowClosed), + IsClosed: optional.Some(isShowClosed), }) if err != nil { ctx.ServerError("CountMilestonesByRepoIDs", err) @@ -227,7 +227,7 @@ func Milestones(ctx *context.Context) { PageSize: setting.UI.IssuePagingNum, }, RepoCond: repoCond, - IsClosed: util.OptionalBoolOf(isShowClosed), + IsClosed: optional.Some(isShowClosed), SortType: sortType, Name: keyword, }) @@ -439,9 +439,9 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { isPullList := unitType == unit.TypePullRequests opts := &issues_model.IssuesOptions{ - IsPull: util.OptionalBoolOf(isPullList), + IsPull: optional.Some(isPullList), SortType: sortType, - IsArchived: util.OptionalBoolFalse, + IsArchived: optional.Some(false), Org: org, Team: team, User: ctx.Doer, @@ -465,9 +465,9 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { Private: true, AllPublic: false, AllLimited: false, - Collaborate: util.OptionalBoolNone, + Collaborate: optional.None[bool](), UnitType: unitType, - Archived: util.OptionalBoolFalse, + Archived: optional.Some(false), } if team != nil { repoOpts.TeamID = team.ID @@ -515,7 +515,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { // Educated guess: Do or don't show closed issues. isShowClosed := ctx.FormString("state") == "closed" - opts.IsClosed = util.OptionalBoolOf(isShowClosed) + opts.IsClosed = optional.Some(isShowClosed) // Make sure page number is at least 1. Will be posted to ctx.Data. page := ctx.FormInt("page") @@ -799,12 +799,12 @@ func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMod case issues_model.FilterModeReviewed: openClosedOpts.ReviewedID = &doerID } - openClosedOpts.IsClosed = util.OptionalBoolFalse + openClosedOpts.IsClosed = optional.Some(false) ret.OpenCount, err = issue_indexer.CountIssues(ctx, openClosedOpts) if err != nil { return nil, err } - openClosedOpts.IsClosed = util.OptionalBoolTrue + openClosedOpts.IsClosed = optional.Some(true) ret.ClosedCount, err = issue_indexer.CountIssues(ctx, openClosedOpts) if err != nil { return nil, err diff --git a/routers/web/user/home_test.go b/routers/web/user/home_test.go index 3f5fd26689..1cc9886308 100644 --- a/routers/web/user/home_test.go +++ b/routers/web/user/home_test.go @@ -11,6 +11,8 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/contexttest" "github.com/stretchr/testify/assert" @@ -113,3 +115,18 @@ func TestMilestonesForSpecificRepo(t *testing.T) { assert.Len(t, ctx.Data["Milestones"], 1) assert.Len(t, ctx.Data["Repos"], 2) // both repo 42 and 1 have milestones and both are owned by user 2 } + +func TestDashboardPagination(t *testing.T) { + ctx, _ := contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()}) + page := context.NewPagination(10, 3, 1, 3) + + setting.AppSubURL = "/SubPath" + out, err := ctx.RenderToHTML("base/paginate", map[string]any{"Link": setting.AppSubURL, "Page": page}) + assert.NoError(t, err) + assert.Contains(t, out, ``) + + setting.AppSubURL = "" + out, err = ctx.RenderToHTML("base/paginate", map[string]any{"Link": setting.AppSubURL, "Page": page}) + assert.NoError(t, err) + assert.Contains(t, out, ``) +} diff --git a/routers/web/user/notification.go b/routers/web/user/notification.go index 05034f8efa..324205ed91 100644 --- a/routers/web/user/notification.go +++ b/routers/web/user/notification.go @@ -17,6 +17,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" @@ -232,26 +233,25 @@ func NotificationSubscriptions(ctx *context.Context) { if !util.SliceContainsString([]string{"all", "open", "closed"}, state, true) { state = "all" } + ctx.Data["State"] = state - var showClosed util.OptionalBool + // default state filter is "all" + showClosed := optional.None[bool]() switch state { - case "all": - showClosed = util.OptionalBoolNone case "closed": - showClosed = util.OptionalBoolTrue + showClosed = optional.Some(true) case "open": - showClosed = util.OptionalBoolFalse + showClosed = optional.Some(false) } - var issueTypeBool util.OptionalBool issueType := ctx.FormString("issueType") + // default issue type is no filter + issueTypeBool := optional.None[bool]() switch issueType { case "issues": - issueTypeBool = util.OptionalBoolFalse + issueTypeBool = optional.Some(false) case "pulls": - issueTypeBool = util.OptionalBoolTrue - default: - issueTypeBool = util.OptionalBoolNone + issueTypeBool = optional.Some(true) } ctx.Data["IssueType"] = issueType @@ -389,6 +389,21 @@ func NotificationWatching(ctx *context.Context) { orderBy = db.SearchOrderByRecentUpdated } + archived := ctx.FormOptionalBool("archived") + ctx.Data["IsArchived"] = archived + + fork := ctx.FormOptionalBool("fork") + ctx.Data["IsFork"] = fork + + mirror := ctx.FormOptionalBool("mirror") + ctx.Data["IsMirror"] = mirror + + template := ctx.FormOptionalBool("template") + ctx.Data["IsTemplate"] = template + + private := ctx.FormOptionalBool("private") + ctx.Data["IsPrivate"] = private + repos, count, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ PageSize: setting.UI.User.RepoPagingNum, @@ -399,9 +414,14 @@ func NotificationWatching(ctx *context.Context) { OrderBy: orderBy, Private: ctx.IsSigned, WatchedByID: ctx.Doer.ID, - Collaborate: util.OptionalBoolFalse, + Collaborate: optional.Some(false), TopicOnly: ctx.FormBool("topic"), IncludeDescription: setting.UI.SearchRepoDescription, + Archived: archived, + Fork: fork, + Mirror: mirror, + Template: template, + IsPrivate: private, }) if err != nil { ctx.ServerError("SearchRepository", err) diff --git a/routers/web/user/package.go b/routers/web/user/package.go index d03b28309f..3ecc59a2ab 100644 --- a/routers/web/user/package.go +++ b/routers/web/user/package.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" alpine_module "code.gitea.io/gitea/modules/packages/alpine" debian_module "code.gitea.io/gitea/modules/packages/debian" rpm_module "code.gitea.io/gitea/modules/packages/rpm" @@ -54,7 +55,7 @@ func ListPackages(ctx *context.Context) { OwnerID: ctx.ContextUser.ID, Type: packages_model.Type(packageType), Name: packages_model.SearchValue{Value: query}, - IsInternal: util.OptionalBoolFalse, + IsInternal: optional.Some(false), }) if err != nil { ctx.ServerError("SearchLatestVersions", err) @@ -145,7 +146,7 @@ func RedirectToLastVersion(ctx *context.Context) { pvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{ PackageID: p.ID, - IsInternal: util.OptionalBoolFalse, + IsInternal: optional.Some(false), }) if err != nil { ctx.ServerError("GetPackageByName", err) @@ -162,7 +163,7 @@ func RedirectToLastVersion(ctx *context.Context) { return } - ctx.Redirect(pd.FullWebLink()) + ctx.Redirect(pd.VersionWebLink()) } // ViewPackageVersion displays a single package version @@ -255,7 +256,7 @@ func ViewPackageVersion(ctx *context.Context) { pvs, total, err = packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ Paginator: db.NewAbsoluteListOptions(0, 5), PackageID: pd.Package.ID, - IsInternal: util.OptionalBoolFalse, + IsInternal: optional.Some(false), }) } if err != nil { @@ -359,7 +360,7 @@ func ListPackageVersions(ctx *context.Context) { ExactMatch: false, Value: query, }, - IsInternal: util.OptionalBoolFalse, + IsInternal: optional.Some(false), Sort: sort, }) if err != nil { @@ -467,7 +468,7 @@ func PackageSettingsPost(ctx *context.Context) { redirectURL := ctx.Package.Owner.HomeLink() + "/-/packages" // redirect to the package if there are still versions available - if has, _ := packages_model.ExistVersion(ctx, &packages_model.PackageSearchOptions{PackageID: ctx.Package.Descriptor.Package.ID, IsInternal: util.OptionalBoolFalse}); has { + if has, _ := packages_model.ExistVersion(ctx, &packages_model.PackageSearchOptions{PackageID: ctx.Package.Descriptor.Package.ID, IsInternal: optional.Some(false)}); has { redirectURL = ctx.Package.Descriptor.PackageWebLink() } diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index e7890b7c12..9851ea90a6 100644 --- a/routers/web/user/profile.go +++ b/routers/web/user/profile.go @@ -19,6 +19,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/markdown" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/web/feed" @@ -161,6 +162,21 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb } ctx.Data["NumFollowing"] = numFollowing + archived := ctx.FormOptionalBool("archived") + ctx.Data["IsArchived"] = archived + + fork := ctx.FormOptionalBool("fork") + ctx.Data["IsFork"] = fork + + mirror := ctx.FormOptionalBool("mirror") + ctx.Data["IsMirror"] = mirror + + template := ctx.FormOptionalBool("template") + ctx.Data["IsTemplate"] = template + + private := ctx.FormOptionalBool("private") + ctx.Data["IsPrivate"] = private + switch tab { case "followers": ctx.Data["Cards"] = followers @@ -203,10 +219,15 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb OrderBy: orderBy, Private: ctx.IsSigned, StarredByID: ctx.ContextUser.ID, - Collaborate: util.OptionalBoolFalse, + Collaborate: optional.Some(false), TopicOnly: topicOnly, Language: language, IncludeDescription: setting.UI.SearchRepoDescription, + Archived: archived, + Fork: fork, + Mirror: mirror, + Template: template, + IsPrivate: private, }) if err != nil { ctx.ServerError("SearchRepository", err) @@ -225,10 +246,15 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb OrderBy: orderBy, Private: ctx.IsSigned, WatchedByID: ctx.ContextUser.ID, - Collaborate: util.OptionalBoolFalse, + Collaborate: optional.Some(false), TopicOnly: topicOnly, Language: language, IncludeDescription: setting.UI.SearchRepoDescription, + Archived: archived, + Fork: fork, + Mirror: mirror, + Template: template, + IsPrivate: private, }) if err != nil { ctx.ServerError("SearchRepository", err) @@ -270,10 +296,15 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb OwnerID: ctx.ContextUser.ID, OrderBy: orderBy, Private: ctx.IsSigned, - Collaborate: util.OptionalBoolFalse, + Collaborate: optional.Some(false), TopicOnly: topicOnly, Language: language, IncludeDescription: setting.UI.SearchRepoDescription, + Archived: archived, + Fork: fork, + Mirror: mirror, + Template: template, + IsPrivate: private, }) if err != nil { ctx.ServerError("SearchRepository", err) @@ -308,7 +339,7 @@ func Action(ctx *context.Context) { var err error switch ctx.FormString("action") { case "follow": - err = user_model.FollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID) + err = user_model.FollowUser(ctx, ctx.Doer, ctx.ContextUser) case "unfollow": err = user_model.UnfollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID) } diff --git a/routers/web/user/search.go b/routers/web/user/search.go index 5ef61c88d4..fb7729bbe1 100644 --- a/routers/web/user/search.go +++ b/routers/web/user/search.go @@ -8,7 +8,6 @@ import ( "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" ) @@ -25,7 +24,7 @@ func Search(ctx *context.Context) { Keyword: ctx.FormTrim("q"), UID: ctx.FormInt64("uid"), Type: user_model.UserTypeIndividual, - IsActive: util.OptionalBoolFromGeneric(ctx.FormOptionalBool("active")), + IsActive: ctx.FormOptionalBool("active"), ListOptions: listOptions, }) if err != nil { diff --git a/routers/web/user/setting/block.go b/routers/web/user/setting/block.go new file mode 100644 index 0000000000..94fc380cee --- /dev/null +++ b/routers/web/user/setting/block.go @@ -0,0 +1,38 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "net/http" + + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/setting" + shared_user "code.gitea.io/gitea/routers/web/shared/user" + "code.gitea.io/gitea/services/context" +) + +const ( + tplSettingsBlockedUsers base.TplName = "user/settings/blocked_users" +) + +func BlockedUsers(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("user.block.list") + ctx.Data["PageIsSettingsBlockedUsers"] = true + + shared_user.BlockedUsers(ctx, ctx.Doer) + if ctx.Written() { + return + } + + ctx.HTML(http.StatusOK, tplSettingsBlockedUsers) +} + +func BlockedUsersPost(ctx *context.Context) { + shared_user.BlockedUsersPost(ctx, ctx.Doer) + if ctx.Written() { + return + } + + ctx.Redirect(setting.AppSubURL + "/user/settings/blocked_users") +} diff --git a/routers/web/user/setting/keys.go b/routers/web/user/setting/keys.go index 0a12777e5e..056fcc0ace 100644 --- a/routers/web/user/setting/keys.go +++ b/routers/web/user/setting/keys.go @@ -5,6 +5,7 @@ package setting import ( + "fmt" "net/http" asymkey_model "code.gitea.io/gitea/models/asymkey" @@ -61,7 +62,7 @@ func KeysPost(ctx *context.Context) { ctx.Redirect(setting.AppSubURL + "/user/settings/keys") return } - if _, err = asymkey_model.AddPrincipalKey(ctx, ctx.Doer.ID, content, 0); err != nil { + if _, err = asymkey_service.AddPrincipalKey(ctx, ctx.Doer.ID, content, 0); err != nil { ctx.Data["HasPrincipalError"] = true switch { case asymkey_model.IsErrKeyAlreadyExist(err), asymkey_model.IsErrKeyNameAlreadyUsed(err): @@ -77,6 +78,11 @@ func KeysPost(ctx *context.Context) { ctx.Flash.Success(ctx.Tr("settings.add_principal_success", form.Content)) ctx.Redirect(setting.AppSubURL + "/user/settings/keys") case "gpg": + if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageGPGKeys) { + ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited")) + return + } + token := asymkey_model.VerificationToken(ctx.Doer, 1) lastToken := asymkey_model.VerificationToken(ctx.Doer, 0) @@ -153,6 +159,11 @@ func KeysPost(ctx *context.Context) { ctx.Flash.Success(ctx.Tr("settings.verify_gpg_key_success", keyID)) ctx.Redirect(setting.AppSubURL + "/user/settings/keys") case "ssh": + if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageSSHKeys) { + ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited")) + return + } + content, err := asymkey_model.CheckPublicKeyString(form.Content) if err != nil { if db.IsErrSSHDisabled(err) { @@ -192,6 +203,11 @@ func KeysPost(ctx *context.Context) { ctx.Flash.Success(ctx.Tr("settings.add_key_success", form.Title)) ctx.Redirect(setting.AppSubURL + "/user/settings/keys") case "verify_ssh": + if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageSSHKeys) { + ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited")) + return + } + token := asymkey_model.VerificationToken(ctx.Doer, 1) lastToken := asymkey_model.VerificationToken(ctx.Doer, 0) @@ -224,12 +240,21 @@ func KeysPost(ctx *context.Context) { func DeleteKey(ctx *context.Context) { switch ctx.FormString("type") { case "gpg": + if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageGPGKeys) { + ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited")) + return + } if err := asymkey_model.DeleteGPGKey(ctx, ctx.Doer, ctx.FormInt64("id")); err != nil { ctx.Flash.Error("DeleteGPGKey: " + err.Error()) } else { ctx.Flash.Success(ctx.Tr("settings.gpg_key_deletion_success")) } case "ssh": + if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageSSHKeys) { + ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited")) + return + } + keyID := ctx.FormInt64("id") external, err := asymkey_model.PublicKeyIsExternallyManaged(ctx, keyID) if err != nil { @@ -308,4 +333,5 @@ func loadKeysData(ctx *context.Context) { ctx.Data["VerifyingID"] = ctx.FormString("verify_gpg") ctx.Data["VerifyingFingerprint"] = ctx.FormString("verify_ssh") + ctx.Data["UserDisabledFeatures"] = &setting.Admin.UserDisabledFeatures } diff --git a/routers/web/user/setting/security/security.go b/routers/web/user/setting/security/security.go index 30611dd9f1..8d6859ab87 100644 --- a/routers/web/user/setting/security/security.go +++ b/routers/web/user/setting/security/security.go @@ -12,8 +12,8 @@ import ( "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/auth/source/oauth2" "code.gitea.io/gitea/services/context" ) @@ -112,7 +112,7 @@ func loadSecurityData(ctx *context.Context) { ctx.Data["AccountLinks"] = sources authSources, err := db.Find[auth_model.Source](ctx, auth_model.FindSourcesOptions{ - IsActive: util.OptionalBoolNone, + IsActive: optional.None[bool](), LoginType: auth_model.OAuth2, }) if err != nil { diff --git a/routers/web/web.go b/routers/web/web.go index 3551ae2225..dd4e2ca7b0 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -647,6 +647,11 @@ func registerRoutes(m *web.Route) { }) addWebhookEditRoutes() }, webhooksEnabled) + + m.Group("/blocked_users", func() { + m.Get("", user_setting.BlockedUsers) + m.Post("", web.Bind(forms.BlockUserForm{}), user_setting.BlockedUsersPost) + }) }, reqSignIn, ctxDataSet("PageIsUserSettings", true, "AllThemes", setting.UI.Themes, "EnablePackages", setting.Packages.Enabled)) m.Group("/user", func() { @@ -946,6 +951,11 @@ func registerRoutes(m *web.Route) { m.Post("/rebuild", org.RebuildCargoIndex) }) }, packagesEnabled) + + m.Group("/blocked_users", func() { + m.Get("", org.BlockedUsers) + m.Post("", web.Bind(forms.BlockUserForm{}), org.BlockedUsersPost) + }) }, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled, "PageIsOrgSettings", true)) }, context.OrgAssignment(true, true)) }, reqSignIn) @@ -957,10 +967,6 @@ func registerRoutes(m *web.Route) { m.Post("/create", web.Bind(forms.CreateRepoForm{}), repo.CreatePost) m.Get("/migrate", repo.Migrate) m.Post("/migrate", web.Bind(forms.MigrateRepoForm{}), repo.MigratePost) - m.Group("/fork", func() { - m.Combo("/{repoid}").Get(repo.Fork). - Post(web.Bind(forms.CreateRepoForm{}), repo.ForkPost) - }, context.RepoIDAssignment(), context.UnitTypes(), reqRepoCodeReader) m.Get("/search", repo.SearchRepo) }, reqSignIn) @@ -1256,6 +1262,8 @@ func registerRoutes(m *web.Route) { m.Post("/delete", repo.DeleteBranchPost) m.Post("/restore", repo.RestoreBranchPost) }, context.RepoMustNotBeArchived(), reqRepoCodeWriter, repo.MustBeNotEmpty) + + m.Combo("/fork", reqRepoCodeReader).Get(repo.Fork).Post(web.Bind(forms.CreateRepoForm{}), repo.ForkPost) }, reqSignIn, context.RepoAssignment, context.UnitTypes()) // Tags @@ -1347,7 +1355,7 @@ func registerRoutes(m *web.Route) { }) }) }, reqRepoProjectsWriter, context.RepoMustNotBeArchived()) - }, reqRepoProjectsReader, repo.MustEnableProjects) + }, reqRepoProjectsReader, repo.MustEnableRepoProjects) m.Group("/actions", func() { m.Get("", actions.List) diff --git a/services/actions/notifier.go b/services/actions/notifier.go index 1e99c51a8b..aa88d4e0d8 100644 --- a/services/actions/notifier.go +++ b/services/actions/notifier.go @@ -171,14 +171,26 @@ func (n *actionsNotifier) IssueChangeMilestone(ctx context.Context, doer *user_m } else { action = api.HookIssueDemilestoned } - notifyIssueChange(ctx, doer, issue, webhook_module.HookEventPullRequestMilestone, action) + + hookEvent := webhook_module.HookEventIssueMilestone + if issue.IsPull { + hookEvent = webhook_module.HookEventPullRequestMilestone + } + + notifyIssueChange(ctx, doer, issue, hookEvent, action) } func (n *actionsNotifier) IssueChangeLabels(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, _, _ []*issues_model.Label, ) { ctx = withMethod(ctx, "IssueChangeLabels") - notifyIssueChange(ctx, doer, issue, webhook_module.HookEventPullRequestLabel, api.HookIssueLabelUpdated) + + hookEvent := webhook_module.HookEventIssueLabel + if issue.IsPull { + hookEvent = webhook_module.HookEventPullRequestLabel + } + + notifyIssueChange(ctx, doer, issue, hookEvent, api.HookIssueLabelUpdated) } func notifyIssueChange(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, event webhook_module.HookEventType, action api.HookIssueAction) { diff --git a/services/asymkey/deploy_key.go b/services/asymkey/deploy_key.go index e127cbfc6e..324688c534 100644 --- a/services/asymkey/deploy_key.go +++ b/services/asymkey/deploy_key.go @@ -7,7 +7,6 @@ import ( "context" "code.gitea.io/gitea/models" - asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" ) @@ -27,5 +26,5 @@ func DeleteDeployKey(ctx context.Context, doer *user_model.User, id int64) error return err } - return asymkey_model.RewriteAllPublicKeys(ctx) + return RewriteAllPublicKeys(ctx) } diff --git a/services/asymkey/ssh_key.go b/services/asymkey/ssh_key.go index 83d7edafa3..da57059d4b 100644 --- a/services/asymkey/ssh_key.go +++ b/services/asymkey/ssh_key.go @@ -43,8 +43,8 @@ func DeletePublicKey(ctx context.Context, doer *user_model.User, id int64) (err committer.Close() if key.Type == asymkey_model.KeyTypePrincipal { - return asymkey_model.RewriteAllPrincipalKeys(ctx) + return RewriteAllPrincipalKeys(ctx) } - return asymkey_model.RewriteAllPublicKeys(ctx) + return RewriteAllPublicKeys(ctx) } diff --git a/services/asymkey/ssh_key_authorized_keys.go b/services/asymkey/ssh_key_authorized_keys.go new file mode 100644 index 0000000000..5caa5bbfb6 --- /dev/null +++ b/services/asymkey/ssh_key_authorized_keys.go @@ -0,0 +1,79 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package asymkey + +import ( + "context" + "fmt" + "os" + "path/filepath" + "time" + + asymkey_model "code.gitea.io/gitea/models/asymkey" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" +) + +// RewriteAllPublicKeys removes any authorized key and rewrite all keys from database again. +// Note: db.GetEngine(ctx).Iterate does not get latest data after insert/delete, so we have to call this function +// outside any session scope independently. +func RewriteAllPublicKeys(ctx context.Context) error { + // Don't rewrite key if internal server + if setting.SSH.StartBuiltinServer || !setting.SSH.CreateAuthorizedKeysFile { + return nil + } + + return asymkey_model.WithSSHOpLocker(func() error { + return rewriteAllPublicKeys(ctx) + }) +} + +func rewriteAllPublicKeys(ctx context.Context) error { + if setting.SSH.RootPath != "" { + // First of ensure that the RootPath is present, and if not make it with 0700 permissions + // This of course doesn't guarantee that this is the right directory for authorized_keys + // but at least if it's supposed to be this directory and it doesn't exist and we're the + // right user it will at least be created properly. + err := os.MkdirAll(setting.SSH.RootPath, 0o700) + if err != nil { + log.Error("Unable to MkdirAll(%s): %v", setting.SSH.RootPath, err) + return err + } + } + + fPath := filepath.Join(setting.SSH.RootPath, "authorized_keys") + tmpPath := fPath + ".tmp" + t, err := os.OpenFile(tmpPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600) + if err != nil { + return err + } + defer func() { + t.Close() + if err := util.Remove(tmpPath); err != nil { + log.Warn("Unable to remove temporary authorized keys file: %s: Error: %v", tmpPath, err) + } + }() + + if setting.SSH.AuthorizedKeysBackup { + isExist, err := util.IsExist(fPath) + if err != nil { + log.Error("Unable to check if %s exists. Error: %v", fPath, err) + return err + } + if isExist { + bakPath := fmt.Sprintf("%s_%d.gitea_bak", fPath, time.Now().Unix()) + if err = util.CopyFile(fPath, bakPath); err != nil { + return err + } + } + } + + if err := asymkey_model.RegeneratePublicKeys(ctx, t); err != nil { + return err + } + + t.Close() + return util.Rename(tmpPath, fPath) +} diff --git a/models/asymkey/ssh_key_authorized_principals.go b/services/asymkey/ssh_key_authorized_principals.go similarity index 73% rename from models/asymkey/ssh_key_authorized_principals.go rename to services/asymkey/ssh_key_authorized_principals.go index 107d70c766..9154db7dbb 100644 --- a/models/asymkey/ssh_key_authorized_principals.go +++ b/services/asymkey/ssh_key_authorized_principals.go @@ -13,31 +13,22 @@ import ( "strings" "time" + asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" ) -// _____ __ .__ .__ .___ -// / _ \ __ ___/ |_| |__ ___________|__|_______ ____ __| _/ -// / /_\ \| | \ __\ | \ / _ \_ __ \ \___ // __ \ / __ | -// / | \ | /| | | Y ( <_> ) | \/ |/ /\ ___// /_/ | -// \____|__ /____/ |__| |___| /\____/|__| |__/_____ \\___ >____ | -// \/ \/ \/ \/ \/ -// __________ .__ .__ .__ -// \______ _______|__| ____ ____ |_____________ | | ______ -// | ___\_ __ | |/ \_/ ___\| \____ \__ \ | | / ___/ -// | | | | \| | | \ \___| | |_> / __ \| |__\___ \ -// |____| |__| |__|___| /\___ |__| __(____ |____/____ > -// \/ \/ |__| \/ \/ -// // This file contains functions for creating authorized_principals files // // There is a dependence on the database within RewriteAllPrincipalKeys & RegeneratePrincipalKeys // The sshOpLocker is used from ssh_key_authorized_keys.go -const authorizedPrincipalsFile = "authorized_principals" +const ( + authorizedPrincipalsFile = "authorized_principals" + tplCommentPrefix = `# gitea public key` +) // RewriteAllPrincipalKeys removes any authorized principal and rewrite all keys from database again. // Note: db.GetEngine(ctx).Iterate does not get latest data after insert/delete, so we have to call this function @@ -48,9 +39,12 @@ func RewriteAllPrincipalKeys(ctx context.Context) error { return nil } - sshOpLocker.Lock() - defer sshOpLocker.Unlock() + return asymkey_model.WithSSHOpLocker(func() error { + return rewriteAllPrincipalKeys(ctx) + }) +} +func rewriteAllPrincipalKeys(ctx context.Context) error { if setting.SSH.RootPath != "" { // First of ensure that the RootPath is present, and if not make it with 0700 permissions // This of course doesn't guarantee that this is the right directory for authorized_keys @@ -97,8 +91,8 @@ func RewriteAllPrincipalKeys(ctx context.Context) error { } func regeneratePrincipalKeys(ctx context.Context, t io.StringWriter) error { - if err := db.GetEngine(ctx).Where("type = ?", KeyTypePrincipal).Iterate(new(PublicKey), func(idx int, bean any) (err error) { - _, err = t.WriteString((bean.(*PublicKey)).AuthorizedString()) + if err := db.GetEngine(ctx).Where("type = ?", asymkey_model.KeyTypePrincipal).Iterate(new(asymkey_model.PublicKey), func(idx int, bean any) (err error) { + _, err = t.WriteString((bean.(*asymkey_model.PublicKey)).AuthorizedString()) return err }); err != nil { return err diff --git a/services/asymkey/ssh_key_principals.go b/services/asymkey/ssh_key_principals.go new file mode 100644 index 0000000000..5ed5cfa782 --- /dev/null +++ b/services/asymkey/ssh_key_principals.go @@ -0,0 +1,54 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package asymkey + +import ( + "context" + "fmt" + + asymkey_model "code.gitea.io/gitea/models/asymkey" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/perm" +) + +// AddPrincipalKey adds new principal to database and authorized_principals file. +func AddPrincipalKey(ctx context.Context, ownerID int64, content string, authSourceID int64) (*asymkey_model.PublicKey, error) { + dbCtx, committer, err := db.TxContext(ctx) + if err != nil { + return nil, err + } + defer committer.Close() + + // Principals cannot be duplicated. + has, err := db.GetEngine(dbCtx). + Where("content = ? AND type = ?", content, asymkey_model.KeyTypePrincipal). + Get(new(asymkey_model.PublicKey)) + if err != nil { + return nil, err + } else if has { + return nil, asymkey_model.ErrKeyAlreadyExist{ + Content: content, + } + } + + key := &asymkey_model.PublicKey{ + OwnerID: ownerID, + Name: content, + Content: content, + Mode: perm.AccessModeWrite, + Type: asymkey_model.KeyTypePrincipal, + LoginSourceID: authSourceID, + } + if err = db.Insert(dbCtx, key); err != nil { + return nil, fmt.Errorf("addKey: %w", err) + } + + if err = committer.Commit(); err != nil { + return nil, err + } + + committer.Close() + + return key, RewriteAllPrincipalKeys(ctx) +} diff --git a/services/attachment/attachment.go b/services/attachment/attachment.go index eab3d0b142..0fd51e4fa5 100644 --- a/services/attachment/attachment.go +++ b/services/attachment/attachment.go @@ -39,14 +39,14 @@ func NewAttachment(ctx context.Context, attach *repo_model.Attachment, file io.R } // UploadAttachment upload new attachment into storage and update database -func UploadAttachment(ctx context.Context, file io.Reader, allowedTypes string, fileSize int64, opts *repo_model.Attachment) (*repo_model.Attachment, error) { +func UploadAttachment(ctx context.Context, file io.Reader, allowedTypes string, fileSize int64, attach *repo_model.Attachment) (*repo_model.Attachment, error) { buf := make([]byte, 1024) n, _ := util.ReadAtMost(file, buf) buf = buf[:n] - if err := upload.Verify(buf, opts.Name, allowedTypes); err != nil { + if err := upload.Verify(buf, attach.Name, allowedTypes); err != nil { return nil, err } - return NewAttachment(ctx, opts, io.MultiReader(bytes.NewReader(buf), file), fileSize) + return NewAttachment(ctx, attach, io.MultiReader(bytes.NewReader(buf), file), fileSize) } diff --git a/services/auth/signin.go b/services/auth/signin.go index fafe3ef3c6..e116a088e0 100644 --- a/services/auth/signin.go +++ b/services/auth/signin.go @@ -11,7 +11,7 @@ import ( "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/services/auth/source/oauth2" "code.gitea.io/gitea/services/auth/source/smtp" @@ -87,7 +87,7 @@ func UserSignIn(ctx context.Context, username, password string) (*user_model.Use } sources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{ - IsActive: util.OptionalBoolTrue, + IsActive: optional.Some(true), }) if err != nil { return nil, nil, err diff --git a/services/auth/source/ldap/source_authenticate.go b/services/auth/source/ldap/source_authenticate.go index 68ecd16342..6ebd3ea50a 100644 --- a/services/auth/source/ldap/source_authenticate.go +++ b/services/auth/source/ldap/source_authenticate.go @@ -13,6 +13,7 @@ import ( user_model "code.gitea.io/gitea/models/user" auth_module "code.gitea.io/gitea/modules/auth" "code.gitea.io/gitea/modules/optional" + asymkey_service "code.gitea.io/gitea/services/asymkey" source_service "code.gitea.io/gitea/services/auth/source" user_service "code.gitea.io/gitea/services/user" ) @@ -68,7 +69,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u if user != nil { if isAttributeSSHPublicKeySet && asymkey_model.SynchronizePublicKeys(ctx, user, source.authSource, sr.SSHPublicKey) { - if err := asymkey_model.RewriteAllPublicKeys(ctx); err != nil { + if err := asymkey_service.RewriteAllPublicKeys(ctx); err != nil { return user, err } } @@ -94,7 +95,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u } if isAttributeSSHPublicKeySet && asymkey_model.AddPublicKeysBySource(ctx, user, source.authSource, sr.SSHPublicKey) { - if err := asymkey_model.RewriteAllPublicKeys(ctx); err != nil { + if err := asymkey_service.RewriteAllPublicKeys(ctx); err != nil { return user, err } } diff --git a/services/auth/source/ldap/source_sync.go b/services/auth/source/ldap/source_sync.go index 62f052d68c..0c9491cd09 100644 --- a/services/auth/source/ldap/source_sync.go +++ b/services/auth/source/ldap/source_sync.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" + asymkey_service "code.gitea.io/gitea/services/asymkey" source_service "code.gitea.io/gitea/services/auth/source" user_service "code.gitea.io/gitea/services/user" ) @@ -77,7 +78,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error { log.Warn("SyncExternalUsers: Cancelled at update of %s before completed update of users", source.authSource.Name) // Rewrite authorized_keys file if LDAP Public SSH Key attribute is set and any key was added or removed if sshKeysNeedUpdate { - err = asymkey_model.RewriteAllPublicKeys(ctx) + err = asymkey_service.RewriteAllPublicKeys(ctx) if err != nil { log.Error("RewriteAllPublicKeys: %v", err) } @@ -195,7 +196,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error { // Rewrite authorized_keys file if LDAP Public SSH Key attribute is set and any key was added or removed if sshKeysNeedUpdate { - err = asymkey_model.RewriteAllPublicKeys(ctx) + err = asymkey_service.RewriteAllPublicKeys(ctx) if err != nil { log.Error("RewriteAllPublicKeys: %v", err) } diff --git a/services/auth/source/oauth2/init.go b/services/auth/source/oauth2/init.go index 3ad6e307f1..5c25681548 100644 --- a/services/auth/source/oauth2/init.go +++ b/services/auth/source/oauth2/init.go @@ -12,8 +12,8 @@ import ( "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" "github.com/google/uuid" "github.com/gorilla/sessions" @@ -66,7 +66,7 @@ func ResetOAuth2(ctx context.Context) error { // initOAuth2Sources is used to load and register all active OAuth2 providers func initOAuth2Sources(ctx context.Context) error { authSources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{ - IsActive: util.OptionalBoolTrue, + IsActive: optional.Some(true), LoginType: auth.OAuth2, }) if err != nil { diff --git a/services/auth/source/oauth2/providers.go b/services/auth/source/oauth2/providers.go index f4edb507f2..ac32647839 100644 --- a/services/auth/source/oauth2/providers.go +++ b/services/auth/source/oauth2/providers.go @@ -15,8 +15,8 @@ import ( "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" "github.com/markbates/goth" ) @@ -107,7 +107,7 @@ func CreateProviderFromSource(source *auth.Source) (Provider, error) { } // GetOAuth2Providers returns the list of configured OAuth2 providers -func GetOAuth2Providers(ctx context.Context, isActive util.OptionalBool) ([]Provider, error) { +func GetOAuth2Providers(ctx context.Context, isActive optional.Option[bool]) ([]Provider, error) { authSources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{ IsActive: isActive, LoginType: auth.OAuth2, diff --git a/services/auth/source/source_group_sync.go b/services/auth/source/source_group_sync.go index 3a2411ec55..05293f202f 100644 --- a/services/auth/source/source_group_sync.go +++ b/services/auth/source/source_group_sync.go @@ -100,12 +100,12 @@ func syncGroupsToTeamsCached(ctx context.Context, user *user_model.User, orgTeam } if action == syncAdd && !isMember { - if err := models.AddTeamMember(ctx, team, user.ID); err != nil { + if err := models.AddTeamMember(ctx, team, user); err != nil { log.Error("group sync: Could not add user to team: %v", err) return err } } else if action == syncRemove && isMember { - if err := models.RemoveTeamMember(ctx, team, user.ID); err != nil { + if err := models.RemoveTeamMember(ctx, team, user); err != nil { log.Error("group sync: Could not remove user from team: %v", err) return err } diff --git a/services/auth/sspi.go b/services/auth/sspi.go index 9108a0a668..64a127e97a 100644 --- a/services/auth/sspi.go +++ b/services/auth/sspi.go @@ -17,7 +17,6 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/services/auth/source/sspi" gitea_context "code.gitea.io/gitea/services/context" @@ -132,7 +131,7 @@ func (s *SSPI) Verify(req *http.Request, w http.ResponseWriter, store DataStore, // getConfig retrieves the SSPI configuration from login sources func (s *SSPI) getConfig(ctx context.Context) (*sspi.Source, error) { sources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{ - IsActive: util.OptionalBoolTrue, + IsActive: optional.Some(true), LoginType: auth.SSPI, }) if err != nil { diff --git a/services/context/context_response.go b/services/context/context_response.go index 829bca1f59..372b4cb38b 100644 --- a/services/context/context_response.go +++ b/services/context/context_response.go @@ -6,6 +6,7 @@ package context import ( "errors" "fmt" + "html/template" "net" "net/http" "net/url" @@ -104,11 +105,11 @@ func (ctx *Context) JSONTemplate(tmpl base.TplName) { } } -// RenderToString renders the template content to a string -func (ctx *Context) RenderToString(name base.TplName, data map[string]any) (string, error) { +// RenderToHTML renders the template content to a HTML string +func (ctx *Context) RenderToHTML(name base.TplName, data map[string]any) (template.HTML, error) { var buf strings.Builder - err := ctx.Render.HTML(&buf, http.StatusOK, string(name), data, ctx.TemplateContext) - return buf.String(), err + err := ctx.Render.HTML(&buf, 0, string(name), data, ctx.TemplateContext) + return template.HTML(buf.String()), err } // RenderWithErr used for page has form validation but need to prompt error to users. diff --git a/services/context/repo.go b/services/context/repo.go index d6a68c0c1a..0b15c95e59 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -408,26 +408,6 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) { ctx.Data["IsEmptyRepo"] = ctx.Repo.Repository.IsEmpty } -// RepoIDAssignment returns a handler which assigns the repo to the context. -func RepoIDAssignment() func(ctx *Context) { - return func(ctx *Context) { - repoID := ctx.ParamsInt64(":repoid") - - // Get repository. - repo, err := repo_model.GetRepositoryByID(ctx, repoID) - if err != nil { - if repo_model.IsErrRepoNotExist(err) { - ctx.NotFound("GetRepositoryByID", nil) - } else { - ctx.ServerError("GetRepositoryByID", err) - } - return - } - - repoAssignment(ctx, repo) - } -} - // RepoAssignment returns a middleware to handle repository assignment func RepoAssignment(ctx *Context) context.CancelFunc { if _, repoAssignmentOnce := ctx.Data["repoAssignmentExecuted"]; repoAssignmentOnce { diff --git a/services/contexttest/context_tests.go b/services/contexttest/context_tests.go index 8a7dd69a0f..d3e6de7efe 100644 --- a/services/contexttest/context_tests.go +++ b/services/contexttest/context_tests.go @@ -7,11 +7,13 @@ package contexttest import ( gocontext "context" "io" + "maps" "net/http" "net/http/httptest" "net/url" "strings" "testing" + "time" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" @@ -35,7 +37,7 @@ func mockRequest(t *testing.T, reqPath string) *http.Request { } requestURL, err := url.Parse(path) assert.NoError(t, err) - req := &http.Request{Method: method, URL: requestURL, Form: url.Values{}} + req := &http.Request{Method: method, URL: requestURL, Form: maps.Clone(requestURL.Query()), Header: http.Header{}} req = req.WithContext(middleware.WithContextData(req.Context())) return req } @@ -61,7 +63,8 @@ func MockContext(t *testing.T, reqPath string, opts ...MockContextOption) (*cont base.Locale = &translation.MockLocale{} ctx := context.NewWebContext(base, opt.Render, nil) - + ctx.PageData = map[string]any{} + ctx.Data["PageStartTime"] = time.Now() chiCtx := chi.NewRouteContext() ctx.Base.AppendContextValue(chi.RouteCtxKey, chiCtx) return ctx, resp diff --git a/services/convert/package.go b/services/convert/package.go index e90ce8a00f..b5fca21a3c 100644 --- a/services/convert/package.go +++ b/services/convert/package.go @@ -35,7 +35,7 @@ func ToPackage(ctx context.Context, pd *packages.PackageDescriptor, doer *user_m Name: pd.Package.Name, Version: pd.Version.Version, CreatedAt: pd.Version.CreatedUnix.AsTime(), - HTMLURL: pd.FullWebLink(), + HTMLURL: pd.VersionHTMLURL(), }, nil } diff --git a/services/convert/repository.go b/services/convert/repository.go index 9184bc05c7..39efd304a9 100644 --- a/services/convert/repository.go +++ b/services/convert/repository.go @@ -113,8 +113,11 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR defaultAllowMaintainerEdit = config.DefaultAllowMaintainerEdit } hasProjects := false - if _, err := repo.GetUnit(ctx, unit_model.TypeProjects); err == nil { + projectsMode := repo_model.ProjectsModeAll + if unit, err := repo.GetUnit(ctx, unit_model.TypeProjects); err == nil { hasProjects = true + config := unit.ProjectsConfig() + projectsMode = config.ProjectsMode } hasReleases := false @@ -211,6 +214,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR InternalTracker: internalTracker, HasWiki: hasWiki, HasProjects: hasProjects, + ProjectsMode: string(projectsMode), HasReleases: hasReleases, HasPackages: hasPackages, HasActions: hasActions, diff --git a/services/cron/tasks_extended.go b/services/cron/tasks_extended.go index 1dd5d70a38..0018c5facc 100644 --- a/services/cron/tasks_extended.go +++ b/services/cron/tasks_extended.go @@ -8,13 +8,13 @@ import ( "time" activities_model "code.gitea.io/gitea/models/activities" - asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/system" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" issue_indexer "code.gitea.io/gitea/modules/indexer/issues" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/updatechecker" + asymkey_service "code.gitea.io/gitea/services/asymkey" repo_service "code.gitea.io/gitea/services/repository" archiver_service "code.gitea.io/gitea/services/repository/archiver" user_service "code.gitea.io/gitea/services/user" @@ -71,7 +71,7 @@ func registerRewriteAllPublicKeys() { RunAtStart: false, Schedule: "@every 72h", }, func(ctx context.Context, _ *user_model.User, _ Config) error { - return asymkey_model.RewriteAllPublicKeys(ctx) + return asymkey_service.RewriteAllPublicKeys(ctx) }) } @@ -81,7 +81,7 @@ func registerRewriteAllPrincipalKeys() { RunAtStart: false, Schedule: "@every 72h", }, func(ctx context.Context, _ *user_model.User, _ Config) error { - return asymkey_model.RewriteAllPrincipalKeys(ctx) + return asymkey_service.RewriteAllPrincipalKeys(ctx) }) } diff --git a/services/doctor/authorizedkeys.go b/services/doctor/authorizedkeys.go index 050a4e7974..d5a96605b9 100644 --- a/services/doctor/authorizedkeys.go +++ b/services/doctor/authorizedkeys.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + asymkey_service "code.gitea.io/gitea/services/asymkey" ) const tplCommentPrefix = `# gitea public key` @@ -33,7 +34,7 @@ func checkAuthorizedKeys(ctx context.Context, logger log.Logger, autofix bool) e return fmt.Errorf("Unable to open authorized_keys file. ERROR: %w", err) } logger.Warn("Unable to open authorized_keys. (ERROR: %v). Attempting to rewrite...", err) - if err = asymkey_model.RewriteAllPublicKeys(ctx); err != nil { + if err = asymkey_service.RewriteAllPublicKeys(ctx); err != nil { logger.Critical("Unable to rewrite authorized_keys file. ERROR: %v", err) return fmt.Errorf("Unable to rewrite authorized_keys file. ERROR: %w", err) } @@ -76,7 +77,7 @@ func checkAuthorizedKeys(ctx context.Context, logger log.Logger, autofix bool) e return fmt.Errorf(`authorized_keys is out of date and should be regenerated with "gitea admin regenerate keys" or "gitea doctor --run authorized-keys --fix"`) } logger.Warn("authorized_keys is out of date. Attempting rewrite...") - err = asymkey_model.RewriteAllPublicKeys(ctx) + err = asymkey_service.RewriteAllPublicKeys(ctx) if err != nil { logger.Critical("Unable to rewrite authorized_keys file. ERROR: %v", err) return fmt.Errorf("Unable to rewrite authorized_keys file. ERROR: %w", err) diff --git a/services/forms/admin.go b/services/forms/admin.go index f112013060..81276f8f46 100644 --- a/services/forms/admin.go +++ b/services/forms/admin.go @@ -41,6 +41,7 @@ type AdminEditUserForm struct { Password string `binding:"MaxSize(255)"` Website string `binding:"ValidUrl;MaxSize(255)"` Location string `binding:"MaxSize(50)"` + Language string `binding:"MaxSize(5)"` MaxRepoCreation int Active bool Admin bool diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 10b9f1fe99..345b55e2d9 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -150,6 +150,7 @@ type RepoSettingForm struct { ExternalTrackerRegexpPattern string EnableCloseIssuesViaCommitInAnyBranch bool EnableProjects bool + ProjectsMode string EnableReleases bool EnablePackages bool EnablePulls bool diff --git a/services/forms/user_form.go b/services/forms/user_form.go index 186aa4a878..416592bfda 100644 --- a/services/forms/user_form.go +++ b/services/forms/user_form.go @@ -449,3 +449,14 @@ func (f *PackageSettingForm) Validate(req *http.Request, errs binding.Errors) bi ctx := context.GetValidateContext(req) return middleware.Validate(errs, ctx.Data, f, ctx.Locale) } + +type BlockUserForm struct { + Action string `binding:"Required;In(block,unblock,note)"` + Blockee string `binding:"Required"` + Note string +} + +func (f *BlockUserForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { + ctx := context.GetValidateContext(req) + return middleware.Validate(errs, ctx.Data, f, ctx.Locale) +} diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index 740c748347..b05c210a0c 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -154,7 +154,7 @@ func (d *DiffLine) GetBlobExcerptQuery() string { // GetExpandDirection gets DiffLineExpandDirection func (d *DiffLine) GetExpandDirection() DiffLineExpandDirection { - if d.Type != DiffLineSection || d.SectionInfo == nil || d.SectionInfo.RightIdx-d.SectionInfo.LastRightIdx <= 1 { + if d.Type != DiffLineSection || d.SectionInfo == nil || d.SectionInfo.LeftIdx-d.SectionInfo.LastLeftIdx <= 1 || d.SectionInfo.RightIdx-d.SectionInfo.LastRightIdx <= 1 { return DiffLineExpandNone } if d.SectionInfo.LastLeftIdx <= 0 && d.SectionInfo.LastRightIdx <= 0 { diff --git a/services/issue/comments.go b/services/issue/comments.go index 8d8c575c14..d68623aff6 100644 --- a/services/issue/comments.go +++ b/services/issue/comments.go @@ -9,6 +9,7 @@ import ( "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" + access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/timeutil" @@ -21,6 +22,12 @@ func CreateRefComment(ctx context.Context, doer *user_model.User, repo *repo_mod return fmt.Errorf("cannot create reference with empty commit SHA") } + if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, repo.OwnerID) { + if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, repo, doer); !isAdmin { + return user_model.ErrBlockedUser + } + } + // Check if same reference from same commit has already existed. has, err := db.GetEngine(ctx).Get(&issues_model.Comment{ Type: issues_model.CommentTypeCommitRef, @@ -46,6 +53,12 @@ func CreateRefComment(ctx context.Context, doer *user_model.User, repo *repo_mod // CreateIssueComment creates a plain issue comment. func CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, content string, attachments []string) (*issues_model.Comment, error) { + if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, repo.OwnerID) { + if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, repo, doer); !isAdmin { + return nil, user_model.ErrBlockedUser + } + } + comment, err := issues_model.CreateComment(ctx, &issues_model.CreateCommentOptions{ Type: issues_model.CommentTypeComment, Doer: doer, @@ -70,6 +83,19 @@ func CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_m // UpdateComment updates information of comment. func UpdateComment(ctx context.Context, c *issues_model.Comment, doer *user_model.User, oldContent string) error { + if err := c.LoadIssue(ctx); err != nil { + return err + } + if err := c.Issue.LoadRepo(ctx); err != nil { + return err + } + + if user_model.IsUserBlockedBy(ctx, doer, c.Issue.PosterID, c.Issue.Repo.OwnerID) { + if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, c.Issue.Repo, doer); !isAdmin { + return user_model.ErrBlockedUser + } + } + needsContentHistory := c.Content != oldContent && c.Type.HasContentSupport() if needsContentHistory { hasContentHistory, err := issues_model.HasIssueContentHistory(ctx, c.IssueID, c.ID) diff --git a/services/issue/commit.go b/services/issue/commit.go index e493a03211..0a59088d12 100644 --- a/services/issue/commit.go +++ b/services/issue/commit.go @@ -5,6 +5,7 @@ package issue import ( "context" + "errors" "fmt" "html" "net/url" @@ -160,6 +161,9 @@ func UpdateIssuesCommit(ctx context.Context, doer *user_model.User, repo *repo_m message := fmt.Sprintf(`%s`, html.EscapeString(repo.Link()), html.EscapeString(url.PathEscape(c.Sha1)), html.EscapeString(strings.SplitN(c.Message, "\n", 2)[0])) if err = CreateRefComment(ctx, doer, refRepo, refIssue, message, c.Sha1); err != nil { + if errors.Is(err, user_model.ErrBlockedUser) { + continue + } return err } diff --git a/services/issue/content.go b/services/issue/content.go index 6e56714ddf..2f9bee806a 100644 --- a/services/issue/content.go +++ b/services/issue/content.go @@ -7,12 +7,23 @@ import ( "context" issues_model "code.gitea.io/gitea/models/issues" + access_model "code.gitea.io/gitea/models/perm/access" user_model "code.gitea.io/gitea/models/user" notify_service "code.gitea.io/gitea/services/notify" ) // ChangeContent changes issue content, as the given user. -func ChangeContent(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, content string) (err error) { +func ChangeContent(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, content string) error { + if err := issue.LoadRepo(ctx); err != nil { + return err + } + + if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, issue.Repo.OwnerID) { + if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, issue.Repo, doer); !isAdmin { + return user_model.ErrBlockedUser + } + } + oldContent := issue.Content if err := issues_model.ChangeIssueContent(ctx, issue, doer, content); err != nil { diff --git a/services/issue/issue.go b/services/issue/issue.go index b1f418c32e..0753813b64 100644 --- a/services/issue/issue.go +++ b/services/issue/issue.go @@ -15,21 +15,39 @@ import ( repo_model "code.gitea.io/gitea/models/repo" system_model "code.gitea.io/gitea/models/system" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/storage" notify_service "code.gitea.io/gitea/services/notify" ) // NewIssue creates new issue with labels for repository. -func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, assigneeIDs []int64) error { - if err := issues_model.NewIssue(ctx, repo, issue, labelIDs, uuids); err != nil { +func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, assigneeIDs []int64, projectID int64) error { + if err := issue.LoadPoster(ctx); err != nil { return err } - for _, assigneeID := range assigneeIDs { - if _, err := AddAssigneeIfNotAssigned(ctx, issue, issue.Poster, assigneeID, true); err != nil { + if user_model.IsUserBlockedBy(ctx, issue.Poster, repo.OwnerID) || user_model.IsUserBlockedBy(ctx, issue.Poster, assigneeIDs...) { + return user_model.ErrBlockedUser + } + + if err := db.WithTx(ctx, func(ctx context.Context) error { + if err := issues_model.NewIssue(ctx, repo, issue, labelIDs, uuids); err != nil { return err } + for _, assigneeID := range assigneeIDs { + if _, err := AddAssigneeIfNotAssigned(ctx, issue, issue.Poster, assigneeID, true); err != nil { + return err + } + } + if projectID > 0 { + if err := issues_model.ChangeProjectAssign(ctx, issue, issue.Poster, projectID); err != nil { + return err + } + } + return nil + }); err != nil { + return err } mentions, err := issues_model.FindAndUpdateIssueMentions(ctx, issue, issue.Poster, issue.Content) @@ -57,6 +75,16 @@ func ChangeTitle(ctx context.Context, issue *issues_model.Issue, doer *user_mode return nil } + if err := issue.LoadRepo(ctx); err != nil { + return err + } + + if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, issue.Repo.OwnerID) { + if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, issue.Repo, doer); !isAdmin { + return user_model.ErrBlockedUser + } + } + if err := issues_model.ChangeIssueTitle(ctx, issue, doer, oldTitle); err != nil { return err } @@ -93,31 +121,25 @@ func ChangeIssueRef(ctx context.Context, issue *issues_model.Issue, doer *user_m // Pass one or more user logins to replace the set of assignees on this Issue. // Send an empty array ([]) to clear all assignees from the Issue. func UpdateAssignees(ctx context.Context, issue *issues_model.Issue, oneAssignee string, multipleAssignees []string, doer *user_model.User) (err error) { - var allNewAssignees []*user_model.User + uniqueAssignees := container.SetOf(multipleAssignees...) // Keep the old assignee thingy for compatibility reasons if oneAssignee != "" { - // Prevent double adding assignees - var isDouble bool - for _, assignee := range multipleAssignees { - if assignee == oneAssignee { - isDouble = true - break - } - } - - if !isDouble { - multipleAssignees = append(multipleAssignees, oneAssignee) - } + uniqueAssignees.Add(oneAssignee) } // Loop through all assignees to add them - for _, assigneeName := range multipleAssignees { + allNewAssignees := make([]*user_model.User, 0, len(uniqueAssignees)) + for _, assigneeName := range uniqueAssignees.Values() { assignee, err := user_model.GetUserByName(ctx, assigneeName) if err != nil { return err } + if user_model.IsUserBlockedBy(ctx, doer, assignee.ID) { + return user_model.ErrBlockedUser + } + allNewAssignees = append(allNewAssignees, assignee) } diff --git a/services/issue/reaction.go b/services/issue/reaction.go new file mode 100644 index 0000000000..deb99169e1 --- /dev/null +++ b/services/issue/reaction.go @@ -0,0 +1,50 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package issue + +import ( + "context" + + issues_model "code.gitea.io/gitea/models/issues" + user_model "code.gitea.io/gitea/models/user" +) + +// CreateIssueReaction creates a reaction on an issue. +func CreateIssueReaction(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, content string) (*issues_model.Reaction, error) { + if err := issue.LoadRepo(ctx); err != nil { + return nil, err + } + + if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, issue.Repo.OwnerID) { + return nil, user_model.ErrBlockedUser + } + + return issues_model.CreateReaction(ctx, &issues_model.ReactionOptions{ + Type: content, + DoerID: doer.ID, + IssueID: issue.ID, + }) +} + +// CreateCommentReaction creates a reaction on a comment. +func CreateCommentReaction(ctx context.Context, doer *user_model.User, comment *issues_model.Comment, content string) (*issues_model.Reaction, error) { + if err := comment.LoadIssue(ctx); err != nil { + return nil, err + } + + if err := comment.Issue.LoadRepo(ctx); err != nil { + return nil, err + } + + if user_model.IsUserBlockedBy(ctx, doer, comment.Issue.PosterID, comment.Issue.Repo.OwnerID, comment.PosterID) { + return nil, user_model.ErrBlockedUser + } + + return issues_model.CreateReaction(ctx, &issues_model.ReactionOptions{ + Type: content, + DoerID: doer.ID, + IssueID: comment.Issue.ID, + CommentID: comment.ID, + }) +} diff --git a/models/issues/reaction_test.go b/services/issue/reaction_test.go similarity index 65% rename from models/issues/reaction_test.go rename to services/issue/reaction_test.go index 5dc8e1a5f3..7734860fc0 100644 --- a/models/issues/reaction_test.go +++ b/services/issue/reaction_test.go @@ -1,7 +1,7 @@ // Copyright 2017 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package issues_test +package issue import ( "testing" @@ -16,13 +16,13 @@ import ( "github.com/stretchr/testify/assert" ) -func addReaction(t *testing.T, doerID, issueID, commentID int64, content string) { +func addReaction(t *testing.T, doer *user_model.User, issue *issues_model.Issue, comment *issues_model.Comment, content string) { var reaction *issues_model.Reaction var err error - if commentID == 0 { - reaction, err = issues_model.CreateIssueReaction(db.DefaultContext, doerID, issueID, content) + if comment == nil { + reaction, err = CreateIssueReaction(db.DefaultContext, doer, issue, content) } else { - reaction, err = issues_model.CreateCommentReaction(db.DefaultContext, doerID, issueID, commentID, content) + reaction, err = CreateCommentReaction(db.DefaultContext, doer, comment, content) } assert.NoError(t, err) assert.NotNil(t, reaction) @@ -32,32 +32,26 @@ func TestIssueAddReaction(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) - var issue1ID int64 = 1 + addReaction(t, user1, issue, nil, "heart") - addReaction(t, user1.ID, issue1ID, 0, "heart") - - unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID}) + unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue.ID}) } func TestIssueAddDuplicateReaction(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) - var issue1ID int64 = 1 + addReaction(t, user1, issue, nil, "heart") - addReaction(t, user1.ID, issue1ID, 0, "heart") - - reaction, err := issues_model.CreateReaction(db.DefaultContext, &issues_model.ReactionOptions{ - DoerID: user1.ID, - IssueID: issue1ID, - Type: "heart", - }) + reaction, err := CreateIssueReaction(db.DefaultContext, user1, issue, "heart") assert.Error(t, err) assert.Equal(t, issues_model.ErrReactionAlreadyExist{Reaction: "heart"}, err) - existingR := unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID}) + existingR := unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue.ID}) assert.Equal(t, existingR.ID, reaction.ID) } @@ -65,15 +59,14 @@ func TestIssueDeleteReaction(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) - var issue1ID int64 = 1 + addReaction(t, user1, issue, nil, "heart") - addReaction(t, user1.ID, issue1ID, 0, "heart") - - err := issues_model.DeleteIssueReaction(db.DefaultContext, user1.ID, issue1ID, "heart") + err := issues_model.DeleteIssueReaction(db.DefaultContext, user1.ID, issue.ID, "heart") assert.NoError(t, err) - unittest.AssertNotExistsBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID}) + unittest.AssertNotExistsBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue.ID}) } func TestIssueReactionCount(t *testing.T) { @@ -87,19 +80,19 @@ func TestIssueReactionCount(t *testing.T) { user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) ghost := user_model.NewGhostUser() - var issueID int64 = 2 + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - addReaction(t, user1.ID, issueID, 0, "heart") - addReaction(t, user2.ID, issueID, 0, "heart") - addReaction(t, org3.ID, issueID, 0, "heart") - addReaction(t, org3.ID, issueID, 0, "+1") - addReaction(t, user4.ID, issueID, 0, "+1") - addReaction(t, user4.ID, issueID, 0, "heart") - addReaction(t, ghost.ID, issueID, 0, "-1") + addReaction(t, user1, issue, nil, "heart") + addReaction(t, user2, issue, nil, "heart") + addReaction(t, org3, issue, nil, "heart") + addReaction(t, org3, issue, nil, "+1") + addReaction(t, user4, issue, nil, "+1") + addReaction(t, user4, issue, nil, "heart") + addReaction(t, ghost, issue, nil, "-1") reactionsList, _, err := issues_model.FindReactions(db.DefaultContext, issues_model.FindReactionsOptions{ - IssueID: issueID, + IssueID: issue.ID, }) assert.NoError(t, err) assert.Len(t, reactionsList, 7) @@ -122,13 +115,11 @@ func TestIssueCommentAddReaction(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1}) - var issue1ID int64 = 1 - var comment1ID int64 = 1 + addReaction(t, user1, nil, comment, "heart") - addReaction(t, user1.ID, issue1ID, comment1ID, "heart") - - unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID, CommentID: comment1ID}) + unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: comment.IssueID, CommentID: comment.ID}) } func TestIssueCommentDeleteReaction(t *testing.T) { @@ -139,17 +130,16 @@ func TestIssueCommentDeleteReaction(t *testing.T) { org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) - var issue1ID int64 = 1 - var comment1ID int64 = 1 + comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1}) - addReaction(t, user1.ID, issue1ID, comment1ID, "heart") - addReaction(t, user2.ID, issue1ID, comment1ID, "heart") - addReaction(t, org3.ID, issue1ID, comment1ID, "heart") - addReaction(t, user4.ID, issue1ID, comment1ID, "+1") + addReaction(t, user1, nil, comment, "heart") + addReaction(t, user2, nil, comment, "heart") + addReaction(t, org3, nil, comment, "heart") + addReaction(t, user4, nil, comment, "+1") reactionsList, _, err := issues_model.FindReactions(db.DefaultContext, issues_model.FindReactionsOptions{ - IssueID: issue1ID, - CommentID: comment1ID, + IssueID: comment.IssueID, + CommentID: comment.ID, }) assert.NoError(t, err) assert.Len(t, reactionsList, 4) @@ -163,12 +153,10 @@ func TestIssueCommentReactionCount(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1}) - var issue1ID int64 = 1 - var comment1ID int64 = 1 + addReaction(t, user1, nil, comment, "heart") + assert.NoError(t, issues_model.DeleteCommentReaction(db.DefaultContext, user1.ID, comment.IssueID, comment.ID, "heart")) - addReaction(t, user1.ID, issue1ID, comment1ID, "heart") - assert.NoError(t, issues_model.DeleteCommentReaction(db.DefaultContext, user1.ID, issue1ID, comment1ID, "heart")) - - unittest.AssertNotExistsBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID, CommentID: comment1ID}) + unittest.AssertNotExistsBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: comment.IssueID, CommentID: comment.ID}) } diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go index 1c9824fe3a..87691bf729 100644 --- a/services/migrations/gitea_uploader.go +++ b/services/migrations/gitea_uploader.go @@ -120,7 +120,7 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate r.DefaultBranch = repo.DefaultBranch r.Description = repo.Description - r, err = repo_module.MigrateRepositoryGitData(g.ctx, owner, r, base.MigrateOptions{ + r, err = repo_service.MigrateRepositoryGitData(g.ctx, owner, r, base.MigrateOptions{ RepoName: g.repoName, Description: repo.Description, OriginalURL: repo.OriginalURL, diff --git a/services/migrations/gitea_uploader_test.go b/services/migrations/gitea_uploader_test.go index c8102c6b8b..c9b9248098 100644 --- a/services/migrations/gitea_uploader_test.go +++ b/services/migrations/gitea_uploader_test.go @@ -23,9 +23,9 @@ import ( "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" base "code.gitea.io/gitea/modules/migration" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/test" - "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" ) @@ -68,14 +68,14 @@ func TestGiteaUploadRepo(t *testing.T) { milestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{ RepoID: repo.ID, - IsClosed: util.OptionalBoolFalse, + IsClosed: optional.Some(false), }) assert.NoError(t, err) assert.Len(t, milestones, 1) milestones, err = db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{ RepoID: repo.ID, - IsClosed: util.OptionalBoolTrue, + IsClosed: optional.Some(true), }) assert.NoError(t, err) assert.Empty(t, milestones) @@ -108,7 +108,7 @@ func TestGiteaUploadRepo(t *testing.T) { issues, err := issues_model.Issues(db.DefaultContext, &issues_model.IssuesOptions{ RepoIDs: []int64{repo.ID}, - IsPull: util.OptionalBoolFalse, + IsPull: optional.Some(false), SortType: "oldest", }) assert.NoError(t, err) diff --git a/services/packages/cleanup/cleanup.go b/services/packages/cleanup/cleanup.go index 0ff8077bc9..5d5120c6a0 100644 --- a/services/packages/cleanup/cleanup.go +++ b/services/packages/cleanup/cleanup.go @@ -12,8 +12,8 @@ import ( packages_model "code.gitea.io/gitea/models/packages" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" packages_module "code.gitea.io/gitea/modules/packages" - "code.gitea.io/gitea/modules/util" packages_service "code.gitea.io/gitea/services/packages" alpine_service "code.gitea.io/gitea/services/packages/alpine" cargo_service "code.gitea.io/gitea/services/packages/cargo" @@ -60,7 +60,7 @@ func ExecuteCleanupRules(outerCtx context.Context) error { for _, p := range packages { pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ PackageID: p.ID, - IsInternal: util.OptionalBoolFalse, + IsInternal: optional.Some(false), Sort: packages_model.SortCreatedDesc, Paginator: db.NewAbsoluteListOptions(pcr.KeepCount, 200), }) diff --git a/services/packages/container/cleanup.go b/services/packages/container/cleanup.go index dd3f158dbf..3f5f43bbc0 100644 --- a/services/packages/container/cleanup.go +++ b/services/packages/container/cleanup.go @@ -9,8 +9,8 @@ import ( packages_model "code.gitea.io/gitea/models/packages" container_model "code.gitea.io/gitea/models/packages/container" + "code.gitea.io/gitea/modules/optional" container_module "code.gitea.io/gitea/modules/packages/container" - "code.gitea.io/gitea/modules/util" packages_service "code.gitea.io/gitea/services/packages" digest "github.com/opencontainers/go-digest" @@ -59,8 +59,8 @@ func cleanupExpiredUploadedBlobs(ctx context.Context, olderThan time.Duration) e ExactMatch: true, Value: container_model.UploadVersion, }, - IsInternal: util.OptionalBoolTrue, - HasFiles: util.OptionalBoolFalse, + IsInternal: optional.Some(true), + HasFiles: optional.Some(false), }) if err != nil { return err diff --git a/services/packages/packages.go b/services/packages/packages.go index 56d5cc04de..64b1ddd869 100644 --- a/services/packages/packages.go +++ b/services/packages/packages.go @@ -18,10 +18,10 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" packages_module "code.gitea.io/gitea/modules/packages" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" - "code.gitea.io/gitea/modules/util" notify_service "code.gitea.io/gitea/services/notify" ) @@ -330,7 +330,7 @@ func CheckCountQuotaExceeded(ctx context.Context, doer, owner *user_model.User) if setting.Packages.LimitTotalOwnerCount > -1 { totalCount, err := packages_model.CountVersions(ctx, &packages_model.PackageSearchOptions{ OwnerID: owner.ID, - IsInternal: util.OptionalBoolFalse, + IsInternal: optional.Some(false), }) if err != nil { log.Error("CountVersions failed: %v", err) @@ -640,7 +640,7 @@ func RemoveAllPackages(ctx context.Context, userID int64) (int, error) { Page: 1, }, OwnerID: userID, - IsInternal: util.OptionalBoolNone, + IsInternal: optional.None[bool](), }) if err != nil { return count, fmt.Errorf("GetOwnedPackages[%d]: %w", userID, err) diff --git a/services/pull/pull.go b/services/pull/pull.go index 42363f886d..be3d25d20a 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -40,6 +40,14 @@ var pullWorkingPool = sync.NewExclusivePool() // NewPullRequest creates new pull request with labels for repository. func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, pr *issues_model.PullRequest, assigneeIDs []int64) error { + if err := issue.LoadPoster(ctx); err != nil { + return err + } + + if user_model.IsUserBlockedBy(ctx, issue.Poster, repo.OwnerID) || user_model.IsUserBlockedBy(ctx, issue.Poster, assigneeIDs...) { + return user_model.ErrBlockedUser + } + prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr) if err != nil { if !git_model.IsErrBranchNotExist(err) { diff --git a/services/pull/review.go b/services/pull/review.go index 3ffc276778..90d07c8358 100644 --- a/services/pull/review.go +++ b/services/pull/review.go @@ -18,8 +18,8 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" notify_service "code.gitea.io/gitea/services/notify" ) @@ -56,7 +56,7 @@ func InvalidateCodeComments(ctx context.Context, prs issues_model.PullRequestLis ListAll: true, }, Type: issues_model.CommentTypeCode, - Invalidated: util.OptionalBoolFalse, + Invalidated: optional.Some(false), IssueIDs: issueIDs, }) if err != nil { @@ -327,7 +327,7 @@ func DismissApprovalReviews(ctx context.Context, doer *user_model.User, pull *is }, IssueID: pull.IssueID, Type: issues_model.ReviewTypeApprove, - Dismissed: util.OptionalBoolFalse, + Dismissed: optional.Some(false), }) if err != nil { return err @@ -394,7 +394,7 @@ func DismissReview(ctx context.Context, reviewID, repoID int64, message string, reviews, err := issues_model.FindReviews(ctx, issues_model.FindReviewOptions{ IssueID: review.IssueID, ReviewerID: review.ReviewerID, - Dismissed: util.OptionalBoolFalse, + Dismissed: optional.Some(false), }) if err != nil { return nil, err diff --git a/services/repository/branch.go b/services/repository/branch.go index ec41173da8..55cedf5d84 100644 --- a/services/repository/branch.go +++ b/services/repository/branch.go @@ -24,7 +24,6 @@ import ( "code.gitea.io/gitea/modules/queue" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/util" webhook_module "code.gitea.io/gitea/modules/webhook" notify_service "code.gitea.io/gitea/services/notify" files_service "code.gitea.io/gitea/services/repository/files" @@ -54,7 +53,7 @@ type Branch struct { } // LoadBranches loads branches from the repository limited by page & pageSize. -func LoadBranches(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, isDeletedBranch util.OptionalBool, keyword string, page, pageSize int) (*Branch, []*Branch, int64, error) { +func LoadBranches(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, isDeletedBranch optional.Option[bool], keyword string, page, pageSize int) (*Branch, []*Branch, int64, error) { defaultDBBranch, err := git_model.GetBranch(ctx, repo.ID, repo.DefaultBranch) if err != nil { return nil, nil, 0, err @@ -62,7 +61,7 @@ func LoadBranches(ctx context.Context, repo *repo_model.Repository, gitRepo *git branchOpts := git_model.FindBranchOptions{ RepoID: repo.ID, - IsDeletedBranch: isDeletedBranch.ToGeneric(), + IsDeletedBranch: isDeletedBranch, ListOptions: db.ListOptions{ Page: page, PageSize: pageSize, diff --git a/services/repository/collaboration.go b/services/repository/collaboration.go index dccc124748..4a43ae2a28 100644 --- a/services/repository/collaboration.go +++ b/services/repository/collaboration.go @@ -11,13 +11,14 @@ import ( "code.gitea.io/gitea/models/db" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" ) // DeleteCollaboration removes collaboration relation between the user and repository. -func DeleteCollaboration(ctx context.Context, repo *repo_model.Repository, uid int64) (err error) { +func DeleteCollaboration(ctx context.Context, repo *repo_model.Repository, collaborator *user_model.User) (err error) { collaboration := &repo_model.Collaboration{ RepoID: repo.ID, - UserID: uid, + UserID: collaborator.ID, } ctx, committer, err := db.TxContext(ctx) @@ -31,20 +32,25 @@ func DeleteCollaboration(ctx context.Context, repo *repo_model.Repository, uid i } else if has == 0 { return committer.Commit() } + + if err := repo.LoadOwner(ctx); err != nil { + return err + } + if err = access_model.RecalculateAccesses(ctx, repo); err != nil { return err } - if err = repo_model.WatchRepo(ctx, uid, repo.ID, false); err != nil { + if err = repo_model.WatchRepo(ctx, collaborator, repo, false); err != nil { return err } - if err = models.ReconsiderWatches(ctx, repo, uid); err != nil { + if err = models.ReconsiderWatches(ctx, repo, collaborator); err != nil { return err } // Unassign a user from any issue (s)he has been assigned to in the repository - if err := models.ReconsiderRepoIssuesAssignee(ctx, repo, uid); err != nil { + if err := models.ReconsiderRepoIssuesAssignee(ctx, repo, collaborator); err != nil { return err } diff --git a/services/repository/collaboration_test.go b/services/repository/collaboration_test.go index c3d006bfd8..a2eb06b81a 100644 --- a/services/repository/collaboration_test.go +++ b/services/repository/collaboration_test.go @@ -9,6 +9,7 @@ import ( "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" "github.com/stretchr/testify/assert" ) @@ -16,13 +17,15 @@ import ( func TestRepository_DeleteCollaboration(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) - assert.NoError(t, repo.LoadOwner(db.DefaultContext)) - assert.NoError(t, DeleteCollaboration(db.DefaultContext, repo, 4)) - unittest.AssertNotExistsBean(t, &repo_model.Collaboration{RepoID: repo.ID, UserID: 4}) - assert.NoError(t, DeleteCollaboration(db.DefaultContext, repo, 4)) - unittest.AssertNotExistsBean(t, &repo_model.Collaboration{RepoID: repo.ID, UserID: 4}) + assert.NoError(t, repo.LoadOwner(db.DefaultContext)) + assert.NoError(t, DeleteCollaboration(db.DefaultContext, repo, user)) + unittest.AssertNotExistsBean(t, &repo_model.Collaboration{RepoID: repo.ID, UserID: user.ID}) + + assert.NoError(t, DeleteCollaboration(db.DefaultContext, repo, user)) + unittest.AssertNotExistsBean(t, &repo_model.Collaboration{RepoID: repo.ID, UserID: user.ID}) unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID}) } diff --git a/services/repository/delete.go b/services/repository/delete.go index 08d6800ee7..8d6729f31b 100644 --- a/services/repository/delete.go +++ b/services/repository/delete.go @@ -27,6 +27,7 @@ import ( "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/storage" + asymkey_service "code.gitea.io/gitea/services/asymkey" "xorm.io/builder" ) @@ -277,7 +278,7 @@ func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID committer.Close() if needRewriteKeysFile { - if err := asymkey_model.RewriteAllPublicKeys(ctx); err != nil { + if err := asymkey_service.RewriteAllPublicKeys(ctx); err != nil { log.Error("RewriteAllPublicKeys failed: %v", err) } } @@ -365,24 +366,26 @@ func removeRepositoryFromTeam(ctx context.Context, t *organization.Team, repo *r } } - teamUsers, err := organization.GetTeamUsersByTeamID(ctx, t.ID) + teamMembers, err := organization.GetTeamMembers(ctx, &organization.SearchMembersOptions{ + TeamID: t.ID, + }) if err != nil { - return fmt.Errorf("getTeamUsersByTeamID: %w", err) + return fmt.Errorf("GetTeamMembers: %w", err) } - for _, teamUser := range teamUsers { - has, err := access_model.HasAccess(ctx, teamUser.UID, repo) + for _, member := range teamMembers { + has, err := access_model.HasAccess(ctx, member.ID, repo) if err != nil { return err } else if has { continue } - if err = repo_model.WatchRepo(ctx, teamUser.UID, repo.ID, false); err != nil { + if err = repo_model.WatchRepo(ctx, member, repo, false); err != nil { return err } // Remove all IssueWatches a user has subscribed to in the repositories - if err := issues_model.RemoveIssueWatchersByRepoID(ctx, teamUser.UID, repo.ID); err != nil { + if err := issues_model.RemoveIssueWatchersByRepoID(ctx, member.ID, repo.ID); err != nil { return err } } diff --git a/services/repository/fork.go b/services/repository/fork.go index f9c13a109e..f074fd1082 100644 --- a/services/repository/fork.go +++ b/services/repository/fork.go @@ -53,6 +53,14 @@ type ForkRepoOptions struct { // ForkRepository forks a repository func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts ForkRepoOptions) (*repo_model.Repository, error) { + if err := opts.BaseRepo.LoadOwner(ctx); err != nil { + return nil, err + } + + if user_model.IsUserBlockedBy(ctx, doer, opts.BaseRepo.Owner.ID) { + return nil, user_model.ErrBlockedUser + } + // Fork is prohibited, if user has reached maximum limit of repositories if !owner.CanForkRepo() { return nil, repo_model.ErrReachLimitOfRepo{ diff --git a/services/repository/migrate.go b/services/repository/migrate.go new file mode 100644 index 0000000000..51fdd90f54 --- /dev/null +++ b/services/repository/migrate.go @@ -0,0 +1,264 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repository + +import ( + "context" + "errors" + "fmt" + "net/http" + "strings" + "time" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/organization" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/lfs" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/migration" + repo_module "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" +) + +// MigrateRepositoryGitData starts migrating git related data after created migrating repository +func MigrateRepositoryGitData(ctx context.Context, u *user_model.User, + repo *repo_model.Repository, opts migration.MigrateOptions, + httpTransport *http.Transport, +) (*repo_model.Repository, error) { + repoPath := repo_model.RepoPath(u.Name, opts.RepoName) + + if u.IsOrganization() { + t, err := organization.OrgFromUser(u).GetOwnerTeam(ctx) + if err != nil { + return nil, err + } + repo.NumWatches = t.NumMembers + } else { + repo.NumWatches = 1 + } + + migrateTimeout := time.Duration(setting.Git.Timeout.Migrate) * time.Second + + var err error + if err = util.RemoveAll(repoPath); err != nil { + return repo, fmt.Errorf("Failed to remove %s: %w", repoPath, err) + } + + if err = git.Clone(ctx, opts.CloneAddr, repoPath, git.CloneRepoOptions{ + Mirror: true, + Quiet: true, + Timeout: migrateTimeout, + SkipTLSVerify: setting.Migrations.SkipTLSVerify, + }); err != nil { + if errors.Is(err, context.DeadlineExceeded) { + return repo, fmt.Errorf("Clone timed out. Consider increasing [git.timeout] MIGRATE in app.ini. Underlying Error: %w", err) + } + return repo, fmt.Errorf("Clone: %w", err) + } + + if err := git.WriteCommitGraph(ctx, repoPath); err != nil { + return repo, err + } + + if opts.Wiki { + wikiPath := repo_model.WikiPath(u.Name, opts.RepoName) + wikiRemotePath := repo_module.WikiRemoteURL(ctx, opts.CloneAddr) + if len(wikiRemotePath) > 0 { + if err := util.RemoveAll(wikiPath); err != nil { + return repo, fmt.Errorf("Failed to remove %s: %w", wikiPath, err) + } + + if err := git.Clone(ctx, wikiRemotePath, wikiPath, git.CloneRepoOptions{ + Mirror: true, + Quiet: true, + Timeout: migrateTimeout, + Branch: "master", + SkipTLSVerify: setting.Migrations.SkipTLSVerify, + }); err != nil { + log.Warn("Clone wiki: %v", err) + if err := util.RemoveAll(wikiPath); err != nil { + return repo, fmt.Errorf("Failed to remove %s: %w", wikiPath, err) + } + } else { + if err := git.WriteCommitGraph(ctx, wikiPath); err != nil { + return repo, err + } + } + } + } + + if repo.OwnerID == u.ID { + repo.Owner = u + } + + if err = repo_module.CheckDaemonExportOK(ctx, repo); err != nil { + return repo, fmt.Errorf("checkDaemonExportOK: %w", err) + } + + if stdout, _, err := git.NewCommand(ctx, "update-server-info"). + SetDescription(fmt.Sprintf("MigrateRepositoryGitData(git update-server-info): %s", repoPath)). + RunStdString(&git.RunOpts{Dir: repoPath}); err != nil { + log.Error("MigrateRepositoryGitData(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err) + return repo, fmt.Errorf("error in MigrateRepositoryGitData(git update-server-info): %w", err) + } + + gitRepo, err := git.OpenRepository(ctx, repoPath) + if err != nil { + return repo, fmt.Errorf("OpenRepository: %w", err) + } + defer gitRepo.Close() + + repo.IsEmpty, err = gitRepo.IsEmpty() + if err != nil { + return repo, fmt.Errorf("git.IsEmpty: %w", err) + } + + if !repo.IsEmpty { + if len(repo.DefaultBranch) == 0 { + // Try to get HEAD branch and set it as default branch. + headBranch, err := gitRepo.GetHEADBranch() + if err != nil { + return repo, fmt.Errorf("GetHEADBranch: %w", err) + } + if headBranch != nil { + repo.DefaultBranch = headBranch.Name + } + } + + if _, err := repo_module.SyncRepoBranchesWithRepo(ctx, repo, gitRepo, u.ID); err != nil { + return repo, fmt.Errorf("SyncRepoBranchesWithRepo: %v", err) + } + + if !opts.Releases { + // note: this will greatly improve release (tag) sync + // for pull-mirrors with many tags + repo.IsMirror = opts.Mirror + if err = repo_module.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil { + log.Error("Failed to synchronize tags to releases for repository: %v", err) + } + } + + if opts.LFS { + endpoint := lfs.DetermineEndpoint(opts.CloneAddr, opts.LFSEndpoint) + lfsClient := lfs.NewClient(endpoint, httpTransport) + if err = repo_module.StoreMissingLfsObjectsInRepository(ctx, repo, gitRepo, lfsClient); err != nil { + log.Error("Failed to store missing LFS objects for repository: %v", err) + } + } + } + + ctx, committer, err := db.TxContext(ctx) + if err != nil { + return nil, err + } + defer committer.Close() + + if opts.Mirror { + remoteAddress, err := util.SanitizeURL(opts.CloneAddr) + if err != nil { + return repo, err + } + mirrorModel := repo_model.Mirror{ + RepoID: repo.ID, + Interval: setting.Mirror.DefaultInterval, + EnablePrune: true, + NextUpdateUnix: timeutil.TimeStampNow().AddDuration(setting.Mirror.DefaultInterval), + LFS: opts.LFS, + RemoteAddress: remoteAddress, + } + if opts.LFS { + mirrorModel.LFSEndpoint = opts.LFSEndpoint + } + + if opts.MirrorInterval != "" { + parsedInterval, err := time.ParseDuration(opts.MirrorInterval) + if err != nil { + log.Error("Failed to set Interval: %v", err) + return repo, err + } + if parsedInterval == 0 { + mirrorModel.Interval = 0 + mirrorModel.NextUpdateUnix = 0 + } else if parsedInterval < setting.Mirror.MinInterval { + err := fmt.Errorf("interval %s is set below Minimum Interval of %s", parsedInterval, setting.Mirror.MinInterval) + log.Error("Interval: %s is too frequent", opts.MirrorInterval) + return repo, err + } else { + mirrorModel.Interval = parsedInterval + mirrorModel.NextUpdateUnix = timeutil.TimeStampNow().AddDuration(parsedInterval) + } + } + + if err = repo_model.InsertMirror(ctx, &mirrorModel); err != nil { + return repo, fmt.Errorf("InsertOne: %w", err) + } + + repo.IsMirror = true + if err = UpdateRepository(ctx, repo, false); err != nil { + return nil, err + } + + // this is necessary for sync local tags from remote + configName := fmt.Sprintf("remote.%s.fetch", mirrorModel.GetRemoteName()) + if stdout, _, err := git.NewCommand(ctx, "config"). + AddOptionValues("--add", configName, `+refs/tags/*:refs/tags/*`). + RunStdString(&git.RunOpts{Dir: repoPath}); err != nil { + log.Error("MigrateRepositoryGitData(git config --add +refs/tags/*:refs/tags/*) in %v: Stdout: %s\nError: %v", repo, stdout, err) + return repo, fmt.Errorf("error in MigrateRepositoryGitData(git config --add +refs/tags/*:refs/tags/*): %w", err) + } + } else { + if err = repo_module.UpdateRepoSize(ctx, repo); err != nil { + log.Error("Failed to update size for repository: %v", err) + } + if repo, err = CleanUpMigrateInfo(ctx, repo); err != nil { + return nil, err + } + } + + return repo, committer.Commit() +} + +// cleanUpMigrateGitConfig removes mirror info which prevents "push --all". +// This also removes possible user credentials. +func cleanUpMigrateGitConfig(ctx context.Context, repoPath string) error { + cmd := git.NewCommand(ctx, "remote", "rm", "origin") + // if the origin does not exist + _, stderr, err := cmd.RunStdString(&git.RunOpts{ + Dir: repoPath, + }) + if err != nil && !strings.HasPrefix(stderr, "fatal: No such remote") { + return err + } + return nil +} + +// CleanUpMigrateInfo finishes migrating repository and/or wiki with things that don't need to be done for mirrors. +func CleanUpMigrateInfo(ctx context.Context, repo *repo_model.Repository) (*repo_model.Repository, error) { + repoPath := repo.RepoPath() + if err := repo_module.CreateDelegateHooks(repoPath); err != nil { + return repo, fmt.Errorf("createDelegateHooks: %w", err) + } + if repo.HasWiki() { + if err := repo_module.CreateDelegateHooks(repo.WikiPath()); err != nil { + return repo, fmt.Errorf("createDelegateHooks.(wiki): %w", err) + } + } + + _, _, err := git.NewCommand(ctx, "remote", "rm", "origin").RunStdString(&git.RunOpts{Dir: repoPath}) + if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") { + return repo, fmt.Errorf("CleanUpMigrateInfo: %w", err) + } + + if repo.HasWiki() { + if err := cleanUpMigrateGitConfig(ctx, repo.WikiPath()); err != nil { + return repo, fmt.Errorf("cleanUpMigrateGitConfig (wiki): %w", err) + } + } + + return repo, UpdateRepository(ctx, repo, false) +} diff --git a/services/repository/transfer.go b/services/repository/transfer.go index 59a4eb260e..83d3032188 100644 --- a/services/repository/transfer.go +++ b/services/repository/transfer.go @@ -139,9 +139,9 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName } // Remove redundant collaborators. - collaborators, err := repo_model.GetCollaborators(ctx, repo.ID, db.ListOptions{}) + collaborators, _, err := repo_model.GetCollaborators(ctx, &repo_model.FindCollaborationOptions{RepoID: repo.ID}) if err != nil { - return fmt.Errorf("getCollaborators: %w", err) + return fmt.Errorf("GetCollaborators: %w", err) } // Dummy object. @@ -201,13 +201,13 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName return fmt.Errorf("decrease old owner repository count: %w", err) } - if err := repo_model.WatchRepo(ctx, doer.ID, repo.ID, true); err != nil { + if err := repo_model.WatchRepo(ctx, doer, repo, true); err != nil { return fmt.Errorf("watchRepo: %w", err) } // Remove watch for organization. if oldOwner.IsOrganization() { - if err := repo_model.WatchRepo(ctx, oldOwner.ID, repo.ID, false); err != nil { + if err := repo_model.WatchRepo(ctx, oldOwner, repo, false); err != nil { return fmt.Errorf("watchRepo [false]: %w", err) } } @@ -371,6 +371,10 @@ func StartRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.Use return TransferOwnership(ctx, doer, newOwner, repo, teams) } + if user_model.IsUserBlockedBy(ctx, doer, newOwner.ID) { + return user_model.ErrBlockedUser + } + // If new owner is an org and user can create repos he can transfer directly too if newOwner.IsOrganization() { allowed, err := organization.CanCreateOrgRepo(ctx, newOwner.ID, doer.ID) diff --git a/services/user/block.go b/services/user/block.go new file mode 100644 index 0000000000..0b3b618aae --- /dev/null +++ b/services/user/block.go @@ -0,0 +1,308 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import ( + "context" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" + org_model "code.gitea.io/gitea/models/organization" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + repo_service "code.gitea.io/gitea/services/repository" +) + +func CanBlockUser(ctx context.Context, doer, blocker, blockee *user_model.User) bool { + if blocker.ID == blockee.ID { + return false + } + if doer.ID == blockee.ID { + return false + } + + if blockee.IsOrganization() { + return false + } + + if user_model.IsUserBlockedBy(ctx, blockee, blocker.ID) { + return false + } + + if blocker.IsOrganization() { + org := org_model.OrgFromUser(blocker) + if isMember, _ := org.IsOrgMember(ctx, blockee.ID); isMember { + return false + } + if isAdmin, _ := org.IsOwnedBy(ctx, doer.ID); !isAdmin && !doer.IsAdmin { + return false + } + } else if !doer.IsAdmin && doer.ID != blocker.ID { + return false + } + + return true +} + +func CanUnblockUser(ctx context.Context, doer, blocker, blockee *user_model.User) bool { + if doer.ID == blockee.ID { + return false + } + + if !user_model.IsUserBlockedBy(ctx, blockee, blocker.ID) { + return false + } + + if blocker.IsOrganization() { + org := org_model.OrgFromUser(blocker) + if isAdmin, _ := org.IsOwnedBy(ctx, doer.ID); !isAdmin && !doer.IsAdmin { + return false + } + } else if !doer.IsAdmin && doer.ID != blocker.ID { + return false + } + + return true +} + +func BlockUser(ctx context.Context, doer, blocker, blockee *user_model.User, note string) error { + if blockee.IsOrganization() { + return user_model.ErrBlockOrganization + } + + if !CanBlockUser(ctx, doer, blocker, blockee) { + return user_model.ErrCanNotBlock + } + + return db.WithTx(ctx, func(ctx context.Context) error { + // unfollow each other + if err := user_model.UnfollowUser(ctx, blocker.ID, blockee.ID); err != nil { + return err + } + if err := user_model.UnfollowUser(ctx, blockee.ID, blocker.ID); err != nil { + return err + } + + // unstar each other + if err := unstarRepos(ctx, blocker, blockee); err != nil { + return err + } + if err := unstarRepos(ctx, blockee, blocker); err != nil { + return err + } + + // unwatch each others repositories + if err := unwatchRepos(ctx, blocker, blockee); err != nil { + return err + } + if err := unwatchRepos(ctx, blockee, blocker); err != nil { + return err + } + + // unassign each other from issues + if err := unassignIssues(ctx, blocker, blockee); err != nil { + return err + } + if err := unassignIssues(ctx, blockee, blocker); err != nil { + return err + } + + // remove each other from repository collaborations + if err := removeCollaborations(ctx, blocker, blockee); err != nil { + return err + } + if err := removeCollaborations(ctx, blockee, blocker); err != nil { + return err + } + + // cancel each other repository transfers + if err := cancelRepositoryTransfers(ctx, blocker, blockee); err != nil { + return err + } + if err := cancelRepositoryTransfers(ctx, blockee, blocker); err != nil { + return err + } + + return db.Insert(ctx, &user_model.Blocking{ + BlockerID: blocker.ID, + BlockeeID: blockee.ID, + Note: note, + }) + }) +} + +func unstarRepos(ctx context.Context, starrer, repoOwner *user_model.User) error { + opts := &repo_model.StarredReposOptions{ + ListOptions: db.ListOptions{ + Page: 1, + PageSize: 25, + }, + StarrerID: starrer.ID, + RepoOwnerID: repoOwner.ID, + } + + for { + repos, err := repo_model.GetStarredRepos(ctx, opts) + if err != nil { + return err + } + + if len(repos) == 0 { + return nil + } + + for _, repo := range repos { + if err := repo_model.StarRepo(ctx, starrer, repo, false); err != nil { + return err + } + } + + opts.Page++ + } +} + +func unwatchRepos(ctx context.Context, watcher, repoOwner *user_model.User) error { + opts := &repo_model.WatchedReposOptions{ + ListOptions: db.ListOptions{ + Page: 1, + PageSize: 25, + }, + WatcherID: watcher.ID, + RepoOwnerID: repoOwner.ID, + } + + for { + repos, _, err := repo_model.GetWatchedRepos(ctx, opts) + if err != nil { + return err + } + + if len(repos) == 0 { + return nil + } + + for _, repo := range repos { + if err := repo_model.WatchRepo(ctx, watcher, repo, false); err != nil { + return err + } + } + + opts.Page++ + } +} + +func cancelRepositoryTransfers(ctx context.Context, sender, recipient *user_model.User) error { + transfers, err := models.GetPendingRepositoryTransfers(ctx, &models.PendingRepositoryTransferOptions{ + SenderID: sender.ID, + RecipientID: recipient.ID, + }) + if err != nil { + return err + } + + for _, transfer := range transfers { + repo, err := repo_model.GetRepositoryByID(ctx, transfer.RepoID) + if err != nil { + return err + } + + if err := repo_service.CancelRepositoryTransfer(ctx, repo); err != nil { + return err + } + } + + return nil +} + +func unassignIssues(ctx context.Context, assignee, repoOwner *user_model.User) error { + opts := &issues_model.AssignedIssuesOptions{ + ListOptions: db.ListOptions{ + Page: 1, + PageSize: 25, + }, + AssigneeID: assignee.ID, + RepoOwnerID: repoOwner.ID, + } + + for { + issues, _, err := issues_model.GetAssignedIssues(ctx, opts) + if err != nil { + return err + } + + if len(issues) == 0 { + return nil + } + + for _, issue := range issues { + if err := issue.LoadAssignees(ctx); err != nil { + return err + } + + if _, _, err := issues_model.ToggleIssueAssignee(ctx, issue, assignee, assignee.ID); err != nil { + return err + } + } + + opts.Page++ + } +} + +func removeCollaborations(ctx context.Context, repoOwner, collaborator *user_model.User) error { + opts := &repo_model.FindCollaborationOptions{ + ListOptions: db.ListOptions{ + Page: 1, + PageSize: 25, + }, + CollaboratorID: collaborator.ID, + RepoOwnerID: repoOwner.ID, + } + + for { + collaborations, _, err := repo_model.GetCollaborators(ctx, opts) + if err != nil { + return err + } + + if len(collaborations) == 0 { + return nil + } + + for _, collaboration := range collaborations { + repo, err := repo_model.GetRepositoryByID(ctx, collaboration.Collaboration.RepoID) + if err != nil { + return err + } + + if err := repo_service.DeleteCollaboration(ctx, repo, collaborator); err != nil { + return err + } + } + + opts.Page++ + } +} + +func UnblockUser(ctx context.Context, doer, blocker, blockee *user_model.User) error { + if blockee.IsOrganization() { + return user_model.ErrBlockOrganization + } + + if !CanUnblockUser(ctx, doer, blocker, blockee) { + return user_model.ErrCanNotUnblock + } + + return db.WithTx(ctx, func(ctx context.Context) error { + block, err := user_model.GetBlocking(ctx, blocker.ID, blockee.ID) + if err != nil { + return err + } + if block != nil { + _, err = db.DeleteByID[user_model.Blocking](ctx, block.ID) + return err + } + return nil + }) +} diff --git a/services/user/block_test.go b/services/user/block_test.go new file mode 100644 index 0000000000..aec3e03cf3 --- /dev/null +++ b/services/user/block_test.go @@ -0,0 +1,66 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + + "github.com/stretchr/testify/assert" +) + +func TestCanBlockUser(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + user29 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29}) + org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) + + // Doer can't self block + assert.False(t, CanBlockUser(db.DefaultContext, user1, user2, user1)) + // Blocker can't be blockee + assert.False(t, CanBlockUser(db.DefaultContext, user1, user2, user2)) + // Can't block already blocked user + assert.False(t, CanBlockUser(db.DefaultContext, user1, user2, user29)) + // Blockee can't be an organization + assert.False(t, CanBlockUser(db.DefaultContext, user1, user2, org3)) + // Doer must be blocker or admin + assert.False(t, CanBlockUser(db.DefaultContext, user2, user4, user29)) + // Organization can't block a member + assert.False(t, CanBlockUser(db.DefaultContext, user1, org3, user4)) + // Doer must be organization owner or admin if blocker is an organization + assert.False(t, CanBlockUser(db.DefaultContext, user4, org3, user2)) + + assert.True(t, CanBlockUser(db.DefaultContext, user1, user2, user4)) + assert.True(t, CanBlockUser(db.DefaultContext, user2, user2, user4)) + assert.True(t, CanBlockUser(db.DefaultContext, user2, org3, user29)) +} + +func TestCanUnblockUser(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + user28 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 28}) + user29 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29}) + org17 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 17}) + + // Doer can't self unblock + assert.False(t, CanUnblockUser(db.DefaultContext, user1, user2, user1)) + // Can't unblock not blocked user + assert.False(t, CanUnblockUser(db.DefaultContext, user1, user2, user28)) + // Doer must be blocker or admin + assert.False(t, CanUnblockUser(db.DefaultContext, user28, user2, user29)) + // Doer must be organization owner or admin if blocker is an organization + assert.False(t, CanUnblockUser(db.DefaultContext, user2, org17, user28)) + + assert.True(t, CanUnblockUser(db.DefaultContext, user1, user2, user29)) + assert.True(t, CanUnblockUser(db.DefaultContext, user2, user2, user29)) + assert.True(t, CanUnblockUser(db.DefaultContext, user1, org17, user28)) +} diff --git a/services/user/delete.go b/services/user/delete.go index 000910319a..212cb83e03 100644 --- a/services/user/delete.go +++ b/services/user/delete.go @@ -92,6 +92,8 @@ func deleteUser(ctx context.Context, u *user_model.User, purge bool) (err error) &pull_model.ReviewState{UserID: u.ID}, &user_model.Redirect{RedirectUserID: u.ID}, &actions_model.ActionRunner{OwnerID: u.ID}, + &user_model.Blocking{BlockerID: u.ID}, + &user_model.Blocking{BlockeeID: u.ID}, ); err != nil { return fmt.Errorf("deleteBeans: %w", err) } diff --git a/services/user/user.go b/services/user/user.go index f2648db409..4fcb81581d 100644 --- a/services/user/user.go +++ b/services/user/user.go @@ -11,7 +11,6 @@ import ( "time" "code.gitea.io/gitea/models" - asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" packages_model "code.gitea.io/gitea/models/packages" @@ -24,6 +23,7 @@ import ( "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/agit" + asymkey_service "code.gitea.io/gitea/services/asymkey" org_service "code.gitea.io/gitea/services/org" "code.gitea.io/gitea/services/packages" container_service "code.gitea.io/gitea/services/packages/container" @@ -188,7 +188,7 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error { break } for _, org := range orgs { - if err := models.RemoveOrgUser(ctx, org.ID, u.ID); err != nil { + if err := models.RemoveOrgUser(ctx, org, u); err != nil { if organization.IsErrLastOrgOwner(err) { err = org_service.DeleteOrganization(ctx, org, true) if err != nil { @@ -252,10 +252,10 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error { } committer.Close() - if err = asymkey_model.RewriteAllPublicKeys(ctx); err != nil { + if err = asymkey_service.RewriteAllPublicKeys(ctx); err != nil { return err } - if err = asymkey_model.RewriteAllPrincipalKeys(ctx); err != nil { + if err = asymkey_service.RewriteAllPrincipalKeys(ctx); err != nil { return err } diff --git a/services/user/user_test.go b/services/user/user_test.go index 2ebcded925..f110bd26d0 100644 --- a/services/user/user_test.go +++ b/services/user/user_test.go @@ -41,7 +41,8 @@ func TestDeleteUser(t *testing.T) { orgUsers := make([]*organization.OrgUser, 0, 10) assert.NoError(t, db.GetEngine(db.DefaultContext).Find(&orgUsers, &organization.OrgUser{UID: userID})) for _, orgUser := range orgUsers { - if err := models.RemoveOrgUser(db.DefaultContext, orgUser.OrgID, orgUser.UID); err != nil { + org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: orgUser.OrgID}) + if err := models.RemoveOrgUser(db.DefaultContext, org, user); err != nil { assert.True(t, organization.IsErrLastOrgOwner(err)) return } diff --git a/services/webhook/webhook.go b/services/webhook/webhook.go index ac18da3525..35c760dc62 100644 --- a/services/webhook/webhook.go +++ b/services/webhook/webhook.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/queue" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" @@ -225,7 +226,7 @@ func PrepareWebhooks(ctx context.Context, source EventSource, event webhook_modu if source.Repository != nil { repoHooks, err := db.Find[webhook_model.Webhook](ctx, webhook_model.ListWebhookOptions{ RepoID: source.Repository.ID, - IsActive: util.OptionalBoolTrue, + IsActive: optional.Some(true), }) if err != nil { return fmt.Errorf("ListWebhooksByOpts: %w", err) @@ -239,7 +240,7 @@ func PrepareWebhooks(ctx context.Context, source EventSource, event webhook_modu if owner != nil { ownerHooks, err := db.Find[webhook_model.Webhook](ctx, webhook_model.ListWebhookOptions{ OwnerID: owner.ID, - IsActive: util.OptionalBoolTrue, + IsActive: optional.Some(true), }) if err != nil { return fmt.Errorf("ListWebhooksByOpts: %w", err) @@ -248,7 +249,7 @@ func PrepareWebhooks(ctx context.Context, source EventSource, event webhook_modu } // Add any admin-defined system webhooks - systemHooks, err := webhook_model.GetSystemWebhooks(ctx, util.OptionalBoolTrue) + systemHooks, err := webhook_model.GetSystemWebhooks(ctx, optional.Some(true)) if err != nil { return fmt.Errorf("GetSystemWebhooks: %w", err) } diff --git a/tailwind.config.js b/tailwind.config.js index 7f36822001..63a5387d19 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,17 +1,38 @@ import {readFileSync} from 'node:fs'; import {env} from 'node:process'; -import {parse} from 'css-variables-parser'; +import {parse} from 'postcss'; const isProduction = env.NODE_ENV !== 'development'; +function extractRootVars(css) { + const root = parse(css); + const vars = new Set(); + root.walkRules((rule) => { + if (rule.selector !== ':root') return; + rule.each((decl) => { + if (decl.value && decl.prop.startsWith('--')) { + vars.add(decl.prop.substring(2)); + } + }); + }); + return Array.from(vars); +} + +const vars = extractRootVars([ + readFileSync(new URL('web_src/css/themes/theme-gitea-light.css', import.meta.url), 'utf8'), + readFileSync(new URL('web_src/css/themes/theme-gitea-dark.css', import.meta.url), 'utf8'), +].join('\n')); + export default { prefix: 'tw-', important: true, // the frameworks are mixed together, so tailwind needs to override other framework's styles content: [ isProduction && '!./templates/devtest/**/*', isProduction && '!./web_src/js/standalone/devtest.js', + '!./templates/swagger/v1_json.tmpl', + '!./templates/user/auth/oidc_wellknown.tmpl', './templates/**/*.tmpl', - './web_src/**/*.{js,vue}', + './web_src/js/**/*.{js,vue}', ].filter(Boolean), blocklist: [ // classes that don't work without CSS variables from "@tailwind base" which we don't use @@ -23,15 +44,10 @@ export default { theme: { colors: { // make `tw-bg-red` etc work with our CSS variables - ...Object.fromEntries( - Object.keys(parse([ - readFileSync(new URL('web_src/css/themes/theme-gitea-light.css', import.meta.url), 'utf8'), - readFileSync(new URL('web_src/css/themes/theme-gitea-dark.css', import.meta.url), 'utf8'), - ].join('\n'), {})).filter((prop) => prop.startsWith('color-')).map((prop) => { - const color = prop.substring(6); - return [color, `var(--color-${color})`]; - }) - ), + ...Object.fromEntries(vars.filter((prop) => prop.startsWith('color-')).map((prop) => { + const color = prop.substring(6); + return [color, `var(--color-${color})`]; + })), inherit: 'inherit', current: 'currentcolor', transparent: 'transparent', diff --git a/templates/admin/emails/list.tmpl b/templates/admin/emails/list.tmpl index bcd80368e6..29fbb5f039 100644 --- a/templates/admin/emails/list.tmpl +++ b/templates/admin/emails/list.tmpl @@ -47,8 +47,8 @@ {{range .Emails}} {{.Name}} - {{.FullName}} - {{.Email}} + {{.FullName}} + {{.Email}} {{if .IsPrimary}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}} {{if .CanChange}} diff --git a/templates/admin/layout_head.tmpl b/templates/admin/layout_head.tmpl index 0067f336e0..b326c82a6c 100644 --- a/templates/admin/layout_head.tmpl +++ b/templates/admin/layout_head.tmpl @@ -3,7 +3,7 @@
{{template "base/alert" .ctxData}}
-
+
{{template "admin/navbar" .ctxData}}
{{/* block: admin-setting-content */}} diff --git a/templates/admin/notice.tmpl b/templates/admin/notice.tmpl index ed410425b5..e0abe4f8c0 100644 --- a/templates/admin/notice.tmpl +++ b/templates/admin/notice.tmpl @@ -31,7 +31,7 @@ -
+ {{.CsrfTokenHtml}}
diff --git a/templates/admin/packages/list.tmpl b/templates/admin/packages/list.tmpl index cf860dab2a..aef4815424 100644 --- a/templates/admin/packages/list.tmpl +++ b/templates/admin/packages/list.tmpl @@ -62,8 +62,8 @@ {{end}} {{.Package.Type.Name}} - {{.Package.Name}} - {{.Version.Version}} + {{.Package.Name}} + {{.Version.Version}} {{.Creator.Name}} {{if .Repository}} diff --git a/templates/admin/repo/list.tmpl b/templates/admin/repo/list.tmpl index 7dd14aa364..c40eb04a95 100644 --- a/templates/admin/repo/list.tmpl +++ b/templates/admin/repo/list.tmpl @@ -28,7 +28,7 @@
- {{template "admin/repo/search" .}} + {{template "shared/repo_search" .}}
diff --git a/templates/admin/repo/search.tmpl b/templates/admin/repo/search.tmpl deleted file mode 100644 index 247ec5491a..0000000000 --- a/templates/admin/repo/search.tmpl +++ /dev/null @@ -1,29 +0,0 @@ - diff --git a/templates/admin/user/edit.tmpl b/templates/admin/user/edit.tmpl index fcb8ce0827..159c821099 100644 --- a/templates/admin/user/edit.tmpl +++ b/templates/admin/user/edit.tmpl @@ -70,6 +70,21 @@

{{ctx.Locale.Tr "admin.users.password_helper"}}

+ +
+ + +
+
diff --git a/templates/admin/user/list.tmpl b/templates/admin/user/list.tmpl index 8fdc80fc70..e9ce17ac90 100644 --- a/templates/admin/user/list.tmpl +++ b/templates/admin/user/list.tmpl @@ -96,7 +96,7 @@ {{ctx.Locale.Tr "admin.users.remote"}} {{end}} -
+ diff --git a/templates/admin/user/view_details.tmpl b/templates/admin/user/view_details.tmpl index 21425eecb4..be2f32b5ec 100644 --- a/templates/admin/user/view_details.tmpl +++ b/templates/admin/user/view_details.tmpl @@ -48,6 +48,14 @@ {{svg "octicon-x"}} {{end}} + {{if .User.Language}} +
+ + {{ctx.Locale.Tr "settings.language"}}: + {{range .AllLangs}}{{if eq $.User.Language .Lang}}{{.Name}}{{end}}{{end}} + +
+ {{end}} {{if .User.Location}}
{{svg "octicon-location"}}{{.User.Location}} diff --git a/templates/base/alert.tmpl b/templates/base/alert.tmpl index 160584f769..760d3bfa2c 100644 --- a/templates/base/alert.tmpl +++ b/templates/base/alert.tmpl @@ -1,20 +1,20 @@ {{if .Flash.ErrorMsg}}
-

{{.Flash.ErrorMsg | Str2html}}

+

{{.Flash.ErrorMsg | SanitizeHTML}}

{{end}} {{if .Flash.SuccessMsg}}
-

{{.Flash.SuccessMsg | Str2html}}

+

{{.Flash.SuccessMsg | SanitizeHTML}}

{{end}} {{if .Flash.InfoMsg}}
-

{{.Flash.InfoMsg | Str2html}}

+

{{.Flash.InfoMsg | SanitizeHTML}}

{{end}} {{if .Flash.WarningMsg}}
-

{{.Flash.WarningMsg | Str2html}}

+

{{.Flash.WarningMsg | SanitizeHTML}}

{{end}} diff --git a/templates/base/alert_details.tmpl b/templates/base/alert_details.tmpl index 1d7ec15dc0..6801c8240f 100644 --- a/templates/base/alert_details.tmpl +++ b/templates/base/alert_details.tmpl @@ -2,6 +2,6 @@
{{.Summary}} - {{.Details | Str2html}} + {{.Details | SanitizeHTML}}
diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl index 3797de0a0f..4f48dc82c3 100644 --- a/templates/base/head_navbar.tmpl +++ b/templates/base/head_navbar.tmpl @@ -14,13 +14,13 @@
- + diff --git a/templates/package/shared/list.tmpl b/templates/package/shared/list.tmpl index 51e080f495..09205b19a5 100644 --- a/templates/package/shared/list.tmpl +++ b/templates/package/shared/list.tmpl @@ -20,7 +20,7 @@
- {{.Package.Name}} + {{.Package.Name}} {{svg .Package.Type.SVGName 16}} {{.Package.Type.Name}}
diff --git a/templates/package/shared/versionlist.tmpl b/templates/package/shared/versionlist.tmpl index eee952c096..59d6d89b53 100644 --- a/templates/package/shared/versionlist.tmpl +++ b/templates/package/shared/versionlist.tmpl @@ -23,7 +23,7 @@
- {{.Version.LowerVersion}} + {{.Version.LowerVersion}}
{{ctx.Locale.Tr "packages.published_by" (TimeSinceUnix .Version.CreatedUnix ctx.Locale) .Creator.HomeLink .Creator.GetDisplayName}}
diff --git a/templates/package/view.tmpl b/templates/package/view.tmpl index 0fa23d67fd..54af71126f 100644 --- a/templates/package/view.tmpl +++ b/templates/package/view.tmpl @@ -87,7 +87,7 @@ {{end}}
{{ctx.Locale.Tr "packages.versions"}} ({{.TotalVersionCount}}) - {{ctx.Locale.Tr "packages.versions.view_all"}} + {{ctx.Locale.Tr "packages.versions.view_all"}}
{{range .LatestVersions}}
diff --git a/templates/projects/list.tmpl b/templates/projects/list.tmpl index cbff82dd70..54a41221bf 100644 --- a/templates/projects/list.tmpl +++ b/templates/projects/list.tmpl @@ -10,7 +10,7 @@ {{ctx.Locale.PrettyNumber .ClosedCount}} {{ctx.Locale.Tr "repo.issues.closed_title"}}
- @@ -75,7 +75,7 @@
{{if .Description}}
- {{.RenderedContent|Str2html}} + {{.RenderedContent}}
{{end}} diff --git a/templates/projects/new.tmpl b/templates/projects/new.tmpl index 711dbe842a..92ee36c1c4 100644 --- a/templates/projects/new.tmpl +++ b/templates/projects/new.tmpl @@ -55,7 +55,7 @@
-
+
{{ctx.Locale.Tr "repo.milestones.cancel"}} diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index b3ad03c354..1d03477a9f 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -58,7 +58,7 @@ {{end}}
-
{{$.Project.RenderedContent|Str2html}}
+
{{$.Project.RenderedContent}}
@@ -165,9 +165,9 @@
-
+
{{range (index $.IssuesMap .ID)}} -
+
{{template "repo/issue/card" (dict "Issue" . "Page" $)}}
{{end}} diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl index 46503cb5df..916111faca 100644 --- a/templates/repo/branch/list.tmpl +++ b/templates/repo/branch/list.tmpl @@ -72,7 +72,7 @@
{{ctx.Locale.Tr "repo.branches"}}
-
+
{{.Email}}{{.Email}} {{if .IsActive}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}} {{if .IsRestricted}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}} {{if index $.UsersTwoFaStatus .ID}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
{{.Package.Type.Name}} {{.Package.Name}}{{.Version.Version}}{{.Version.Version}} {{.Creator.Name}} {{FileSize .CalculateBlobSize}} {{DateTime "short" .Version.CreatedUnix}}
{{if $isImage}} {{template "repo/diff/image_diff" dict "file" . "root" $ "blobBase" $blobBase "blobHead" $blobHead "sniffedTypeBase" $sniffedTypeBase "sniffedTypeHead" $sniffedTypeHead}} @@ -251,5 +251,6 @@ {{end}} {{if (not .DiffNotAvailable)}} {{template "repo/issue/view_content/reference_issue_dialog" .}} + {{template "shared/user/block_user_dialog" .}} {{end}} diff --git a/templates/repo/diff/comment_form.tmpl b/templates/repo/diff/comment_form.tmpl index 54817d4740..6005ea28ef 100644 --- a/templates/repo/diff/comment_form.tmpl +++ b/templates/repo/diff/comment_form.tmpl @@ -27,7 +27,7 @@ - diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl index 694aba6a03..1e00e6c6b4 100644 --- a/templates/repo/settings/options.tmpl +++ b/templates/repo/settings/options.tmpl @@ -90,7 +90,7 @@
{{if .Repository.IsArchived}} -
+
{{ctx.Locale.Tr "repo.settings.archive.mirrors_unavailable"}}
{{else}} @@ -456,13 +456,45 @@ {{$isProjectsEnabled := .Repository.UnitEnabled $.Context $.UnitTypeProjects}} {{$isProjectsGlobalDisabled := .UnitTypeProjects.UnitGlobalDisabled}} + {{$projectsUnit := .Repository.MustGetUnit $.Context $.UnitTypeProjects}}
- +
+
+

+ {{ctx.Locale.Tr "repo.settings.projects_mode_desc"}} +

+ +
+ +
{{$isReleasesEnabled := .Repository.UnitEnabled $.Context $.UnitTypeReleases}} {{$isReleasesGlobalDisabled := .UnitTypeReleases.UnitGlobalDisabled}} diff --git a/templates/repo/settings/tags.tmpl b/templates/repo/settings/tags.tmpl index e4fcf2ee6b..31fb59e5e3 100644 --- a/templates/repo/settings/tags.tmpl +++ b/templates/repo/settings/tags.tmpl @@ -1,7 +1,7 @@ {{template "repo/settings/layout_head" (dict "ctxData" . "pageClass" "repository settings edit")}}
{{if .Repository.IsArchived}} -
+
{{ctx.Locale.Tr "repo.settings.archive.tagsettings_unavailable"}}
{{else}} diff --git a/templates/repo/settings/webhook/base_list.tmpl b/templates/repo/settings/webhook/base_list.tmpl index 5a3fc0e7b8..e56929b70f 100644 --- a/templates/repo/settings/webhook/base_list.tmpl +++ b/templates/repo/settings/webhook/base_list.tmpl @@ -10,7 +10,7 @@
- {{.Description | Str2html}} + {{.Description}}
{{range .Webhooks}}
diff --git a/templates/repo/star_unstar.tmpl b/templates/repo/star_unstar.tmpl index 9c342f4065..1cdb98bf27 100644 --- a/templates/repo/star_unstar.tmpl +++ b/templates/repo/star_unstar.tmpl @@ -1,11 +1,10 @@ - +
- {{CountFmt .Repository.NumStars}} diff --git a/templates/repo/unicode_escape_prompt.tmpl b/templates/repo/unicode_escape_prompt.tmpl index c9f8cd38c1..8bceafa8bb 100644 --- a/templates/repo/unicode_escape_prompt.tmpl +++ b/templates/repo/unicode_escape_prompt.tmpl @@ -1,6 +1,6 @@ {{if .EscapeStatus}} {{if .EscapeStatus.HasInvisible}} -
+
{{ctx.Locale.Tr "repo.invisible_runes_header"}} @@ -11,7 +11,7 @@ {{end}}
{{else if .EscapeStatus.HasAmbiguous}} -
+
{{ctx.Locale.Tr "repo.ambiguous_runes_header"}} diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl index 1f9e0b5028..75f45b293f 100644 --- a/templates/repo/view_file.tmpl +++ b/templates/repo/view_file.tmpl @@ -1,12 +1,12 @@
{{- if .FileError}}
-
{{.FileError}}
+
{{.FileError}}
{{end}} {{- if .FileWarning}}
-
{{.FileWarning}}
+
{{.FileWarning}}
{{end}} diff --git a/templates/repo/view_list.tmpl b/templates/repo/view_list.tmpl index c1ef4ff4cb..988a5ddd50 100644 --- a/templates/repo/view_list.tmpl +++ b/templates/repo/view_list.tmpl @@ -1,7 +1,7 @@ -
{{/* */}}{{if and $.root.SignedUserID $.root.PageIsPullFiles}}{{/* - */}}{{/* */}}{{end}}{{/* @@ -62,7 +62,7 @@ {{if $match.RightIdx}}{{end}} {{/* */}}{{if and $.root.SignedUserID $.root.PageIsPullFiles}}{{/* - */}}{{/* */}}{{end}}{{/* @@ -79,7 +79,7 @@ {{if $line.LeftIdx}}{{end}} {{/* */}}{{if and $.root.SignedUserID $.root.PageIsPullFiles (not (eq .GetType 2))}}{{/* - */}}{{/* */}}{{end}}{{/* @@ -94,7 +94,7 @@ {{if $line.RightIdx}}{{end}} {{/* */}}{{if and $.root.SignedUserID $.root.PageIsPullFiles (not (eq .GetType 3))}}{{/* - */}}{{/* */}}{{end}}{{/* diff --git a/templates/repo/diff/section_unified.tmpl b/templates/repo/diff/section_unified.tmpl index 2c271d0866..2fc116a991 100644 --- a/templates/repo/diff/section_unified.tmpl +++ b/templates/repo/diff/section_unified.tmpl @@ -52,7 +52,7 @@ {{else}} {{/* */}}{{if and $.root.SignedUserID $.root.PageIsPullFiles}}{{/* - */}}{{/* */}}{{end}}{{/* diff --git a/templates/repo/diff/whitespace_dropdown.tmpl b/templates/repo/diff/whitespace_dropdown.tmpl index 7bf2ac9aec..cfabf836d6 100644 --- a/templates/repo/diff/whitespace_dropdown.tmpl +++ b/templates/repo/diff/whitespace_dropdown.tmpl @@ -2,26 +2,26 @@ {{svg "gitea-whitespace"}} {{if .InRepo}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}} {{if .Exists}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}} {{if .Accessible}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}} + {{ctx.Locale.Tr "repo.settings.lfs_findcommits"}}
+
- @@ -55,7 +55,7 @@ {{$commitLink := printf "%s/commit/%s" $.RepoLink (PathEscape $commit.ID.String)}} {{RenderCommitMessageLinkSubject $.Context $commit.Message $commitLink ($.Repository.ComposeMetas ctx)}} {{else}} -
+
{{end}} diff --git a/templates/repo/watch_unwatch.tmpl b/templates/repo/watch_unwatch.tmpl index c42bc5a9e7..2bf2c7bd21 100644 --- a/templates/repo/watch_unwatch.tmpl +++ b/templates/repo/watch_unwatch.tmpl @@ -1,11 +1,10 @@ - +
- {{CountFmt .Repository.NumWatches}} diff --git a/templates/repo/wiki/view.tmpl b/templates/repo/wiki/view.tmpl index 9fa05b5b5c..541b1e9b42 100644 --- a/templates/repo/wiki/view.tmpl +++ b/templates/repo/wiki/view.tmpl @@ -79,19 +79,19 @@ {{if .sidebarPresent}} {{end}} -
+
{{if .footerPresent}}
    + {{if .UserBlocking}} +
  • {{svg "octicon-circle-slash"}} {{ctx.Locale.Tr "user.block.blocked"}}
  • + {{if .UserBlocking.Note}} +
  • {{ctx.Locale.Tr "user.block.note"}}: {{.UserBlocking.Note}}
  • + {{end}} + {{end}} {{if .ContextUser.Location}}
  • {{svg "octicon-location"}} @@ -70,7 +76,7 @@ {{end}} {{if $.RenderedDescription}}
  • -
    {{$.RenderedDescription|Str2html}}
    +
    {{$.RenderedDescription}}
  • {{end}} {{range .OpenIDs}} @@ -109,18 +115,29 @@ {{end}} {{if and .IsSigned (ne .SignedUserID .ContextUser.ID)}} - {{end}} - +
  • + {{if not .UserBlocking}} + {{ctx.Locale.Tr "user.block.block.user"}} + {{else}} + {{ctx.Locale.Tr "user.block.unblock"}} + {{end}} +
  • {{end}}
+ +{{template "shared/user/block_user_dialog" .}} diff --git a/templates/status/500.tmpl b/templates/status/500.tmpl index d6cff28174..a821fe55da 100644 --- a/templates/status/500.tmpl +++ b/templates/status/500.tmpl @@ -1,5 +1,5 @@ {{/* This page should only depend the minimal template functions/variables, to avoid triggering new panics. -* base template functions: AppName, AssetUrlPrefix, AssetVersion, AppSubUrl, ThemeName, Str2html +* base template functions: AppName, AssetUrlPrefix, AssetVersion, AppSubUrl, ThemeName * ctx.Locale * .Flash * .ErrorMsg @@ -38,7 +38,7 @@
{{if .ErrorMsg}}

{{ctx.Locale.Tr "error.occurred"}}:

-
{{.ErrorMsg}}
+
{{.ErrorMsg}}
{{end}}
{{if or .SignedUser.IsAdmin .ShowFooterVersion}}

{{ctx.Locale.Tr "admin.config.app_ver"}}: {{AppVer}}

{{end}} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index f0205baa20..b154cba196 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -689,6 +689,109 @@ } } }, + "/admin/users/{username}/badges": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "List a user's badges", + "operationId": "adminListUserBadges", + "parameters": [ + { + "type": "string", + "description": "username of user", + "name": "username", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/BadgeList" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Add a badge to a user", + "operationId": "adminAddUserBadges", + "parameters": [ + { + "type": "string", + "description": "username of user", + "name": "username", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/UserBadgeOption" + } + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "403": { + "$ref": "#/responses/forbidden" + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Remove a badge from a user", + "operationId": "adminDeleteUserBadges", + "parameters": [ + { + "type": "string", + "description": "username of user", + "name": "username", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/UserBadgeOption" + } + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "422": { + "$ref": "#/responses/validationError" + } + } + } + }, "/admin/users/{username}/keys": { "post": { "consumes": [ @@ -1852,6 +1955,151 @@ } } }, + "/orgs/{org}/blocks": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "List users blocked by the organization", + "operationId": "organizationListBlocks", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/UserList" + } + } + } + }, + "/orgs/{org}/blocks/{username}": { + "get": { + "tags": [ + "organization" + ], + "summary": "Check if a user is blocked by the organization", + "operationId": "organizationCheckUserBlock", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "user to check", + "name": "username", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "put": { + "tags": [ + "organization" + ], + "summary": "Block a user", + "operationId": "organizationBlockUser", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "user to block", + "name": "username", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "optional note for the block", + "name": "note", + "in": "query" + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/validationError" + } + } + }, + "delete": { + "tags": [ + "organization" + ], + "summary": "Unblock a user", + "operationId": "organizationUnblockUser", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "user to unblock", + "name": "username", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/validationError" + } + } + } + }, "/orgs/{org}/hooks": { "get": { "produces": [ @@ -4237,6 +4485,9 @@ "204": { "$ref": "#/responses/empty" }, + "403": { + "$ref": "#/responses/forbidden" + }, "404": { "$ref": "#/responses/notFound" }, @@ -6589,6 +6840,9 @@ "400": { "$ref": "#/responses/error" }, + "403": { + "$ref": "#/responses/forbidden" + }, "404": { "$ref": "#/responses/error" }, @@ -10358,6 +10612,9 @@ "201": { "$ref": "#/responses/PullRequest" }, + "403": { + "$ref": "#/responses/forbidden" + }, "404": { "$ref": "#/responses/notFound" }, @@ -12240,7 +12497,8 @@ }, "post": { "consumes": [ - "multipart/form-data" + "multipart/form-data", + "application/octet-stream" ], "produces": [ "application/json" @@ -12283,8 +12541,7 @@ "type": "file", "description": "attachment to upload", "name": "attachment", - "in": "formData", - "required": true + "in": "formData" } ], "responses": { @@ -12856,6 +13113,9 @@ "200": { "$ref": "#/responses/WatchInfo" }, + "403": { + "$ref": "#/responses/forbidden" + }, "404": { "$ref": "#/responses/notFound" } @@ -14410,6 +14670,9 @@ "204": { "$ref": "#/responses/empty" }, + "403": { + "$ref": "#/responses/forbidden" + }, "404": { "$ref": "#/responses/notFound" } @@ -14978,6 +15241,123 @@ } } }, + "/user/blocks": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "List users blocked by the authenticated user", + "operationId": "userListBlocks", + "parameters": [ + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/UserList" + } + } + } + }, + "/user/blocks/{username}": { + "get": { + "tags": [ + "user" + ], + "summary": "Check if a user is blocked by the authenticated user", + "operationId": "userCheckUserBlock", + "parameters": [ + { + "type": "string", + "description": "user to check", + "name": "username", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "put": { + "tags": [ + "user" + ], + "summary": "Block a user", + "operationId": "userBlockUser", + "parameters": [ + { + "type": "string", + "description": "user to block", + "name": "username", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "optional note for the block", + "name": "note", + "in": "query" + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/validationError" + } + } + }, + "delete": { + "tags": [ + "user" + ], + "summary": "Unblock a user", + "operationId": "userUnblockUser", + "parameters": [ + { + "type": "string", + "description": "user to unblock", + "name": "username", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/validationError" + } + } + } + }, "/user/emails": { "get": { "produces": [ @@ -15155,6 +15535,9 @@ "204": { "$ref": "#/responses/empty" }, + "403": { + "$ref": "#/responses/forbidden" + }, "404": { "$ref": "#/responses/notFound" } @@ -15862,6 +16245,9 @@ "204": { "$ref": "#/responses/empty" }, + "403": { + "$ref": "#/responses/forbidden" + }, "404": { "$ref": "#/responses/notFound" } @@ -17003,6 +17389,45 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "Badge": { + "description": "Badge represents a user badge", + "type": "object", + "properties": { + "description": { + "type": "string", + "x-go-name": "Description" + }, + "id": { + "type": "integer", + "format": "int64", + "x-go-name": "ID" + }, + "image_url": { + "type": "string", + "x-go-name": "ImageURL" + }, + "slug": { + "type": "string", + "x-go-name": "Slug" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, + "BadgeList": { + "description": "BadgeList", + "type": "object", + "properties": { + "body": { + "description": "in:body", + "type": "array", + "items": { + "$ref": "#/definitions/Badge" + }, + "x-go-name": "Body" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "Branch": { "description": "Branch represents a repository branch", "type": "object", @@ -19434,6 +19859,11 @@ "type": "boolean", "x-go-name": "Private" }, + "projects_mode": { + "description": "`repo` to only allow repo-level projects, `owner` to only allow owner projects, `all` to allow both.", + "type": "string", + "x-go-name": "ProjectsMode" + }, "size_limit": { "description": "SizeLimit of the repository.", "type": "integer", @@ -20493,6 +20923,13 @@ "type": "object", "additionalProperties": {}, "x-go-name": "Validations" + }, + "visible": { + "type": "array", + "items": { + "$ref": "#/definitions/IssueFormFieldVisible" + }, + "x-go-name": "Visible" } }, "x-go-package": "code.gitea.io/gitea/modules/structs" @@ -20502,6 +20939,11 @@ "title": "IssueFormFieldType defines issue form field type, can be \"markdown\", \"textarea\", \"input\", \"dropdown\" or \"checkboxes\"", "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "IssueFormFieldVisible": { + "description": "IssueFormFieldVisible defines issue form field visible", + "type": "string", + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "IssueLabelsOption": { "description": "IssueLabelsOption a collection of labels", "type": "object", @@ -22349,6 +22791,10 @@ "type": "boolean", "x-go-name": "Private" }, + "projects_mode": { + "type": "string", + "x-go-name": "ProjectsMode" + }, "release_counter": { "type": "integer", "format": "int64", @@ -23059,6 +23505,24 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "UserBadgeOption": { + "description": "UserBadgeOption options for link between users and badges", + "type": "object", + "properties": { + "badge_slugs": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "BadgeSlugs", + "example": [ + "badge1", + "badge2" + ] + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "UserHeatmapData": { "description": "UserHeatmapData represents the data needed to create a heatmap", "type": "object", @@ -23348,6 +23812,15 @@ } } }, + "BadgeList": { + "description": "BadgeList", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Badge" + } + } + }, "Branch": { "description": "Branch", "schema": { @@ -24261,7 +24734,7 @@ "parameterBodies": { "description": "parameterBodies", "schema": { - "$ref": "#/definitions/CreateOrUpdateSecretOption" + "$ref": "#/definitions/BadgeList" } }, "redirect": { diff --git a/templates/user/auth/captcha.tmpl b/templates/user/auth/captcha.tmpl index 1c3379e629..d4d1a82418 100644 --- a/templates/user/auth/captcha.tmpl +++ b/templates/user/auth/captcha.tmpl @@ -24,7 +24,7 @@
{{else if eq .CaptchaType "cfturnstile"}} -
+
diff --git a/templates/user/dashboard/feeds.tmpl b/templates/user/dashboard/feeds.tmpl index 6dec610e93..0e7371ad83 100644 --- a/templates/user/dashboard/feeds.tmpl +++ b/templates/user/dashboard/feeds.tmpl @@ -7,7 +7,7 @@
{{if gt .ActUser.ID 0}} - {{.GetDisplayName ctx}} + {{.GetActDisplayName ctx}} {{else}} {{.ShortActUserName ctx}} {{end}} diff --git a/templates/user/dashboard/issues.tmpl b/templates/user/dashboard/issues.tmpl index 82622366e7..fd5960c31e 100644 --- a/templates/user/dashboard/issues.tmpl +++ b/templates/user/dashboard/issues.tmpl @@ -57,7 +57,7 @@ {{if .Content}}
- {{.RenderedContent|Str2html}} + {{.RenderedContent}}
{{end}} diff --git a/templates/user/notification/notification_div.tmpl b/templates/user/notification/notification_div.tmpl index d8f8d462d3..431aca0975 100644 --- a/templates/user/notification/notification_div.tmpl +++ b/templates/user/notification/notification_div.tmpl @@ -36,7 +36,7 @@ {{else}} {{range $notification := .Notifications}}
-
+
{{if .Issue}} {{template "shared/issueicon" .Issue}} {{else}} diff --git a/templates/user/notification/notification_subscriptions.tmpl b/templates/user/notification/notification_subscriptions.tmpl index ec40d3afea..a37f0c352e 100644 --- a/templates/user/notification/notification_subscriptions.tmpl +++ b/templates/user/notification/notification_subscriptions.tmpl @@ -69,7 +69,7 @@ {{template "shared/issuelist" dict "." . "listType" "dashboard"}} {{end}} {{else}} - {{template "explore/repo_search" .}} + {{template "shared/repo_search" .}} {{template "explore/repo_list" .}} {{template "base/paginate" .}} {{end}} diff --git a/templates/user/profile.tmpl b/templates/user/profile.tmpl index 426b5f042a..1495d58dd3 100644 --- a/templates/user/profile.tmpl +++ b/templates/user/profile.tmpl @@ -20,7 +20,7 @@ {{template "user/dashboard/feeds" .}} {{else if eq .TabName "stars"}}
- {{template "explore/repo_search" .}} + {{template "shared/repo_search" .}} {{template "explore/repo_list" .}} {{template "base/paginate" .}}
@@ -29,9 +29,9 @@ {{else if eq .TabName "followers"}} {{template "repo/user_cards" .}} {{else if eq .TabName "overview"}} -
{{.ProfileReadme | Str2html}}
+
{{.ProfileReadme}}
{{else}} - {{template "explore/repo_search" .}} + {{template "shared/repo_search" .}} {{template "explore/repo_list" .}} {{template "base/paginate" .}} {{end}} diff --git a/templates/user/settings/applications.tmpl b/templates/user/settings/applications.tmpl index 7ce9a4b70f..e43cf2ebbe 100644 --- a/templates/user/settings/applications.tmpl +++ b/templates/user/settings/applications.tmpl @@ -61,11 +61,11 @@
-
+ {{template "repo/latest_commit" .}} {{if .LatestCommit}}{{if .LatestCommit.Committer}}{{TimeSince .LatestCommit.Committer.When ctx.Locale}}{{end}}{{end}}