Merge pull request '[gitea] cherry-pick' (#2545) from earl-warren/forgejo:wip-gitea-cherry-pick into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/2545
Reviewed-by: Otto <otto@codeberg.org>
This commit is contained in:
Earl Warren 2024-03-06 08:59:04 +00:00
commit 025798d0f6
639 changed files with 5676 additions and 2833 deletions

View file

@ -162,9 +162,6 @@ package "code.gitea.io/gitea/modules/cache"
package "code.gitea.io/gitea/modules/charset" package "code.gitea.io/gitea/modules/charset"
func (*BreakWriter).Write func (*BreakWriter).Write
package "code.gitea.io/gitea/modules/context"
func GetPrivateContext
package "code.gitea.io/gitea/modules/emoji" package "code.gitea.io/gitea/modules/emoji"
func ReplaceCodes func ReplaceCodes
@ -296,7 +293,6 @@ package "code.gitea.io/gitea/modules/translation"
package "code.gitea.io/gitea/modules/util" package "code.gitea.io/gitea/modules/util"
func UnsafeStringToBytes func UnsafeStringToBytes
func OptionalBoolFromGeneric
package "code.gitea.io/gitea/modules/util/filebuffer" package "code.gitea.io/gitea/modules/util/filebuffer"
func CreateFromReader func CreateFromReader
@ -316,6 +312,9 @@ package "code.gitea.io/gitea/routers/web/org"
func getActionIssues func getActionIssues
func UpdateIssueProject func UpdateIssueProject
package "code.gitea.io/gitea/services/context"
func GetPrivateContext
package "code.gitea.io/gitea/services/convert" package "code.gitea.io/gitea/services/convert"
func ToSecret func ToSecret

2
.gitignore vendored
View file

@ -18,7 +18,7 @@ _test
# MS VSCode # MS VSCode
.vscode .vscode
__debug_bin __debug_bin*
*.cgo1.go *.cgo1.go
*.cgo2.c *.cgo2.c

View file

@ -64,6 +64,7 @@ rules:
"@stylistic/media-query-list-comma-newline-before": null "@stylistic/media-query-list-comma-newline-before": null
"@stylistic/media-query-list-comma-space-after": null "@stylistic/media-query-list-comma-space-after": null
"@stylistic/media-query-list-comma-space-before": null "@stylistic/media-query-list-comma-space-before": null
"@stylistic/named-grid-areas-alignment": null
"@stylistic/no-empty-first-line": null "@stylistic/no-empty-first-line": null
"@stylistic/no-eol-whitespace": true "@stylistic/no-eol-whitespace": true
"@stylistic/no-extra-semicolons": true "@stylistic/no-extra-semicolons": true

View file

@ -969,6 +969,12 @@ LEVEL = Info
;GO_GET_CLONE_URL_PROTOCOL = https ;GO_GET_CLONE_URL_PROTOCOL = https
;; ;;
;; Close issues as long as a commit on any branch marks it as fixed ;; 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. ;; 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 = ;DISABLED_REPO_UNITS =
;; ;;
@ -1490,10 +1496,11 @@ LEVEL = Info
;; ;;
;; Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled ;; Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled
;DEFAULT_EMAIL_NOTIFICATIONS = enabled ;DEFAULT_EMAIL_NOTIFICATIONS = enabled
;; Send an email to all admins when a new user signs up to inform the admins about this act. Options: true, false ;; Disabled features for users, could be "deletion","manage_gpg_keys" more features can be disabled in future
;SEND_NOTIFICATION_EMAIL_ON_NEW_USER = false ;SEND_NOTIFICATION_EMAIL_ON_NEW_USER = false
;; Disabled features for users, could be "deletion", more features can be disabled in future ;; Disabled features for users, could be "deletion", more features can be disabled in future
;; - deletion: a user cannot delete their own account ;; - deletion: a user cannot delete their own account
;; - manage_gpg_keys: a user cannot configure gpg keys
;USER_DISABLED_FEATURES = ;USER_DISABLED_FEATURES =
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -518,7 +518,9 @@ And the following unique queues:
- `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**: Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled - `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. - `DISABLE_REGULAR_ORG_CREATION`: **false**: Disallow regular (non-admin) users from creating organizations.
- `SEND_NOTIFICATION_EMAIL_ON_NEW_USER`: **false**: Send an email to all admins when a new user signs up to inform the admins about this act. - `USER_DISABLED_FEATURES`: **_empty_** Disabled features for users, could be `deletion`, `manage_gpg_keys` and more features can be added in future.
- `deletion`: User cannot delete their own account.
- `manage_gpg_keys`: User cannot configure gpg keys
## Security (`security`) ## Security (`security`)

View file

@ -497,6 +497,9 @@ Gitea 创建以下非唯一队列:
- `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**用户电子邮件通知的默认配置用户可配置。选项enabled、onmention、disabled - `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**用户电子邮件通知的默认配置用户可配置。选项enabled、onmention、disabled
- `DISABLE_REGULAR_ORG_CREATION`: **false**:禁止普通(非管理员)用户创建组织。 - `DISABLE_REGULAR_ORG_CREATION`: **false**:禁止普通(非管理员)用户创建组织。
- `USER_DISABLED_FEATURES`:**_empty_** 禁用的用户特性,当前允许为空或者 `deletion``manage_gpg_keys` 未来可以增加更多设置。
- `deletion`: 用户不能通过界面或者API删除他自己。
- `manage_gpg_keys`: 用户不能配置 GPG 密钥
## 安全性 (`security`) ## 安全性 (`security`)

View file

@ -222,9 +222,9 @@ Please check [Gitea's logs](administration/logging-config.md) for error messages
<a href="{{.Link}}">{{.Repo}}#{{.Issue.Index}}</a>. <a href="{{.Link}}">{{.Repo}}#{{.Issue.Index}}</a>.
</p> </p>
{{if not (eq .Body "")}} {{if not (eq .Body "")}}
<h3>Message content:</h3> <h3>Message content</h3>
<hr> <hr>
{{.Body | Str2html}} {{.Body | SanitizeHTML}}
{{end}} {{end}}
</p> </p>
<hr> <hr>
@ -245,7 +245,7 @@ This template produces something along these lines:
> [@rhonda](#) (Rhonda Myers) updated [mike/stuff#38](#). > [@rhonda](#) (Rhonda Myers) updated [mike/stuff#38](#).
> >
> #### Message content: > #### Message content
> >
> \_********************************\_******************************** > \_********************************\_********************************
> >
@ -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 template system contains several functions that can be used to further process and format
the messages. Here's a list of some of them: the messages. Here's a list of some of them:
| Name | Parameters | Available | Usage | | Name | Parameters | Available | Usage |
| ---------------- | ----------- | --------- | --------------------------------------------------------------------------- | | ---------------- | ----------- | --------- | ------------------------------------------------------------------- |
| `AppUrl` | - | Any | Gitea's URL | | `AppUrl` | - | Any | Gitea's URL |
| `AppName` | - | Any | Set from `app.ini`, usually "Gitea" | | `AppName` | - | Any | Set from `app.ini`, usually "Gitea" |
| `AppDomain` | - | Any | Gitea's host name | | `AppDomain` | - | Any | Gitea's host name |
| `EllipsisString` | string, int | Any | Truncates a string to the specified length; adds ellipsis as needed | | `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. | | `SanitizeHTML` | string | Body only | Sanitizes text by removing any dangerous HTML tags from it |
| `Safe` | string | Body only | Takes the input as HTML; can be used for `.ReviewComments.RenderedContent`. | | `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: These are _functions_, not metadata, so they have to be used:
```html ```html
Like this: {{Str2html "Escape<my>text"}} Like this: {{SanitizeHTML "Escape<my>text"}}
Or this: {{"Escape<my>text" | Str2html}} Or this: {{"Escape<my>text" | SanitizeHTML}}
Or this: {{AppUrl}} Or this: {{AppUrl}}
But not like this: {{.AppUrl}} But not like this: {{.AppUrl}}
``` ```

View file

@ -207,7 +207,7 @@ _主题_ 和 _邮件正文_ 由 [Golang的模板引擎](https://go.dev/pkg/text/
{{if not (eq .Body "")}} {{if not (eq .Body "")}}
<h3>消息内容:</h3> <h3>消息内容:</h3>
<hr> <hr>
{{.Body | Str2html}} {{.Body | SanitizeHTML}}
{{end}} {{end}}
</p> </p>
<hr> <hr>
@ -228,7 +228,7 @@ _主题_ 和 _邮件正文_ 由 [Golang的模板引擎](https://go.dev/pkg/text/
> [@rhonda](#)Rhonda Myers更新了 [mike/stuff#38](#)。 > [@rhonda](#)Rhonda Myers更新了 [mike/stuff#38](#)。
> >
> #### 消息内容 > #### 消息内容
> >
> \_********************************\_******************************** > \_********************************\_********************************
> >
@ -242,20 +242,20 @@ _主题_ 和 _邮件正文_ 由 [Golang的模板引擎](https://go.dev/pkg/text/
模板系统包含一些函数,可用于进一步处理和格式化消息。以下是其中一些函数的列表: 模板系统包含一些函数,可用于进一步处理和格式化消息。以下是其中一些函数的列表:
| 函数名 | 参数 | 可用于 | 用法 | | 函数名 | 参数 | 可用于 | 用法 |
| ----------------- | ----------- | ------------ | --------------------------------------------------------------------------------- | |------------------| ----------- | ------------ | ------------------------------ |
| `AppUrl` | - | 任何地方 | Gitea 的 URL | | `AppUrl` | - | 任何地方 | Gitea 的 URL |
| `AppName` | - | 任何地方 | 从 `app.ini` 中设置,通常为 "Gitea" | | `AppName` | - | 任何地方 | 从 `app.ini` 中设置,通常为 "Gitea" |
| `AppDomain` | - | 任何地方 | Gitea 的主机名 | | `AppDomain` | - | 任何地方 | Gitea 的主机名 |
| `EllipsisString` | string, int | 任何地方 | 将字符串截断为指定长度;根据需要添加省略号 | | `EllipsisString` | string, int | 任何地方 | 将字符串截断为指定长度;根据需要添加省略号 |
| `Str2html` | string | 仅正文部分 | 通过删除其中的 HTML 标签对文本进行清理 | | `SanitizeHTML` | string | 仅正文部分 | 通过删除其中的危险 HTML 标签对文本进行清理 |
| `Safe` | string | 仅正文部分 | 将输入作为 HTML 处理;可用于 `.ReviewComments.RenderedContent` 等字段 | | `SafeHTML` | string | 仅正文部分 | 将输入作为 HTML 处理;可用于输出原始的 HTML 内容 |
这些都是 _函数_,而不是元数据,因此必须按以下方式使用: 这些都是 _函数_,而不是元数据,因此必须按以下方式使用:
```html ```html
像这样使用: {{Str2html "Escape<my>text"}} 像这样使用: {{SanitizeHTML "Escape<my>text"}}
或者这样使用: {{"Escape<my>text" | Str2html}} 或者这样使用: {{"Escape<my>text" | SanitizeHTML}}
或者这样使用: {{AppUrl}} 或者这样使用: {{AppUrl}}
但不要像这样使用: {{.AppUrl}} 但不要像这样使用: {{.AppUrl}}
``` ```

View file

@ -135,6 +135,12 @@ body:
attributes: attributes:
value: | value: |
Thanks for taking the time to fill out this bug report! 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 - type: input
id: contact id: contact
attributes: attributes:
@ -186,11 +192,16 @@ body:
options: options:
- label: I agree to follow this project's Code of Conduct - label: I agree to follow this project's Code of Conduct
required: true 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 ### 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: Attributes:
@ -198,6 +209,8 @@ Attributes:
|-------|--------------------------------------------------------------|----------|--------|---------|--------------| |-------|--------------------------------------------------------------|----------|--------|---------|--------------|
| value | The text that is rendered. Markdown formatting is supported. | Required | String | - | - | | value | The text that is rendered. Markdown formatting is supported. | Required | String | - | - |
visible: Default is **[form]**
### Textarea ### 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. You can use a `textarea` element to add a multi-line text field to your form. Contributors can also attach files in `textarea` fields.
@ -218,6 +231,8 @@ Validations:
|----------|------------------------------------------------------|----------|---------|---------|--------------| |----------|------------------------------------------------------|----------|---------|---------|--------------|
| required | Prevents form submission until element is completed. | Optional | Boolean | false | - | | required | Prevents form submission until element is completed. | Optional | Boolean | false | - |
visible: Default is **[form, content]**
### Input ### Input
You can use an `input` element to add a single-line text field to your form. You can use an `input` element to add a single-line text field to your form.
@ -239,6 +254,8 @@ Validations:
| is_number | Prevents form submission until element is filled with a number. | Optional | Boolean | false | - | | 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) | | 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 ### Dropdown
You can use a `dropdown` element to add a dropdown menu in your form. You can use a `dropdown` element to add a dropdown menu in your form.
@ -258,6 +275,8 @@ Validations:
|----------|------------------------------------------------------|----------|---------|---------|--------------| |----------|------------------------------------------------------|----------|---------|---------|--------------|
| required | Prevents form submission until element is completed. | Optional | Boolean | false | - | | required | Prevents form submission until element is completed. | Optional | Boolean | false | - |
visible: Default is **[form, content]**
### Checkboxes ### Checkboxes
You can use the `checkboxes` element to add a set of checkboxes to your form. You can use the `checkboxes` element to add a set of checkboxes to your form.
@ -265,17 +284,20 @@ You can use the `checkboxes` element to add a set of checkboxes to your form.
Attributes: Attributes:
| Key | Description | Required | Type | Default | Valid values | | Key | Description | Required | Type | Default | Valid values |
|-------------|-------------------------------------------------------------------------------------------------------|----------|--------|--------------|--------------| | ----------- | ----------------------------------------------------------------------------------------------------- | -------- | ------ | ------------ | ------------ |
| label | A brief description of the expected user input, which is displayed in the form. | Required | String | - | - | | 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 | - | | 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 | - | - | | 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. For each value in the options array, you can set the following keys.
| Key | Description | Required | Type | Default | Options | | 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 | - | - | | 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 | - | | 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 ## Syntax for issue config
@ -291,15 +313,15 @@ contact_links:
### Possible Options ### Possible Options
| Key | Description | Type | Default | | Key | Description | Type | Default |
|----------------------|-------------------------------------------------------------------------------------------------------|--------------------|----------------| |----------------------|-------------------------------------------------------|--------------------|-------------|
| blank_issues_enabled | If set to false, the User is forced to use a Template | Boolean | true | | 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_links | Custom Links to show in the Choose Box | Contact Link Array | Empty Array |
### Contact Link ### Contact Link
| Key | Description | Type | Required | | Key | Description | Type | Required |
|----------------------|-------------------------------------------------------------------------------------------------------|---------|----------| |-------|----------------------------------|--------|----------|
| name | the name of your link | String | true | | name | the name of your link | String | true |
| url | The URL of your Link | String | true | | url | The URL of your Link | String | true |
| about | A short description of your Link | String | true | | about | A short description of your Link | String | true |

View file

@ -13,6 +13,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/shared/types" "code.gitea.io/gitea/models/shared/types"
user_model "code.gitea.io/gitea/models/user" 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/timeutil"
"code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
@ -159,7 +160,7 @@ type FindRunnerOptions struct {
OwnerID int64 OwnerID int64
Sort string Sort string
Filter string Filter string
IsOnline util.OptionalBool IsOnline optional.Option[bool]
WithAvailable bool // not only runners belong to, but also runners can be used 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}) cond = cond.And(builder.Like{"name", opts.Filter})
} }
if opts.IsOnline.IsTrue() { if opts.IsOnline.Has() {
cond = cond.And(builder.Gt{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()}) if opts.IsOnline.Value() {
} else if opts.IsOnline.IsFalse() { cond = cond.And(builder.Gt{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()})
cond = cond.And(builder.Lte{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()}) } else {
cond = cond.And(builder.Lte{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()})
}
} }
return cond return cond
} }

View file

@ -227,8 +227,8 @@ func (a *Action) ShortActUserName(ctx context.Context) string {
return base.EllipsisString(a.GetActUserName(ctx), 20) 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. // 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) GetDisplayName(ctx context.Context) string { func (a *Action) GetActDisplayName(ctx context.Context) string {
if setting.UI.DefaultShowFullName { if setting.UI.DefaultShowFullName {
trimmedFullName := strings.TrimSpace(a.GetActFullName(ctx)) trimmedFullName := strings.TrimSpace(a.GetActFullName(ctx))
if len(trimmedFullName) > 0 { if len(trimmedFullName) > 0 {
@ -238,8 +238,8 @@ func (a *Action) GetDisplayName(ctx context.Context) string {
return a.ShortActUserName(ctx) return a.ShortActUserName(ctx)
} }
// GetDisplayNameTitle gets the action's display name used for the title (tooltip) based on DEFAULT_SHOW_FULL_NAME // GetActDisplayNameTitle 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 { func (a *Action) GetActDisplayNameTitle(ctx context.Context) string {
if setting.UI.DefaultShowFullName { if setting.UI.DefaultShowFullName {
return a.ShortActUserName(ctx) return a.ShortActUserName(ctx)
} }

View file

@ -11,6 +11,7 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
@ -243,14 +244,14 @@ func CreateSource(ctx context.Context, source *Source) error {
type FindSourcesOptions struct { type FindSourcesOptions struct {
db.ListOptions db.ListOptions
IsActive util.OptionalBool IsActive optional.Option[bool]
LoginType Type LoginType Type
} }
func (opts FindSourcesOptions) ToConds() builder.Cond { func (opts FindSourcesOptions) ToConds() builder.Cond {
conds := builder.NewCond() conds := builder.NewCond()
if !opts.IsActive.IsNone() { if opts.IsActive.Has() {
conds = conds.And(builder.Eq{"is_active": opts.IsActive.IsTrue()}) conds = conds.And(builder.Eq{"is_active": opts.IsActive.Value()})
} }
if opts.LoginType != NoType { if opts.LoginType != NoType {
conds = conds.And(builder.Eq{"`type`": opts.LoginType}) conds = conds.And(builder.Eq{"`type`": opts.LoginType})
@ -262,7 +263,7 @@ func (opts FindSourcesOptions) ToConds() builder.Cond {
// source of type LoginSSPI // source of type LoginSSPI
func IsSSPIEnabled(ctx context.Context) bool { func IsSSPIEnabled(ctx context.Context) bool {
exist, err := db.Exist[Source](ctx, FindSourcesOptions{ exist, err := db.Exist[Source](ctx, FindSourcesOptions{
IsActive: util.OptionalBoolTrue, IsActive: optional.Some(true),
LoginType: SSPI, LoginType: SSPI,
}.ToConds()) }.ToConds())
if err != nil { if err != nil {

View file

@ -17,3 +17,22 @@
updated: 1683636626 updated: 1683636626
need_approval: 0 need_approval: 0
approved_by: 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

View file

@ -12,3 +12,17 @@
status: 1 status: 1
started: 1683636528 started: 1683636528
stopped: 1683636626 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

View file

@ -18,3 +18,23 @@
log_length: 707 log_length: 707
log_size: 90179 log_size: 90179
log_expired: 0 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

View file

@ -8,6 +8,7 @@ package issues
import ( import (
"context" "context"
"fmt" "fmt"
"html/template"
"strconv" "strconv"
"unicode/utf8" "unicode/utf8"
@ -21,6 +22,7 @@ import (
"code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/references" "code.gitea.io/gitea/modules/references"
"code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
@ -259,8 +261,8 @@ type Comment struct {
CommitID int64 CommitID int64
Line int64 // - previous line / + proposed line Line int64 // - previous line / + proposed line
TreePath string TreePath string
Content string `xorm:"LONGTEXT"` Content string `xorm:"LONGTEXT"`
RenderedContent string `xorm:"-"` RenderedContent template.HTML `xorm:"-"`
// Path represents the 4 lines of code cemented by this comment // Path represents the 4 lines of code cemented by this comment
Patch string `xorm:"-"` Patch string `xorm:"-"`
@ -1043,8 +1045,8 @@ type FindCommentsOptions struct {
TreePath string TreePath string
Type CommentType Type CommentType
IssueIDs []int64 IssueIDs []int64
Invalidated util.OptionalBool Invalidated optional.Option[bool]
IsPull util.OptionalBool IsPull optional.Option[bool]
} }
// ToConds implements FindOptions interface // ToConds implements FindOptions interface
@ -1076,11 +1078,11 @@ func (opts FindCommentsOptions) ToConds() builder.Cond {
if len(opts.TreePath) > 0 { if len(opts.TreePath) > 0 {
cond = cond.And(builder.Eq{"comment.tree_path": opts.TreePath}) cond = cond.And(builder.Eq{"comment.tree_path": opts.TreePath})
} }
if !opts.Invalidated.IsNone() { if opts.Invalidated.Has() {
cond = cond.And(builder.Eq{"comment.invalidated": opts.Invalidated.IsTrue()}) cond = cond.And(builder.Eq{"comment.invalidated": opts.Invalidated.Value()})
} }
if opts.IsPull != util.OptionalBoolNone { if opts.IsPull.Has() {
cond = cond.And(builder.Eq{"issue.is_pull": opts.IsPull.IsTrue()}) cond = cond.And(builder.Eq{"issue.is_pull": opts.IsPull.Value()})
} }
return cond return cond
} }
@ -1089,7 +1091,7 @@ func (opts FindCommentsOptions) ToConds() builder.Cond {
func FindComments(ctx context.Context, opts *FindCommentsOptions) (CommentList, error) { func FindComments(ctx context.Context, opts *FindCommentsOptions) (CommentList, error) {
comments := make([]*Comment, 0, 10) comments := make([]*Comment, 0, 10)
sess := db.GetEngine(ctx).Where(opts.ToConds()) 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") sess.Join("INNER", "issue", "issue.id = comment.issue_id")
} }

View file

@ -7,6 +7,7 @@ package issues
import ( import (
"context" "context"
"fmt" "fmt"
"html/template"
"regexp" "regexp"
"slices" "slices"
@ -105,7 +106,7 @@ type Issue struct {
OriginalAuthorID int64 `xorm:"index"` OriginalAuthorID int64 `xorm:"index"`
Title string `xorm:"name"` Title string `xorm:"name"`
Content string `xorm:"LONGTEXT"` Content string `xorm:"LONGTEXT"`
RenderedContent string `xorm:"-"` RenderedContent template.HTML `xorm:"-"`
Labels []*Label `xorm:"-"` Labels []*Label `xorm:"-"`
MilestoneID int64 `xorm:"INDEX"` MilestoneID int64 `xorm:"INDEX"`
Milestone *Milestone `xorm:"-"` Milestone *Milestone `xorm:"-"`

View file

@ -13,7 +13,7 @@ import (
repo_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/models/unit"
user_model "code.gitea.io/gitea/models/user" 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/builder"
"xorm.io/xorm" "xorm.io/xorm"
@ -34,8 +34,8 @@ type IssuesOptions struct { //nolint
MilestoneIDs []int64 MilestoneIDs []int64
ProjectID int64 ProjectID int64
ProjectBoardID int64 ProjectBoardID int64
IsClosed util.OptionalBool IsClosed optional.Option[bool]
IsPull util.OptionalBool IsPull optional.Option[bool]
LabelIDs []int64 LabelIDs []int64
IncludedLabelNames []string IncludedLabelNames []string
ExcludedLabelNames []string ExcludedLabelNames []string
@ -46,7 +46,7 @@ type IssuesOptions struct { //nolint
UpdatedBeforeUnix int64 UpdatedBeforeUnix int64
// prioritize issues from this repo // prioritize issues from this repo
PriorityRepoID int64 PriorityRepoID int64
IsArchived util.OptionalBool IsArchived optional.Option[bool]
Org *organization.Organization // issues permission scope Org *organization.Organization // issues permission scope
Team *organization.Team // issues permission scope Team *organization.Team // issues permission scope
User *user_model.User // 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) applyRepoConditions(sess, opts)
if !opts.IsClosed.IsNone() { if opts.IsClosed.Has() {
sess.And("issue.is_closed=?", opts.IsClosed.IsTrue()) sess.And("issue.is_closed=?", opts.IsClosed.Value())
} }
if opts.AssigneeID > 0 { if opts.AssigneeID > 0 {
@ -260,21 +260,18 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
applyProjectBoardCondition(sess, opts) applyProjectBoardCondition(sess, opts)
switch opts.IsPull { if opts.IsPull.Has() {
case util.OptionalBoolTrue: sess.And("issue.is_pull=?", opts.IsPull.Value())
sess.And("issue.is_pull=?", true)
case util.OptionalBoolFalse:
sess.And("issue.is_pull=?", false)
} }
if opts.IsArchived != util.OptionalBoolNone { if opts.IsArchived.Has() {
sess.And(builder.Eq{"repository.is_archived": opts.IsArchived.IsTrue()}) sess.And(builder.Eq{"repository.is_archived": opts.IsArchived.Value()})
} }
applyLabelsCondition(sess, opts) applyLabelsCondition(sess, opts)
if opts.User != nil { 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 return sess

View file

@ -8,7 +8,6 @@ import (
"fmt" "fmt"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/util"
"xorm.io/builder" "xorm.io/builder"
"xorm.io/xorm" "xorm.io/xorm"
@ -170,11 +169,8 @@ func applyIssuesOptions(sess *xorm.Session, opts *IssuesOptions, issueIDs []int6
applyReviewedCondition(sess, opts.ReviewedID) applyReviewedCondition(sess, opts.ReviewedID)
} }
switch opts.IsPull { if opts.IsPull.Has() {
case util.OptionalBoolTrue: sess.And("issue.is_pull=?", opts.IsPull.Value())
sess.And("issue.is_pull=?", true)
case util.OptionalBoolFalse:
sess.And("issue.is_pull=?", false)
} }
return sess return sess

View file

@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/label" "code.gitea.io/gitea/modules/label"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
@ -126,7 +127,7 @@ func (l *Label) CalOpenOrgIssues(ctx context.Context, repoID, labelID int64) {
counts, _ := CountIssuesByRepo(ctx, &IssuesOptions{ counts, _ := CountIssuesByRepo(ctx, &IssuesOptions{
RepoIDs: []int64{repoID}, RepoIDs: []int64{repoID},
LabelIDs: []int64{labelID}, LabelIDs: []int64{labelID},
IsClosed: util.OptionalBoolFalse, IsClosed: optional.Some(false),
}) })
for _, count := range counts { for _, count := range counts {

View file

@ -6,10 +6,12 @@ package issues
import ( import (
"context" "context"
"fmt" "fmt"
"html/template"
"strings" "strings"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/optional"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
@ -47,8 +49,8 @@ type Milestone struct {
RepoID int64 `xorm:"INDEX"` RepoID int64 `xorm:"INDEX"`
Repo *repo_model.Repository `xorm:"-"` Repo *repo_model.Repository `xorm:"-"`
Name string Name string
Content string `xorm:"TEXT"` Content string `xorm:"TEXT"`
RenderedContent string `xorm:"-"` RenderedContent template.HTML `xorm:"-"`
IsClosed bool IsClosed bool
NumIssues int NumIssues int
NumClosedIssues int NumClosedIssues int
@ -313,7 +315,7 @@ func DeleteMilestoneByRepoID(ctx context.Context, repoID, id int64) error {
} }
numClosedMilestones, err := db.Count[Milestone](ctx, FindMilestoneOptions{ numClosedMilestones, err := db.Count[Milestone](ctx, FindMilestoneOptions{
RepoID: repo.ID, RepoID: repo.ID,
IsClosed: util.OptionalBoolTrue, IsClosed: optional.Some(true),
}) })
if err != nil { if err != nil {
return err return err

View file

@ -8,7 +8,7 @@ import (
"strings" "strings"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/optional"
"xorm.io/builder" "xorm.io/builder"
) )
@ -28,7 +28,7 @@ func (milestones MilestoneList) getMilestoneIDs() []int64 {
type FindMilestoneOptions struct { type FindMilestoneOptions struct {
db.ListOptions db.ListOptions
RepoID int64 RepoID int64
IsClosed util.OptionalBool IsClosed optional.Option[bool]
Name string Name string
SortType string SortType string
RepoCond builder.Cond RepoCond builder.Cond
@ -40,8 +40,8 @@ func (opts FindMilestoneOptions) ToConds() builder.Cond {
if opts.RepoID != 0 { if opts.RepoID != 0 {
cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
} }
if opts.IsClosed != util.OptionalBoolNone { if opts.IsClosed.Has() {
cond = cond.And(builder.Eq{"is_closed": opts.IsClosed.IsTrue()}) cond = cond.And(builder.Eq{"is_closed": opts.IsClosed.Value()})
} }
if opts.RepoCond != nil && opts.RepoCond.IsValid() { if opts.RepoCond != nil && opts.RepoCond.IsValid() {
cond = cond.And(builder.In("repo_id", builder.Select("id").From("repository").Where(opts.RepoCond))) cond = cond.And(builder.In("repo_id", builder.Select("id").From("repository").Where(opts.RepoCond)))

View file

@ -11,10 +11,10 @@ import (
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -39,10 +39,10 @@ func TestGetMilestoneByRepoID(t *testing.T) {
func TestGetMilestonesByRepoID(t *testing.T) { func TestGetMilestonesByRepoID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
test := func(repoID int64, state api.StateType) { test := func(repoID int64, state api.StateType) {
var isClosed util.OptionalBool var isClosed optional.Option[bool]
switch state { switch state {
case api.StateClosed, api.StateOpen: 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}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
milestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{ 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{ milestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
RepoID: unittest.NonexistentID, RepoID: unittest.NonexistentID,
IsClosed: util.OptionalBoolFalse, IsClosed: optional.Some(false),
}) })
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, milestones, 0) assert.Len(t, milestones, 0)
@ -101,7 +101,7 @@ func TestGetMilestones(t *testing.T) {
PageSize: setting.UI.IssuePagingNum, PageSize: setting.UI.IssuePagingNum,
}, },
RepoID: repo.ID, RepoID: repo.ID,
IsClosed: util.OptionalBoolFalse, IsClosed: optional.Some(false),
SortType: sortType, SortType: sortType,
}) })
assert.NoError(t, err) assert.NoError(t, err)
@ -118,7 +118,7 @@ func TestGetMilestones(t *testing.T) {
PageSize: setting.UI.IssuePagingNum, PageSize: setting.UI.IssuePagingNum,
}, },
RepoID: repo.ID, RepoID: repo.ID,
IsClosed: util.OptionalBoolTrue, IsClosed: optional.Some(true),
Name: "", Name: "",
SortType: sortType, SortType: sortType,
}) })
@ -178,7 +178,7 @@ func TestCountRepoClosedMilestones(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
count, err := db.Count[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{ count, err := db.Count[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
RepoID: repoID, RepoID: repoID,
IsClosed: util.OptionalBoolTrue, IsClosed: optional.Some(true),
}) })
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, repo.NumClosedMilestones, count) 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{ count, err := db.Count[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
RepoID: unittest.NonexistentID, RepoID: unittest.NonexistentID,
IsClosed: util.OptionalBoolTrue, IsClosed: optional.Some(true),
}) })
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, 0, count) assert.EqualValues(t, 0, count)
@ -206,7 +206,7 @@ func TestCountMilestonesByRepoIDs(t *testing.T) {
openCounts, err := issues_model.CountMilestonesMap(db.DefaultContext, issues_model.FindMilestoneOptions{ openCounts, err := issues_model.CountMilestonesMap(db.DefaultContext, issues_model.FindMilestoneOptions{
RepoIDs: []int64{1, 2}, RepoIDs: []int64{1, 2},
IsClosed: util.OptionalBoolFalse, IsClosed: optional.Some(false),
}) })
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, repo1OpenCount, openCounts[1]) assert.EqualValues(t, repo1OpenCount, openCounts[1])
@ -215,7 +215,7 @@ func TestCountMilestonesByRepoIDs(t *testing.T) {
closedCounts, err := issues_model.CountMilestonesMap(db.DefaultContext, closedCounts, err := issues_model.CountMilestonesMap(db.DefaultContext,
issues_model.FindMilestoneOptions{ issues_model.FindMilestoneOptions{
RepoIDs: []int64{1, 2}, RepoIDs: []int64{1, 2},
IsClosed: util.OptionalBoolTrue, IsClosed: optional.Some(true),
}) })
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, repo1ClosedCount, closedCounts[1]) assert.EqualValues(t, repo1ClosedCount, closedCounts[1])
@ -234,7 +234,7 @@ func TestGetMilestonesByRepoIDs(t *testing.T) {
PageSize: setting.UI.IssuePagingNum, PageSize: setting.UI.IssuePagingNum,
}, },
RepoIDs: []int64{repo1.ID, repo2.ID}, RepoIDs: []int64{repo1.ID, repo2.ID},
IsClosed: util.OptionalBoolFalse, IsClosed: optional.Some(false),
SortType: sortType, SortType: sortType,
}) })
assert.NoError(t, err) assert.NoError(t, err)
@ -252,7 +252,7 @@ func TestGetMilestonesByRepoIDs(t *testing.T) {
PageSize: setting.UI.IssuePagingNum, PageSize: setting.UI.IssuePagingNum,
}, },
RepoIDs: []int64{repo1.ID, repo2.ID}, RepoIDs: []int64{repo1.ID, repo2.ID},
IsClosed: util.OptionalBoolTrue, IsClosed: optional.Some(true),
SortType: sortType, SortType: sortType,
}) })
assert.NoError(t, err) assert.NoError(t, err)

View file

@ -9,7 +9,7 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/optional"
"xorm.io/builder" "xorm.io/builder"
) )
@ -68,7 +68,7 @@ type FindReviewOptions struct {
IssueID int64 IssueID int64
ReviewerID int64 ReviewerID int64
OfficialOnly bool OfficialOnly bool
Dismissed util.OptionalBool Dismissed optional.Option[bool]
} }
func (opts *FindReviewOptions) toCond() builder.Cond { func (opts *FindReviewOptions) toCond() builder.Cond {
@ -85,8 +85,8 @@ func (opts *FindReviewOptions) toCond() builder.Cond {
if opts.OfficialOnly { if opts.OfficialOnly {
cond = cond.And(builder.Eq{"official": true}) cond = cond.And(builder.Eq{"official": true})
} }
if !opts.Dismissed.IsNone() { if opts.Dismissed.Has() {
cond = cond.And(builder.Eq{"dismissed": opts.Dismissed.IsTrue()}) cond = cond.And(builder.Eq{"dismissed": opts.Dismissed.Value()})
} }
return cond return cond
} }

View file

@ -11,6 +11,7 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user" 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/setting"
"code.gitea.io/gitea/modules/util" "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. // 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 { if len(opts.IssueIDs) <= MaxQueryParameters {
return getIssueTotalTrackedTimeChunk(ctx, opts, isClosed, opts.IssueIDs) return getIssueTotalTrackedTimeChunk(ctx, opts, isClosed, opts.IssueIDs)
} }
@ -363,7 +364,7 @@ func GetIssueTotalTrackedTime(ctx context.Context, opts *IssuesOptions, isClosed
return accum, nil 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 { sumSession := func(opts *IssuesOptions, issueIDs []int64) *xorm.Session {
sess := db.GetEngine(ctx). sess := db.GetEngine(ctx).
Table("tracked_time"). Table("tracked_time").
@ -378,8 +379,8 @@ func getIssueTotalTrackedTimeChunk(ctx context.Context, opts *IssuesOptions, isC
} }
session := sumSession(opts, issueIDs) session := sumSession(opts, issueIDs)
if !isClosed.IsNone() { if isClosed.Has() {
session = session.And("issue.is_closed = ?", isClosed.IsTrue()) session = session.And("issue.is_closed = ?", isClosed.Value())
} }
return session.SumInt(new(trackedTime), "tracked_time.time") return session.SumInt(new(trackedTime), "tracked_time.time")
} }

View file

@ -11,7 +11,7 @@ import (
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" 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" "github.com/stretchr/testify/assert"
) )
@ -120,15 +120,15 @@ func TestTotalTimesForEachUser(t *testing.T) {
func TestGetIssueTotalTrackedTime(t *testing.T) { func TestGetIssueTotalTrackedTime(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) 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.NoError(t, err)
assert.EqualValues(t, 3682, ttt) 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.NoError(t, err)
assert.EqualValues(t, 0, ttt) 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.NoError(t, err)
assert.EqualValues(t, 3682, ttt) assert.EqualValues(t, 3682, ttt)
} }

View file

@ -70,16 +70,26 @@ type PackageFileDescriptor struct {
Properties PackagePropertyList Properties PackagePropertyList
} }
// PackageWebLink returns the package web link // PackageWebLink returns the relative package web link
func (pd *PackageDescriptor) PackageWebLink() string { func (pd *PackageDescriptor) PackageWebLink() string {
return fmt.Sprintf("%s/-/packages/%s/%s", pd.Owner.HomeLink(), string(pd.Package.Type), url.PathEscape(pd.Package.LowerName)) 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 // VersionWebLink returns the relative package version web link
func (pd *PackageDescriptor) FullWebLink() string { func (pd *PackageDescriptor) VersionWebLink() string {
return fmt.Sprintf("%s/%s", pd.PackageWebLink(), url.PathEscape(pd.Version.LowerVersion)) 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 // CalculateBlobSize returns the total blobs size in bytes
func (pd *PackageDescriptor) CalculateBlobSize() int64 { func (pd *PackageDescriptor) CalculateBlobSize() int64 {
size := int64(0) size := int64(0)

View file

@ -55,7 +55,7 @@ func CountPackages(ctx context.Context, opts *packages_model.PackageSearchOption
func toConds(opts *packages_model.PackageSearchOptions) builder.Cond { func toConds(opts *packages_model.PackageSearchOptions) builder.Cond {
var cond builder.Cond = builder.Eq{ var cond builder.Cond = builder.Eq{
"package.is_internal": opts.IsInternal.IsTrue(), "package.is_internal": opts.IsInternal.Value(),
"package.owner_id": opts.OwnerID, "package.owner_id": opts.OwnerID,
"package.type": packages_model.TypeNuGet, "package.type": packages_model.TypeNuGet,
} }

View file

@ -9,6 +9,7 @@ import (
"strings" "strings"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
@ -105,7 +106,7 @@ func getVersionByNameAndVersion(ctx context.Context, ownerID int64, packageType
ExactMatch: true, ExactMatch: true,
Value: version, Value: version,
}, },
IsInternal: util.OptionalBoolOf(isInternal), IsInternal: optional.Some(isInternal),
Paginator: db.NewAbsoluteListOptions(0, 1), Paginator: db.NewAbsoluteListOptions(0, 1),
}) })
if err != nil { if err != nil {
@ -122,7 +123,7 @@ func GetVersionsByPackageType(ctx context.Context, ownerID int64, packageType Ty
pvs, _, err := SearchVersions(ctx, &PackageSearchOptions{ pvs, _, err := SearchVersions(ctx, &PackageSearchOptions{
OwnerID: ownerID, OwnerID: ownerID,
Type: packageType, Type: packageType,
IsInternal: util.OptionalBoolFalse, IsInternal: optional.Some(false),
}) })
return pvs, err return pvs, err
} }
@ -136,7 +137,7 @@ func GetVersionsByPackageName(ctx context.Context, ownerID int64, packageType Ty
ExactMatch: true, ExactMatch: true,
Value: name, Value: name,
}, },
IsInternal: util.OptionalBoolFalse, IsInternal: optional.Some(false),
}) })
return pvs, err return pvs, err
} }
@ -182,18 +183,18 @@ type PackageSearchOptions struct {
Name SearchValue // only results with the specific name are found Name SearchValue // only results with the specific name are found
Version SearchValue // only results with the specific version 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 Properties map[string]string // only results are found which contain all listed version properties with the specific value
IsInternal util.OptionalBool IsInternal optional.Option[bool]
HasFileWithName string // only results are found which are associated with a file with the specific name 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 HasFiles optional.Option[bool] // only results are found which have associated files
Sort VersionSort Sort VersionSort
db.Paginator db.Paginator
} }
func (opts *PackageSearchOptions) ToConds() builder.Cond { func (opts *PackageSearchOptions) ToConds() builder.Cond {
cond := builder.NewCond() cond := builder.NewCond()
if !opts.IsInternal.IsNone() { if opts.IsInternal.Has() {
cond = builder.Eq{ 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))) 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"))) 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} filesCond = builder.Not{filesCond}
} }
@ -307,8 +308,8 @@ func SearchLatestVersions(ctx context.Context, opts *PackageSearchOptions) ([]*P
And(builder.Expr("pv2.id IS NULL")) 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))") 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() { if opts.IsInternal.Has() {
joinCond = joinCond.And(builder.Eq{"pv2.is_internal": opts.IsInternal.IsTrue()}) joinCond = joinCond.And(builder.Eq{"pv2.is_internal": opts.IsInternal.Value()})
} }
sess := db.GetEngine(ctx). sess := db.GetEngine(ctx).

View file

@ -6,11 +6,13 @@ package project
import ( import (
"context" "context"
"fmt" "fmt"
"html/template"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
@ -100,7 +102,7 @@ type Project struct {
CardType CardType CardType CardType
Type Type Type Type
RenderedContent string `xorm:"-"` RenderedContent template.HTML `xorm:"-"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
@ -195,7 +197,7 @@ type SearchOptions struct {
db.ListOptions db.ListOptions
OwnerID int64 OwnerID int64
RepoID int64 RepoID int64
IsClosed util.OptionalBool IsClosed optional.Option[bool]
OrderBy db.SearchOrderBy OrderBy db.SearchOrderBy
Type Type Type Type
Title string Title string
@ -206,11 +208,8 @@ func (opts SearchOptions) ToConds() builder.Cond {
if opts.RepoID > 0 { if opts.RepoID > 0 {
cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
} }
switch opts.IsClosed { if opts.IsClosed.Has() {
case util.OptionalBoolTrue: cond = cond.And(builder.Eq{"is_closed": opts.IsClosed.Value()})
cond = cond.And(builder.Eq{"is_closed": true})
case util.OptionalBoolFalse:
cond = cond.And(builder.Eq{"is_closed": false})
} }
if opts.Type > 0 { if opts.Type > 0 {

View file

@ -7,6 +7,7 @@ package repo
import ( import (
"context" "context"
"fmt" "fmt"
"html/template"
"net/url" "net/url"
"sort" "sort"
"strconv" "strconv"
@ -15,6 +16,7 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
@ -79,7 +81,7 @@ type Release struct {
NumCommits int64 NumCommits int64
NumCommitsBehind int64 `xorm:"-"` NumCommitsBehind int64 `xorm:"-"`
Note string `xorm:"TEXT"` Note string `xorm:"TEXT"`
RenderedNote string `xorm:"-"` RenderedNote template.HTML `xorm:"-"`
IsDraft bool `xorm:"NOT NULL DEFAULT false"` IsDraft bool `xorm:"NOT NULL DEFAULT false"`
IsPrerelease 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 IsTag bool `xorm:"NOT NULL DEFAULT false"` // will be true only if the record is a tag and has no related releases
@ -228,10 +230,10 @@ type FindReleasesOptions struct {
RepoID int64 RepoID int64
IncludeDrafts bool IncludeDrafts bool
IncludeTags bool IncludeTags bool
IsPreRelease util.OptionalBool IsPreRelease optional.Option[bool]
IsDraft util.OptionalBool IsDraft optional.Option[bool]
TagNames []string TagNames []string
HasSha1 util.OptionalBool // useful to find draft releases which are created with existing tags HasSha1 optional.Option[bool] // useful to find draft releases which are created with existing tags
} }
func (opts FindReleasesOptions) ToConds() builder.Cond { func (opts FindReleasesOptions) ToConds() builder.Cond {
@ -246,14 +248,14 @@ func (opts FindReleasesOptions) ToConds() builder.Cond {
if len(opts.TagNames) > 0 { if len(opts.TagNames) > 0 {
cond = cond.And(builder.In("tag_name", opts.TagNames)) cond = cond.And(builder.In("tag_name", opts.TagNames))
} }
if !opts.IsPreRelease.IsNone() { if opts.IsPreRelease.Has() {
cond = cond.And(builder.Eq{"is_prerelease": opts.IsPreRelease.IsTrue()}) cond = cond.And(builder.Eq{"is_prerelease": opts.IsPreRelease.Value()})
} }
if !opts.IsDraft.IsNone() { if opts.IsDraft.Has() {
cond = cond.And(builder.Eq{"is_draft": opts.IsDraft.IsTrue()}) cond = cond.And(builder.Eq{"is_draft": opts.IsDraft.Value()})
} }
if !opts.HasSha1.IsNone() { if opts.HasSha1.Has() {
if opts.HasSha1.IsTrue() { if opts.HasSha1.Value() {
cond = cond.And(builder.Neq{"sha1": ""}) cond = cond.And(builder.Neq{"sha1": ""})
} else { } else {
cond = cond.And(builder.Eq{"sha1": ""}) cond = cond.And(builder.Eq{"sha1": ""})
@ -275,7 +277,7 @@ func GetTagNamesByRepoID(ctx context.Context, repoID int64) ([]string, error) {
ListOptions: listOptions, ListOptions: listOptions,
IncludeDrafts: true, IncludeDrafts: true,
IncludeTags: true, IncludeTags: true,
HasSha1: util.OptionalBoolTrue, HasSha1: optional.Some(true),
RepoID: repoID, RepoID: repoID,
} }

View file

@ -20,6 +20,7 @@ import (
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
@ -873,7 +874,7 @@ func (repo *Repository) TemplateRepo(ctx context.Context) *Repository {
type CountRepositoryOptions struct { type CountRepositoryOptions struct {
OwnerID int64 OwnerID int64
Private util.OptionalBool Private optional.Option[bool]
} }
// CountRepositories returns number of repositories. // CountRepositories returns number of repositories.
@ -885,8 +886,8 @@ func CountRepositories(ctx context.Context, opts CountRepositoryOptions) (int64,
if opts.OwnerID > 0 { if opts.OwnerID > 0 {
sess.And("owner_id = ?", opts.OwnerID) sess.And("owner_id = ?", opts.OwnerID)
} }
if !opts.Private.IsNone() { if opts.Private.Has() {
sess.And("is_private=?", opts.Private.IsTrue()) sess.And("is_private=?", opts.Private.Value())
} }
count, err := sess.Count(new(Repository)) count, err := sess.Count(new(Repository))

View file

@ -13,6 +13,7 @@ import (
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
@ -125,11 +126,11 @@ type SearchRepoOptions struct {
// None -> include public and private // None -> include public and private
// True -> include just private // True -> include just private
// False -> include just public // False -> include just public
IsPrivate util.OptionalBool IsPrivate optional.Option[bool]
// None -> include collaborative AND non-collaborative // None -> include collaborative AND non-collaborative
// True -> include just collaborative // True -> include just collaborative
// False -> include just non-collaborative // False -> include just non-collaborative
Collaborate util.OptionalBool Collaborate optional.Option[bool]
// What type of unit the user can be collaborative in, // What type of unit the user can be collaborative in,
// it is ignored if Collaborate is False. // it is ignored if Collaborate is False.
// TypeInvalid means any unit type. // TypeInvalid means any unit type.
@ -137,19 +138,19 @@ type SearchRepoOptions struct {
// None -> include forks AND non-forks // None -> include forks AND non-forks
// True -> include just forks // True -> include just forks
// False -> include just non-forks // False -> include just non-forks
Fork util.OptionalBool Fork optional.Option[bool]
// None -> include templates AND non-templates // None -> include templates AND non-templates
// True -> include just templates // True -> include just templates
// False -> include just non-templates // False -> include just non-templates
Template util.OptionalBool Template optional.Option[bool]
// None -> include mirrors AND non-mirrors // None -> include mirrors AND non-mirrors
// True -> include just mirrors // True -> include just mirrors
// False -> include just non-mirrors // False -> include just non-mirrors
Mirror util.OptionalBool Mirror optional.Option[bool]
// None -> include archived AND non-archived // None -> include archived AND non-archived
// True -> include just archived // True -> include just archived
// False -> include just non-archived // False -> include just non-archived
Archived util.OptionalBool Archived optional.Option[bool]
// only search topic name // only search topic name
TopicOnly bool TopicOnly bool
// only search repositories with specified primary language // only search repositories with specified primary language
@ -159,7 +160,7 @@ type SearchRepoOptions struct {
// None -> include has milestones AND has no milestone // None -> include has milestones AND has no milestone
// True -> include just has milestones // True -> include just has milestones
// False -> include just has no milestone // False -> include just has no milestone
HasMilestones util.OptionalBool HasMilestones optional.Option[bool]
// LowerNames represents valid lower names to restrict to // LowerNames represents valid lower names to restrict to
LowerNames []string LowerNames []string
// When specified true, apply some filters over the conditions: // When specified true, apply some filters over the conditions:
@ -359,12 +360,12 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond {
))) )))
} }
if opts.IsPrivate != util.OptionalBoolNone { if opts.IsPrivate.Has() {
cond = cond.And(builder.Eq{"is_private": opts.IsPrivate.IsTrue()}) cond = cond.And(builder.Eq{"is_private": opts.IsPrivate.Value()})
} }
if opts.Template != util.OptionalBoolNone { if opts.Template.Has() {
cond = cond.And(builder.Eq{"is_template": opts.Template == util.OptionalBoolTrue}) cond = cond.And(builder.Eq{"is_template": opts.Template.Value()})
} }
// Restrict to starred repositories // 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 // Restrict repositories to those the OwnerID owns or contributes to as per opts.Collaborate
if opts.OwnerID > 0 { if opts.OwnerID > 0 {
accessCond := builder.NewCond() accessCond := builder.NewCond()
if opts.Collaborate != util.OptionalBoolTrue { if !opts.Collaborate.Value() {
accessCond = builder.Eq{"owner_id": opts.OwnerID} accessCond = builder.Eq{"owner_id": opts.OwnerID}
} }
if opts.Collaborate != util.OptionalBoolFalse { if opts.Collaborate.ValueOrDefault(true) {
// A Collaboration is: // A Collaboration is:
collaborateCond := builder.NewCond() 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}))) Where(builder.Eq{"language": opts.Language}).And(builder.Eq{"is_primary": true})))
} }
if opts.Fork != util.OptionalBoolNone || opts.OnlyShowRelevant { if opts.Fork.Has() || opts.OnlyShowRelevant {
if opts.OnlyShowRelevant && opts.Fork == util.OptionalBoolNone { if opts.OnlyShowRelevant && !opts.Fork.Has() {
cond = cond.And(builder.Eq{"is_fork": false}) cond = cond.And(builder.Eq{"is_fork": false})
} else { } 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 { if opts.Mirror.Has() {
cond = cond.And(builder.Eq{"is_mirror": opts.Mirror == util.OptionalBoolTrue}) cond = cond.And(builder.Eq{"is_mirror": opts.Mirror.Value()})
} }
if opts.Actor != nil && opts.Actor.IsRestricted { if opts.Actor != nil && opts.Actor.IsRestricted {
cond = cond.And(AccessibleRepositoryCondition(opts.Actor, unit.TypeInvalid)) cond = cond.And(AccessibleRepositoryCondition(opts.Actor, unit.TypeInvalid))
} }
if opts.Archived != util.OptionalBoolNone { if opts.Archived.Has() {
cond = cond.And(builder.Eq{"is_archived": opts.Archived == util.OptionalBoolTrue}) cond = cond.And(builder.Eq{"is_archived": opts.Archived.Value()})
} }
switch opts.HasMilestones { if opts.HasMilestones.Has() {
case util.OptionalBoolTrue: if opts.HasMilestones.Value() {
cond = cond.And(builder.Gt{"num_milestones": 0}) cond = cond.And(builder.Gt{"num_milestones": 0})
case util.OptionalBoolFalse: } else {
cond = cond.And(builder.Eq{"num_milestones": 0}.Or(builder.IsNull{"num_milestones"})) cond = cond.And(builder.Eq{"num_milestones": 0}.Or(builder.IsNull{"num_milestones"}))
}
} }
if opts.OnlyShowRelevant { if opts.OnlyShowRelevant {

View file

@ -10,7 +10,7 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/optional"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -27,62 +27,62 @@ func getTestCases() []struct {
}{ }{
{ {
name: "PublicRepositoriesByName", 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, count: 7,
}, },
{ {
name: "PublicAndPrivateRepositoriesByName", 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, count: 14,
}, },
{ {
name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitFirstPage", 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, count: 14,
}, },
{ {
name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitSecondPage", 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, count: 14,
}, },
{ {
name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitThirdPage", 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, count: 14,
}, },
{ {
name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitFourthPage", 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, count: 14,
}, },
{ {
name: "PublicRepositoriesOfUser", 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, count: 2,
}, },
{ {
name: "PublicRepositoriesOfUser2", 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, count: 0,
}, },
{ {
name: "PublicRepositoriesOfOrg3", 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, count: 2,
}, },
{ {
name: "PublicAndPrivateRepositoriesOfUser", 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, count: 4,
}, },
{ {
name: "PublicAndPrivateRepositoriesOfUser2", 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, count: 0,
}, },
{ {
name: "PublicAndPrivateRepositoriesOfOrg3", 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, count: 4,
}, },
{ {
@ -117,32 +117,32 @@ func getTestCases() []struct {
}, },
{ {
name: "PublicRepositoriesOfOrganization", 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, count: 1,
}, },
{ {
name: "PublicAndPrivateRepositoriesOfOrganization", 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, count: 2,
}, },
{ {
name: "AllPublic/PublicRepositoriesByName", 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, count: 7,
}, },
{ {
name: "AllPublic/PublicAndPrivateRepositoriesByName", 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, count: 14,
}, },
{ {
name: "AllPublic/PublicRepositoriesOfUserIncludingCollaborative", 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: 34, count: 34,
}, },
{ {
name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborative", 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: 39, count: 39,
}, },
{ {
@ -157,12 +157,12 @@ func getTestCases() []struct {
}, },
{ {
name: "AllPublic/PublicRepositoriesOfOrganization", 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: 34, count: 34,
}, },
{ {
name: "AllTemplates", 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, count: 2,
}, },
{ {
@ -190,7 +190,7 @@ func TestSearchRepository(t *testing.T) {
PageSize: 10, PageSize: 10,
}, },
Keyword: "repo_12", Keyword: "repo_12",
Collaborate: util.OptionalBoolFalse, Collaborate: optional.Some(false),
}) })
assert.NoError(t, err) assert.NoError(t, err)
@ -205,7 +205,7 @@ func TestSearchRepository(t *testing.T) {
PageSize: 10, PageSize: 10,
}, },
Keyword: "test_repo", Keyword: "test_repo",
Collaborate: util.OptionalBoolFalse, Collaborate: optional.Some(false),
}) })
assert.NoError(t, err) assert.NoError(t, err)
@ -220,7 +220,7 @@ func TestSearchRepository(t *testing.T) {
}, },
Keyword: "repo_13", Keyword: "repo_13",
Private: true, Private: true,
Collaborate: util.OptionalBoolFalse, Collaborate: optional.Some(false),
}) })
assert.NoError(t, err) assert.NoError(t, err)
@ -236,7 +236,7 @@ func TestSearchRepository(t *testing.T) {
}, },
Keyword: "test_repo", Keyword: "test_repo",
Private: true, Private: true,
Collaborate: util.OptionalBoolFalse, Collaborate: optional.Some(false),
}) })
assert.NoError(t, err) assert.NoError(t, err)
@ -257,7 +257,7 @@ func TestSearchRepository(t *testing.T) {
PageSize: 10, PageSize: 10,
}, },
Keyword: "description_14", Keyword: "description_14",
Collaborate: util.OptionalBoolFalse, Collaborate: optional.Some(false),
IncludeDescription: true, IncludeDescription: true,
}) })
@ -274,7 +274,7 @@ func TestSearchRepository(t *testing.T) {
PageSize: 10, PageSize: 10,
}, },
Keyword: "description_14", Keyword: "description_14",
Collaborate: util.OptionalBoolFalse, Collaborate: optional.Some(false),
IncludeDescription: false, IncludeDescription: false,
}) })
@ -327,30 +327,25 @@ func TestSearchRepository(t *testing.T) {
assert.False(t, repo.IsPrivate) assert.False(t, repo.IsPrivate)
} }
if testCase.opts.Fork == util.OptionalBoolTrue && testCase.opts.Mirror == util.OptionalBoolTrue { if testCase.opts.Fork.Value() && testCase.opts.Mirror.Value() {
assert.True(t, repo.IsFork || repo.IsMirror) assert.True(t, repo.IsFork && repo.IsMirror)
} else { } else {
switch testCase.opts.Fork { if testCase.opts.Fork.Has() {
case util.OptionalBoolFalse: assert.Equal(t, testCase.opts.Fork.Value(), repo.IsFork)
assert.False(t, repo.IsFork)
case util.OptionalBoolTrue:
assert.True(t, repo.IsFork)
} }
switch testCase.opts.Mirror { if testCase.opts.Mirror.Has() {
case util.OptionalBoolFalse: assert.Equal(t, testCase.opts.Mirror.Value(), repo.IsMirror)
assert.False(t, repo.IsMirror)
case util.OptionalBoolTrue:
assert.True(t, repo.IsMirror)
} }
} }
if testCase.opts.OwnerID > 0 && !testCase.opts.AllPublic { if testCase.opts.OwnerID > 0 && !testCase.opts.AllPublic {
switch testCase.opts.Collaborate { if testCase.opts.Collaborate.Has() {
case util.OptionalBoolFalse: if testCase.opts.Collaborate.Value() {
assert.Equal(t, testCase.opts.OwnerID, repo.Owner.ID) assert.NotEqual(t, testCase.opts.OwnerID, repo.Owner.ID)
case util.OptionalBoolTrue: } else {
assert.NotEqual(t, testCase.opts.OwnerID, repo.Owner.ID) assert.Equal(t, testCase.opts.OwnerID, repo.Owner.ID)
}
} }
} }
} }

View file

@ -12,17 +12,17 @@ import (
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
var ( var (
countRepospts = repo_model.CountRepositoryOptions{OwnerID: 10} countRepospts = repo_model.CountRepositoryOptions{OwnerID: 10}
countReposptsPublic = repo_model.CountRepositoryOptions{OwnerID: 10, Private: util.OptionalBoolFalse} countReposptsPublic = repo_model.CountRepositoryOptions{OwnerID: 10, Private: optional.Some(false)}
countReposptsPrivate = repo_model.CountRepositoryOptions{OwnerID: 10, Private: util.OptionalBoolTrue} countReposptsPrivate = repo_model.CountRepositoryOptions{OwnerID: 10, Private: optional.Some(true)}
) )
func TestGetRepositoryCount(t *testing.T) { func TestGetRepositoryCount(t *testing.T) {

View file

@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/validation" "code.gitea.io/gitea/modules/validation"
@ -425,8 +426,8 @@ type SearchEmailOptions struct {
db.ListOptions db.ListOptions
Keyword string Keyword string
SortType SearchEmailOrderBy SortType SearchEmailOrderBy
IsPrimary util.OptionalBool IsPrimary optional.Option[bool]
IsActivated util.OptionalBool IsActivated optional.Option[bool]
} }
// SearchEmailResult is an e-mail address found in the user or email_address table // SearchEmailResult is an e-mail address found in the user or email_address table
@ -453,18 +454,12 @@ func SearchEmails(ctx context.Context, opts *SearchEmailOptions) ([]*SearchEmail
)) ))
} }
switch { if opts.IsPrimary.Has() {
case opts.IsPrimary.IsTrue(): cond = cond.And(builder.Eq{"email_address.is_primary": opts.IsPrimary.Value()})
cond = cond.And(builder.Eq{"email_address.is_primary": true})
case opts.IsPrimary.IsFalse():
cond = cond.And(builder.Eq{"email_address.is_primary": false})
} }
switch { if opts.IsActivated.Has() {
case opts.IsActivated.IsTrue(): cond = cond.And(builder.Eq{"email_address.is_activated": opts.IsActivated.Value()})
cond = cond.And(builder.Eq{"email_address.is_activated": true})
case opts.IsActivated.IsFalse():
cond = cond.And(builder.Eq{"email_address.is_activated": false})
} }
count, err := db.GetEngine(ctx).Join("INNER", "`user`", "`user`.ID = email_address.uid"). count, err := db.GetEngine(ctx).Join("INNER", "`user`", "`user`.ID = email_address.uid").

View file

@ -10,7 +10,7 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" 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" "github.com/stretchr/testify/assert"
) )
@ -138,14 +138,14 @@ func TestListEmails(t *testing.T) {
assert.True(t, contains(func(s *user_model.SearchEmailResult) bool { return s.UID == 27 })) 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) // 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) emails, _, err = user_model.SearchEmails(db.DefaultContext, opts)
assert.NoError(t, err) assert.NoError(t, err)
assert.True(t, contains(func(s *user_model.SearchEmailResult) bool { return s.IsPrimary })) 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 })) assert.False(t, contains(func(s *user_model.SearchEmailResult) bool { return !s.IsPrimary }))
// Must find only inactive addresses (i.e. not validated) // 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) emails, _, err = user_model.SearchEmails(db.DefaultContext, opts)
assert.NoError(t, err) assert.NoError(t, err)
assert.True(t, contains(func(s *user_model.SearchEmailResult) bool { return !s.IsActivated })) assert.True(t, contains(func(s *user_model.SearchEmailResult) bool { return !s.IsActivated }))

View file

@ -9,8 +9,9 @@ import (
"strings" "strings"
"code.gitea.io/gitea/models/db" "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/structs"
"code.gitea.io/gitea/modules/util"
"xorm.io/builder" "xorm.io/builder"
"xorm.io/xorm" "xorm.io/xorm"
@ -30,11 +31,13 @@ type SearchUserOptions struct {
Actor *User // The user doing the search Actor *User // The user doing the search
SearchByEmail bool // Search by email as well as username/full name SearchByEmail bool // Search by email as well as username/full name
IsActive util.OptionalBool SupportedSortOrders container.Set[string] // if not nil, only allow to use the sort orders in this set
IsAdmin util.OptionalBool
IsRestricted util.OptionalBool IsActive optional.Option[bool]
IsTwoFactorEnabled util.OptionalBool IsAdmin optional.Option[bool]
IsProhibitLogin util.OptionalBool IsRestricted optional.Option[bool]
IsTwoFactorEnabled optional.Option[bool]
IsProhibitLogin optional.Option[bool]
IncludeReserved bool IncludeReserved bool
ExtraParamStrings map[string]string ExtraParamStrings map[string]string
@ -86,24 +89,24 @@ func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Sess
cond = cond.And(builder.Eq{"login_name": opts.LoginName}) cond = cond.And(builder.Eq{"login_name": opts.LoginName})
} }
if !opts.IsActive.IsNone() { if opts.IsActive.Has() {
cond = cond.And(builder.Eq{"is_active": opts.IsActive.IsTrue()}) cond = cond.And(builder.Eq{"is_active": opts.IsActive.Value()})
} }
if !opts.IsAdmin.IsNone() { if opts.IsAdmin.Has() {
cond = cond.And(builder.Eq{"is_admin": opts.IsAdmin.IsTrue()}) cond = cond.And(builder.Eq{"is_admin": opts.IsAdmin.Value()})
} }
if !opts.IsRestricted.IsNone() { if opts.IsRestricted.Has() {
cond = cond.And(builder.Eq{"is_restricted": opts.IsRestricted.IsTrue()}) cond = cond.And(builder.Eq{"is_restricted": opts.IsRestricted.Value()})
} }
if !opts.IsProhibitLogin.IsNone() { if opts.IsProhibitLogin.Has() {
cond = cond.And(builder.Eq{"prohibit_login": opts.IsProhibitLogin.IsTrue()}) cond = cond.And(builder.Eq{"prohibit_login": opts.IsProhibitLogin.Value()})
} }
e := db.GetEngine(ctx) e := db.GetEngine(ctx)
if opts.IsTwoFactorEnabled.IsNone() { if !opts.IsTwoFactorEnabled.Has() {
return e.Where(cond) return e.Where(cond)
} }
@ -111,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. // 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): // 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) // (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")) cond = cond.And(builder.Expr("two_factor.uid IS NOT NULL"))
} else { } else {
cond = cond.And(builder.Expr("two_factor.uid IS NULL")) cond = cond.And(builder.Expr("two_factor.uid IS NULL"))
@ -128,7 +131,7 @@ func SearchUsers(ctx context.Context, opts *SearchUserOptions) (users []*User, _
defer sessCount.Close() defer sessCount.Close()
count, err := sessCount.Count(new(User)) count, err := sessCount.Count(new(User))
if err != nil { if err != nil {
return nil, 0, fmt.Errorf("Count: %w", err) return nil, 0, fmt.Errorf("count: %w", err)
} }
if len(opts.OrderBy) == 0 { if len(opts.OrderBy) == 0 {

View file

@ -727,7 +727,7 @@ func CreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOve
// IsLastAdminUser check whether user is the last admin // IsLastAdminUser check whether user is the last admin
func IsLastAdminUser(ctx context.Context, user *User) bool { 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 true
} }
return false return false
@ -736,7 +736,7 @@ func IsLastAdminUser(ctx context.Context, user *User) bool {
// CountUserFilter represent optional filters for CountUsers // CountUserFilter represent optional filters for CountUsers
type CountUserFilter struct { type CountUserFilter struct {
LastLoginSince *int64 LastLoginSince *int64
IsAdmin util.OptionalBool IsAdmin optional.Option[bool]
} }
// CountUsers returns number of users. // CountUsers returns number of users.
@ -754,8 +754,8 @@ func countUsers(ctx context.Context, opts *CountUserFilter) int64 {
cond = cond.And(builder.Gte{"last_login_unix": *opts.LastLoginSince}) cond = cond.And(builder.Gte{"last_login_unix": *opts.LastLoginSince})
} }
if !opts.IsAdmin.IsNone() { if opts.IsAdmin.Has() {
cond = cond.And(builder.Eq{"is_admin": opts.IsAdmin.IsTrue()}) cond = cond.And(builder.Eq{"is_admin": opts.IsAdmin.Value()})
} }
} }

View file

@ -16,10 +16,10 @@ import (
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/auth/password/hash" "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/setting"
"code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert" "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}}, 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}) []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}) []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}) []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}) []int64{1, 10, 11, 12, 13, 14, 15, 16, 18})
// order by name asc default // 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}) []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}) []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}) []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}) []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}) []int64{24})
} }

View file

@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/secret" "code.gitea.io/gitea/modules/secret"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
@ -433,7 +434,7 @@ type ListWebhookOptions struct {
db.ListOptions db.ListOptions
RepoID int64 RepoID int64
OwnerID int64 OwnerID int64
IsActive util.OptionalBool IsActive optional.Option[bool]
} }
func (opts ListWebhookOptions) ToConds() builder.Cond { func (opts ListWebhookOptions) ToConds() builder.Cond {
@ -444,8 +445,8 @@ func (opts ListWebhookOptions) ToConds() builder.Cond {
if opts.OwnerID != 0 { if opts.OwnerID != 0 {
cond = cond.And(builder.Eq{"webhook.owner_id": opts.OwnerID}) cond = cond.And(builder.Eq{"webhook.owner_id": opts.OwnerID})
} }
if !opts.IsActive.IsNone() { if opts.IsActive.Has() {
cond = cond.And(builder.Eq{"webhook.is_active": opts.IsActive.IsTrue()}) cond = cond.And(builder.Eq{"webhook.is_active": opts.IsActive.Value()})
} }
return cond return cond
} }

View file

@ -8,7 +8,7 @@ import (
"fmt" "fmt"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/optional"
) )
// GetDefaultWebhooks returns all admin-default webhooks. // 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. // 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) webhooks := make([]*Webhook, 0, 5)
if isActive.IsNone() { if !isActive.Has() {
return webhooks, db.GetEngine(ctx). return webhooks, db.GetEngine(ctx).
Where("repo_id=? AND owner_id=? AND is_system_webhook=?", 0, 0, true). Where("repo_id=? AND owner_id=? AND is_system_webhook=?", 0, 0, true).
Find(&webhooks) Find(&webhooks)
} }
return webhooks, db.GetEngine(ctx). 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) Find(&webhooks)
} }

View file

@ -11,9 +11,9 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/optional"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
webhook_module "code.gitea.io/gitea/modules/webhook" webhook_module "code.gitea.io/gitea/modules/webhook"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -123,7 +123,7 @@ func TestGetWebhookByOwnerID(t *testing.T) {
func TestGetActiveWebhooksByRepoID(t *testing.T) { func TestGetActiveWebhooksByRepoID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) 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) assert.NoError(t, err)
if assert.Len(t, hooks, 1) { if assert.Len(t, hooks, 1) {
assert.Equal(t, int64(1), hooks[0].ID) assert.Equal(t, int64(1), hooks[0].ID)
@ -143,7 +143,7 @@ func TestGetWebhooksByRepoID(t *testing.T) {
func TestGetActiveWebhooksByOwnerID(t *testing.T) { func TestGetActiveWebhooksByOwnerID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) 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) assert.NoError(t, err)
if assert.Len(t, hooks, 1) { if assert.Len(t, hooks, 1) {
assert.Equal(t, int64(3), hooks[0].ID) assert.Equal(t, int64(3), hooks[0].ID)

View file

@ -35,6 +35,9 @@ func FullSteps(task *actions_model.ActionTask) []*actions_model.ActionTaskStep {
} else if task.Status.IsDone() { } else if task.Status.IsDone() {
preStep.Stopped = task.Stopped preStep.Stopped = task.Stopped
preStep.Status = actions_model.StatusFailure preStep.Status = actions_model.StatusFailure
if task.Status.IsSkipped() {
preStep.Status = actions_model.StatusSkipped
}
} }
logIndex += preStep.LogLength logIndex += preStep.LogLength

View file

@ -406,6 +406,9 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
// all acts conditions should be satisfied // all acts conditions should be satisfied
for cond, vals := range acts { for cond, vals := range acts {
switch cond { switch cond {
case "types":
// types have been checked
continue
case "branches": case "branches":
refName := git.RefName(prPayload.PullRequest.Base.Ref) refName := git.RefName(prPayload.PullRequest.Base.Ref)
patterns, err := workflowpattern.CompilePatterns(vals...) patterns, err := workflowpattern.CompilePatterns(vals...)

View file

@ -12,6 +12,7 @@ import (
"io" "io"
"os" "os"
"os/exec" "os/exec"
"runtime"
"strings" "strings"
"time" "time"
@ -344,6 +345,17 @@ func (c *Command) Run(opts *RunOpts) error {
log.Debug("slow git.Command.Run: %s (%s)", c, elapsed) 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 { if err != nil && ctx.Err() != context.DeadlineExceeded {
return err return err
} }

View file

@ -175,11 +175,11 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
queries = append(queries, bleve.NewDisjunctionQuery(repoQueries...)) queries = append(queries, bleve.NewDisjunctionQuery(repoQueries...))
} }
if !options.IsPull.IsNone() { if options.IsPull.Has() {
queries = append(queries, inner_bleve.BoolFieldQuery(options.IsPull.IsTrue(), "is_pull")) queries = append(queries, inner_bleve.BoolFieldQuery(options.IsPull.Value(), "is_pull"))
} }
if !options.IsClosed.IsNone() { if options.IsClosed.Has() {
queries = append(queries, inner_bleve.BoolFieldQuery(options.IsClosed.IsTrue(), "is_closed")) queries = append(queries, inner_bleve.BoolFieldQuery(options.IsClosed.Value(), "is_closed"))
} }
if options.NoLabelOnly { if options.NoLabelOnly {

View file

@ -11,6 +11,7 @@ import (
issue_model "code.gitea.io/gitea/models/issues" issue_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/indexer/issues/internal" "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) { 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), UpdatedAfterUnix: convertInt64(options.UpdatedAfterUnix),
UpdatedBeforeUnix: convertInt64(options.UpdatedBeforeUnix), UpdatedBeforeUnix: convertInt64(options.UpdatedBeforeUnix),
PriorityRepoID: 0, PriorityRepoID: 0,
IsArchived: 0, IsArchived: optional.None[bool](),
Org: nil, Org: nil,
Team: nil, Team: nil,
User: nil, User: nil,

View file

@ -153,11 +153,11 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
query.Must(q) query.Must(q)
} }
if !options.IsPull.IsNone() { if options.IsPull.Has() {
query.Must(elastic.NewTermQuery("is_pull", options.IsPull.IsTrue())) query.Must(elastic.NewTermQuery("is_pull", options.IsPull.Value()))
} }
if !options.IsClosed.IsNone() { if options.IsClosed.Has() {
query.Must(elastic.NewTermQuery("is_closed", options.IsClosed.IsTrue())) query.Must(elastic.NewTermQuery("is_closed", options.IsClosed.Value()))
} }
if options.NoLabelOnly { if options.NoLabelOnly {

View file

@ -20,10 +20,10 @@ import (
"code.gitea.io/gitea/modules/indexer/issues/internal" "code.gitea.io/gitea/modules/indexer/issues/internal"
"code.gitea.io/gitea/modules/indexer/issues/meilisearch" "code.gitea.io/gitea/modules/indexer/issues/meilisearch"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/queue" "code.gitea.io/gitea/modules/queue"
"code.gitea.io/gitea/modules/setting" "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. // 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}, ListOptions: db_model.ListOptions{Page: page, PageSize: repo_model.RepositoryListDefaultPageSize},
OrderBy: db_model.SearchOrderByID, OrderBy: db_model.SearchOrderByID,
Private: true, Private: true,
Collaborate: util.OptionalBoolFalse, Collaborate: optional.Some(false),
}) })
if err != nil { if err != nil {
log.Error("SearchRepositoryByName: %v", err) log.Error("SearchRepositoryByName: %v", err)

View file

@ -10,8 +10,8 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/indexer/issues/internal" "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/setting"
"code.gitea.io/gitea/modules/util"
_ "code.gitea.io/gitea/models" _ "code.gitea.io/gitea/models"
_ "code.gitea.io/gitea/models/actions" _ "code.gitea.io/gitea/models/actions"
@ -210,13 +210,13 @@ func searchIssueIsPull(t *testing.T) {
}{ }{
{ {
SearchOptions{ SearchOptions{
IsPull: util.OptionalBoolFalse, IsPull: optional.Some(false),
}, },
[]int64{17, 16, 15, 14, 13, 6, 5, 18, 10, 7, 4, 1}, []int64{17, 16, 15, 14, 13, 6, 5, 18, 10, 7, 4, 1},
}, },
{ {
SearchOptions{ SearchOptions{
IsPull: util.OptionalBoolTrue, IsPull: optional.Some(true),
}, },
[]int64{22, 21, 12, 11, 20, 19, 9, 8, 3, 2}, []int64{22, 21, 12, 11, 20, 19, 9, 8, 3, 2},
}, },
@ -237,13 +237,13 @@ func searchIssueIsClosed(t *testing.T) {
}{ }{
{ {
SearchOptions{ 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}, []int64{22, 21, 17, 16, 15, 14, 13, 12, 11, 20, 6, 19, 18, 10, 7, 9, 8, 3, 2, 1},
}, },
{ {
SearchOptions{ SearchOptions{
IsClosed: util.OptionalBoolTrue, IsClosed: optional.Some(true),
}, },
[]int64{5, 4}, []int64{5, 4},
}, },

View file

@ -5,8 +5,8 @@ package internal
import ( import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
) )
// IndexerData data stored in the issue indexer // IndexerData data stored in the issue indexer
@ -77,8 +77,8 @@ type SearchOptions struct {
RepoIDs []int64 // repository IDs which the issues belong to RepoIDs []int64 // repository IDs which the issues belong to
AllPublic bool // if include all public repositories AllPublic bool // if include all public repositories
IsPull util.OptionalBool // if the issues is a pull request IsPull optional.Option[bool] // if the issues is a pull request
IsClosed util.OptionalBool // if the issues is closed IsClosed optional.Option[bool] // if the issues is closed
IncludedLabelIDs []int64 // labels the issues have IncludedLabelIDs []int64 // labels the issues have
ExcludedLabelIDs []int64 // labels the issues don't have ExcludedLabelIDs []int64 // labels the issues don't have

View file

@ -16,8 +16,8 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/indexer/issues/internal" "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/timeutil"
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -166,7 +166,7 @@ var cases = []*testIndexerCase{
Paginator: &db.ListOptions{ Paginator: &db.ListOptions{
PageSize: 5, PageSize: 5,
}, },
IsPull: util.OptionalBoolFalse, IsPull: optional.Some(false),
}, },
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
assert.Equal(t, 5, len(result.Hits)) assert.Equal(t, 5, len(result.Hits))
@ -182,7 +182,7 @@ var cases = []*testIndexerCase{
Paginator: &db.ListOptions{ Paginator: &db.ListOptions{
PageSize: 5, PageSize: 5,
}, },
IsPull: util.OptionalBoolTrue, IsPull: optional.Some(true),
}, },
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
assert.Equal(t, 5, len(result.Hits)) assert.Equal(t, 5, len(result.Hits))
@ -198,7 +198,7 @@ var cases = []*testIndexerCase{
Paginator: &db.ListOptions{ Paginator: &db.ListOptions{
PageSize: 5, PageSize: 5,
}, },
IsClosed: util.OptionalBoolFalse, IsClosed: optional.Some(false),
}, },
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
assert.Equal(t, 5, len(result.Hits)) assert.Equal(t, 5, len(result.Hits))
@ -214,7 +214,7 @@ var cases = []*testIndexerCase{
Paginator: &db.ListOptions{ Paginator: &db.ListOptions{
PageSize: 5, PageSize: 5,
}, },
IsClosed: util.OptionalBoolTrue, IsClosed: optional.Some(true),
}, },
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
assert.Equal(t, 5, len(result.Hits)) assert.Equal(t, 5, len(result.Hits))

View file

@ -131,11 +131,11 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
query.And(q) query.And(q)
} }
if !options.IsPull.IsNone() { if options.IsPull.Has() {
query.And(inner_meilisearch.NewFilterEq("is_pull", options.IsPull.IsTrue())) query.And(inner_meilisearch.NewFilterEq("is_pull", options.IsPull.Value()))
} }
if !options.IsClosed.IsNone() { if options.IsClosed.Has() {
query.And(inner_meilisearch.NewFilterEq("is_closed", options.IsClosed.IsTrue())) query.And(inner_meilisearch.NewFilterEq("is_closed", options.IsClosed.Value()))
} }
if options.NoLabelOnly { if options.NoLabelOnly {

View file

@ -122,7 +122,13 @@ func validateRequired(field *api.IssueFormField, idx int) error {
// The label is not required for a markdown or checkboxes field // The label is not required for a markdown or checkboxes field
return nil 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 { 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") 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 required, ok := opt["required"]; ok {
if _, ok := required.(bool); !ok { if _, ok := required.(bool); !ok {
return position.Errorf("'required' should be a bool") 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, IssueFormField: field,
Values: values, Values: values,
} }
if f.ID == "" { if f.ID == "" || !f.VisibleInContent() {
continue continue
} }
f.WriteTo(builder) f.WriteTo(builder)
@ -253,11 +287,6 @@ type valuedField struct {
} }
func (f *valuedField) WriteTo(builder *strings.Builder) { func (f *valuedField) WriteTo(builder *strings.Builder) {
if f.Type == api.IssueFormFieldTypeMarkdown {
// markdown blocks do not appear in output
return
}
// write label // write label
if !f.HideLabel() { if !f.HideLabel() {
_, _ = fmt.Fprintf(builder, "### %s\n\n", f.Label()) _, _ = fmt.Fprintf(builder, "### %s\n\n", f.Label())
@ -269,6 +298,9 @@ func (f *valuedField) WriteTo(builder *strings.Builder) {
switch f.Type { switch f.Type {
case api.IssueFormFieldTypeCheckboxes: case api.IssueFormFieldTypeCheckboxes:
for _, option := range f.Options() { for _, option := range f.Options() {
if !option.VisibleInContent() {
continue
}
checked := " " checked := " "
if option.IsChecked() { if option.IsChecked() {
checked = "x" checked = "x"
@ -302,6 +334,10 @@ func (f *valuedField) WriteTo(builder *strings.Builder) {
} else { } else {
_, _ = fmt.Fprintf(builder, "%s\n", value) _, _ = 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) _, _ = fmt.Fprintln(builder)
} }
@ -314,6 +350,9 @@ func (f *valuedField) Label() string {
} }
func (f *valuedField) HideLabel() bool { func (f *valuedField) HideLabel() bool {
if f.Type == api.IssueFormFieldTypeMarkdown {
return true
}
if label, ok := f.Attributes["hide_label"].(bool); ok { if label, ok := f.Attributes["hide_label"].(bool); ok {
return label return label
} }
@ -385,6 +424,22 @@ func (o *valuedOption) IsChecked() bool {
return false 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,}") var minQuotesRegex = regexp.MustCompilePOSIX("^`{3,}")
// minQuotes return 3 or more back-quotes. // minQuotes return 3 or more back-quotes.

View file

@ -10,6 +10,7 @@ import (
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -318,6 +319,42 @@ body:
`, `,
wantErr: "body[0](checkboxes), option[0]: 'required' should be a bool", 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", name: "valid",
content: ` content: `
@ -374,8 +411,11 @@ body:
required: true required: true
- label: Option 2 of checkboxes - label: Option 2 of checkboxes
required: false required: false
- label: Option 3 of checkboxes - label: Hidden Option 3 of checkboxes
visible: [content]
- label: Required but not submitted
required: true required: true
visible: [form]
`, `,
want: &api.IssueTemplate{ want: &api.IssueTemplate{
Name: "Name", Name: "Name",
@ -390,6 +430,7 @@ body:
Attributes: map[string]any{ Attributes: map[string]any{
"value": "Value of the markdown", "value": "Value of the markdown",
}, },
Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm},
}, },
{ {
Type: "textarea", Type: "textarea",
@ -404,6 +445,7 @@ body:
Validations: map[string]any{ Validations: map[string]any{
"required": true, "required": true,
}, },
Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent},
}, },
{ {
Type: "input", Type: "input",
@ -419,6 +461,7 @@ body:
"is_number": true, "is_number": true,
"regex": "[a-zA-Z0-9]+", "regex": "[a-zA-Z0-9]+",
}, },
Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent},
}, },
{ {
Type: "dropdown", Type: "dropdown",
@ -436,6 +479,7 @@ body:
Validations: map[string]any{ Validations: map[string]any{
"required": true, "required": true,
}, },
Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent},
}, },
{ {
Type: "checkboxes", Type: "checkboxes",
@ -446,9 +490,11 @@ body:
"options": []any{ "options": []any{
map[string]any{"label": "Option 1 of checkboxes", "required": true}, 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 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", FileName: "test.yaml",
@ -467,7 +513,12 @@ body:
- type: markdown - type: markdown
id: id1 id: id1
attributes: 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{ want: &api.IssueTemplate{
Name: "Name", Name: "Name",
@ -480,8 +531,17 @@ body:
Type: "markdown", Type: "markdown",
ID: "id1", ID: "id1",
Attributes: map[string]any{ 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", FileName: "test.yaml",
@ -515,6 +575,7 @@ body:
Attributes: map[string]any{ Attributes: map[string]any{
"value": "Value of the markdown", "value": "Value of the markdown",
}, },
Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm},
}, },
}, },
FileName: "test.yaml", FileName: "test.yaml",
@ -548,6 +609,7 @@ body:
Attributes: map[string]any{ Attributes: map[string]any{
"value": "Value of the markdown", "value": "Value of the markdown",
}, },
Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm},
}, },
}, },
FileName: "test.yaml", FileName: "test.yaml",
@ -622,9 +684,14 @@ body:
- type: markdown - type: markdown
id: id1 id: id1
attributes: attributes:
value: Value of the markdown value: Value of the markdown shown in form
- type: textarea - type: markdown
id: id2 id: id2
attributes:
value: Value of the markdown shown in created issue
visible: [content]
- type: textarea
id: id3
attributes: attributes:
label: Label of textarea label: Label of textarea
description: Description of textarea description: Description of textarea
@ -634,7 +701,7 @@ body:
validations: validations:
required: true required: true
- type: input - type: input
id: id3 id: id4
attributes: attributes:
label: Label of input label: Label of input
description: Description of input description: Description of input
@ -646,7 +713,7 @@ body:
is_number: true is_number: true
regex: "[a-zA-Z0-9]+" regex: "[a-zA-Z0-9]+"
- type: dropdown - type: dropdown
id: id4 id: id5
attributes: attributes:
label: Label of dropdown label: Label of dropdown
description: Description of dropdown description: Description of dropdown
@ -658,7 +725,7 @@ body:
validations: validations:
required: true required: true
- type: checkboxes - type: checkboxes
id: id5 id: id6
attributes: attributes:
label: Label of checkboxes label: Label of checkboxes
description: Description of checkboxes description: Description of checkboxes
@ -669,20 +736,26 @@ body:
required: false required: false
- label: Option 3 of checkboxes - label: Option 3 of checkboxes
required: true required: true
visible: [form]
- label: Hidden Option of checkboxes
visible: [content]
`, `,
values: map[string][]string{ values: map[string][]string{
"form-field-id2": {"Value of id2"},
"form-field-id3": {"Value of id3"}, "form-field-id3": {"Value of id3"},
"form-field-id4": {"0,1"}, "form-field-id4": {"Value of id4"},
"form-field-id5-0": {"on"}, "form-field-id5": {"0,1"},
"form-field-id5-2": {"on"}, "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 ### Label of dropdown
@ -692,7 +765,7 @@ Option 1 of dropdown, Option 2 of dropdown
- [x] Option 1 of checkboxes - [x] Option 1 of checkboxes
- [ ] Option 2 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) t.Fatal(err)
} }
if got := RenderToMarkdown(template, tt.args.values); got != tt.want { 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)
} }
}) })
} }

View file

@ -128,9 +128,18 @@ func unmarshal(filename string, content []byte) (*api.IssueTemplate, error) {
} }
} }
for i, v := range it.Fields { for i, v := range it.Fields {
// set default id value
if v.ID == "" { if v.ID == "" {
v.ID = strconv.Itoa(i) 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)
}
}
} }
} }

View file

@ -388,7 +388,7 @@ func TestRender_ShortLinks(t *testing.T) {
}, },
}, input) }, input)
assert.NoError(t, err) 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{ buffer, err = markdown.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
Links: markup.Links{ Links: markup.Links{
@ -398,7 +398,7 @@ func TestRender_ShortLinks(t *testing.T) {
IsWiki: true, IsWiki: true,
}, input) }, input)
assert.NoError(t, err) 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") mediatree := util.URLJoin(markup.TestRepoURL, "media", "master")
@ -501,7 +501,7 @@ func TestRender_RelativeImages(t *testing.T) {
Metas: localMetas, Metas: localMetas,
}, input) }, input)
assert.NoError(t, err) 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{ buffer, err = markdown.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
Links: markup.Links{ Links: markup.Links{
@ -511,7 +511,7 @@ func TestRender_RelativeImages(t *testing.T) {
IsWiki: true, IsWiki: true,
}, input) }, input)
assert.NoError(t, err) 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") rawwiki := util.URLJoin(markup.TestRepoURL, "wiki", "raw")

View file

@ -6,6 +6,7 @@ package markdown
import ( import (
"fmt" "fmt"
"html/template"
"io" "io"
"strings" "strings"
"sync" "sync"
@ -266,12 +267,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 // 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 var buf strings.Builder
if err := Render(ctx, strings.NewReader(content), &buf); err != nil { if err := Render(ctx, strings.NewReader(content), &buf); err != nil {
return "", err return "", err
} }
return buf.String(), nil return template.HTML(buf.String()), nil
} }
// RenderRaw renders Markdown to HTML without handling special links. // RenderRaw renders Markdown to HTML without handling special links.

View file

@ -5,6 +5,7 @@ package markdown_test
import ( import (
"context" "context"
"html/template"
"os" "os"
"strings" "strings"
"testing" "testing"
@ -59,7 +60,7 @@ func TestRender_StandardLinks(t *testing.T) {
}, },
}, input) }, input)
assert.NoError(t, err) 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{ buffer, err = markdown.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
@ -69,7 +70,7 @@ func TestRender_StandardLinks(t *testing.T) {
IsWiki: true, IsWiki: true,
}, input) }, input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer)))
} }
googleRendered := `<p><a href="https://google.com/" rel="nofollow">https://google.com/</a></p>` googleRendered := `<p><a href="https://google.com/" rel="nofollow">https://google.com/</a></p>`
@ -94,7 +95,7 @@ func TestRender_Images(t *testing.T) {
}, },
}, input) }, input)
assert.NoError(t, err) 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" url := "../../.images/src/02/train.jpg"
@ -304,7 +305,7 @@ func TestTotal_RenderWiki(t *testing.T) {
IsWiki: true, IsWiki: true,
}, sameCases[i]) }, sameCases[i])
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, answers[i], line) assert.Equal(t, template.HTML(answers[i]), line)
} }
testCases := []string{ testCases := []string{
@ -329,7 +330,7 @@ func TestTotal_RenderWiki(t *testing.T) {
IsWiki: true, IsWiki: true,
}, testCases[i]) }, testCases[i])
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, testCases[i+1], line) assert.Equal(t, template.HTML(testCases[i+1]), line)
} }
} }
@ -349,7 +350,7 @@ func TestTotal_RenderString(t *testing.T) {
Metas: localMetas, Metas: localMetas,
}, sameCases[i]) }, sameCases[i])
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, answers[i], line) assert.Equal(t, template.HTML(answers[i]), line)
} }
testCases := []string{} testCases := []string{}
@ -362,7 +363,7 @@ func TestTotal_RenderString(t *testing.T) {
}, },
}, testCases[i]) }, testCases[i])
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, testCases[i+1], line) assert.Equal(t, template.HTML(testCases[i+1]), line)
} }
} }
@ -429,7 +430,7 @@ func TestRenderEmojiInLinks_Issue12331(t *testing.T) {
` `
res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, testcase) res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, testcase)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, expected, res) assert.Equal(t, template.HTML(expected), res)
} }
func TestColorPreview(t *testing.T) { func TestColorPreview(t *testing.T) {
@ -463,7 +464,7 @@ func TestColorPreview(t *testing.T) {
for _, test := range positiveTests { for _, test := range positiveTests {
res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase) res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase)
assert.NoError(t, err, "Unexpected error in testcase: %q", 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)
} }
@ -542,7 +543,7 @@ func TestMathBlock(t *testing.T) {
for _, test := range testcases { for _, test := range testcases {
res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase) res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase)
assert.NoError(t, err, "Unexpected error in testcase: %q", 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)
} }
} }
@ -741,7 +742,7 @@ Citation needed[^0].`,
for _, test := range testcases { for _, test := range testcases {
res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase) res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase)
assert.NoError(t, err, "Unexpected error in testcase: %q", 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, test.expected, string(res), "Unexpected result in testcase %q", test.testcase)
} }
} }
@ -778,12 +779,12 @@ foo: bar
for _, test := range testcases { for _, test := range testcases {
res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase) res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase)
assert.NoError(t, err, "Unexpected error in testcase: %q", 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) { func TestRenderLinks(t *testing.T) {
input := ` space @mention-user input := ` space @mention-user${SPACE}${SPACE}
/just/a/path.bin /just/a/path.bin
https://example.com/file.bin https://example.com/file.bin
[local link](file.bin) [local link](file.bin)
@ -804,8 +805,9 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
mail@domain.com mail@domain.com
@mention-user test @mention-user test
#123 #123
space space${SPACE}${SPACE}
` `
input = strings.ReplaceAll(input, "${SPACE}", " ") // replace ${SPACE} with " ", to avoid some editor's auto-trimming
cases := []struct { cases := []struct {
Links markup.Links Links markup.Links
IsWiki bool IsWiki bool
@ -1168,7 +1170,7 @@ space</p>
for i, c := range cases { for i, c := range cases {
result, err := markdown.RenderString(&markup.RenderContext{Ctx: context.Background(), Links: c.Links, IsWiki: c.IsWiki}, input) 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.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)
} }
} }
@ -1187,7 +1189,7 @@ func TestCustomMarkdownURL(t *testing.T) {
}, },
}, input) }, input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
} }
test("[test](abp:subscribe?location=https://codeberg.org/filters.txt&amp;title=joy)", test("[test](abp:subscribe?location=https://codeberg.org/filters.txt&amp;title=joy)",

View file

@ -27,6 +27,16 @@ func TestOption(t *testing.T) {
assert.Equal(t, int(1), some.Value()) assert.Equal(t, int(1), some.Value())
assert.Equal(t, int(1), some.ValueOrDefault(2)) 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 var ptr *int
assert.False(t, optional.FromPtr(ptr).Has()) assert.False(t, optional.FromPtr(ptr).Has())

View file

@ -60,6 +60,9 @@ func (q *WorkerPoolQueue[T]) doDispatchBatchToWorker(wg *workerGroup[T], flushCh
full = true 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() q.workerNumMu.Lock()
noWorker := q.workerNum == 0 noWorker := q.workerNum == 0
if full || noWorker { if full || noWorker {
@ -143,7 +146,11 @@ func (q *WorkerPoolQueue[T]) doStartNewWorker(wp *workerGroup[T]) {
log.Debug("Queue %q starts new worker", q.GetName()) log.Debug("Queue %q starts new worker", q.GetName())
defer log.Debug("Queue %q stops idle 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) t := time.NewTicker(workerIdleDuration)
defer t.Stop()
keepWorking := true keepWorking := true
stopWorking := func() { stopWorking := func() {
q.workerNumMu.Lock() q.workerNumMu.Lock()
@ -158,13 +165,18 @@ func (q *WorkerPoolQueue[T]) doStartNewWorker(wp *workerGroup[T]) {
case batch, ok := <-q.batchChan: case batch, ok := <-q.batchChan:
if !ok { if !ok {
stopWorking() stopWorking()
} else { continue
q.doWorkerHandle(batch) }
t.Reset(workerIdleDuration) 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: case <-t.C:
q.workerNumMu.Lock() q.workerNumMu.Lock()
keepWorking = q.workerNum <= 1 keepWorking = q.workerNum <= 1 // keep the last worker running
if !keepWorking { if !keepWorking {
q.workerNum-- q.workerNum--
} }

View file

@ -40,6 +40,8 @@ type WorkerPoolQueue[T any] struct {
workerMaxNum int workerMaxNum int
workerActiveNum int workerActiveNum int
workerNumMu sync.Mutex workerNumMu sync.Mutex
workerStartedCounter int32
} }
type flushType chan struct{} type flushType chan struct{}

View file

@ -11,6 +11,7 @@ import (
"time" "time"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -175,11 +176,7 @@ func testWorkerPoolQueuePersistence(t *testing.T, queueSetting setting.QueueSett
} }
func TestWorkerPoolQueueActiveWorkers(t *testing.T) { func TestWorkerPoolQueueActiveWorkers(t *testing.T) {
oldWorkerIdleDuration := workerIdleDuration defer test.MockVariableValue(&workerIdleDuration, 300*time.Millisecond)()
workerIdleDuration = 300 * time.Millisecond
defer func() {
workerIdleDuration = oldWorkerIdleDuration
}()
handler := func(items ...int) (unhandled []int) { handler := func(items ...int) (unhandled []int) {
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
@ -250,3 +247,25 @@ func TestWorkerPoolQueueShutdown(t *testing.T) {
q, _ = newWorkerPoolQueueForTest("test-workpoolqueue", qs, handler, false) q, _ = newWorkerPoolQueueForTest("test-workpoolqueue", qs, handler, false)
assert.EqualValues(t, 20, q.GetQueueItemNumber()) 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()
}

View file

@ -31,9 +31,9 @@ var (
// mentionPattern matches all mentions in the form of "@user" or "@org/team" // 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|[:,;.?!]?$|\)|\])`) 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 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 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 // crossReferenceIssueNumericPattern matches string that references a numeric issue in a different repository
// e.g. org/repo#12345 // e.g. org/repo#12345
crossReferenceIssueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-zA-Z-_\.]+/[0-9a-zA-Z-_\.]+[#!][0-9]+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`) crossReferenceIssueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-zA-Z-_\.]+/[0-9a-zA-Z-_\.]+[#!][0-9]+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`)

View file

@ -432,6 +432,8 @@ func TestRegExp_issueNumericPattern(t *testing.T) {
" #12", " #12",
"#12:", "#12:",
"ref: #12: msg", "ref: #12: msg",
"\"#1234\"",
"'#1234'",
} }
falseTestCases := []string{ falseTestCases := []string{
"# 1234", "# 1234",
@ -462,6 +464,8 @@ func TestRegExp_issueAlphanumericPattern(t *testing.T) {
"(ABC-123)", "(ABC-123)",
"[ABC-123]", "[ABC-123]",
"ABC-123:", "ABC-123:",
"\"ABC-123\"",
"'ABC-123'",
} }
falseTestCases := []string{ falseTestCases := []string{
"RC-08", "RC-08",

View file

@ -6,22 +6,18 @@ package repository
import ( import (
"context" "context"
"fmt" "fmt"
"os"
"path/filepath" "path/filepath"
"sort" "sort"
"strings" "strings"
"time"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo" 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/git"
"code.gitea.io/gitea/modules/label" "code.gitea.io/gitea/modules/label"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/options" "code.gitea.io/gitea/modules/options"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
asymkey_service "code.gitea.io/gitea/services/asymkey"
) )
type OptionFile struct { type OptionFile struct {
@ -124,70 +120,6 @@ func LoadRepoConfig() error {
return nil return nil
} }
// InitRepoCommit temporarily changes with work directory.
func InitRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Repository, u *user_model.User, defaultBranch string) (err error) {
commitTimeStr := time.Now().Format(time.RFC3339)
sig := u.NewGitSig()
// Because this may call hooks we should pass in the environment
env := append(os.Environ(),
"GIT_AUTHOR_NAME="+sig.Name,
"GIT_AUTHOR_EMAIL="+sig.Email,
"GIT_AUTHOR_DATE="+commitTimeStr,
"GIT_COMMITTER_DATE="+commitTimeStr,
)
committerName := sig.Name
committerEmail := sig.Email
if stdout, _, err := git.NewCommand(ctx, "add", "--all").
SetDescription(fmt.Sprintf("initRepoCommit (git add): %s", tmpPath)).
RunStdString(&git.RunOpts{Dir: tmpPath}); err != nil {
log.Error("git add --all failed: Stdout: %s\nError: %v", stdout, err)
return fmt.Errorf("git add --all: %w", err)
}
cmd := git.NewCommand(ctx, "commit", "--message=Initial commit").
AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email)
sign, keyID, signer, _ := asymkey_service.SignInitialCommit(ctx, tmpPath, u)
if sign {
cmd.AddOptionFormat("-S%s", keyID)
if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
// need to set the committer to the KeyID owner
committerName = signer.Name
committerEmail = signer.Email
}
} else {
cmd.AddArguments("--no-gpg-sign")
}
env = append(env,
"GIT_COMMITTER_NAME="+committerName,
"GIT_COMMITTER_EMAIL="+committerEmail,
)
if stdout, _, err := cmd.
SetDescription(fmt.Sprintf("initRepoCommit (git commit): %s", tmpPath)).
RunStdString(&git.RunOpts{Dir: tmpPath, Env: env}); err != nil {
log.Error("Failed to commit: %v: Stdout: %s\nError: %v", cmd.String(), stdout, err)
return fmt.Errorf("git commit: %w", err)
}
if len(defaultBranch) == 0 {
defaultBranch = setting.Repository.DefaultBranch
}
if stdout, _, err := git.NewCommand(ctx, "push", "origin").AddDynamicArguments("HEAD:" + defaultBranch).
SetDescription(fmt.Sprintf("initRepoCommit (git push): %s", tmpPath)).
RunStdString(&git.RunOpts{Dir: tmpPath, Env: InternalPushingEnvironment(u, repo)}); err != nil {
log.Error("Failed to push back to HEAD: Stdout: %s\nError: %v", stdout, err)
return fmt.Errorf("git push: %w", err)
}
return nil
}
func CheckInitRepository(ctx context.Context, owner, name, objectFormatName string) (err error) { func CheckInitRepository(ctx context.Context, owner, name, objectFormatName string) (err error) {
// Somehow the directory could exist. // Somehow the directory could exist.
repoPath := repo_model.RepoPath(owner, name) repoPath := repo_model.RepoPath(owner, name)

View file

@ -21,5 +21,6 @@ func loadAdminFrom(rootCfg ConfigProvider) {
} }
const ( const (
UserFeatureDeletion = "deletion" UserFeatureDeletion = "deletion"
UserFeatureManageGPGKeys = "manage_gpg_keys"
) )

View file

@ -21,7 +21,7 @@ var SessionConfig = struct {
ProviderConfig string ProviderConfig string
// Cookie name to save session ID. Default is "MacaronSession". // Cookie name to save session ID. Default is "MacaronSession".
CookieName string 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 CookiePath string
// GC interval time in seconds. Default is 3600. // GC interval time in seconds. Default is 3600.
Gclifetime int64 Gclifetime int64
@ -49,7 +49,10 @@ func loadSessionFrom(rootCfg ConfigProvider) {
SessionConfig.ProviderConfig = path.Join(AppWorkPath, SessionConfig.ProviderConfig) SessionConfig.ProviderConfig = path.Join(AppWorkPath, SessionConfig.ProviderConfig)
} }
SessionConfig.CookieName = sec.Key("COOKIE_NAME").MustString("i_like_gitea") 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.Secure = sec.Key("COOKIE_SECURE").MustBool(strings.HasPrefix(strings.ToLower(AppURL), "https://"))
SessionConfig.Gclifetime = sec.Key("GC_INTERVAL_TIME").MustInt64(86400) SessionConfig.Gclifetime = sec.Key("GC_INTERVAL_TIME").MustInt64(86400)
SessionConfig.Maxlifetime = sec.Key("SESSION_LIFE_TIME").MustInt64(86400) SessionConfig.Maxlifetime = sec.Key("SESSION_LIFE_TIME").MustInt64(86400)

View file

@ -6,6 +6,7 @@ package structs
import ( import (
"fmt" "fmt"
"path" "path"
"slices"
"strings" "strings"
"time" "time"
@ -143,12 +144,37 @@ const (
// IssueFormField represents a form field // IssueFormField represents a form field
// swagger:model // swagger:model
type IssueFormField struct { type IssueFormField struct {
Type IssueFormFieldType `json:"type" yaml:"type"` Type IssueFormFieldType `json:"type" yaml:"type"`
ID string `json:"id" yaml:"id"` ID string `json:"id" yaml:"id"`
Attributes map[string]any `json:"attributes" yaml:"attributes"` Attributes map[string]any `json:"attributes" yaml:"attributes"`
Validations map[string]any `json:"validations" yaml:"validations"` 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 // IssueTemplate represents an issue template for a repository
// swagger:model // swagger:model
type IssueTemplate struct { type IssueTemplate struct {

View file

@ -33,16 +33,16 @@ func NewFuncMap() template.FuncMap {
// ----------------------------------------------------------------- // -----------------------------------------------------------------
// html/template related functions // html/template related functions
"dict": dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names. "dict": dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names.
"Eval": Eval, "Eval": Eval,
"SafeHTML": SafeHTML, "SafeHTML": SafeHTML,
"HTMLFormat": HTMLFormat, "HTMLFormat": HTMLFormat,
"HTMLEscape": HTMLEscape, "HTMLEscape": HTMLEscape,
"QueryEscape": url.QueryEscape, "QueryEscape": url.QueryEscape,
"JSEscape": JSEscapeSafe, "JSEscape": JSEscapeSafe,
"Str2html": Str2html, // TODO: rename it to SanitizeHTML "SanitizeHTML": SanitizeHTML,
"URLJoin": util.URLJoin, "URLJoin": util.URLJoin,
"DotEscape": DotEscape, "DotEscape": DotEscape,
"PathEscape": url.PathEscape, "PathEscape": url.PathEscape,
"PathEscapeSegments": util.PathEscapeSegments, "PathEscapeSegments": util.PathEscapeSegments,
@ -210,8 +210,8 @@ func SafeHTML(s any) template.HTML {
panic(fmt.Sprintf("unexpected type %T", s)) panic(fmt.Sprintf("unexpected type %T", s))
} }
// Str2html sanitizes the input by pre-defined markdown rules // SanitizeHTML sanitizes the input by pre-defined markdown rules
func Str2html(s any) template.HTML { func SanitizeHTML(s any) template.HTML {
switch v := s.(type) { switch v := s.(type) {
case string: case string:
return template.HTML(markup.Sanitize(v)) return template.HTML(markup.Sanitize(v))

View file

@ -61,3 +61,8 @@ func TestJSEscapeSafe(t *testing.T) {
func TestHTMLFormat(t *testing.T) { func TestHTMLFormat(t *testing.T) {
assert.Equal(t, template.HTML("<a>&lt; < 1</a>"), HTMLFormat("<a>%s %s %d</a>", "<", template.HTML("<"), 1)) assert.Equal(t, template.HTML("<a>&lt; < 1</a>"), HTMLFormat("<a>%s %s %d</a>", "<", template.HTML("<"), 1))
} }
func TestSanitizeHTML(t *testing.T) {
assert.Equal(t, template.HTML(`<a href="/" rel="nofollow">link</a> xss <div>inline</div>`), SanitizeHTML(`<a href="/">link</a> <a href="javascript:">xss</a> <div style="dangerous">inline</div>`))
assert.Equal(t, template.HTML(`<a href="/" rel="nofollow">link</a> xss <div>inline</div>`), SanitizeHTML(template.HTML(`<a href="/">link</a> <a href="javascript:">xss</a> <div style="dangerous">inline</div>`)))
}

View file

@ -5,6 +5,7 @@ package templates
import ( import (
"context" "context"
"fmt"
"html/template" "html/template"
"regexp" "regexp"
"strings" "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 // Split template into subject and body
var subjectContent []byte var subjectContent []byte
bodyContent := content bodyContent := content
@ -42,14 +43,13 @@ func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template,
subjectContent = content[0:loc[0]] subjectContent = content[0:loc[0]]
bodyContent = content[loc[1]:] bodyContent = content[loc[1]:]
} }
if _, err := stpl.New(name). if _, err := stpl.New(name).Parse(string(subjectContent)); err != nil {
Parse(string(subjectContent)); err != nil { return fmt.Errorf("failed to parse template [%s/subject]: %w", name, err)
log.Warn("Failed to parse template [%s/subject]: %v", name, err)
} }
if _, err := btpl.New(name). if _, err := btpl.New(name).Parse(string(bodyContent)); err != nil {
Parse(string(bodyContent)); err != nil { return fmt.Errorf("failed to parse template [%s/body]: %w", name, err)
log.Warn("Failed to parse template [%s/body]: %v", name, err)
} }
return nil
} }
// Mailer provides the templates required for sending notification mails. // Mailer provides the templates required for sending notification mails.
@ -81,7 +81,13 @@ func Mailer(ctx context.Context) (*texttmpl.Template, *template.Template) {
if firstRun { if firstRun {
log.Trace("Adding mail template %s: %s by %s", tmplName, assetPath, layerName) 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)
}
}
} }
} }

View file

@ -208,7 +208,7 @@ func RenderMarkdownToHtml(ctx context.Context, input string) template.HTML { //n
if err != nil { if err != nil {
log.Error("RenderString: %v", err) log.Error("RenderString: %v", err)
} }
return template.HTML(output) return output
} }
func RenderLabels(ctx context.Context, labels []*issues_model.Label, repoLink string) template.HTML { func RenderLabels(ctx context.Context, labels []*issues_model.Label, repoLink string) template.HTML {

View file

@ -4,6 +4,7 @@
package templates package templates
import ( import (
"fmt"
"html/template" "html/template"
"strings" "strings"
@ -28,6 +29,19 @@ func (su *StringUtils) HasPrefix(s any, prefix string) bool {
return false return false
} }
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) Contains(s, substr string) bool { func (su *StringUtils) Contains(s, substr string) bool {
return strings.Contains(s, substr) return strings.Contains(s, substr)
} }

View file

@ -9,7 +9,9 @@ import (
) )
// MockLocale provides a mocked locale without any translations // 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) var _ Locale = (*MockLocale)(nil)

View file

@ -144,7 +144,7 @@ func Match(tags ...language.Tag) language.Tag {
// locale represents the information of localization. // locale represents the information of localization.
type locale struct { type locale struct {
i18n.Locale 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 msgPrinter *message.Printer
} }

View file

@ -17,64 +17,13 @@ import (
"golang.org/x/text/language" "golang.org/x/text/language"
) )
// OptionalBool a boolean that can be "null" // OptionalBoolParse get the corresponding optional.Option[bool] of a string using strconv.ParseBool
type OptionalBool byte func OptionalBoolParse(s string) optional.Option[bool] {
v, e := strconv.ParseBool(s)
const ( if e != nil {
// 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() {
return optional.None[bool]() return optional.None[bool]()
} }
return optional.Some[bool](o.IsTrue()) return optional.Some(v)
}
// 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)
} }
// IsEmptyString checks if the provided string is empty // IsEmptyString checks if the provided string is empty

View file

@ -8,6 +8,8 @@ import (
"strings" "strings"
"testing" "testing"
"code.gitea.io/gitea/modules/optional"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -173,17 +175,17 @@ func Test_RandomBytes(t *testing.T) {
assert.NotEqual(t, bytes3, bytes4) assert.NotEqual(t, bytes3, bytes4)
} }
func Test_OptionalBool(t *testing.T) { func TestOptionalBoolParse(t *testing.T) {
assert.Equal(t, OptionalBoolNone, OptionalBoolParse("")) assert.Equal(t, optional.None[bool](), OptionalBoolParse(""))
assert.Equal(t, OptionalBoolNone, OptionalBoolParse("x")) assert.Equal(t, optional.None[bool](), OptionalBoolParse("x"))
assert.Equal(t, OptionalBoolFalse, OptionalBoolParse("0")) assert.Equal(t, optional.Some(false), OptionalBoolParse("0"))
assert.Equal(t, OptionalBoolFalse, OptionalBoolParse("f")) assert.Equal(t, optional.Some(false), OptionalBoolParse("f"))
assert.Equal(t, OptionalBoolFalse, OptionalBoolParse("False")) assert.Equal(t, optional.Some(false), OptionalBoolParse("False"))
assert.Equal(t, OptionalBoolTrue, OptionalBoolParse("1")) assert.Equal(t, optional.Some(true), OptionalBoolParse("1"))
assert.Equal(t, OptionalBoolTrue, OptionalBoolParse("t")) assert.Equal(t, optional.Some(true), OptionalBoolParse("t"))
assert.Equal(t, OptionalBoolTrue, OptionalBoolParse("True")) assert.Equal(t, optional.Some(true), OptionalBoolParse("True"))
} }
// Test case for any function which accepts and returns a single string. // Test case for any function which accepts and returns a single string.

View file

@ -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.

View file

@ -124,6 +124,7 @@ pin=Připnout
unpin=Odepnout unpin=Odepnout
artifacts=Artefakty artifacts=Artefakty
confirm_delete_artifact=Jste si jisti, že chcete odstranit artefakt „%s“?
archived=Archivováno archived=Archivováno
@ -434,7 +435,7 @@ password_pwned_err=Nelze dokončit požadavek na HaveIBeenPwned
change_unconfirmed_email = Pokud jste při registraci zadali nesprávnou e-mailovou adresu, můžete ji změnit níže. Potvrzovací e-mail bude místo toho odeslán na novou adresu. change_unconfirmed_email = Pokud jste při registraci zadali nesprávnou e-mailovou adresu, můžete ji změnit níže. Potvrzovací e-mail bude místo toho odeslán na novou adresu.
change_unconfirmed_email_error = Nepodařilo se změnit e-mailovou adresu: %v change_unconfirmed_email_error = Nepodařilo se změnit e-mailovou adresu: %v
change_unconfirmed_email_summary = Změna e-mailové adresy, na kterou bude odeslán aktivační e-mail. change_unconfirmed_email_summary = Změna e-mailové adresy, na kterou bude odeslán aktivační e-mail.
last_admin = Nemůžete odebrat posledního administrátora. Vždy musí existovat alespoň jeden administrátor. last_admin=Nelze odstranit posledního správce. Musí existovat alespoň jeden správce.
[mail] [mail]
view_it_on=Zobrazit na %s view_it_on=Zobrazit na %s
@ -605,6 +606,7 @@ target_branch_not_exist=Cílová větev neexistuje.
admin_cannot_delete_self = Nemůžete odstranit sami sebe, když jste administrátorem. Nejprve prosím odeberte svá práva administrátora. admin_cannot_delete_self = Nemůžete odstranit sami sebe, když jste administrátorem. Nejprve prosím odeberte svá práva administrátora.
username_error_no_dots = ` může obsahovat pouze alfanumerické znaky („0-9“, „a-z“, „A-Z“), pomlčky („-“) a podtržítka („_“). Nemůže začínat nebo končit nealfanumerickými znaky. Jsou také zakázány po sobě jdoucí nealfanumerické znaky.` username_error_no_dots = ` může obsahovat pouze alfanumerické znaky („0-9“, „a-z“, „A-Z“), pomlčky („-“) a podtržítka („_“). Nemůže začínat nebo končit nealfanumerickými znaky. Jsou také zakázány po sobě jdoucí nealfanumerické znaky.`
admin_cannot_delete_self=Nemůžete se smazat, dokud jste správce. Nejdříve prosím odeberte svá administrátorská oprávnění.
[user] [user]
change_avatar=Změnit váš avatar… change_avatar=Změnit váš avatar…
@ -999,6 +1001,8 @@ issue_labels_helper=Vyberte sadu štítků úkolů.
license=Licence license=Licence
license_helper=Vyberte licenční soubor. 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 <a target="_blank" rel="noopener noreferrer" href="%s">Zvolte licenci</a> 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 <a target="_blank" rel="noopener noreferrer" href="%s">Zvolte licenci</a>
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=README
readme_helper=Vyberte šablonu souboru README. readme_helper=Vyberte šablonu souboru README.
readme_helper_desc=Toto je místo, kde můžete napsat úplný popis vašeho projektu. readme_helper_desc=Toto je místo, kde můžete napsat úplný popis vašeho projektu.
@ -1065,6 +1069,7 @@ desc.public=Veřejný
desc.template=Šablona desc.template=Šablona
desc.internal=Interní desc.internal=Interní
desc.archived=Archivováno desc.archived=Archivováno
desc.sha256=SHA256
template.items=Položky šablony template.items=Položky šablony
template.git_content=Obsah gitu (výchozí větev) template.git_content=Obsah gitu (výchozí větev)
@ -1215,6 +1220,8 @@ audio_not_supported_in_browser=Váš prohlížeč nepodporuje značku HTML5 „a
stored_lfs=Uloženo pomocí Git LFS stored_lfs=Uloženo pomocí Git LFS
symbolic_link=Symbolický odkaz symbolic_link=Symbolický odkaz
executable_file=Spustitelný soubor executable_file=Spustitelný soubor
vendored=Vendorováno
generated=Generováno
commit_graph=Graf commitů commit_graph=Graf commitů
commit_graph.select=Vybrat větve commit_graph.select=Vybrat větve
commit_graph.hide_pr_refs=Skrýt požadavky na natažení commit_graph.hide_pr_refs=Skrýt požadavky na natažení
@ -1550,7 +1557,11 @@ issues.label_title=Název štítku
issues.label_description=Popis štítku issues.label_description=Popis štítku
issues.label_color=Barva štítku issues.label_color=Barva štítku
issues.label_exclusive=Exkluzivní issues.label_exclusive=Exkluzivní
issues.label_archive=Archivovat štítek
issues.label_archived_filter=Zobrazit archivované popisky 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 <code>rozsah/položka</code>, aby se stal vzájemně exkluzivním s jinými štítky <code>rozsah/</code>.
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_count=%d štítků
issues.label_open_issues=%d otevřených úkolů issues.label_open_issues=%d otevřených úkolů
issues.label_edit=Upravit issues.label_edit=Upravit
@ -1651,6 +1662,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.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.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_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.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.blocks_short=Blokuje
issues.dependency.blocked_by_short=Závisí na issues.dependency.blocked_by_short=Závisí na
@ -1732,6 +1744,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.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.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=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.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: <a href="%[1]s">%[2]s#%[3]d</a>` pulls.has_pull_request=`Požadavek na natažení mezi těmito větvemi již existuje: <a href="%[1]s">%[2]s#%[3]d</a>`
pulls.create=Vytvořit požadavek na natažení pulls.create=Vytvořit požadavek na natažení
@ -1854,6 +1867,7 @@ milestones.update_ago=Aktualizováno %s
milestones.no_due_date=Bez lhůty dokončení milestones.no_due_date=Bez lhůty dokončení
milestones.open=Otevřít milestones.open=Otevřít
milestones.close=Zavří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.completeness=%d%% Dokončeno
milestones.create=Vytvořit milník milestones.create=Vytvořit milník
milestones.title=Název milestones.title=Název
@ -1987,6 +2001,7 @@ activity.git_stats_and_deletions=a
activity.git_stats_deletion_1=%d odebrání activity.git_stats_deletion_1=%d odebrání
activity.git_stats_deletion_n=%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 contributors.contribution_type.commits=Commity
search=Vyhledat search=Vyhledat
@ -2374,6 +2389,7 @@ settings.matrix.room_id=ID místnosti
settings.matrix.message_type=Typ zprávy settings.matrix.message_type=Typ zprávy
settings.archive.button=Archivovat repozitář settings.archive.button=Archivovat repozitář
settings.archive.header=Archivovat tento 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.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=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ář. settings.archive.error_ismirror=Nemůžete archivovat zrcadlený repozitář.
@ -2835,6 +2851,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=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_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.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.update_mirrors=Aktualizovat zrcadla
dashboard.repo_health_check=Kontrola stavu všech repozitářů dashboard.repo_health_check=Kontrola stavu všech repozitářů
dashboard.check_repo_stats=Zkontrolovat všechny statistiky repositáře dashboard.check_repo_stats=Zkontrolovat všechny statistiky repositáře
@ -2882,11 +2899,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.delete_old_actions.started=Začalo odstraňování všech starých akcí z databáze.
dashboard.update_checker=Kontrola aktualizací dashboard.update_checker=Kontrola aktualizací
dashboard.delete_old_system_notices=Odstranit všechna stará systémová upozornění z databáze 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_zombie_tasks=Zastavit zombie úlohy
dashboard.stop_endless_tasks=Zastavit nekonečné úlohy dashboard.stop_endless_tasks=Zastavit nekonečné úlohy
dashboard.cancel_abandoned_jobs=Zrušit opuštěné úlohy dashboard.cancel_abandoned_jobs=Zrušit opuštěné úlohy
dashboard.start_schedule_tasks=Spustit naplánované úlohy dashboard.start_schedule_tasks=Spustit naplánované úlohy
dashboard.sync_branch.started=Synchronizace větví byla spuštěna 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.user_manage_panel=Správa uživatelských účtů
users.new_account=Vytvořit uživatelský účet users.new_account=Vytvořit uživatelský účet
@ -3326,6 +3346,12 @@ self_check.database_fix_mssql = Uživatelé MSSQL mohou tento problém vyřešit
auths.oauth2_map_group_to_team = Zmapovat zabrané skupiny u týmů organizací (volitelné - vyžaduje název zabrání výše) auths.oauth2_map_group_to_team = Zmapovat zabrané skupiny u týmů organizací (volitelné - vyžaduje název zabrání výše)
monitor.queue.settings.desc = Pooly dynamicky rostou podle blokování fronty jejich workerů. monitor.queue.settings.desc = Pooly dynamicky rostou podle blokování fronty jejich workerů.
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] [action]
create_repo=vytvořil/a repozitář <a href="%s">%s</a> create_repo=vytvořil/a repozitář <a href="%s">%s</a>
@ -3513,6 +3539,7 @@ rpm.distros.suse=na distribuce založené na SUSE
rpm.install=Pro instalaci balíčku spusťte následující příkaz: rpm.install=Pro instalaci balíčku spusťte následující příkaz:
rpm.repository=Informace o repozitáři rpm.repository=Informace o repozitáři
rpm.repository.architectures=Architektury 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.install=Pro instalaci balíčku pomocí gem spusťte následující příkaz:
rubygems.install2=nebo ho přidejte do Gemfie: rubygems.install2=nebo ho přidejte do Gemfie:
rubygems.dependencies.runtime=Běhové závislosti rubygems.dependencies.runtime=Běhové závislosti
@ -3642,6 +3669,8 @@ runs.actors_no_select=Všichni aktéři
runs.status_no_select=Všechny stavy runs.status_no_select=Všechny stavy
runs.no_results=Nebyly nalezeny žádné výsledky. runs.no_results=Nebyly nalezeny žádné výsledky.
runs.no_workflows=Zatím neexistují žádné pracovní postupy. 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 <a target="_blank" rel="noopener noreferrer" href="%s">průvodce rychlým startem</a>.
runs.no_workflows.documentation=Další informace o Gitea Actions naleznete v <a target="_blank" rel="noopener noreferrer" href="%s">dokumentaci</a>.
runs.no_runs=Pracovní postup zatím nebyl spuštěn. runs.no_runs=Pracovní postup zatím nebyl spuštěn.
runs.empty_commit_message=(prázdná zpráva commitu) runs.empty_commit_message=(prázdná zpráva commitu)
@ -3659,6 +3688,7 @@ variables.none=Zatím nejsou žádné proměnné.
variables.deletion=Odstranit proměnnou variables.deletion=Odstranit proměnnou
variables.deletion.description=Odstranění proměnné je trvalé a nelze jej vrátit zpět. Pokračovat? 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.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.edit=Upravit proměnnou
variables.deletion.failed=Nepodařilo se odstranit proměnnou. variables.deletion.failed=Nepodařilo se odstranit proměnnou.
variables.deletion.success=Proměnná byla odstraněna. variables.deletion.success=Proměnná byla odstraněna.

View file

@ -143,6 +143,19 @@ confirm_delete_selected = Confirm to delete all selected items?
name = Name name = Name
value = Value 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] [aria]
navbar = Navigation Bar navbar = Navigation Bar
footer = Footer footer = Footer
@ -1834,9 +1847,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.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.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.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_summary = Full Rejection Message
pulls.push_rejected_no_message = Merge Failed: The push was rejected but there was no remote message.<br>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.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_checking = Some checks are pending
pulls.status_checks_success = All checks were successful pulls.status_checks_success = All checks were successful

View file

@ -124,6 +124,7 @@ pin=Épingler
unpin=Désépingler unpin=Désépingler
artifacts=Artefacts artifacts=Artefacts
confirm_delete_artifact=Êtes-vous sûr de vouloir supprimer lartefact « %s » ?
archived=Archivé archived=Archivé
@ -366,6 +367,7 @@ disable_register_prompt=Les inscriptions sont désactivées. Veuillez contacter
disable_register_mail=La confirmation par courriel à linscription est désactivée. disable_register_mail=La confirmation par courriel à linscription est désactivée.
manual_activation_only=Contactez l'administrateur de votre site pour terminer l'activation. manual_activation_only=Contactez l'administrateur de votre site pour terminer l'activation.
remember_me=Mémoriser cet appareil remember_me=Mémoriser cet appareil
remember_me.compromised=Le jeton de connexion nest plus valide, ce qui peut indiquer un compte compromis. Veuillez inspecter les activités inhabituelles de votre compte.
forgot_password_title=Mot de passe oublié forgot_password_title=Mot de passe oublié
forgot_password=Mot de passe oublié ? forgot_password=Mot de passe oublié ?
sign_up_now=Pas de compte ? Inscrivez-vous maintenant. sign_up_now=Pas de compte ? Inscrivez-vous maintenant.
@ -602,6 +604,7 @@ target_branch_not_exist=La branche cible n'existe pas.
username_error_no_dots = ` peut uniquement contenir des caractères alphanumériques ('0-9','a-z','A-Z'), tiret ('-') et souligné ('_'). Ne peut commencer ou terminer avec un caractère non-alphanumérique, et l'utilisation de caractères non-alphanumériques consécutifs n'est pas permise.` username_error_no_dots = ` peut uniquement contenir des caractères alphanumériques ('0-9','a-z','A-Z'), tiret ('-') et souligné ('_'). Ne peut commencer ou terminer avec un caractère non-alphanumérique, et l'utilisation de caractères non-alphanumériques consécutifs n'est pas permise.`
admin_cannot_delete_self = Vous ne pouvez supprimer votre compte lorsque vous disposez de droits d'administration. Veuillez d'abord renoncer à vos droits d'administration. admin_cannot_delete_self = Vous ne pouvez supprimer votre compte lorsque vous disposez de droits d'administration. Veuillez d'abord renoncer à vos droits d'administration.
admin_cannot_delete_self=Vous ne pouvez pas vous supprimer vous-même lorsque vous êtes admin. Veuillez dabord supprimer vos privilèges dadministrateur.
[user] [user]
change_avatar=Changer votre avatar… change_avatar=Changer votre avatar…
@ -817,7 +820,7 @@ valid_until_date=Valable jusqu'au %s
valid_forever=Valide pour toujours valid_forever=Valide pour toujours
last_used=Dernière utilisation le last_used=Dernière utilisation le
no_activity=Aucune activité récente no_activity=Aucune activité récente
can_read_info=Lue(s) can_read_info=Lecture
can_write_info=Écriture can_write_info=Écriture
key_state_desc=Cette clé a été utilisée au cours des 7 derniers jours key_state_desc=Cette clé a été utilisée au cours des 7 derniers jours
token_state_desc=Ce jeton a été utilisé au cours des 7 derniers jours token_state_desc=Ce jeton a été utilisé au cours des 7 derniers jours
@ -850,7 +853,7 @@ permissions_public_only=Publique uniquement
permissions_access_all=Tout (public, privé et limité) permissions_access_all=Tout (public, privé et limité)
select_permissions=Sélectionner les autorisations select_permissions=Sélectionner les autorisations
permission_no_access=Aucun accès permission_no_access=Aucun accès
permission_read=Lue(s) permission_read=Lecture
permission_write=Lecture et écriture permission_write=Lecture et écriture
access_token_desc=Les autorisations des jetons sélectionnées se limitent aux <a %s>routes API</a> correspondantes. Lisez la <a %s>documentation</a> pour plus dinformations. access_token_desc=Les autorisations des jetons sélectionnées se limitent aux <a %s>routes API</a> correspondantes. Lisez la <a %s>documentation</a> pour plus dinformations.
at_least_one_permission=Vous devez sélectionner au moins une permission pour créer un jeton at_least_one_permission=Vous devez sélectionner au moins une permission pour créer un jeton
@ -1013,6 +1016,7 @@ mirror_prune=Purger
mirror_prune_desc=Supprimer les références externes obsolètes mirror_prune_desc=Supprimer les références externes obsolètes
mirror_interval=Intervalle de synchronisation (les unités de temps valides sont 'h', 'm' et 's'). 0 pour désactiver la synchronisation automatique. (Intervalle minimum : %s) mirror_interval=Intervalle de synchronisation (les unités de temps valides sont 'h', 'm' et 's'). 0 pour désactiver la synchronisation automatique. (Intervalle minimum : %s)
mirror_interval_invalid=L'intervalle de synchronisation est invalide. mirror_interval_invalid=L'intervalle de synchronisation est invalide.
mirror_sync=synchronisé
mirror_sync_on_commit=Synchroniser quand les révisions sont soumis mirror_sync_on_commit=Synchroniser quand les révisions sont soumis
mirror_address=Cloner depuis une URL mirror_address=Cloner depuis une URL
mirror_address_desc=Insérez tous les identifiants requis dans la section Autorisation. mirror_address_desc=Insérez tous les identifiants requis dans la section Autorisation.
@ -1063,6 +1067,7 @@ desc.public=Publique
desc.template=Modèle desc.template=Modèle
desc.internal=Interne desc.internal=Interne
desc.archived=Archivé desc.archived=Archivé
desc.sha256=SHA256
template.items=Élément du modèle template.items=Élément du modèle
template.git_content=Contenu Git (branche par défaut) template.git_content=Contenu Git (branche par défaut)
@ -1213,6 +1218,8 @@ audio_not_supported_in_browser=Votre navigateur ne supporte pas la balise « au
stored_lfs=Stocké avec Git LFS stored_lfs=Stocké avec Git LFS
symbolic_link=Lien symbolique symbolic_link=Lien symbolique
executable_file=Fichiers exécutables executable_file=Fichiers exécutables
vendored=Externe
generated=Générée
commit_graph=Graphe des révisions commit_graph=Graphe des révisions
commit_graph.select=Sélectionner les branches commit_graph.select=Sélectionner les branches
commit_graph.hide_pr_refs=Masquer les demandes d'ajout commit_graph.hide_pr_refs=Masquer les demandes d'ajout
@ -1794,6 +1801,7 @@ pulls.merge_pull_request=Créer une révision de fusion
pulls.rebase_merge_pull_request=Rebaser puis avancer rapidement pulls.rebase_merge_pull_request=Rebaser puis avancer rapidement
pulls.rebase_merge_commit_pull_request=Rebaser puis créer une révision de fusion pulls.rebase_merge_commit_pull_request=Rebaser puis créer une révision de fusion
pulls.squash_merge_pull_request=Créer une révision de concaténation pulls.squash_merge_pull_request=Créer une révision de concaténation
pulls.fast_forward_only_merge_pull_request=Avance rapide uniquement
pulls.merge_manually=Fusionner manuellement pulls.merge_manually=Fusionner manuellement
pulls.merge_commit_id=L'ID de la révision de fusion pulls.merge_commit_id=L'ID de la révision de fusion
pulls.require_signed_wont_sign=La branche nécessite des révisions signées mais cette fusion ne sera pas signée pulls.require_signed_wont_sign=La branche nécessite des révisions signées mais cette fusion ne sera pas signée
@ -1930,6 +1938,7 @@ wiki.page_name_desc=Entrez un nom pour cette page Wiki. Certains noms spéciaux
wiki.original_git_entry_tooltip=Voir le fichier Git original au lieu d'utiliser un lien convivial. wiki.original_git_entry_tooltip=Voir le fichier Git original au lieu d'utiliser un lien convivial.
activity=Activité activity=Activité
activity.navbar.contributors=Contributeurs
activity.period.filter_label=Période : activity.period.filter_label=Période :
activity.period.daily=1 jour activity.period.daily=1 jour
activity.period.halfweekly=3 jours activity.period.halfweekly=3 jours
@ -1995,7 +2004,10 @@ activity.git_stats_and_deletions=et
activity.git_stats_deletion_1=%d suppression activity.git_stats_deletion_1=%d suppression
activity.git_stats_deletion_n=%d suppressions activity.git_stats_deletion_n=%d suppressions
contributors.contribution_type.filter_label=Type de contribution :
contributors.contribution_type.commits=Révisions contributors.contribution_type.commits=Révisions
contributors.contribution_type.additions=Ajouts
contributors.contribution_type.deletions=Suppressions
search=Chercher search=Chercher
search.search_repo=Rechercher dans le dépôt search.search_repo=Rechercher dans le dépôt
@ -2344,6 +2356,8 @@ settings.protect_approvals_whitelist_users=Évaluateurs autorisés :
settings.protect_approvals_whitelist_teams=Équipes dévaluateurs autorisés : settings.protect_approvals_whitelist_teams=Équipes dévaluateurs autorisés :
settings.dismiss_stale_approvals=Révoquer automatiquement les approbations périmées settings.dismiss_stale_approvals=Révoquer automatiquement les approbations périmées
settings.dismiss_stale_approvals_desc=Lorsque des nouvelles révisions changent le contenu de la demande dajout, les approbations existantes sont révoquées. settings.dismiss_stale_approvals_desc=Lorsque des nouvelles révisions changent le contenu de la demande dajout, les approbations existantes sont révoquées.
settings.ignore_stale_approvals=Ignorer les approbations obsolètes
settings.ignore_stale_approvals_desc=Ignorer les approbations danciennes révisions (évaluations obsolètes) du décompte des approbations de la demande dajout. Non pertinent quand les évaluations obsolètes sont déjà révoquées.
settings.require_signed_commits=Exiger des révisions signées settings.require_signed_commits=Exiger des révisions signées
settings.require_signed_commits_desc=Rejeter les soumissions sur cette branche lorsqu'ils ne sont pas signés ou vérifiables. settings.require_signed_commits_desc=Rejeter les soumissions sur cette branche lorsqu'ils ne sont pas signés ou vérifiables.
settings.protect_branch_name_pattern=Motif de nom de branche protégé settings.protect_branch_name_pattern=Motif de nom de branche protégé
@ -2399,6 +2413,7 @@ settings.archive.error=Une erreur s'est produite lors de l'archivage du dépôt.
settings.archive.error_ismirror=Vous ne pouvez pas archiver un dépôt en miroir. settings.archive.error_ismirror=Vous ne pouvez pas archiver un dépôt en miroir.
settings.archive.branchsettings_unavailable=Le paramétrage des branches n'est pas disponible quand le dépôt est archivé. settings.archive.branchsettings_unavailable=Le paramétrage des branches n'est pas disponible quand le dépôt est archivé.
settings.archive.tagsettings_unavailable=Le paramétrage des étiquettes n'est pas disponible si le dépôt est archivé. settings.archive.tagsettings_unavailable=Le paramétrage des étiquettes n'est pas disponible si le dépôt est archivé.
settings.archive.mirrors_unavailable=Les miroirs ne sont pas disponibles lorsque le dépôt est archivé.
settings.unarchive.button=Réhabiliter settings.unarchive.button=Réhabiliter
settings.unarchive.header=Réhabiliter ce dépôt settings.unarchive.header=Réhabiliter ce dépôt
settings.unarchive.text=Réhabiliter un dépôt dégèle les actions de révisions et de soumissions, la gestion des tickets et des demandes d'ajouts. settings.unarchive.text=Réhabiliter un dépôt dégèle les actions de révisions et de soumissions, la gestion des tickets et des demandes d'ajouts.
@ -2652,6 +2667,11 @@ activity.navbar.code_frequency = Fréquence de code
activity.navbar.recent_commits = Commits récents activity.navbar.recent_commits = Commits récents
[graphs] [graphs]
component_loading=Chargement de %s…
component_loading_failed=Impossible de charger %s.
component_loading_info=Ça prend son temps…
component_failed_to_load=Une erreur inattendue sest produite.
contributors.what=contributions
[org] [org]
org_name_holder=Nom de l'organisation org_name_holder=Nom de l'organisation
@ -2780,6 +2800,7 @@ follow_blocked_user = Vous ne pouvez pas suivre cette organisation car elle vous
[admin] [admin]
dashboard=Tableau de bord dashboard=Tableau de bord
self_check=Autodiagnostique
identity_access=Identité et accès identity_access=Identité et accès
users=Comptes utilisateurs users=Comptes utilisateurs
organizations=Organisations organizations=Organisations
@ -2825,6 +2846,7 @@ dashboard.delete_missing_repos=Supprimer tous les dépôts dont les fichiers Git
dashboard.delete_missing_repos.started=Tâche de suppression de tous les dépôts sans fichiers Git démarrée. dashboard.delete_missing_repos.started=Tâche de suppression de tous les dépôts sans fichiers Git démarrée.
dashboard.delete_generated_repository_avatars=Supprimer les avatars de dépôt générés dashboard.delete_generated_repository_avatars=Supprimer les avatars de dépôt générés
dashboard.sync_repo_branches=Synchroniser les branches manquantes depuis Git vers la base de donnée. dashboard.sync_repo_branches=Synchroniser les branches manquantes depuis Git vers la base de donnée.
dashboard.sync_repo_tags=Synchroniser les étiquettes git depuis les dépôts vers la base de données
dashboard.update_mirrors=Actualiser les miroirs dashboard.update_mirrors=Actualiser les miroirs
dashboard.repo_health_check=Vérifier l'état de santé de tous les dépôts dashboard.repo_health_check=Vérifier l'état de santé de tous les dépôts
dashboard.check_repo_stats=Voir les statistiques de tous les dépôts dashboard.check_repo_stats=Voir les statistiques de tous les dépôts
@ -2879,6 +2901,7 @@ dashboard.stop_endless_tasks=Arrêter les tâches sans fin
dashboard.cancel_abandoned_jobs=Annuler les jobs abandonnés dashboard.cancel_abandoned_jobs=Annuler les jobs abandonnés
dashboard.start_schedule_tasks=Démarrer les tâches planifiées dashboard.start_schedule_tasks=Démarrer les tâches planifiées
dashboard.sync_branch.started=Début de la synchronisation des branches dashboard.sync_branch.started=Début de la synchronisation des branches
dashboard.sync_tag.started=Synchronisation des étiquettes
dashboard.rebuild_issue_indexer=Reconstruire lindexeur des tickets dashboard.rebuild_issue_indexer=Reconstruire lindexeur des tickets
users.user_manage_panel=Gestion du compte utilisateur users.user_manage_panel=Gestion du compte utilisateur
@ -3314,6 +3337,12 @@ self_check.database_inconsistent_collation_columns = La base de donnée utilise
self_check.database_fix_mysql = Les utilisateurs de MySQL/MariaDB peuvent utiliser la commande "forgejo doctor convert" pour corriger les problèmes de collation, ou bien manuellement avec la commande SQL "ALTER ... COLLATE ...". self_check.database_fix_mysql = Les utilisateurs de MySQL/MariaDB peuvent utiliser la commande "forgejo doctor convert" pour corriger les problèmes de collation, ou bien manuellement avec la commande SQL "ALTER ... COLLATE ...".
self_check.database_fix_mssql = Les utilisateurs de MSSQL sont pour l'instant contraint d'utiliser la commande SQL "ALTER ... COLLATE ..." pour corriger ce problème. self_check.database_fix_mssql = Les utilisateurs de MSSQL sont pour l'instant contraint d'utiliser la commande SQL "ALTER ... COLLATE ..." pour corriger ce problème.
self_check.no_problem_found=Aucun problème trouvé pour linstant.
self_check.database_collation_mismatch=Exige que la base de données utilise la collation %s.
self_check.database_collation_case_insensitive=La base de données utilise la collation %s, insensible à la casse. Bien que Gitea soit compatible, il peut y avoir quelques rares cas qui ne fonctionnent pas comme prévu.
self_check.database_inconsistent_collation_columns=La base de données utilise la collation %s, mais ces colonnes utilisent des collations différentes. Cela peut causer des problèmes imprévus.
self_check.database_fix_mysql=Pour les utilisateurs de MySQL ou MariaDB, vous pouvez utiliser la commande « gitea doctor convert » dans un terminal ou exécuter une requête du type « ALTER … COLLATE ... » pour résoudre les problèmes de collation.
self_check.database_fix_mssql=Pour les utilisateurs de MSSQL, vous ne pouvez résoudre le problème quen exécutant une requête SQL du type « ALTER … COLLATE … ».
[action] [action]
create_repo=a créé le dépôt <a href="%s">%s</a> create_repo=a créé le dépôt <a href="%s">%s</a>
@ -3501,6 +3530,7 @@ rpm.distros.suse=sur les distributions basées sur SUSE
rpm.install=Pour installer le paquet, exécutez la commande suivante : rpm.install=Pour installer le paquet, exécutez la commande suivante :
rpm.repository=Informations sur le Dépôt rpm.repository=Informations sur le Dépôt
rpm.repository.architectures=Architectures rpm.repository.architectures=Architectures
rpm.repository.multiple_groups=Ce paquet est disponible en plusieurs groupes.
rubygems.install=Pour installer le paquet en utilisant gem, exécutez la commande suivante : rubygems.install=Pour installer le paquet en utilisant gem, exécutez la commande suivante :
rubygems.install2=ou ajoutez-le au Gemfile : rubygems.install2=ou ajoutez-le au Gemfile :
rubygems.dependencies.runtime=Dépendances d'exécution rubygems.dependencies.runtime=Dépendances d'exécution
@ -3636,6 +3666,8 @@ runs.actors_no_select=Tous les acteurs
runs.status_no_select=Touts les statuts runs.status_no_select=Touts les statuts
runs.no_results=Aucun résultat correspondant. runs.no_results=Aucun résultat correspondant.
runs.no_workflows=Il n'y a pas encore de workflows. runs.no_workflows=Il n'y a pas encore de workflows.
runs.no_workflows.quick_start=Vous découvrez les Actions Gitea ? Consultez <a target="_blank" rel="noopener noreferrer" href="%s">le didacticiel</a>.
runs.no_workflows.documentation=Pour plus dinformations sur les actions Gitea, voir <a target="_blank" rel="noopener noreferrer" href="%s">la documentation</a>.
runs.no_runs=Le flux de travail n'a pas encore d'exécution. runs.no_runs=Le flux de travail n'a pas encore d'exécution.
runs.empty_commit_message=(message de révision vide) runs.empty_commit_message=(message de révision vide)
@ -3654,6 +3686,7 @@ variables.none=Il n'y a pas encore de variables.
variables.deletion=Retirer la variable variables.deletion=Retirer la variable
variables.deletion.description=La suppression dune variable est permanente et ne peut être défaite. Continuer ? variables.deletion.description=La suppression dune variable est permanente et ne peut être défaite. Continuer ?
variables.description=Les variables sont passées aux actions et ne peuvent être lues autrement. variables.description=Les variables sont passées aux actions et ne peuvent être lues autrement.
variables.id_not_exist=La variable avec lID %d nexiste pas.
variables.edit=Modifier la variable variables.edit=Modifier la variable
variables.deletion.failed=Impossible de retirer la variable. variables.deletion.failed=Impossible de retirer la variable.
variables.deletion.success=La variable a bien été retirée. variables.deletion.success=La variable a bien été retirée.

View file

@ -124,6 +124,7 @@ pin=ピン留め
unpin=ピン留め解除 unpin=ピン留め解除
artifacts=成果物 artifacts=成果物
confirm_delete_artifact=アーティファクト %s を削除してよろしいですか?
archived=アーカイブ archived=アーカイブ
@ -429,7 +430,7 @@ password_pwned_err=HaveIBeenPwnedへのリクエストを完了できません
change_unconfirmed_email = 登録時に間違ったメール アドレスを入力した場合は、以下で変更できます。代わりに確認メールが新しいアドレスに送信されます。 change_unconfirmed_email = 登録時に間違ったメール アドレスを入力した場合は、以下で変更できます。代わりに確認メールが新しいアドレスに送信されます。
change_unconfirmed_email_error = メール アドレスを変更できません: %v change_unconfirmed_email_error = メール アドレスを変更できません: %v
change_unconfirmed_email_summary = アクティベーションメールの送信先メールアドレスを変更します。 change_unconfirmed_email_summary = アクティベーションメールの送信先メールアドレスを変更します。
last_admin = 最後の管理者を削除することはできません。少なくとも 1 人の管理者が必要です。 last_admin=最後の管理者は削除できません。少なくとも一人の管理者が必要です。
[mail] [mail]
view_it_on=%s で見る view_it_on=%s で見る
@ -600,6 +601,7 @@ target_branch_not_exist=ターゲットのブランチが存在していませ
admin_cannot_delete_self = 管理者である場合、自分自身を削除することはできません。最初に管理者権限を削除してください。 admin_cannot_delete_self = 管理者である場合、自分自身を削除することはできません。最初に管理者権限を削除してください。
username_error_no_dots = `英数字 (「0-9」、「a-z」、「A-Z」)、ダッシュ (「-」)、およびアンダースコア (「_」) のみを含めることができます。英数字以外の文字で開始または終了することはできず、連続した英数字以外の文字も禁止されています。` username_error_no_dots = `英数字 (「0-9」、「a-z」、「A-Z」)、ダッシュ (「-」)、およびアンダースコア (「_」) のみを含めることができます。英数字以外の文字で開始または終了することはできず、連続した英数字以外の文字も禁止されています。`
admin_cannot_delete_self=あなたが管理者である場合、自分自身を削除することはできません。最初に管理者権限を削除してください。
[user] [user]
change_avatar=アバターを変更… change_avatar=アバターを変更…
@ -993,6 +995,8 @@ issue_labels_helper=イシューのラベルセットを選択
license=ライセンス license=ライセンス
license_helper=ライセンス ファイルを選択してください。 license_helper=ライセンス ファイルを選択してください。
license_helper_desc=ライセンスにより、他人があなたのコードに対して何ができて何ができないのかを規定します。 どれがプロジェクトにふさわしいか迷っていますか? <a target="_blank" rel="noopener noreferrer" href="%s">ライセンス選択サイト</a> も確認してみてください。 license_helper_desc=ライセンスにより、他人があなたのコードに対して何ができて何ができないのかを規定します。 どれがプロジェクトにふさわしいか迷っていますか? <a target="_blank" rel="noopener noreferrer" href="%s">ライセンス選択サイト</a> も確認してみてください。
object_format=オブジェクトのフォーマット
object_format_helper=リポジトリのオブジェクトフォーマット。後で変更することはできません。SHA1 は最も互換性があります。
readme=README readme=README
readme_helper=READMEファイル テンプレートを選択してください。 readme_helper=READMEファイル テンプレートを選択してください。
readme_helper_desc=プロジェクトについての説明をひととおり書く場所です。 readme_helper_desc=プロジェクトについての説明をひととおり書く場所です。
@ -1060,6 +1064,7 @@ desc.public=公開
desc.template=テンプレート desc.template=テンプレート
desc.internal=組織内 desc.internal=組織内
desc.archived=アーカイブ desc.archived=アーカイブ
desc.sha256=SHA256
template.items=テンプレート項目 template.items=テンプレート項目
template.git_content=Gitコンテンツ (デフォルトブランチ) template.git_content=Gitコンテンツ (デフォルトブランチ)

View file

@ -1036,6 +1036,7 @@ desc.public=Publisks
desc.template=Sagatave desc.template=Sagatave
desc.internal=Iekšējs desc.internal=Iekšējs
desc.archived=Arhivēts desc.archived=Arhivēts
desc.sha256=SHA256
template.items=Sagataves ieraksti template.items=Sagataves ieraksti
template.git_content=Git saturs (noklusētais atzars) template.git_content=Git saturs (noklusētais atzars)
@ -2571,6 +2572,10 @@ error.csv.unexpected=Nevar attēlot šo failu, jo tas satur neparedzētu simbolu
error.csv.invalid_field_count=Nevar attēlot šo failu, jo tas satur nepareizu skaitu ar laukiem %d. līnijā. error.csv.invalid_field_count=Nevar attēlot šo failu, jo tas satur nepareizu skaitu ar laukiem %d. līnijā.
[graphs] [graphs]
component_loading=Ielādē %s...
component_loading_failed=Nevarēja ielādēt %s
component_loading_info=Šis var aizņemt kādu brīdi…
component_failed_to_load=Atgadījās neparedzēta kļūda.
[org] [org]
org_name_holder=Organizācijas nosaukums org_name_holder=Organizācijas nosaukums
@ -2698,6 +2703,7 @@ teams.invite.description=Nospiediet pogu zemāk, lai pievienotos komandai.
[admin] [admin]
dashboard=Infopanelis dashboard=Infopanelis
self_check=Pašpārbaude
identity_access=Identitāte un piekļuve identity_access=Identitāte un piekļuve
users=Lietotāju konti users=Lietotāju konti
organizations=Organizācijas organizations=Organizācijas
@ -3223,6 +3229,7 @@ notices.desc=Apraksts
notices.op=Op. notices.op=Op.
notices.delete_success=Sistēmas paziņojumi ir dzēsti. notices.delete_success=Sistēmas paziņojumi ir dzēsti.
self_check.no_problem_found=Pašlaik nav atrasta neviena problēma.
[action] [action]
create_repo=izveidoja repozitoriju <a href="%s">%s</a> create_repo=izveidoja repozitoriju <a href="%s">%s</a>
@ -3560,6 +3567,7 @@ variables.none=Vēl nav neviena mainīgā.
variables.deletion=Noņemt mainīgo variables.deletion=Noņemt mainīgo
variables.deletion.description=Mainīgā noņemšana ir neatgriezeniska un nav atsaucama. Vai turpināt? variables.deletion.description=Mainīgā noņemšana ir neatgriezeniska un nav atsaucama. Vai turpināt?
variables.description=Mainīgie tiks padoti noteiktām darbībām, un citādāk tos nevar nolasīt. variables.description=Mainīgie tiks padoti noteiktām darbībām, un citādāk tos nevar nolasīt.
variables.id_not_exist=Mainīgais ar identifikatoru %d nepastāv.
variables.edit=Labot mainīgo variables.edit=Labot mainīgo
variables.deletion.failed=Neizdevās noņemt mainīgo. variables.deletion.failed=Neizdevās noņemt mainīgo.
variables.deletion.success=Mainīgais tika noņemts. variables.deletion.success=Mainīgais tika noņemts.

View file

@ -2140,7 +2140,7 @@ settings.trust_model.collaborator.long=协作者:信任协作者的签名
settings.trust_model.collaborator.desc=此仓库中协作者的有效签名将被标记为「可信」(无论它们是否是提交者),签名只符合提交者时将标记为「不可信」,都不匹配时标记为「不匹配」。 settings.trust_model.collaborator.desc=此仓库中协作者的有效签名将被标记为「可信」(无论它们是否是提交者),签名只符合提交者时将标记为「不可信」,都不匹配时标记为「不匹配」。
settings.trust_model.committer=提交者 settings.trust_model.committer=提交者
settings.trust_model.committer.long=提交者: 信任与提交者相符的签名 (此特性类似 GitHub这会强制采用 Forgejo 作为提交者和签名者) settings.trust_model.committer.long=提交者: 信任与提交者相符的签名 (此特性类似 GitHub这会强制采用 Forgejo 作为提交者和签名者)
settings.trust_model.committer.desc=有效签名只有和提交者相匹配才会被标记为“受信任”,否则它们将被标记为“不匹配”。这强制 Forgejo 成为签名提交的提交者,而实际提交者被加上 Co-authored-by: 和 Co-committed-by: 的标记。 默认的 Forgejo 密钥必须撇撇数据库种的一名用户。 settings.trust_model.committer.desc=有效签名只有和提交者相匹配才会被标记为“受信任”,否则它们将被标记为“不匹配”。这强制 Forgejo 成为签名提交的提交者,而实际提交者被加上 Co-authored-by: 和 Co-committed-by: 的标记。 默认的 Forgejo 密钥必须匹配数据库中的一名用户。
settings.trust_model.collaboratorcommitter=协作者+提交者 settings.trust_model.collaboratorcommitter=协作者+提交者
settings.trust_model.collaboratorcommitter.long=协作者+提交者:信任协作者同时是提交者的签名 settings.trust_model.collaboratorcommitter.long=协作者+提交者:信任协作者同时是提交者的签名
settings.trust_model.collaboratorcommitter.desc=此仓库中协作者的有效签名在他同时是提交者时将被标记为「可信」,签名只匹配了提交者时将标记为「不可信」,都不匹配时标记为「不匹配」。这会强制 Forgejo 成为签名者和提交者实际的提交者将被标记于提交消息结尾处的「Co-Authored-By:」和「Co-Committed-By:」。默认的 Forgejo 签名密钥必须匹配数据库中的一个用户密钥。 settings.trust_model.collaboratorcommitter.desc=此仓库中协作者的有效签名在他同时是提交者时将被标记为「可信」,签名只匹配了提交者时将标记为「不可信」,都不匹配时标记为「不匹配」。这会强制 Forgejo 成为签名者和提交者实际的提交者将被标记于提交消息结尾处的「Co-Authored-By:」和「Co-Committed-By:」。默认的 Forgejo 签名密钥必须匹配数据库中的一个用户密钥。
@ -3325,7 +3325,9 @@ self_check.database_fix_mssql = 对于 MSSQL 用户目前您只能通过SQL
self_check.no_problem_found=尚未发现问题。 self_check.no_problem_found=尚未发现问题。
self_check.database_collation_mismatch=期望数据库使用的校验方式:%s self_check.database_collation_mismatch=期望数据库使用的校验方式:%s
self_check.database_collation_case_insensitive=数据库正在使用一个校验 %s, 这是一个不敏感的校验. 虽然Gitea可以与它合作但可能有一些罕见的情况不如预期的那样起作用。 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_mysql=对于MySQL/MariaDB用户您可以使用“gitea doctor convert”命令来解决校验问题。 或者您也可以通过 "ALTER ... COLLATE ..." 这样的SQL 来手动解决这个问题。
self_check.database_fix_mssql=对于MSSQL用户您现在只能通过"ALTER ... COLLATE ..."SQLs手动解决这个问题。
[action] [action]
create_repo=创建了仓库 <a href="%s">%s</a> create_repo=创建了仓库 <a href="%s">%s</a>

1026
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -9,7 +9,7 @@
"@citation-js/plugin-csl": "0.7.6", "@citation-js/plugin-csl": "0.7.6",
"@citation-js/plugin-software-formats": "0.6.1", "@citation-js/plugin-software-formats": "0.6.1",
"@claviska/jquery-minicolors": "2.3.6", "@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/relative-time-element": "4.3.1",
"@github/text-expander-element": "2.6.1", "@github/text-expander-element": "2.6.1",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
@ -17,11 +17,11 @@
"@webcomponents/custom-elements": "1.6.0", "@webcomponents/custom-elements": "1.6.0",
"add-asset-webpack-plugin": "2.0.1", "add-asset-webpack-plugin": "2.0.1",
"ansi_up": "6.0.2", "ansi_up": "6.0.2",
"asciinema-player": "3.6.4", "asciinema-player": "3.7.0",
"chart.js": "4.4.1", "chart.js": "4.4.2",
"chartjs-adapter-dayjs-4": "1.0.4", "chartjs-adapter-dayjs-4": "1.0.4",
"chartjs-plugin-zoom": "2.0.1", "chartjs-plugin-zoom": "2.0.1",
"clippie": "4.0.6", "clippie": "4.0.7",
"css-loader": "6.10.0", "css-loader": "6.10.0",
"css-variables-parser": "1.0.1", "css-variables-parser": "1.0.1",
"dayjs": "1.11.10", "dayjs": "1.11.10",
@ -36,16 +36,16 @@
"katex": "0.16.9", "katex": "0.16.9",
"license-checker-webpack-plugin": "0.2.1", "license-checker-webpack-plugin": "0.2.1",
"mermaid": "10.8.0", "mermaid": "10.8.0",
"mini-css-extract-plugin": "2.8.0", "mini-css-extract-plugin": "2.8.1",
"minimatch": "9.0.3", "minimatch": "9.0.3",
"monaco-editor": "0.46.0", "monaco-editor": "0.46.0",
"monaco-editor-webpack-plugin": "7.1.0", "monaco-editor-webpack-plugin": "7.1.0",
"pdfobject": "2.3.0", "pdfobject": "2.3.0",
"postcss": "8.4.35", "postcss": "8.4.35",
"postcss-loader": "8.1.0", "postcss-loader": "8.1.1",
"pretty-ms": "9.0.0", "pretty-ms": "9.0.0",
"sortablejs": "1.15.2", "sortablejs": "1.15.2",
"swagger-ui-dist": "5.11.6", "swagger-ui-dist": "5.11.8",
"tailwindcss": "3.4.1", "tailwindcss": "3.4.1",
"throttle-debounce": "5.0.0", "throttle-debounce": "5.0.0",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
@ -53,25 +53,25 @@
"toastify-js": "1.12.0", "toastify-js": "1.12.0",
"tributejs": "5.1.3", "tributejs": "5.1.3",
"uint8-to-base64": "0.2.0", "uint8-to-base64": "0.2.0",
"vue": "3.4.19", "vue": "3.4.21",
"vue-bar-graph": "2.0.0", "vue-bar-graph": "2.0.0",
"vue-chartjs": "5.3.0", "vue-chartjs": "5.3.0",
"vue-loader": "17.4.2", "vue-loader": "17.4.2",
"vue3-calendar-heatmap": "2.0.5", "vue3-calendar-heatmap": "2.0.5",
"webpack": "5.90.2", "webpack": "5.90.3",
"webpack-cli": "5.1.4", "webpack-cli": "5.1.4",
"wrap-ansi": "9.0.0" "wrap-ansi": "9.0.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint-community/eslint-plugin-eslint-comments": "4.1.0", "@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", "@stoplight/spectral-cli": "6.11.0",
"@stylistic/eslint-plugin-js": "1.6.2", "@stylistic/eslint-plugin-js": "1.6.3",
"@stylistic/stylelint-plugin": "2.0.0", "@stylistic/stylelint-plugin": "2.1.0",
"@vitejs/plugin-vue": "5.0.4", "@vitejs/plugin-vue": "5.0.4",
"eslint": "8.56.0", "eslint": "8.57.0",
"eslint-plugin-array-func": "4.0.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-i": "2.29.1",
"eslint-plugin-jquery": "1.5.1", "eslint-plugin-jquery": "1.5.1",
"eslint-plugin-no-jquery": "2.7.0", "eslint-plugin-no-jquery": "2.7.0",
@ -81,7 +81,7 @@
"eslint-plugin-unicorn": "51.0.1", "eslint-plugin-unicorn": "51.0.1",
"eslint-plugin-vitest": "0.3.22", "eslint-plugin-vitest": "0.3.22",
"eslint-plugin-vitest-globals": "1.4.0", "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-vue-scoped-css": "2.7.2",
"eslint-plugin-wc": "2.0.4", "eslint-plugin-wc": "2.0.4",
"jsdom": "24.0.0", "jsdom": "24.0.0",
@ -93,7 +93,7 @@
"svgo": "3.2.0", "svgo": "3.2.0",
"updates": "15.1.2", "updates": "15.1.2",
"vite-string-plugin": "1.1.5", "vite-string-plugin": "1.1.5",
"vitest": "1.2.2" "vitest": "1.3.1"
}, },
"browserslist": [ "browserslist": [
"defaults" "defaults"

39
poetry.lock generated
View file

@ -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]] [[package]]
name = "click" name = "click"
@ -27,12 +27,12 @@ files = [
[[package]] [[package]]
name = "cssbeautifier" name = "cssbeautifier"
version = "1.14.11" version = "1.15.1"
description = "CSS unobfuscator and beautifier." description = "CSS unobfuscator and beautifier."
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
{file = "cssbeautifier-1.14.11.tar.gz", hash = "sha256:40544c2b62bbcb64caa5e7f37a02df95654e5ce1bcacadac4ca1f3dc89c31513"}, {file = "cssbeautifier-1.15.1.tar.gz", hash = "sha256:9f7064362aedd559c55eeecf6b6bed65e05f33488dcbe39044f0403c26e1c006"},
] ]
[package.dependencies] [package.dependencies]
@ -67,13 +67,12 @@ tqdm = ">=4.62.2,<5.0.0"
[[package]] [[package]]
name = "editorconfig" name = "editorconfig"
version = "0.12.3" version = "0.12.4"
description = "EditorConfig File Locator and Interpreter for Python" description = "EditorConfig File Locator and Interpreter for Python"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
{file = "EditorConfig-0.12.3-py3-none-any.whl", hash = "sha256:6b0851425aa875b08b16789ee0eeadbd4ab59666e9ebe728e526314c4a2e52c1"}, {file = "EditorConfig-0.12.4.tar.gz", hash = "sha256:24857fa1793917dd9ccf0c7810a07e05404ce9b823521c7dce22a4fb5d125f80"},
{file = "EditorConfig-0.12.3.tar.gz", hash = "sha256:57f8ce78afcba15c8b18d46b5170848c88d56fd38f05c2ec60dbbfcb8996e89e"},
] ]
[[package]] [[package]]
@ -100,12 +99,12 @@ files = [
[[package]] [[package]]
name = "jsbeautifier" name = "jsbeautifier"
version = "1.14.11" version = "1.15.1"
description = "JavaScript unobfuscator and beautifier." description = "JavaScript unobfuscator and beautifier."
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
{file = "jsbeautifier-1.14.11.tar.gz", hash = "sha256:6b632581ea60dd1c133cd25a48ad187b4b91f526623c4b0fb5443ef805250505"}, {file = "jsbeautifier-1.15.1.tar.gz", hash = "sha256:ebd733b560704c602d744eafc839db60a1ee9326e30a2a80c4adb8718adc1b24"},
] ]
[package.dependencies] [package.dependencies]
@ -114,13 +113,13 @@ six = ">=1.13.0"
[[package]] [[package]]
name = "json5" name = "json5"
version = "0.9.14" version = "0.9.18"
description = "A Python implementation of the JSON5 data format." description = "A Python implementation of the JSON5 data format."
optional = false optional = false
python-versions = "*" python-versions = ">=3.8"
files = [ files = [
{file = "json5-0.9.14-py2.py3-none-any.whl", hash = "sha256:740c7f1b9e584a468dbb2939d8d458db3427f2c93ae2139d05f47e453eae964f"}, {file = "json5-0.9.18-py2.py3-none-any.whl", hash = "sha256:3f20193ff8dfdec6ab114b344e7ac5d76fac453c8bab9bdfe1460d1d528ec393"},
{file = "json5-0.9.14.tar.gz", hash = "sha256:9ed66c3a6ca3510a976a9ef9b8c0787de24802724ab1860bc0153c7fdd589b02"}, {file = "json5-0.9.18.tar.gz", hash = "sha256:ecb8ac357004e3522fb989da1bf08b146011edbd14fdffae6caad3bd68493467"},
] ]
[package.extras] [package.extras]
@ -322,13 +321,13 @@ files = [
[[package]] [[package]]
name = "tqdm" name = "tqdm"
version = "4.66.1" version = "4.66.2"
description = "Fast, Extensible Progress Meter" description = "Fast, Extensible Progress Meter"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "tqdm-4.66.1-py3-none-any.whl", hash = "sha256:d302b3c5b53d47bce91fea46679d9c3c6508cf6332229aa1e7d8653723793386"}, {file = "tqdm-4.66.2-py3-none-any.whl", hash = "sha256:1ee4f8a893eb9bef51c6e35730cebf234d5d0b6bd112b0271e10ed7c24a02bd9"},
{file = "tqdm-4.66.1.tar.gz", hash = "sha256:d88e651f9db8d8551a62556d3cff9e3034274ca5d66e93197cf2490e2dcb69c7"}, {file = "tqdm-4.66.2.tar.gz", hash = "sha256:6cd52cdf0fef0e0f543299cfc96fec90d7b8a7e88745f411ec33eb44d5ed3531"},
] ]
[package.dependencies] [package.dependencies]
@ -342,13 +341,13 @@ telegram = ["requests"]
[[package]] [[package]]
name = "yamllint" name = "yamllint"
version = "1.35.0" version = "1.35.1"
description = "A linter for YAML files." description = "A linter for YAML files."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "yamllint-1.35.0-py3-none-any.whl", hash = "sha256:601b0adaaac6d9bacb16a2e612e7ee8d23caf941ceebf9bfe2cff0f196266004"}, {file = "yamllint-1.35.1-py3-none-any.whl", hash = "sha256:2e16e504bb129ff515b37823b472750b36b6de07963bd74b307341ef5ad8bdc3"},
{file = "yamllint-1.35.0.tar.gz", hash = "sha256:9bc99c3e9fe89b4c6ee26e17aa817cf2d14390de6577cb6e2e6ed5f72120c835"}, {file = "yamllint-1.35.1.tar.gz", hash = "sha256:7a003809f88324fd2c877734f2d575ee7881dd9043360657cc8049c809eba6cd"},
] ]
[package.dependencies] [package.dependencies]
@ -360,5 +359,5 @@ dev = ["doc8", "flake8", "flake8-import-order", "rstcheck[sphinx]", "sphinx"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.8" python-versions = "^3.10"
content-hash = "ba1c2c4235872f67354b5f52aa5bf0cd616354961530d9dc907f9fba28cc1ece" content-hash = "cd2ff218e9f27a464dfbc8ec2387824a90f4360e04c3f2e58cc375796b7df33a"

View file

@ -5,11 +5,11 @@ description = ""
authors = [] authors = []
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.8" python = "^3.10"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
djlint = "1.34.1" djlint = "1.34.1"
yamllint = "1.35.0" yamllint = "1.35.1"
[tool.djlint] [tool.djlint]
profile="golang" profile="golang"

File diff suppressed because it is too large Load diff

View file

@ -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;
}

View file

@ -71,7 +71,6 @@ import (
"code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
@ -80,6 +79,7 @@ import (
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
web_types "code.gitea.io/gitea/modules/web/types" web_types "code.gitea.io/gitea/modules/web/types"
actions_service "code.gitea.io/gitea/services/actions" actions_service "code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/context"
) )
const artifactRouteBase = "/_apis/pipelines/workflows/{run_id}/artifacts" const artifactRouteBase = "/_apis/pipelines/workflows/{run_id}/artifacts"

View file

@ -5,11 +5,16 @@ package actions
import ( import (
"crypto/md5" "crypto/md5"
"crypto/sha256"
"encoding/base64" "encoding/base64"
"encoding/hex"
"errors"
"fmt" "fmt"
"hash"
"io" "io"
"path/filepath" "path/filepath"
"sort" "sort"
"strings"
"time" "time"
"code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/actions"
@ -18,6 +23,52 @@ import (
"code.gitea.io/gitea/modules/storage" "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, func saveUploadChunk(st storage.ObjectStorage, ctx *ArtifactContext,
artifact *actions.ActionArtifact, artifact *actions.ActionArtifact,
contentSize, runID int64, 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) log.Warn("parse content range error: %v, content-range: %s", err, contentRange)
return -1, fmt.Errorf("parse content range error: %v", err) return -1, fmt.Errorf("parse content range error: %v", err)
} }
// build chunk store path return saveUploadChunkBase(st, ctx, artifact, contentSize, runID, start, end, length, true)
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 func appendUploadChunk(st storage.ObjectStorage, ctx *ArtifactContext,
// if hash is not matched, delete the read-end result artifact *actions.ActionArtifact,
hasher := md5.New() start, contentSize, runID int64,
r := io.TeeReader(ctx.Req.Body, hasher) ) (int64, error) {
// save chunk to storage end := start + contentSize - 1
writtenSize, err := st.Save(storagePath, r, -1) return saveUploadChunkBase(st, ctx, artifact, contentSize, runID, start, end, contentSize, false)
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
} }
type chunkFileItem struct { 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) log.Debug("artifact %d chunks not found", art.ID)
continue continue
} }
if err := mergeChunksForArtifact(ctx, chunks, st, art); err != nil { if err := mergeChunksForArtifact(ctx, chunks, st, art, ""); err != nil {
return err return err
} }
} }
return nil 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 { sort.Slice(chunks, func(i, j int) bool {
return chunks[i].Start < chunks[j].Start return chunks[i].Start < chunks[j].Start
}) })
@ -157,6 +190,14 @@ func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st st
readers = append(readers, readCloser) readers = append(readers, readCloser)
} }
mergedReader := io.MultiReader(readers...) 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 // if chunk is gzip, use gz as extension
// download-artifact action will use content-encoding header to decide if it should decompress the file // 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 // save storage path to artifact
log.Debug("[artifact] merge chunks to artifact: %d, %s, old:%s", artifact.ID, storagePath, artifact.StoragePath) 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 // if artifact is already uploaded, delete the old file

View file

@ -43,6 +43,17 @@ func validateRunID(ctx *ArtifactContext) (*actions.ActionTask, int64, bool) {
return task, runID, true 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 { func validateArtifactHash(ctx *ArtifactContext, artifactName string) bool {
paramHash := ctx.Params("artifact_hash") paramHash := ctx.Params("artifact_hash")
// use artifact name to create upload url // use artifact name to create upload url

View file

@ -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)
}

View file

@ -15,12 +15,12 @@ import (
packages_model "code.gitea.io/gitea/models/packages" packages_model "code.gitea.io/gitea/models/packages"
alpine_model "code.gitea.io/gitea/models/packages/alpine" alpine_model "code.gitea.io/gitea/models/packages/alpine"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
packages_module "code.gitea.io/gitea/modules/packages" packages_module "code.gitea.io/gitea/modules/packages"
alpine_module "code.gitea.io/gitea/modules/packages/alpine" alpine_module "code.gitea.io/gitea/modules/packages/alpine"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper" "code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages" packages_service "code.gitea.io/gitea/services/packages"
alpine_service "code.gitea.io/gitea/services/packages/alpine" alpine_service "code.gitea.io/gitea/services/packages/alpine"
) )

View file

@ -10,7 +10,6 @@ import (
auth_model "code.gitea.io/gitea/models/auth" auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
@ -36,7 +35,7 @@ import (
"code.gitea.io/gitea/routers/api/packages/swift" "code.gitea.io/gitea/routers/api/packages/swift"
"code.gitea.io/gitea/routers/api/packages/vagrant" "code.gitea.io/gitea/routers/api/packages/vagrant"
"code.gitea.io/gitea/services/auth" "code.gitea.io/gitea/services/auth"
context_service "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
) )
func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) { func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) {
@ -642,7 +641,7 @@ func CommonRoutes() *web.Route {
}) })
}) })
}, reqPackageAccess(perm.AccessModeRead)) }, reqPackageAccess(perm.AccessModeRead))
}, context_service.UserAssignmentWeb(), context.PackageAssignment()) }, context.UserAssignmentWeb(), context.PackageAssignment())
return r return r
} }
@ -812,7 +811,7 @@ func ContainerRoutes() *web.Route {
ctx.Status(http.StatusNotFound) ctx.Status(http.StatusNotFound)
}) })
}, container.ReqContainerAccess, context_service.UserAssignmentWeb(), context.PackageAssignment(), reqPackageAccess(perm.AccessModeRead)) }, container.ReqContainerAccess, context.UserAssignmentWeb(), context.PackageAssignment(), reqPackageAccess(perm.AccessModeRead))
return r return r
} }

View file

@ -12,14 +12,15 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages" packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
packages_module "code.gitea.io/gitea/modules/packages" packages_module "code.gitea.io/gitea/modules/packages"
cargo_module "code.gitea.io/gitea/modules/packages/cargo" cargo_module "code.gitea.io/gitea/modules/packages/cargo"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper" "code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert" "code.gitea.io/gitea/services/convert"
packages_service "code.gitea.io/gitea/services/packages" packages_service "code.gitea.io/gitea/services/packages"
cargo_service "code.gitea.io/gitea/services/packages/cargo" cargo_service "code.gitea.io/gitea/services/packages/cargo"
@ -110,7 +111,7 @@ func SearchPackages(ctx *context.Context) {
OwnerID: ctx.Package.Owner.ID, OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeCargo, Type: packages_model.TypeCargo,
Name: packages_model.SearchValue{Value: ctx.FormTrim("q")}, Name: packages_model.SearchValue{Value: ctx.FormTrim("q")},
IsInternal: util.OptionalBoolFalse, IsInternal: optional.Some(false),
Paginator: &paginator, Paginator: &paginator,
}, },
) )

View file

@ -15,12 +15,13 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages" packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/optional"
packages_module "code.gitea.io/gitea/modules/packages" packages_module "code.gitea.io/gitea/modules/packages"
chef_module "code.gitea.io/gitea/modules/packages/chef" chef_module "code.gitea.io/gitea/modules/packages/chef"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper" "code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages" packages_service "code.gitea.io/gitea/services/packages"
) )
@ -40,7 +41,7 @@ func PackagesUniverse(ctx *context.Context) {
pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
OwnerID: ctx.Package.Owner.ID, OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeChef, Type: packages_model.TypeChef,
IsInternal: util.OptionalBoolFalse, IsInternal: optional.Some(false),
}) })
if err != nil { if err != nil {
apiError(ctx, http.StatusInternalServerError, err) apiError(ctx, http.StatusInternalServerError, err)
@ -85,7 +86,7 @@ func EnumeratePackages(ctx *context.Context) {
OwnerID: ctx.Package.Owner.ID, OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeChef, Type: packages_model.TypeChef,
Name: packages_model.SearchValue{Value: ctx.FormTrim("q")}, Name: packages_model.SearchValue{Value: ctx.FormTrim("q")},
IsInternal: util.OptionalBoolFalse, IsInternal: optional.Some(false),
Paginator: db.NewAbsoluteListOptions( Paginator: db.NewAbsoluteListOptions(
ctx.FormInt("start"), ctx.FormInt("start"),
ctx.FormInt("items"), ctx.FormInt("items"),

Some files were not shown because too many files have changed in this diff Show more