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:
		
				commit
				
					
						025798d0f6
					
				
			
		
					 639 changed files with 5676 additions and 2833 deletions
				
			
		| 
						 | 
				
			
			@ -162,9 +162,6 @@ package "code.gitea.io/gitea/modules/cache"
 | 
			
		|||
package "code.gitea.io/gitea/modules/charset"
 | 
			
		||||
	func (*BreakWriter).Write
 | 
			
		||||
 | 
			
		||||
package "code.gitea.io/gitea/modules/context"
 | 
			
		||||
	func GetPrivateContext
 | 
			
		||||
 | 
			
		||||
package "code.gitea.io/gitea/modules/emoji"
 | 
			
		||||
	func ReplaceCodes
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -296,7 +293,6 @@ package "code.gitea.io/gitea/modules/translation"
 | 
			
		|||
 | 
			
		||||
package "code.gitea.io/gitea/modules/util"
 | 
			
		||||
	func UnsafeStringToBytes
 | 
			
		||||
	func OptionalBoolFromGeneric
 | 
			
		||||
 | 
			
		||||
package "code.gitea.io/gitea/modules/util/filebuffer"
 | 
			
		||||
	func CreateFromReader
 | 
			
		||||
| 
						 | 
				
			
			@ -316,6 +312,9 @@ package "code.gitea.io/gitea/routers/web/org"
 | 
			
		|||
	func getActionIssues
 | 
			
		||||
	func UpdateIssueProject
 | 
			
		||||
 | 
			
		||||
package "code.gitea.io/gitea/services/context"
 | 
			
		||||
	func GetPrivateContext
 | 
			
		||||
 | 
			
		||||
package "code.gitea.io/gitea/services/convert"
 | 
			
		||||
	func ToSecret
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										2
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -18,7 +18,7 @@ _test
 | 
			
		|||
 | 
			
		||||
# MS VSCode
 | 
			
		||||
.vscode
 | 
			
		||||
__debug_bin
 | 
			
		||||
__debug_bin*
 | 
			
		||||
 | 
			
		||||
*.cgo1.go
 | 
			
		||||
*.cgo2.c
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -64,6 +64,7 @@ rules:
 | 
			
		|||
  "@stylistic/media-query-list-comma-newline-before": null
 | 
			
		||||
  "@stylistic/media-query-list-comma-space-after": null
 | 
			
		||||
  "@stylistic/media-query-list-comma-space-before": null
 | 
			
		||||
  "@stylistic/named-grid-areas-alignment": null
 | 
			
		||||
  "@stylistic/no-empty-first-line": null
 | 
			
		||||
  "@stylistic/no-eol-whitespace": true
 | 
			
		||||
  "@stylistic/no-extra-semicolons": true
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -969,6 +969,12 @@ LEVEL = Info
 | 
			
		|||
;GO_GET_CLONE_URL_PROTOCOL = https
 | 
			
		||||
;;
 | 
			
		||||
;; Close issues as long as a commit on any branch marks it as fixed
 | 
			
		||||
;DEFAULT_CLOSE_ISSUES_VIA_COMMITS_IN_ANY_BRANCH = false
 | 
			
		||||
;;
 | 
			
		||||
;; Allow users to push local repositories to Gitea and have them automatically created for a user or an org
 | 
			
		||||
;ENABLE_PUSH_CREATE_USER = false
 | 
			
		||||
;ENABLE_PUSH_CREATE_ORG = false
 | 
			
		||||
;;
 | 
			
		||||
;; Comma separated list of globally disabled repo units. Allowed values: repo.issues, repo.ext_issues, repo.pulls, repo.wiki, repo.ext_wiki, repo.projects, repo.packages, repo.actions.
 | 
			
		||||
;DISABLED_REPO_UNITS =
 | 
			
		||||
;;
 | 
			
		||||
| 
						 | 
				
			
			@ -1490,10 +1496,11 @@ LEVEL = Info
 | 
			
		|||
;;
 | 
			
		||||
;; Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled
 | 
			
		||||
;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
 | 
			
		||||
;; Disabled features for users, could be "deletion", more features can be disabled in future
 | 
			
		||||
;; - deletion: a user cannot delete their own account
 | 
			
		||||
;; - manage_gpg_keys: a user cannot configure gpg keys
 | 
			
		||||
;USER_DISABLED_FEATURES =
 | 
			
		||||
 | 
			
		||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
- `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`)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -497,6 +497,9 @@ Gitea 创建以下非唯一队列:
 | 
			
		|||
 | 
			
		||||
- `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**:用户电子邮件通知的默认配置(用户可配置)。选项:enabled、onmention、disabled
 | 
			
		||||
- `DISABLE_REGULAR_ORG_CREATION`: **false**:禁止普通(非管理员)用户创建组织。
 | 
			
		||||
- `USER_DISABLED_FEATURES`:**_empty_** 禁用的用户特性,当前允许为空或者 `deletion`,`manage_gpg_keys` 未来可以增加更多设置。
 | 
			
		||||
  - `deletion`: 用户不能通过界面或者API删除他自己。
 | 
			
		||||
  - `manage_gpg_keys`: 用户不能配置 GPG 密钥
 | 
			
		||||
 | 
			
		||||
## 安全性 (`security`)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -222,9 +222,9 @@ Please check [Gitea's logs](administration/logging-config.md) for error messages
 | 
			
		|||
        <a href="{{.Link}}">{{.Repo}}#{{.Issue.Index}}</a>.
 | 
			
		||||
        </p>
 | 
			
		||||
        {{if not (eq .Body "")}}
 | 
			
		||||
            <h3>Message content:</h3>
 | 
			
		||||
            <h3>Message content</h3>
 | 
			
		||||
            <hr>
 | 
			
		||||
            {{.Body | Str2html}}
 | 
			
		||||
            {{.Body | SanitizeHTML}}
 | 
			
		||||
        {{end}}
 | 
			
		||||
    </p>
 | 
			
		||||
    <hr>
 | 
			
		||||
| 
						 | 
				
			
			@ -245,7 +245,7 @@ This template produces something along these lines:
 | 
			
		|||
 | 
			
		||||
> [@rhonda](#) (Rhonda Myers) updated [mike/stuff#38](#).
 | 
			
		||||
>
 | 
			
		||||
> #### Message content:
 | 
			
		||||
> #### Message content
 | 
			
		||||
>
 | 
			
		||||
> \_********************************\_********************************
 | 
			
		||||
>
 | 
			
		||||
| 
						 | 
				
			
			@ -260,19 +260,19 @@ The template system contains several functions that can be used to further proce
 | 
			
		|||
the messages. Here's a list of some of them:
 | 
			
		||||
 | 
			
		||||
| Name             | Parameters  | Available | Usage                                                               |
 | 
			
		||||
| ---------------- | ----------- | --------- | --------------------------------------------------------------------------- |
 | 
			
		||||
| ---------------- | ----------- | --------- | ------------------------------------------------------------------- |
 | 
			
		||||
| `AppUrl`         | -           | Any       | Gitea's URL                                                         |
 | 
			
		||||
| `AppName`        | -           | Any       | Set from `app.ini`, usually "Gitea"                                 |
 | 
			
		||||
| `AppDomain`      | -           | Any       | Gitea's host name                                                   |
 | 
			
		||||
| `EllipsisString` | string, int | Any       | Truncates a string to the specified length; adds ellipsis as needed |
 | 
			
		||||
| `Str2html`       | string      | Body only | Sanitizes text by removing any HTML tags from it.                           |
 | 
			
		||||
| `Safe`           | string      | Body only | Takes the input as HTML; can be used for `.ReviewComments.RenderedContent`. |
 | 
			
		||||
| `SanitizeHTML`   | string      | Body only | Sanitizes text by removing any dangerous HTML tags from it          |
 | 
			
		||||
| `SafeHTML`       | string      | Body only | Takes the input as HTML, can be used for outputing raw HTML content |
 | 
			
		||||
 | 
			
		||||
These are _functions_, not metadata, so they have to be used:
 | 
			
		||||
 | 
			
		||||
```html
 | 
			
		||||
Like this:         {{Str2html "Escape<my>text"}}
 | 
			
		||||
Or this:           {{"Escape<my>text" | Str2html}}
 | 
			
		||||
Like this:         {{SanitizeHTML "Escape<my>text"}}
 | 
			
		||||
Or this:           {{"Escape<my>text" | SanitizeHTML}}
 | 
			
		||||
Or this:           {{AppUrl}}
 | 
			
		||||
But not like this: {{.AppUrl}}
 | 
			
		||||
```
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -207,7 +207,7 @@ _主题_ 和 _邮件正文_ 由 [Golang的模板引擎](https://go.dev/pkg/text/
 | 
			
		|||
        {{if not (eq .Body "")}}
 | 
			
		||||
            <h3>消息内容:</h3>
 | 
			
		||||
            <hr>
 | 
			
		||||
            {{.Body | Str2html}}
 | 
			
		||||
            {{.Body | SanitizeHTML}}
 | 
			
		||||
        {{end}}
 | 
			
		||||
    </p>
 | 
			
		||||
    <hr>
 | 
			
		||||
| 
						 | 
				
			
			@ -228,7 +228,7 @@ _主题_ 和 _邮件正文_ 由 [Golang的模板引擎](https://go.dev/pkg/text/
 | 
			
		|||
 | 
			
		||||
> [@rhonda](#)(Rhonda Myers)更新了 [mike/stuff#38](#)。
 | 
			
		||||
>
 | 
			
		||||
> #### 消息内容:
 | 
			
		||||
> #### 消息内容
 | 
			
		||||
>
 | 
			
		||||
> \_********************************\_********************************
 | 
			
		||||
>
 | 
			
		||||
| 
						 | 
				
			
			@ -243,19 +243,19 @@ _主题_ 和 _邮件正文_ 由 [Golang的模板引擎](https://go.dev/pkg/text/
 | 
			
		|||
模板系统包含一些函数,可用于进一步处理和格式化消息。以下是其中一些函数的列表:
 | 
			
		||||
 | 
			
		||||
| 函数名              | 参数        | 可用于       | 用法                             |
 | 
			
		||||
| ----------------- | ----------- | ------------ | --------------------------------------------------------------------------------- |
 | 
			
		||||
|------------------| ----------- | ------------ | ------------------------------ |
 | 
			
		||||
| `AppUrl`         | -           | 任何地方     | Gitea 的 URL                    |
 | 
			
		||||
| `AppName`        | -           | 任何地方     | 从 `app.ini` 中设置,通常为 "Gitea"    |
 | 
			
		||||
| `AppDomain`      | -           | 任何地方     | Gitea 的主机名                     |
 | 
			
		||||
| `EllipsisString` | string, int | 任何地方     | 将字符串截断为指定长度;根据需要添加省略号          |
 | 
			
		||||
| `Str2html`        | string      | 仅正文部分   | 通过删除其中的 HTML 标签对文本进行清理                                              |
 | 
			
		||||
| `Safe`            | string      | 仅正文部分   | 将输入作为 HTML 处理;可用于 `.ReviewComments.RenderedContent` 等字段               |
 | 
			
		||||
| `SanitizeHTML`   | string      | 仅正文部分   | 通过删除其中的危险 HTML 标签对文本进行清理       |
 | 
			
		||||
| `SafeHTML`       | string      | 仅正文部分   | 将输入作为 HTML 处理;可用于输出原始的 HTML 内容 |
 | 
			
		||||
 | 
			
		||||
这些都是 _函数_,而不是元数据,因此必须按以下方式使用:
 | 
			
		||||
 | 
			
		||||
```html
 | 
			
		||||
像这样使用:         {{Str2html "Escape<my>text"}}
 | 
			
		||||
或者这样使用:       {{"Escape<my>text" | Str2html}}
 | 
			
		||||
像这样使用:         {{SanitizeHTML "Escape<my>text"}}
 | 
			
		||||
或者这样使用:       {{"Escape<my>text" | SanitizeHTML}}
 | 
			
		||||
或者这样使用:       {{AppUrl}}
 | 
			
		||||
但不要像这样使用:   {{.AppUrl}}
 | 
			
		||||
```
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -135,6 +135,12 @@ body:
 | 
			
		|||
    attributes:
 | 
			
		||||
      value: |
 | 
			
		||||
        Thanks for taking the time to fill out this bug report!
 | 
			
		||||
  # some markdown that will only be visible once the issue has been created
 | 
			
		||||
  - type: markdown
 | 
			
		||||
    attributes:
 | 
			
		||||
      value: |
 | 
			
		||||
        This issue was created by an issue **template** :)
 | 
			
		||||
    visible: [content]
 | 
			
		||||
  - type: input
 | 
			
		||||
    id: contact
 | 
			
		||||
    attributes:
 | 
			
		||||
| 
						 | 
				
			
			@ -186,11 +192,16 @@ body:
 | 
			
		|||
      options:
 | 
			
		||||
        - label: I agree to follow this project's Code of Conduct
 | 
			
		||||
          required: true
 | 
			
		||||
        - label: I have also read the CONTRIBUTION.MD
 | 
			
		||||
          required: true
 | 
			
		||||
          visible: [form]
 | 
			
		||||
        - label: This is a TODO only visible after issue creation
 | 
			
		||||
          visible: [content]
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Markdown
 | 
			
		||||
 | 
			
		||||
You can use a `markdown` element to display Markdown in your form that provides extra context to the user, but is not submitted.
 | 
			
		||||
You can use a `markdown` element to display Markdown in your form that provides extra context to the user, but is not submitted by default.
 | 
			
		||||
 | 
			
		||||
Attributes:
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -198,6 +209,8 @@ Attributes:
 | 
			
		|||
|-------|--------------------------------------------------------------|----------|--------|---------|--------------|
 | 
			
		||||
| value | The text that is rendered. Markdown formatting is supported. | Required | String | -       | -            |
 | 
			
		||||
 | 
			
		||||
visible: Default is **[form]**
 | 
			
		||||
 | 
			
		||||
### Textarea
 | 
			
		||||
 | 
			
		||||
You can use a `textarea` element to add a multi-line text field to your form. Contributors can also attach files in `textarea` fields.
 | 
			
		||||
| 
						 | 
				
			
			@ -218,6 +231,8 @@ Validations:
 | 
			
		|||
|----------|------------------------------------------------------|----------|---------|---------|--------------|
 | 
			
		||||
| required | Prevents form submission until element is completed. | Optional | Boolean | false   | -            |
 | 
			
		||||
 | 
			
		||||
visible: Default is **[form, content]**
 | 
			
		||||
 | 
			
		||||
### Input
 | 
			
		||||
 | 
			
		||||
You can use an `input` element to add a single-line text field to your form.
 | 
			
		||||
| 
						 | 
				
			
			@ -239,6 +254,8 @@ Validations:
 | 
			
		|||
| is_number | Prevents form submission until element is filled with a number.                                  | Optional | Boolean | false   | -                                                                        |
 | 
			
		||||
| regex     | Prevents form submission until element is filled with a value that match the regular expression. | Optional | String  | -       | a [regular expression](https://en.wikipedia.org/wiki/Regular_expression) |
 | 
			
		||||
 | 
			
		||||
visible: Default is **[form, content]**
 | 
			
		||||
 | 
			
		||||
### Dropdown
 | 
			
		||||
 | 
			
		||||
You can use a `dropdown` element to add a dropdown menu in your form.
 | 
			
		||||
| 
						 | 
				
			
			@ -258,6 +275,8 @@ Validations:
 | 
			
		|||
|----------|------------------------------------------------------|----------|---------|---------|--------------|
 | 
			
		||||
| required | Prevents form submission until element is completed. | Optional | Boolean | false   | -            |
 | 
			
		||||
 | 
			
		||||
visible: Default is **[form, content]**
 | 
			
		||||
 | 
			
		||||
### Checkboxes
 | 
			
		||||
 | 
			
		||||
You can use the `checkboxes` element to add a set of checkboxes to your form.
 | 
			
		||||
| 
						 | 
				
			
			@ -265,7 +284,7 @@ You can use the `checkboxes` element to add a set of checkboxes to your form.
 | 
			
		|||
Attributes:
 | 
			
		||||
 | 
			
		||||
| Key         | Description                                                                                           | Required | Type   | Default      | Valid values |
 | 
			
		||||
|-------------|-------------------------------------------------------------------------------------------------------|----------|--------|--------------|--------------|
 | 
			
		||||
| ----------- | ----------------------------------------------------------------------------------------------------- | -------- | ------ | ------------ | ------------ |
 | 
			
		||||
| label       | A brief description of the expected user input, which is displayed in the form.                       | Required | String | -            | -            |
 | 
			
		||||
| description | A description of the set of checkboxes, which is displayed in the form. Supports Markdown formatting. | Optional | String | Empty String | -            |
 | 
			
		||||
| options     | An array of checkboxes that the user can select. For syntax, see below.                               | Required | Array  | -            | -            |
 | 
			
		||||
| 
						 | 
				
			
			@ -273,9 +292,12 @@ Attributes:
 | 
			
		|||
For each value in the options array, you can set the following keys.
 | 
			
		||||
 | 
			
		||||
| Key          | Description                                                                                                                              | Required | Type         | Default | Options |
 | 
			
		||||
|----------|------------------------------------------------------------------------------------------------------------------------------------------|----------|---------|---------|---------|
 | 
			
		||||
|--------------|------------------------------------------------------------------------------------------------------------------------------------------|----------|--------------|---------|---------|
 | 
			
		||||
| label        | The identifier for the option, which is displayed in the form. Markdown is supported for bold or italic text formatting, and hyperlinks. | Required | String       | -       | -       |
 | 
			
		||||
| required     | Prevents form submission until element is completed.                                                                                     | Optional | Boolean      | false   | -       |
 | 
			
		||||
| visible      | Whether a specific checkbox appears in the form only, in the created issue only, or both. Valid options are "form" and "content".        | Optional | String array | false   | -       |
 | 
			
		||||
 | 
			
		||||
visible: Default is **[form, content]**
 | 
			
		||||
 | 
			
		||||
## Syntax for issue config
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -292,14 +314,14 @@ contact_links:
 | 
			
		|||
### Possible Options
 | 
			
		||||
 | 
			
		||||
| Key                  | Description                                           | Type               | Default     |
 | 
			
		||||
|----------------------|-------------------------------------------------------------------------------------------------------|--------------------|----------------|
 | 
			
		||||
|----------------------|-------------------------------------------------------|--------------------|-------------|
 | 
			
		||||
| blank_issues_enabled | If set to false, the User is forced to use a Template | Boolean            | true        |
 | 
			
		||||
| contact_links        | Custom Links to show in the Choose Box                | Contact Link Array | Empty Array |
 | 
			
		||||
 | 
			
		||||
### Contact Link
 | 
			
		||||
 | 
			
		||||
| Key   | Description                      | Type   | Required |
 | 
			
		||||
|----------------------|-------------------------------------------------------------------------------------------------------|---------|----------|
 | 
			
		||||
|-------|----------------------------------|--------|----------|
 | 
			
		||||
| name  | the name of your link            | String | true     |
 | 
			
		||||
| url   | The URL of your Link             | String | true     |
 | 
			
		||||
| about | A short description of your Link | String | true     |
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,6 +13,7 @@ import (
 | 
			
		|||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
			
		||||
	"code.gitea.io/gitea/models/shared/types"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
			
		||||
	"code.gitea.io/gitea/modules/translation"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
| 
						 | 
				
			
			@ -159,7 +160,7 @@ type FindRunnerOptions struct {
 | 
			
		|||
	OwnerID       int64
 | 
			
		||||
	Sort          string
 | 
			
		||||
	Filter        string
 | 
			
		||||
	IsOnline      util.OptionalBool
 | 
			
		||||
	IsOnline      optional.Option[bool]
 | 
			
		||||
	WithAvailable bool // not only runners belong to, but also runners can be used
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -186,11 +187,13 @@ func (opts FindRunnerOptions) ToConds() builder.Cond {
 | 
			
		|||
		cond = cond.And(builder.Like{"name", opts.Filter})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if opts.IsOnline.IsTrue() {
 | 
			
		||||
	if opts.IsOnline.Has() {
 | 
			
		||||
		if opts.IsOnline.Value() {
 | 
			
		||||
			cond = cond.And(builder.Gt{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()})
 | 
			
		||||
	} else if opts.IsOnline.IsFalse() {
 | 
			
		||||
		} else {
 | 
			
		||||
			cond = cond.And(builder.Lte{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()})
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return cond
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -227,8 +227,8 @@ func (a *Action) ShortActUserName(ctx context.Context) string {
 | 
			
		|||
	return base.EllipsisString(a.GetActUserName(ctx), 20)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetDisplayName gets the action's display name based on DEFAULT_SHOW_FULL_NAME, or falls back to the username if it is blank.
 | 
			
		||||
func (a *Action) GetDisplayName(ctx context.Context) string {
 | 
			
		||||
// GetActDisplayName gets the action's display name based on DEFAULT_SHOW_FULL_NAME, or falls back to the username if it is blank.
 | 
			
		||||
func (a *Action) GetActDisplayName(ctx context.Context) string {
 | 
			
		||||
	if setting.UI.DefaultShowFullName {
 | 
			
		||||
		trimmedFullName := strings.TrimSpace(a.GetActFullName(ctx))
 | 
			
		||||
		if len(trimmedFullName) > 0 {
 | 
			
		||||
| 
						 | 
				
			
			@ -238,8 +238,8 @@ func (a *Action) GetDisplayName(ctx context.Context) string {
 | 
			
		|||
	return a.ShortActUserName(ctx)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetDisplayNameTitle gets the action's display name used for the title (tooltip) based on DEFAULT_SHOW_FULL_NAME
 | 
			
		||||
func (a *Action) GetDisplayNameTitle(ctx context.Context) string {
 | 
			
		||||
// GetActDisplayNameTitle gets the action's display name used for the title (tooltip) based on DEFAULT_SHOW_FULL_NAME
 | 
			
		||||
func (a *Action) GetActDisplayNameTitle(ctx context.Context) string {
 | 
			
		||||
	if setting.UI.DefaultShowFullName {
 | 
			
		||||
		return a.ShortActUserName(ctx)
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,6 +11,7 @@ import (
 | 
			
		|||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -243,14 +244,14 @@ func CreateSource(ctx context.Context, source *Source) error {
 | 
			
		|||
 | 
			
		||||
type FindSourcesOptions struct {
 | 
			
		||||
	db.ListOptions
 | 
			
		||||
	IsActive  util.OptionalBool
 | 
			
		||||
	IsActive  optional.Option[bool]
 | 
			
		||||
	LoginType Type
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (opts FindSourcesOptions) ToConds() builder.Cond {
 | 
			
		||||
	conds := builder.NewCond()
 | 
			
		||||
	if !opts.IsActive.IsNone() {
 | 
			
		||||
		conds = conds.And(builder.Eq{"is_active": opts.IsActive.IsTrue()})
 | 
			
		||||
	if opts.IsActive.Has() {
 | 
			
		||||
		conds = conds.And(builder.Eq{"is_active": opts.IsActive.Value()})
 | 
			
		||||
	}
 | 
			
		||||
	if opts.LoginType != NoType {
 | 
			
		||||
		conds = conds.And(builder.Eq{"`type`": opts.LoginType})
 | 
			
		||||
| 
						 | 
				
			
			@ -262,7 +263,7 @@ func (opts FindSourcesOptions) ToConds() builder.Cond {
 | 
			
		|||
// source of type LoginSSPI
 | 
			
		||||
func IsSSPIEnabled(ctx context.Context) bool {
 | 
			
		||||
	exist, err := db.Exist[Source](ctx, FindSourcesOptions{
 | 
			
		||||
		IsActive:  util.OptionalBoolTrue,
 | 
			
		||||
		IsActive:  optional.Some(true),
 | 
			
		||||
		LoginType: SSPI,
 | 
			
		||||
	}.ToConds())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,3 +17,22 @@
 | 
			
		|||
  updated: 1683636626
 | 
			
		||||
  need_approval: 0
 | 
			
		||||
  approved_by: 0
 | 
			
		||||
-
 | 
			
		||||
  id: 792
 | 
			
		||||
  title: "update actions"
 | 
			
		||||
  repo_id: 4
 | 
			
		||||
  owner_id: 1
 | 
			
		||||
  workflow_id: "artifact.yaml"
 | 
			
		||||
  index: 188
 | 
			
		||||
  trigger_user_id: 1
 | 
			
		||||
  ref: "refs/heads/master"
 | 
			
		||||
  commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0"
 | 
			
		||||
  event: "push"
 | 
			
		||||
  is_fork_pull_request: 0
 | 
			
		||||
  status: 1
 | 
			
		||||
  started: 1683636528
 | 
			
		||||
  stopped: 1683636626
 | 
			
		||||
  created: 1683636108
 | 
			
		||||
  updated: 1683636626
 | 
			
		||||
  need_approval: 0
 | 
			
		||||
  approved_by: 0
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,3 +12,17 @@
 | 
			
		|||
  status: 1
 | 
			
		||||
  started: 1683636528
 | 
			
		||||
  stopped: 1683636626
 | 
			
		||||
-
 | 
			
		||||
  id: 193
 | 
			
		||||
  run_id: 792
 | 
			
		||||
  repo_id: 4
 | 
			
		||||
  owner_id: 1
 | 
			
		||||
  commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
 | 
			
		||||
  is_fork_pull_request: 0
 | 
			
		||||
  name: job_2
 | 
			
		||||
  attempt: 1
 | 
			
		||||
  job_id: job_2
 | 
			
		||||
  task_id: 48
 | 
			
		||||
  status: 1
 | 
			
		||||
  started: 1683636528
 | 
			
		||||
  stopped: 1683636626
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,3 +18,23 @@
 | 
			
		|||
  log_length: 707
 | 
			
		||||
  log_size: 90179
 | 
			
		||||
  log_expired: 0
 | 
			
		||||
-
 | 
			
		||||
  id: 48
 | 
			
		||||
  job_id: 193
 | 
			
		||||
  attempt: 1
 | 
			
		||||
  runner_id: 1
 | 
			
		||||
  status: 6 # 6 is the status code for "running", running task can upload artifacts
 | 
			
		||||
  started: 1683636528
 | 
			
		||||
  stopped: 1683636626
 | 
			
		||||
  repo_id: 4
 | 
			
		||||
  owner_id: 1
 | 
			
		||||
  commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
 | 
			
		||||
  is_fork_pull_request: 0
 | 
			
		||||
  token_hash: ffffcfffffffbffffffffffffffffefffffffafffffffffffffffffffffffffffffdffffffffffffffffffffffffffffffff
 | 
			
		||||
  token_salt: ffffffffff
 | 
			
		||||
  token_last_eight: ffffffff
 | 
			
		||||
  log_filename: artifact-test2/2f/47.log
 | 
			
		||||
  log_in_storage: 1
 | 
			
		||||
  log_length: 707
 | 
			
		||||
  log_size: 90179
 | 
			
		||||
  log_expired: 0
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,6 +8,7 @@ package issues
 | 
			
		|||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"unicode/utf8"
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -21,6 +22,7 @@ import (
 | 
			
		|||
	"code.gitea.io/gitea/modules/gitrepo"
 | 
			
		||||
	"code.gitea.io/gitea/modules/json"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
	"code.gitea.io/gitea/modules/references"
 | 
			
		||||
	"code.gitea.io/gitea/modules/structs"
 | 
			
		||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
			
		||||
| 
						 | 
				
			
			@ -260,7 +262,7 @@ type Comment struct {
 | 
			
		|||
	Line            int64 // - previous line / + proposed line
 | 
			
		||||
	TreePath        string
 | 
			
		||||
	Content         string        `xorm:"LONGTEXT"`
 | 
			
		||||
	RenderedContent string `xorm:"-"`
 | 
			
		||||
	RenderedContent template.HTML `xorm:"-"`
 | 
			
		||||
 | 
			
		||||
	// Path represents the 4 lines of code cemented by this comment
 | 
			
		||||
	Patch       string `xorm:"-"`
 | 
			
		||||
| 
						 | 
				
			
			@ -1043,8 +1045,8 @@ type FindCommentsOptions struct {
 | 
			
		|||
	TreePath    string
 | 
			
		||||
	Type        CommentType
 | 
			
		||||
	IssueIDs    []int64
 | 
			
		||||
	Invalidated util.OptionalBool
 | 
			
		||||
	IsPull      util.OptionalBool
 | 
			
		||||
	Invalidated optional.Option[bool]
 | 
			
		||||
	IsPull      optional.Option[bool]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ToConds implements FindOptions interface
 | 
			
		||||
| 
						 | 
				
			
			@ -1076,11 +1078,11 @@ func (opts FindCommentsOptions) ToConds() builder.Cond {
 | 
			
		|||
	if len(opts.TreePath) > 0 {
 | 
			
		||||
		cond = cond.And(builder.Eq{"comment.tree_path": opts.TreePath})
 | 
			
		||||
	}
 | 
			
		||||
	if !opts.Invalidated.IsNone() {
 | 
			
		||||
		cond = cond.And(builder.Eq{"comment.invalidated": opts.Invalidated.IsTrue()})
 | 
			
		||||
	if opts.Invalidated.Has() {
 | 
			
		||||
		cond = cond.And(builder.Eq{"comment.invalidated": opts.Invalidated.Value()})
 | 
			
		||||
	}
 | 
			
		||||
	if opts.IsPull != util.OptionalBoolNone {
 | 
			
		||||
		cond = cond.And(builder.Eq{"issue.is_pull": opts.IsPull.IsTrue()})
 | 
			
		||||
	if opts.IsPull.Has() {
 | 
			
		||||
		cond = cond.And(builder.Eq{"issue.is_pull": opts.IsPull.Value()})
 | 
			
		||||
	}
 | 
			
		||||
	return cond
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1089,7 +1091,7 @@ func (opts FindCommentsOptions) ToConds() builder.Cond {
 | 
			
		|||
func FindComments(ctx context.Context, opts *FindCommentsOptions) (CommentList, error) {
 | 
			
		||||
	comments := make([]*Comment, 0, 10)
 | 
			
		||||
	sess := db.GetEngine(ctx).Where(opts.ToConds())
 | 
			
		||||
	if opts.RepoID > 0 || opts.IsPull != util.OptionalBoolNone {
 | 
			
		||||
	if opts.RepoID > 0 || opts.IsPull.Has() {
 | 
			
		||||
		sess.Join("INNER", "issue", "issue.id = comment.issue_id")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,6 +7,7 @@ package issues
 | 
			
		|||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"slices"
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -105,7 +106,7 @@ type Issue struct {
 | 
			
		|||
	OriginalAuthorID int64                  `xorm:"index"`
 | 
			
		||||
	Title            string                 `xorm:"name"`
 | 
			
		||||
	Content          string                 `xorm:"LONGTEXT"`
 | 
			
		||||
	RenderedContent  string                 `xorm:"-"`
 | 
			
		||||
	RenderedContent  template.HTML          `xorm:"-"`
 | 
			
		||||
	Labels           []*Label               `xorm:"-"`
 | 
			
		||||
	MilestoneID      int64                  `xorm:"INDEX"`
 | 
			
		||||
	Milestone        *Milestone             `xorm:"-"`
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,7 +13,7 @@ import (
 | 
			
		|||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
			
		||||
	"code.gitea.io/gitea/models/unit"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
 | 
			
		||||
	"xorm.io/builder"
 | 
			
		||||
	"xorm.io/xorm"
 | 
			
		||||
| 
						 | 
				
			
			@ -34,8 +34,8 @@ type IssuesOptions struct { //nolint
 | 
			
		|||
	MilestoneIDs       []int64
 | 
			
		||||
	ProjectID          int64
 | 
			
		||||
	ProjectBoardID     int64
 | 
			
		||||
	IsClosed           util.OptionalBool
 | 
			
		||||
	IsPull             util.OptionalBool
 | 
			
		||||
	IsClosed           optional.Option[bool]
 | 
			
		||||
	IsPull             optional.Option[bool]
 | 
			
		||||
	LabelIDs           []int64
 | 
			
		||||
	IncludedLabelNames []string
 | 
			
		||||
	ExcludedLabelNames []string
 | 
			
		||||
| 
						 | 
				
			
			@ -46,7 +46,7 @@ type IssuesOptions struct { //nolint
 | 
			
		|||
	UpdatedBeforeUnix  int64
 | 
			
		||||
	// prioritize issues from this repo
 | 
			
		||||
	PriorityRepoID int64
 | 
			
		||||
	IsArchived     util.OptionalBool
 | 
			
		||||
	IsArchived     optional.Option[bool]
 | 
			
		||||
	Org            *organization.Organization // issues permission scope
 | 
			
		||||
	Team           *organization.Team         // issues permission scope
 | 
			
		||||
	User           *user_model.User           // issues permission scope
 | 
			
		||||
| 
						 | 
				
			
			@ -217,8 +217,8 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
 | 
			
		|||
 | 
			
		||||
	applyRepoConditions(sess, opts)
 | 
			
		||||
 | 
			
		||||
	if !opts.IsClosed.IsNone() {
 | 
			
		||||
		sess.And("issue.is_closed=?", opts.IsClosed.IsTrue())
 | 
			
		||||
	if opts.IsClosed.Has() {
 | 
			
		||||
		sess.And("issue.is_closed=?", opts.IsClosed.Value())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if opts.AssigneeID > 0 {
 | 
			
		||||
| 
						 | 
				
			
			@ -260,21 +260,18 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
 | 
			
		|||
 | 
			
		||||
	applyProjectBoardCondition(sess, opts)
 | 
			
		||||
 | 
			
		||||
	switch opts.IsPull {
 | 
			
		||||
	case util.OptionalBoolTrue:
 | 
			
		||||
		sess.And("issue.is_pull=?", true)
 | 
			
		||||
	case util.OptionalBoolFalse:
 | 
			
		||||
		sess.And("issue.is_pull=?", false)
 | 
			
		||||
	if opts.IsPull.Has() {
 | 
			
		||||
		sess.And("issue.is_pull=?", opts.IsPull.Value())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if opts.IsArchived != util.OptionalBoolNone {
 | 
			
		||||
		sess.And(builder.Eq{"repository.is_archived": opts.IsArchived.IsTrue()})
 | 
			
		||||
	if opts.IsArchived.Has() {
 | 
			
		||||
		sess.And(builder.Eq{"repository.is_archived": opts.IsArchived.Value()})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	applyLabelsCondition(sess, opts)
 | 
			
		||||
 | 
			
		||||
	if opts.User != nil {
 | 
			
		||||
		sess.And(issuePullAccessibleRepoCond("issue.repo_id", opts.User.ID, opts.Org, opts.Team, opts.IsPull.IsTrue()))
 | 
			
		||||
		sess.And(issuePullAccessibleRepoCond("issue.repo_id", opts.User.ID, opts.Org, opts.Team, opts.IsPull.Value()))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return sess
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,7 +8,6 @@ import (
 | 
			
		|||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
 | 
			
		||||
	"xorm.io/builder"
 | 
			
		||||
	"xorm.io/xorm"
 | 
			
		||||
| 
						 | 
				
			
			@ -170,11 +169,8 @@ func applyIssuesOptions(sess *xorm.Session, opts *IssuesOptions, issueIDs []int6
 | 
			
		|||
		applyReviewedCondition(sess, opts.ReviewedID)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	switch opts.IsPull {
 | 
			
		||||
	case util.OptionalBoolTrue:
 | 
			
		||||
		sess.And("issue.is_pull=?", true)
 | 
			
		||||
	case util.OptionalBoolFalse:
 | 
			
		||||
		sess.And("issue.is_pull=?", false)
 | 
			
		||||
	if opts.IsPull.Has() {
 | 
			
		||||
		sess.And("issue.is_pull=?", opts.IsPull.Value())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return sess
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,6 +12,7 @@ import (
 | 
			
		|||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/modules/label"
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -126,7 +127,7 @@ func (l *Label) CalOpenOrgIssues(ctx context.Context, repoID, labelID int64) {
 | 
			
		|||
	counts, _ := CountIssuesByRepo(ctx, &IssuesOptions{
 | 
			
		||||
		RepoIDs:  []int64{repoID},
 | 
			
		||||
		LabelIDs: []int64{labelID},
 | 
			
		||||
		IsClosed: util.OptionalBoolFalse,
 | 
			
		||||
		IsClosed: optional.Some(false),
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	for _, count := range counts {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,10 +6,12 @@ package issues
 | 
			
		|||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
	api "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
| 
						 | 
				
			
			@ -48,7 +50,7 @@ type Milestone struct {
 | 
			
		|||
	Repo            *repo_model.Repository `xorm:"-"`
 | 
			
		||||
	Name            string
 | 
			
		||||
	Content         string        `xorm:"TEXT"`
 | 
			
		||||
	RenderedContent string `xorm:"-"`
 | 
			
		||||
	RenderedContent template.HTML `xorm:"-"`
 | 
			
		||||
	IsClosed        bool
 | 
			
		||||
	NumIssues       int
 | 
			
		||||
	NumClosedIssues int
 | 
			
		||||
| 
						 | 
				
			
			@ -313,7 +315,7 @@ func DeleteMilestoneByRepoID(ctx context.Context, repoID, id int64) error {
 | 
			
		|||
	}
 | 
			
		||||
	numClosedMilestones, err := db.Count[Milestone](ctx, FindMilestoneOptions{
 | 
			
		||||
		RepoID:   repo.ID,
 | 
			
		||||
		IsClosed: util.OptionalBoolTrue,
 | 
			
		||||
		IsClosed: optional.Some(true),
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,7 +8,7 @@ import (
 | 
			
		|||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
 | 
			
		||||
	"xorm.io/builder"
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			@ -28,7 +28,7 @@ func (milestones MilestoneList) getMilestoneIDs() []int64 {
 | 
			
		|||
type FindMilestoneOptions struct {
 | 
			
		||||
	db.ListOptions
 | 
			
		||||
	RepoID   int64
 | 
			
		||||
	IsClosed util.OptionalBool
 | 
			
		||||
	IsClosed optional.Option[bool]
 | 
			
		||||
	Name     string
 | 
			
		||||
	SortType string
 | 
			
		||||
	RepoCond builder.Cond
 | 
			
		||||
| 
						 | 
				
			
			@ -40,8 +40,8 @@ func (opts FindMilestoneOptions) ToConds() builder.Cond {
 | 
			
		|||
	if opts.RepoID != 0 {
 | 
			
		||||
		cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
 | 
			
		||||
	}
 | 
			
		||||
	if opts.IsClosed != util.OptionalBoolNone {
 | 
			
		||||
		cond = cond.And(builder.Eq{"is_closed": opts.IsClosed.IsTrue()})
 | 
			
		||||
	if opts.IsClosed.Has() {
 | 
			
		||||
		cond = cond.And(builder.Eq{"is_closed": opts.IsClosed.Value()})
 | 
			
		||||
	}
 | 
			
		||||
	if opts.RepoCond != nil && opts.RepoCond.IsValid() {
 | 
			
		||||
		cond = cond.And(builder.In("repo_id", builder.Select("id").From("repository").Where(opts.RepoCond)))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,10 +11,10 @@ import (
 | 
			
		|||
	issues_model "code.gitea.io/gitea/models/issues"
 | 
			
		||||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
			
		||||
	"code.gitea.io/gitea/models/unittest"
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	api "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			@ -39,10 +39,10 @@ func TestGetMilestoneByRepoID(t *testing.T) {
 | 
			
		|||
func TestGetMilestonesByRepoID(t *testing.T) {
 | 
			
		||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
			
		||||
	test := func(repoID int64, state api.StateType) {
 | 
			
		||||
		var isClosed util.OptionalBool
 | 
			
		||||
		var isClosed optional.Option[bool]
 | 
			
		||||
		switch state {
 | 
			
		||||
		case api.StateClosed, api.StateOpen:
 | 
			
		||||
			isClosed = util.OptionalBoolOf(state == api.StateClosed)
 | 
			
		||||
			isClosed = optional.Some(state == api.StateClosed)
 | 
			
		||||
		}
 | 
			
		||||
		repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
 | 
			
		||||
		milestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
 | 
			
		||||
| 
						 | 
				
			
			@ -84,7 +84,7 @@ func TestGetMilestonesByRepoID(t *testing.T) {
 | 
			
		|||
 | 
			
		||||
	milestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
 | 
			
		||||
		RepoID:   unittest.NonexistentID,
 | 
			
		||||
		IsClosed: util.OptionalBoolFalse,
 | 
			
		||||
		IsClosed: optional.Some(false),
 | 
			
		||||
	})
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.Len(t, milestones, 0)
 | 
			
		||||
| 
						 | 
				
			
			@ -101,7 +101,7 @@ func TestGetMilestones(t *testing.T) {
 | 
			
		|||
					PageSize: setting.UI.IssuePagingNum,
 | 
			
		||||
				},
 | 
			
		||||
				RepoID:   repo.ID,
 | 
			
		||||
				IsClosed: util.OptionalBoolFalse,
 | 
			
		||||
				IsClosed: optional.Some(false),
 | 
			
		||||
				SortType: sortType,
 | 
			
		||||
			})
 | 
			
		||||
			assert.NoError(t, err)
 | 
			
		||||
| 
						 | 
				
			
			@ -118,7 +118,7 @@ func TestGetMilestones(t *testing.T) {
 | 
			
		|||
					PageSize: setting.UI.IssuePagingNum,
 | 
			
		||||
				},
 | 
			
		||||
				RepoID:   repo.ID,
 | 
			
		||||
				IsClosed: util.OptionalBoolTrue,
 | 
			
		||||
				IsClosed: optional.Some(true),
 | 
			
		||||
				Name:     "",
 | 
			
		||||
				SortType: sortType,
 | 
			
		||||
			})
 | 
			
		||||
| 
						 | 
				
			
			@ -178,7 +178,7 @@ func TestCountRepoClosedMilestones(t *testing.T) {
 | 
			
		|||
		repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
 | 
			
		||||
		count, err := db.Count[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
 | 
			
		||||
			RepoID:   repoID,
 | 
			
		||||
			IsClosed: util.OptionalBoolTrue,
 | 
			
		||||
			IsClosed: optional.Some(true),
 | 
			
		||||
		})
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.EqualValues(t, repo.NumClosedMilestones, count)
 | 
			
		||||
| 
						 | 
				
			
			@ -189,7 +189,7 @@ func TestCountRepoClosedMilestones(t *testing.T) {
 | 
			
		|||
 | 
			
		||||
	count, err := db.Count[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
 | 
			
		||||
		RepoID:   unittest.NonexistentID,
 | 
			
		||||
		IsClosed: util.OptionalBoolTrue,
 | 
			
		||||
		IsClosed: optional.Some(true),
 | 
			
		||||
	})
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.EqualValues(t, 0, count)
 | 
			
		||||
| 
						 | 
				
			
			@ -206,7 +206,7 @@ func TestCountMilestonesByRepoIDs(t *testing.T) {
 | 
			
		|||
 | 
			
		||||
	openCounts, err := issues_model.CountMilestonesMap(db.DefaultContext, issues_model.FindMilestoneOptions{
 | 
			
		||||
		RepoIDs:  []int64{1, 2},
 | 
			
		||||
		IsClosed: util.OptionalBoolFalse,
 | 
			
		||||
		IsClosed: optional.Some(false),
 | 
			
		||||
	})
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.EqualValues(t, repo1OpenCount, openCounts[1])
 | 
			
		||||
| 
						 | 
				
			
			@ -215,7 +215,7 @@ func TestCountMilestonesByRepoIDs(t *testing.T) {
 | 
			
		|||
	closedCounts, err := issues_model.CountMilestonesMap(db.DefaultContext,
 | 
			
		||||
		issues_model.FindMilestoneOptions{
 | 
			
		||||
			RepoIDs:  []int64{1, 2},
 | 
			
		||||
			IsClosed: util.OptionalBoolTrue,
 | 
			
		||||
			IsClosed: optional.Some(true),
 | 
			
		||||
		})
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.EqualValues(t, repo1ClosedCount, closedCounts[1])
 | 
			
		||||
| 
						 | 
				
			
			@ -234,7 +234,7 @@ func TestGetMilestonesByRepoIDs(t *testing.T) {
 | 
			
		|||
					PageSize: setting.UI.IssuePagingNum,
 | 
			
		||||
				},
 | 
			
		||||
				RepoIDs:  []int64{repo1.ID, repo2.ID},
 | 
			
		||||
				IsClosed: util.OptionalBoolFalse,
 | 
			
		||||
				IsClosed: optional.Some(false),
 | 
			
		||||
				SortType: sortType,
 | 
			
		||||
			})
 | 
			
		||||
			assert.NoError(t, err)
 | 
			
		||||
| 
						 | 
				
			
			@ -252,7 +252,7 @@ func TestGetMilestonesByRepoIDs(t *testing.T) {
 | 
			
		|||
						PageSize: setting.UI.IssuePagingNum,
 | 
			
		||||
					},
 | 
			
		||||
					RepoIDs:  []int64{repo1.ID, repo2.ID},
 | 
			
		||||
					IsClosed: util.OptionalBoolTrue,
 | 
			
		||||
					IsClosed: optional.Some(true),
 | 
			
		||||
					SortType: sortType,
 | 
			
		||||
				})
 | 
			
		||||
			assert.NoError(t, err)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,7 +9,7 @@ import (
 | 
			
		|||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/container"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
 | 
			
		||||
	"xorm.io/builder"
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			@ -68,7 +68,7 @@ type FindReviewOptions struct {
 | 
			
		|||
	IssueID      int64
 | 
			
		||||
	ReviewerID   int64
 | 
			
		||||
	OfficialOnly bool
 | 
			
		||||
	Dismissed    util.OptionalBool
 | 
			
		||||
	Dismissed    optional.Option[bool]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (opts *FindReviewOptions) toCond() builder.Cond {
 | 
			
		||||
| 
						 | 
				
			
			@ -85,8 +85,8 @@ func (opts *FindReviewOptions) toCond() builder.Cond {
 | 
			
		|||
	if opts.OfficialOnly {
 | 
			
		||||
		cond = cond.And(builder.Eq{"official": true})
 | 
			
		||||
	}
 | 
			
		||||
	if !opts.Dismissed.IsNone() {
 | 
			
		||||
		cond = cond.And(builder.Eq{"dismissed": opts.Dismissed.IsTrue()})
 | 
			
		||||
	if opts.Dismissed.Has() {
 | 
			
		||||
		cond = cond.And(builder.Eq{"dismissed": opts.Dismissed.Value()})
 | 
			
		||||
	}
 | 
			
		||||
	return cond
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,6 +11,7 @@ import (
 | 
			
		|||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -340,7 +341,7 @@ func GetTrackedTimeByID(ctx context.Context, id int64) (*TrackedTime, error) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
// GetIssueTotalTrackedTime returns the total tracked time for issues by given conditions.
 | 
			
		||||
func GetIssueTotalTrackedTime(ctx context.Context, opts *IssuesOptions, isClosed util.OptionalBool) (int64, error) {
 | 
			
		||||
func GetIssueTotalTrackedTime(ctx context.Context, opts *IssuesOptions, isClosed optional.Option[bool]) (int64, error) {
 | 
			
		||||
	if len(opts.IssueIDs) <= MaxQueryParameters {
 | 
			
		||||
		return getIssueTotalTrackedTimeChunk(ctx, opts, isClosed, opts.IssueIDs)
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -363,7 +364,7 @@ func GetIssueTotalTrackedTime(ctx context.Context, opts *IssuesOptions, isClosed
 | 
			
		|||
	return accum, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getIssueTotalTrackedTimeChunk(ctx context.Context, opts *IssuesOptions, isClosed util.OptionalBool, issueIDs []int64) (int64, error) {
 | 
			
		||||
func getIssueTotalTrackedTimeChunk(ctx context.Context, opts *IssuesOptions, isClosed optional.Option[bool], issueIDs []int64) (int64, error) {
 | 
			
		||||
	sumSession := func(opts *IssuesOptions, issueIDs []int64) *xorm.Session {
 | 
			
		||||
		sess := db.GetEngine(ctx).
 | 
			
		||||
			Table("tracked_time").
 | 
			
		||||
| 
						 | 
				
			
			@ -378,8 +379,8 @@ func getIssueTotalTrackedTimeChunk(ctx context.Context, opts *IssuesOptions, isC
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	session := sumSession(opts, issueIDs)
 | 
			
		||||
	if !isClosed.IsNone() {
 | 
			
		||||
		session = session.And("issue.is_closed = ?", isClosed.IsTrue())
 | 
			
		||||
	if isClosed.Has() {
 | 
			
		||||
		session = session.And("issue.is_closed = ?", isClosed.Value())
 | 
			
		||||
	}
 | 
			
		||||
	return session.SumInt(new(trackedTime), "tracked_time.time")
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,7 +11,7 @@ import (
 | 
			
		|||
	issues_model "code.gitea.io/gitea/models/issues"
 | 
			
		||||
	"code.gitea.io/gitea/models/unittest"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			@ -120,15 +120,15 @@ func TestTotalTimesForEachUser(t *testing.T) {
 | 
			
		|||
func TestGetIssueTotalTrackedTime(t *testing.T) {
 | 
			
		||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
			
		||||
 | 
			
		||||
	ttt, err := issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, util.OptionalBoolFalse)
 | 
			
		||||
	ttt, err := issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, optional.Some(false))
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.EqualValues(t, 3682, ttt)
 | 
			
		||||
 | 
			
		||||
	ttt, err = issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, util.OptionalBoolTrue)
 | 
			
		||||
	ttt, err = issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, optional.Some(true))
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.EqualValues(t, 0, ttt)
 | 
			
		||||
 | 
			
		||||
	ttt, err = issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, util.OptionalBoolNone)
 | 
			
		||||
	ttt, err = issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, optional.None[bool]())
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.EqualValues(t, 3682, ttt)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -70,16 +70,26 @@ type PackageFileDescriptor struct {
 | 
			
		|||
	Properties PackagePropertyList
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PackageWebLink returns the package web link
 | 
			
		||||
// PackageWebLink returns the relative package web link
 | 
			
		||||
func (pd *PackageDescriptor) PackageWebLink() string {
 | 
			
		||||
	return fmt.Sprintf("%s/-/packages/%s/%s", pd.Owner.HomeLink(), string(pd.Package.Type), url.PathEscape(pd.Package.LowerName))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FullWebLink returns the package version web link
 | 
			
		||||
func (pd *PackageDescriptor) FullWebLink() string {
 | 
			
		||||
// VersionWebLink returns the relative package version web link
 | 
			
		||||
func (pd *PackageDescriptor) VersionWebLink() string {
 | 
			
		||||
	return fmt.Sprintf("%s/%s", pd.PackageWebLink(), url.PathEscape(pd.Version.LowerVersion))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PackageHTMLURL returns the absolute package HTML URL
 | 
			
		||||
func (pd *PackageDescriptor) PackageHTMLURL() string {
 | 
			
		||||
	return fmt.Sprintf("%s/-/packages/%s/%s", pd.Owner.HTMLURL(), string(pd.Package.Type), url.PathEscape(pd.Package.LowerName))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// VersionHTMLURL returns the absolute package version HTML URL
 | 
			
		||||
func (pd *PackageDescriptor) VersionHTMLURL() string {
 | 
			
		||||
	return fmt.Sprintf("%s/%s", pd.PackageHTMLURL(), url.PathEscape(pd.Version.LowerVersion))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CalculateBlobSize returns the total blobs size in bytes
 | 
			
		||||
func (pd *PackageDescriptor) CalculateBlobSize() int64 {
 | 
			
		||||
	size := int64(0)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -55,7 +55,7 @@ func CountPackages(ctx context.Context, opts *packages_model.PackageSearchOption
 | 
			
		|||
 | 
			
		||||
func toConds(opts *packages_model.PackageSearchOptions) builder.Cond {
 | 
			
		||||
	var cond builder.Cond = builder.Eq{
 | 
			
		||||
		"package.is_internal": opts.IsInternal.IsTrue(),
 | 
			
		||||
		"package.is_internal": opts.IsInternal.Value(),
 | 
			
		||||
		"package.owner_id":    opts.OwnerID,
 | 
			
		||||
		"package.type":        packages_model.TypeNuGet,
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,6 +9,7 @@ import (
 | 
			
		|||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -105,7 +106,7 @@ func getVersionByNameAndVersion(ctx context.Context, ownerID int64, packageType
 | 
			
		|||
			ExactMatch: true,
 | 
			
		||||
			Value:      version,
 | 
			
		||||
		},
 | 
			
		||||
		IsInternal: util.OptionalBoolOf(isInternal),
 | 
			
		||||
		IsInternal: optional.Some(isInternal),
 | 
			
		||||
		Paginator:  db.NewAbsoluteListOptions(0, 1),
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
| 
						 | 
				
			
			@ -122,7 +123,7 @@ func GetVersionsByPackageType(ctx context.Context, ownerID int64, packageType Ty
 | 
			
		|||
	pvs, _, err := SearchVersions(ctx, &PackageSearchOptions{
 | 
			
		||||
		OwnerID:    ownerID,
 | 
			
		||||
		Type:       packageType,
 | 
			
		||||
		IsInternal: util.OptionalBoolFalse,
 | 
			
		||||
		IsInternal: optional.Some(false),
 | 
			
		||||
	})
 | 
			
		||||
	return pvs, err
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -136,7 +137,7 @@ func GetVersionsByPackageName(ctx context.Context, ownerID int64, packageType Ty
 | 
			
		|||
			ExactMatch: true,
 | 
			
		||||
			Value:      name,
 | 
			
		||||
		},
 | 
			
		||||
		IsInternal: util.OptionalBoolFalse,
 | 
			
		||||
		IsInternal: optional.Some(false),
 | 
			
		||||
	})
 | 
			
		||||
	return pvs, err
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -182,18 +183,18 @@ type PackageSearchOptions struct {
 | 
			
		|||
	Name            SearchValue       // only results with the specific name are found
 | 
			
		||||
	Version         SearchValue       // only results with the specific version are found
 | 
			
		||||
	Properties      map[string]string // only results are found which contain all listed version properties with the specific value
 | 
			
		||||
	IsInternal      util.OptionalBool
 | 
			
		||||
	IsInternal      optional.Option[bool]
 | 
			
		||||
	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
 | 
			
		||||
	db.Paginator
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (opts *PackageSearchOptions) ToConds() builder.Cond {
 | 
			
		||||
	cond := builder.NewCond()
 | 
			
		||||
	if !opts.IsInternal.IsNone() {
 | 
			
		||||
	if opts.IsInternal.Has() {
 | 
			
		||||
		cond = builder.Eq{
 | 
			
		||||
			"package_version.is_internal": opts.IsInternal.IsTrue(),
 | 
			
		||||
			"package_version.is_internal": opts.IsInternal.Value(),
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -250,10 +251,10 @@ func (opts *PackageSearchOptions) ToConds() builder.Cond {
 | 
			
		|||
		cond = cond.And(builder.Exists(builder.Select("package_file.id").From("package_file").Where(fileCond)))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !opts.HasFiles.IsNone() {
 | 
			
		||||
	if opts.HasFiles.Has() {
 | 
			
		||||
		filesCond := builder.Exists(builder.Select("package_file.id").From("package_file").Where(builder.Expr("package_file.version_id = package_version.id")))
 | 
			
		||||
 | 
			
		||||
		if opts.HasFiles.IsFalse() {
 | 
			
		||||
		if !opts.HasFiles.Value() {
 | 
			
		||||
			filesCond = builder.Not{filesCond}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -307,8 +308,8 @@ func SearchLatestVersions(ctx context.Context, opts *PackageSearchOptions) ([]*P
 | 
			
		|||
		And(builder.Expr("pv2.id IS NULL"))
 | 
			
		||||
 | 
			
		||||
	joinCond := builder.Expr("package_version.package_id = pv2.package_id AND (package_version.created_unix < pv2.created_unix OR (package_version.created_unix = pv2.created_unix AND package_version.id < pv2.id))")
 | 
			
		||||
	if !opts.IsInternal.IsNone() {
 | 
			
		||||
		joinCond = joinCond.And(builder.Eq{"pv2.is_internal": opts.IsInternal.IsTrue()})
 | 
			
		||||
	if opts.IsInternal.Has() {
 | 
			
		||||
		joinCond = joinCond.And(builder.Eq{"pv2.is_internal": opts.IsInternal.Value()})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	sess := db.GetEngine(ctx).
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,11 +6,13 @@ package project
 | 
			
		|||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"html/template"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
| 
						 | 
				
			
			@ -100,7 +102,7 @@ type Project struct {
 | 
			
		|||
	CardType    CardType
 | 
			
		||||
	Type        Type
 | 
			
		||||
 | 
			
		||||
	RenderedContent string `xorm:"-"`
 | 
			
		||||
	RenderedContent template.HTML `xorm:"-"`
 | 
			
		||||
 | 
			
		||||
	CreatedUnix    timeutil.TimeStamp `xorm:"INDEX created"`
 | 
			
		||||
	UpdatedUnix    timeutil.TimeStamp `xorm:"INDEX updated"`
 | 
			
		||||
| 
						 | 
				
			
			@ -195,7 +197,7 @@ type SearchOptions struct {
 | 
			
		|||
	db.ListOptions
 | 
			
		||||
	OwnerID  int64
 | 
			
		||||
	RepoID   int64
 | 
			
		||||
	IsClosed util.OptionalBool
 | 
			
		||||
	IsClosed optional.Option[bool]
 | 
			
		||||
	OrderBy  db.SearchOrderBy
 | 
			
		||||
	Type     Type
 | 
			
		||||
	Title    string
 | 
			
		||||
| 
						 | 
				
			
			@ -206,11 +208,8 @@ func (opts SearchOptions) ToConds() builder.Cond {
 | 
			
		|||
	if opts.RepoID > 0 {
 | 
			
		||||
		cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
 | 
			
		||||
	}
 | 
			
		||||
	switch opts.IsClosed {
 | 
			
		||||
	case util.OptionalBoolTrue:
 | 
			
		||||
		cond = cond.And(builder.Eq{"is_closed": true})
 | 
			
		||||
	case util.OptionalBoolFalse:
 | 
			
		||||
		cond = cond.And(builder.Eq{"is_closed": false})
 | 
			
		||||
	if opts.IsClosed.Has() {
 | 
			
		||||
		cond = cond.And(builder.Eq{"is_closed": opts.IsClosed.Value()})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if opts.Type > 0 {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,6 +7,7 @@ package repo
 | 
			
		|||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"sort"
 | 
			
		||||
	"strconv"
 | 
			
		||||
| 
						 | 
				
			
			@ -15,6 +16,7 @@ import (
 | 
			
		|||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/container"
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
	"code.gitea.io/gitea/modules/structs"
 | 
			
		||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
| 
						 | 
				
			
			@ -79,7 +81,7 @@ type Release struct {
 | 
			
		|||
	NumCommits       int64
 | 
			
		||||
	NumCommitsBehind int64              `xorm:"-"`
 | 
			
		||||
	Note             string             `xorm:"TEXT"`
 | 
			
		||||
	RenderedNote     string             `xorm:"-"`
 | 
			
		||||
	RenderedNote     template.HTML      `xorm:"-"`
 | 
			
		||||
	IsDraft          bool               `xorm:"NOT NULL DEFAULT false"`
 | 
			
		||||
	IsPrerelease     bool               `xorm:"NOT NULL DEFAULT false"`
 | 
			
		||||
	IsTag            bool               `xorm:"NOT NULL DEFAULT false"` // will be true only if the record is a tag and has no related releases
 | 
			
		||||
| 
						 | 
				
			
			@ -228,10 +230,10 @@ type FindReleasesOptions struct {
 | 
			
		|||
	RepoID        int64
 | 
			
		||||
	IncludeDrafts bool
 | 
			
		||||
	IncludeTags   bool
 | 
			
		||||
	IsPreRelease  util.OptionalBool
 | 
			
		||||
	IsDraft       util.OptionalBool
 | 
			
		||||
	IsPreRelease  optional.Option[bool]
 | 
			
		||||
	IsDraft       optional.Option[bool]
 | 
			
		||||
	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 {
 | 
			
		||||
| 
						 | 
				
			
			@ -246,14 +248,14 @@ func (opts FindReleasesOptions) ToConds() builder.Cond {
 | 
			
		|||
	if len(opts.TagNames) > 0 {
 | 
			
		||||
		cond = cond.And(builder.In("tag_name", opts.TagNames))
 | 
			
		||||
	}
 | 
			
		||||
	if !opts.IsPreRelease.IsNone() {
 | 
			
		||||
		cond = cond.And(builder.Eq{"is_prerelease": opts.IsPreRelease.IsTrue()})
 | 
			
		||||
	if opts.IsPreRelease.Has() {
 | 
			
		||||
		cond = cond.And(builder.Eq{"is_prerelease": opts.IsPreRelease.Value()})
 | 
			
		||||
	}
 | 
			
		||||
	if !opts.IsDraft.IsNone() {
 | 
			
		||||
		cond = cond.And(builder.Eq{"is_draft": opts.IsDraft.IsTrue()})
 | 
			
		||||
	if opts.IsDraft.Has() {
 | 
			
		||||
		cond = cond.And(builder.Eq{"is_draft": opts.IsDraft.Value()})
 | 
			
		||||
	}
 | 
			
		||||
	if !opts.HasSha1.IsNone() {
 | 
			
		||||
		if opts.HasSha1.IsTrue() {
 | 
			
		||||
	if opts.HasSha1.Has() {
 | 
			
		||||
		if opts.HasSha1.Value() {
 | 
			
		||||
			cond = cond.And(builder.Neq{"sha1": ""})
 | 
			
		||||
		} else {
 | 
			
		||||
			cond = cond.And(builder.Eq{"sha1": ""})
 | 
			
		||||
| 
						 | 
				
			
			@ -275,7 +277,7 @@ func GetTagNamesByRepoID(ctx context.Context, repoID int64) ([]string, error) {
 | 
			
		|||
		ListOptions:   listOptions,
 | 
			
		||||
		IncludeDrafts: true,
 | 
			
		||||
		IncludeTags:   true,
 | 
			
		||||
		HasSha1:       util.OptionalBoolTrue,
 | 
			
		||||
		HasSha1:       optional.Some(true),
 | 
			
		||||
		RepoID:        repoID,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,6 +20,7 @@ import (
 | 
			
		|||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/markup"
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	api "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
			
		||||
| 
						 | 
				
			
			@ -873,7 +874,7 @@ func (repo *Repository) TemplateRepo(ctx context.Context) *Repository {
 | 
			
		|||
 | 
			
		||||
type CountRepositoryOptions struct {
 | 
			
		||||
	OwnerID int64
 | 
			
		||||
	Private util.OptionalBool
 | 
			
		||||
	Private optional.Option[bool]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CountRepositories returns number of repositories.
 | 
			
		||||
| 
						 | 
				
			
			@ -885,8 +886,8 @@ func CountRepositories(ctx context.Context, opts CountRepositoryOptions) (int64,
 | 
			
		|||
	if opts.OwnerID > 0 {
 | 
			
		||||
		sess.And("owner_id = ?", opts.OwnerID)
 | 
			
		||||
	}
 | 
			
		||||
	if !opts.Private.IsNone() {
 | 
			
		||||
		sess.And("is_private=?", opts.Private.IsTrue())
 | 
			
		||||
	if opts.Private.Has() {
 | 
			
		||||
		sess.And("is_private=?", opts.Private.Value())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	count, err := sess.Count(new(Repository))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,6 +13,7 @@ import (
 | 
			
		|||
	"code.gitea.io/gitea/models/unit"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/container"
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/structs"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
| 
						 | 
				
			
			@ -125,11 +126,11 @@ type SearchRepoOptions struct {
 | 
			
		|||
	// None -> include public and private
 | 
			
		||||
	// True -> include just private
 | 
			
		||||
	// False -> include just public
 | 
			
		||||
	IsPrivate util.OptionalBool
 | 
			
		||||
	IsPrivate optional.Option[bool]
 | 
			
		||||
	// None -> include collaborative AND non-collaborative
 | 
			
		||||
	// True -> include just collaborative
 | 
			
		||||
	// False -> include just non-collaborative
 | 
			
		||||
	Collaborate util.OptionalBool
 | 
			
		||||
	Collaborate optional.Option[bool]
 | 
			
		||||
	// What type of unit the user can be collaborative in,
 | 
			
		||||
	// it is ignored if Collaborate is False.
 | 
			
		||||
	// TypeInvalid means any unit type.
 | 
			
		||||
| 
						 | 
				
			
			@ -137,19 +138,19 @@ type SearchRepoOptions struct {
 | 
			
		|||
	// None -> include forks AND non-forks
 | 
			
		||||
	// True -> include just forks
 | 
			
		||||
	// False -> include just non-forks
 | 
			
		||||
	Fork util.OptionalBool
 | 
			
		||||
	Fork optional.Option[bool]
 | 
			
		||||
	// None -> include templates AND non-templates
 | 
			
		||||
	// True -> include just templates
 | 
			
		||||
	// False -> include just non-templates
 | 
			
		||||
	Template util.OptionalBool
 | 
			
		||||
	Template optional.Option[bool]
 | 
			
		||||
	// None -> include mirrors AND non-mirrors
 | 
			
		||||
	// True -> include just mirrors
 | 
			
		||||
	// False -> include just non-mirrors
 | 
			
		||||
	Mirror util.OptionalBool
 | 
			
		||||
	Mirror optional.Option[bool]
 | 
			
		||||
	// None -> include archived AND non-archived
 | 
			
		||||
	// True -> include just archived
 | 
			
		||||
	// False -> include just non-archived
 | 
			
		||||
	Archived util.OptionalBool
 | 
			
		||||
	Archived optional.Option[bool]
 | 
			
		||||
	// only search topic name
 | 
			
		||||
	TopicOnly bool
 | 
			
		||||
	// only search repositories with specified primary language
 | 
			
		||||
| 
						 | 
				
			
			@ -159,7 +160,7 @@ type SearchRepoOptions struct {
 | 
			
		|||
	// None -> include has milestones AND has no milestone
 | 
			
		||||
	// True -> include just has milestones
 | 
			
		||||
	// False -> include just has no milestone
 | 
			
		||||
	HasMilestones util.OptionalBool
 | 
			
		||||
	HasMilestones optional.Option[bool]
 | 
			
		||||
	// LowerNames represents valid lower names to restrict to
 | 
			
		||||
	LowerNames []string
 | 
			
		||||
	// When specified true, apply some filters over the conditions:
 | 
			
		||||
| 
						 | 
				
			
			@ -359,12 +360,12 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond {
 | 
			
		|||
			)))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if opts.IsPrivate != util.OptionalBoolNone {
 | 
			
		||||
		cond = cond.And(builder.Eq{"is_private": opts.IsPrivate.IsTrue()})
 | 
			
		||||
	if opts.IsPrivate.Has() {
 | 
			
		||||
		cond = cond.And(builder.Eq{"is_private": opts.IsPrivate.Value()})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if opts.Template != util.OptionalBoolNone {
 | 
			
		||||
		cond = cond.And(builder.Eq{"is_template": opts.Template == util.OptionalBoolTrue})
 | 
			
		||||
	if opts.Template.Has() {
 | 
			
		||||
		cond = cond.And(builder.Eq{"is_template": opts.Template.Value()})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Restrict to starred repositories
 | 
			
		||||
| 
						 | 
				
			
			@ -380,11 +381,11 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond {
 | 
			
		|||
	// Restrict repositories to those the OwnerID owns or contributes to as per opts.Collaborate
 | 
			
		||||
	if opts.OwnerID > 0 {
 | 
			
		||||
		accessCond := builder.NewCond()
 | 
			
		||||
		if opts.Collaborate != util.OptionalBoolTrue {
 | 
			
		||||
		if !opts.Collaborate.Value() {
 | 
			
		||||
			accessCond = builder.Eq{"owner_id": opts.OwnerID}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if opts.Collaborate != util.OptionalBoolFalse {
 | 
			
		||||
		if opts.Collaborate.ValueOrDefault(true) {
 | 
			
		||||
			// A Collaboration is:
 | 
			
		||||
 | 
			
		||||
			collaborateCond := builder.NewCond()
 | 
			
		||||
| 
						 | 
				
			
			@ -472,32 +473,33 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond {
 | 
			
		|||
			Where(builder.Eq{"language": opts.Language}).And(builder.Eq{"is_primary": true})))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if opts.Fork != util.OptionalBoolNone || opts.OnlyShowRelevant {
 | 
			
		||||
		if opts.OnlyShowRelevant && opts.Fork == util.OptionalBoolNone {
 | 
			
		||||
	if opts.Fork.Has() || opts.OnlyShowRelevant {
 | 
			
		||||
		if opts.OnlyShowRelevant && !opts.Fork.Has() {
 | 
			
		||||
			cond = cond.And(builder.Eq{"is_fork": false})
 | 
			
		||||
		} else {
 | 
			
		||||
			cond = cond.And(builder.Eq{"is_fork": opts.Fork == util.OptionalBoolTrue})
 | 
			
		||||
			cond = cond.And(builder.Eq{"is_fork": opts.Fork.Value()})
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if opts.Mirror != util.OptionalBoolNone {
 | 
			
		||||
		cond = cond.And(builder.Eq{"is_mirror": opts.Mirror == util.OptionalBoolTrue})
 | 
			
		||||
	if opts.Mirror.Has() {
 | 
			
		||||
		cond = cond.And(builder.Eq{"is_mirror": opts.Mirror.Value()})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if opts.Actor != nil && opts.Actor.IsRestricted {
 | 
			
		||||
		cond = cond.And(AccessibleRepositoryCondition(opts.Actor, unit.TypeInvalid))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if opts.Archived != util.OptionalBoolNone {
 | 
			
		||||
		cond = cond.And(builder.Eq{"is_archived": opts.Archived == util.OptionalBoolTrue})
 | 
			
		||||
	if opts.Archived.Has() {
 | 
			
		||||
		cond = cond.And(builder.Eq{"is_archived": opts.Archived.Value()})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	switch opts.HasMilestones {
 | 
			
		||||
	case util.OptionalBoolTrue:
 | 
			
		||||
	if opts.HasMilestones.Has() {
 | 
			
		||||
		if opts.HasMilestones.Value() {
 | 
			
		||||
			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"}))
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if opts.OnlyShowRelevant {
 | 
			
		||||
		// Only show a repo that has at least a topic, an icon, or a description
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,7 +10,7 @@ import (
 | 
			
		|||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
			
		||||
	"code.gitea.io/gitea/models/unittest"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			@ -27,62 +27,62 @@ func getTestCases() []struct {
 | 
			
		|||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name:  "PublicRepositoriesByName",
 | 
			
		||||
			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{PageSize: 10}, Collaborate: util.OptionalBoolFalse},
 | 
			
		||||
			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{PageSize: 10}, Collaborate: optional.Some(false)},
 | 
			
		||||
			count: 7,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:  "PublicAndPrivateRepositoriesByName",
 | 
			
		||||
			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, Collaborate: util.OptionalBoolFalse},
 | 
			
		||||
			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, Collaborate: optional.Some(false)},
 | 
			
		||||
			count: 14,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:  "PublicAndPrivateRepositoriesByNameWithPagesizeLimitFirstPage",
 | 
			
		||||
			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 5}, Private: true, Collaborate: util.OptionalBoolFalse},
 | 
			
		||||
			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 5}, Private: true, Collaborate: optional.Some(false)},
 | 
			
		||||
			count: 14,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:  "PublicAndPrivateRepositoriesByNameWithPagesizeLimitSecondPage",
 | 
			
		||||
			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 2, PageSize: 5}, Private: true, Collaborate: util.OptionalBoolFalse},
 | 
			
		||||
			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 2, PageSize: 5}, Private: true, Collaborate: optional.Some(false)},
 | 
			
		||||
			count: 14,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:  "PublicAndPrivateRepositoriesByNameWithPagesizeLimitThirdPage",
 | 
			
		||||
			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 3, PageSize: 5}, Private: true, Collaborate: util.OptionalBoolFalse},
 | 
			
		||||
			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 3, PageSize: 5}, Private: true, Collaborate: optional.Some(false)},
 | 
			
		||||
			count: 14,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:  "PublicAndPrivateRepositoriesByNameWithPagesizeLimitFourthPage",
 | 
			
		||||
			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 3, PageSize: 5}, Private: true, Collaborate: util.OptionalBoolFalse},
 | 
			
		||||
			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 3, PageSize: 5}, Private: true, Collaborate: optional.Some(false)},
 | 
			
		||||
			count: 14,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:  "PublicRepositoriesOfUser",
 | 
			
		||||
			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Collaborate: util.OptionalBoolFalse},
 | 
			
		||||
			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Collaborate: optional.Some(false)},
 | 
			
		||||
			count: 2,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:  "PublicRepositoriesOfUser2",
 | 
			
		||||
			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Collaborate: util.OptionalBoolFalse},
 | 
			
		||||
			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Collaborate: optional.Some(false)},
 | 
			
		||||
			count: 0,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:  "PublicRepositoriesOfOrg3",
 | 
			
		||||
			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Collaborate: util.OptionalBoolFalse},
 | 
			
		||||
			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Collaborate: optional.Some(false)},
 | 
			
		||||
			count: 2,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:  "PublicAndPrivateRepositoriesOfUser",
 | 
			
		||||
			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, Collaborate: util.OptionalBoolFalse},
 | 
			
		||||
			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, Collaborate: optional.Some(false)},
 | 
			
		||||
			count: 4,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:  "PublicAndPrivateRepositoriesOfUser2",
 | 
			
		||||
			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true, Collaborate: util.OptionalBoolFalse},
 | 
			
		||||
			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true, Collaborate: optional.Some(false)},
 | 
			
		||||
			count: 0,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:  "PublicAndPrivateRepositoriesOfOrg3",
 | 
			
		||||
			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Private: true, Collaborate: util.OptionalBoolFalse},
 | 
			
		||||
			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Private: true, Collaborate: optional.Some(false)},
 | 
			
		||||
			count: 4,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
| 
						 | 
				
			
			@ -117,32 +117,32 @@ func getTestCases() []struct {
 | 
			
		|||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:  "PublicRepositoriesOfOrganization",
 | 
			
		||||
			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, Collaborate: util.OptionalBoolFalse},
 | 
			
		||||
			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, Collaborate: optional.Some(false)},
 | 
			
		||||
			count: 1,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:  "PublicAndPrivateRepositoriesOfOrganization",
 | 
			
		||||
			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, Private: true, Collaborate: util.OptionalBoolFalse},
 | 
			
		||||
			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, Private: true, Collaborate: optional.Some(false)},
 | 
			
		||||
			count: 2,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:  "AllPublic/PublicRepositoriesByName",
 | 
			
		||||
			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{PageSize: 10}, AllPublic: true, Collaborate: util.OptionalBoolFalse},
 | 
			
		||||
			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{PageSize: 10}, AllPublic: true, Collaborate: optional.Some(false)},
 | 
			
		||||
			count: 7,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:  "AllPublic/PublicAndPrivateRepositoriesByName",
 | 
			
		||||
			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, AllPublic: true, Collaborate: util.OptionalBoolFalse},
 | 
			
		||||
			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, AllPublic: true, Collaborate: optional.Some(false)},
 | 
			
		||||
			count: 14,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:  "AllPublic/PublicRepositoriesOfUserIncludingCollaborative",
 | 
			
		||||
			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, AllPublic: true, Template: util.OptionalBoolFalse},
 | 
			
		||||
			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, AllPublic: true, Template: optional.Some(false)},
 | 
			
		||||
			count: 34,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			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,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
| 
						 | 
				
			
			@ -157,12 +157,12 @@ func getTestCases() []struct {
 | 
			
		|||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:  "AllPublic/PublicRepositoriesOfOrganization",
 | 
			
		||||
			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, AllPublic: true, Collaborate: util.OptionalBoolFalse, Template: util.OptionalBoolFalse},
 | 
			
		||||
			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, AllPublic: true, Collaborate: optional.Some(false), Template: optional.Some(false)},
 | 
			
		||||
			count: 34,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:  "AllTemplates",
 | 
			
		||||
			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Template: util.OptionalBoolTrue},
 | 
			
		||||
			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Template: optional.Some(true)},
 | 
			
		||||
			count: 2,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
| 
						 | 
				
			
			@ -190,7 +190,7 @@ func TestSearchRepository(t *testing.T) {
 | 
			
		|||
			PageSize: 10,
 | 
			
		||||
		},
 | 
			
		||||
		Keyword:     "repo_12",
 | 
			
		||||
		Collaborate: util.OptionalBoolFalse,
 | 
			
		||||
		Collaborate: optional.Some(false),
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
| 
						 | 
				
			
			@ -205,7 +205,7 @@ func TestSearchRepository(t *testing.T) {
 | 
			
		|||
			PageSize: 10,
 | 
			
		||||
		},
 | 
			
		||||
		Keyword:     "test_repo",
 | 
			
		||||
		Collaborate: util.OptionalBoolFalse,
 | 
			
		||||
		Collaborate: optional.Some(false),
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
| 
						 | 
				
			
			@ -220,7 +220,7 @@ func TestSearchRepository(t *testing.T) {
 | 
			
		|||
		},
 | 
			
		||||
		Keyword:     "repo_13",
 | 
			
		||||
		Private:     true,
 | 
			
		||||
		Collaborate: util.OptionalBoolFalse,
 | 
			
		||||
		Collaborate: optional.Some(false),
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
| 
						 | 
				
			
			@ -236,7 +236,7 @@ func TestSearchRepository(t *testing.T) {
 | 
			
		|||
		},
 | 
			
		||||
		Keyword:     "test_repo",
 | 
			
		||||
		Private:     true,
 | 
			
		||||
		Collaborate: util.OptionalBoolFalse,
 | 
			
		||||
		Collaborate: optional.Some(false),
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
| 
						 | 
				
			
			@ -257,7 +257,7 @@ func TestSearchRepository(t *testing.T) {
 | 
			
		|||
			PageSize: 10,
 | 
			
		||||
		},
 | 
			
		||||
		Keyword:            "description_14",
 | 
			
		||||
		Collaborate:        util.OptionalBoolFalse,
 | 
			
		||||
		Collaborate:        optional.Some(false),
 | 
			
		||||
		IncludeDescription: true,
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -274,7 +274,7 @@ func TestSearchRepository(t *testing.T) {
 | 
			
		|||
			PageSize: 10,
 | 
			
		||||
		},
 | 
			
		||||
		Keyword:            "description_14",
 | 
			
		||||
		Collaborate:        util.OptionalBoolFalse,
 | 
			
		||||
		Collaborate:        optional.Some(false),
 | 
			
		||||
		IncludeDescription: false,
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -327,30 +327,25 @@ func TestSearchRepository(t *testing.T) {
 | 
			
		|||
						assert.False(t, repo.IsPrivate)
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					if testCase.opts.Fork == util.OptionalBoolTrue && testCase.opts.Mirror == util.OptionalBoolTrue {
 | 
			
		||||
						assert.True(t, repo.IsFork || repo.IsMirror)
 | 
			
		||||
					if testCase.opts.Fork.Value() && testCase.opts.Mirror.Value() {
 | 
			
		||||
						assert.True(t, repo.IsFork && repo.IsMirror)
 | 
			
		||||
					} else {
 | 
			
		||||
						switch testCase.opts.Fork {
 | 
			
		||||
						case util.OptionalBoolFalse:
 | 
			
		||||
							assert.False(t, repo.IsFork)
 | 
			
		||||
						case util.OptionalBoolTrue:
 | 
			
		||||
							assert.True(t, repo.IsFork)
 | 
			
		||||
						if testCase.opts.Fork.Has() {
 | 
			
		||||
							assert.Equal(t, testCase.opts.Fork.Value(), repo.IsFork)
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						switch testCase.opts.Mirror {
 | 
			
		||||
						case util.OptionalBoolFalse:
 | 
			
		||||
							assert.False(t, repo.IsMirror)
 | 
			
		||||
						case util.OptionalBoolTrue:
 | 
			
		||||
							assert.True(t, repo.IsMirror)
 | 
			
		||||
						if testCase.opts.Mirror.Has() {
 | 
			
		||||
							assert.Equal(t, testCase.opts.Mirror.Value(), repo.IsMirror)
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					if testCase.opts.OwnerID > 0 && !testCase.opts.AllPublic {
 | 
			
		||||
						switch testCase.opts.Collaborate {
 | 
			
		||||
						case util.OptionalBoolFalse:
 | 
			
		||||
							assert.Equal(t, testCase.opts.OwnerID, repo.Owner.ID)
 | 
			
		||||
						case util.OptionalBoolTrue:
 | 
			
		||||
						if testCase.opts.Collaborate.Has() {
 | 
			
		||||
							if testCase.opts.Collaborate.Value() {
 | 
			
		||||
								assert.NotEqual(t, testCase.opts.OwnerID, repo.Owner.ID)
 | 
			
		||||
							} else {
 | 
			
		||||
								assert.Equal(t, testCase.opts.OwnerID, repo.Owner.ID)
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,17 +12,17 @@ import (
 | 
			
		|||
	"code.gitea.io/gitea/models/unittest"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/markup"
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/test"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	countRepospts        = repo_model.CountRepositoryOptions{OwnerID: 10}
 | 
			
		||||
	countReposptsPublic  = repo_model.CountRepositoryOptions{OwnerID: 10, Private: util.OptionalBoolFalse}
 | 
			
		||||
	countReposptsPrivate = repo_model.CountRepositoryOptions{OwnerID: 10, Private: util.OptionalBoolTrue}
 | 
			
		||||
	countReposptsPublic  = repo_model.CountRepositoryOptions{OwnerID: 10, Private: optional.Some(false)}
 | 
			
		||||
	countReposptsPrivate = repo_model.CountRepositoryOptions{OwnerID: 10, Private: optional.Some(true)}
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestGetRepositoryCount(t *testing.T) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,6 +14,7 @@ import (
 | 
			
		|||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/modules/base"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	"code.gitea.io/gitea/modules/validation"
 | 
			
		||||
| 
						 | 
				
			
			@ -425,8 +426,8 @@ type SearchEmailOptions struct {
 | 
			
		|||
	db.ListOptions
 | 
			
		||||
	Keyword     string
 | 
			
		||||
	SortType    SearchEmailOrderBy
 | 
			
		||||
	IsPrimary   util.OptionalBool
 | 
			
		||||
	IsActivated util.OptionalBool
 | 
			
		||||
	IsPrimary   optional.Option[bool]
 | 
			
		||||
	IsActivated optional.Option[bool]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SearchEmailResult is an e-mail address found in the user or email_address table
 | 
			
		||||
| 
						 | 
				
			
			@ -453,18 +454,12 @@ func SearchEmails(ctx context.Context, opts *SearchEmailOptions) ([]*SearchEmail
 | 
			
		|||
		))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	switch {
 | 
			
		||||
	case opts.IsPrimary.IsTrue():
 | 
			
		||||
		cond = cond.And(builder.Eq{"email_address.is_primary": true})
 | 
			
		||||
	case opts.IsPrimary.IsFalse():
 | 
			
		||||
		cond = cond.And(builder.Eq{"email_address.is_primary": false})
 | 
			
		||||
	if opts.IsPrimary.Has() {
 | 
			
		||||
		cond = cond.And(builder.Eq{"email_address.is_primary": opts.IsPrimary.Value()})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	switch {
 | 
			
		||||
	case opts.IsActivated.IsTrue():
 | 
			
		||||
		cond = cond.And(builder.Eq{"email_address.is_activated": true})
 | 
			
		||||
	case opts.IsActivated.IsFalse():
 | 
			
		||||
		cond = cond.And(builder.Eq{"email_address.is_activated": false})
 | 
			
		||||
	if opts.IsActivated.Has() {
 | 
			
		||||
		cond = cond.And(builder.Eq{"email_address.is_activated": opts.IsActivated.Value()})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	count, err := db.GetEngine(ctx).Join("INNER", "`user`", "`user`.ID = email_address.uid").
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,7 +10,7 @@ import (
 | 
			
		|||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/models/unittest"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			@ -138,14 +138,14 @@ func TestListEmails(t *testing.T) {
 | 
			
		|||
	assert.True(t, contains(func(s *user_model.SearchEmailResult) bool { return s.UID == 27 }))
 | 
			
		||||
 | 
			
		||||
	// Must find only primary addresses (i.e. from the `user` table)
 | 
			
		||||
	opts = &user_model.SearchEmailOptions{IsPrimary: util.OptionalBoolTrue}
 | 
			
		||||
	opts = &user_model.SearchEmailOptions{IsPrimary: optional.Some(true)}
 | 
			
		||||
	emails, _, err = user_model.SearchEmails(db.DefaultContext, opts)
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.True(t, contains(func(s *user_model.SearchEmailResult) bool { return s.IsPrimary }))
 | 
			
		||||
	assert.False(t, contains(func(s *user_model.SearchEmailResult) bool { return !s.IsPrimary }))
 | 
			
		||||
 | 
			
		||||
	// Must find only inactive addresses (i.e. not validated)
 | 
			
		||||
	opts = &user_model.SearchEmailOptions{IsActivated: util.OptionalBoolFalse}
 | 
			
		||||
	opts = &user_model.SearchEmailOptions{IsActivated: optional.Some(false)}
 | 
			
		||||
	emails, _, err = user_model.SearchEmails(db.DefaultContext, opts)
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.True(t, contains(func(s *user_model.SearchEmailResult) bool { return !s.IsActivated }))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,8 +9,9 @@ import (
 | 
			
		|||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/modules/container"
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
	"code.gitea.io/gitea/modules/structs"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
 | 
			
		||||
	"xorm.io/builder"
 | 
			
		||||
	"xorm.io/xorm"
 | 
			
		||||
| 
						 | 
				
			
			@ -30,11 +31,13 @@ type SearchUserOptions struct {
 | 
			
		|||
	Actor         *User // The user doing the search
 | 
			
		||||
	SearchByEmail bool  // Search by email as well as username/full name
 | 
			
		||||
 | 
			
		||||
	IsActive           util.OptionalBool
 | 
			
		||||
	IsAdmin            util.OptionalBool
 | 
			
		||||
	IsRestricted       util.OptionalBool
 | 
			
		||||
	IsTwoFactorEnabled util.OptionalBool
 | 
			
		||||
	IsProhibitLogin    util.OptionalBool
 | 
			
		||||
	SupportedSortOrders container.Set[string] // if not nil, only allow to use the sort orders in this set
 | 
			
		||||
 | 
			
		||||
	IsActive           optional.Option[bool]
 | 
			
		||||
	IsAdmin            optional.Option[bool]
 | 
			
		||||
	IsRestricted       optional.Option[bool]
 | 
			
		||||
	IsTwoFactorEnabled optional.Option[bool]
 | 
			
		||||
	IsProhibitLogin    optional.Option[bool]
 | 
			
		||||
	IncludeReserved    bool
 | 
			
		||||
 | 
			
		||||
	ExtraParamStrings map[string]string
 | 
			
		||||
| 
						 | 
				
			
			@ -86,24 +89,24 @@ func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Sess
 | 
			
		|||
		cond = cond.And(builder.Eq{"login_name": opts.LoginName})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !opts.IsActive.IsNone() {
 | 
			
		||||
		cond = cond.And(builder.Eq{"is_active": opts.IsActive.IsTrue()})
 | 
			
		||||
	if opts.IsActive.Has() {
 | 
			
		||||
		cond = cond.And(builder.Eq{"is_active": opts.IsActive.Value()})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !opts.IsAdmin.IsNone() {
 | 
			
		||||
		cond = cond.And(builder.Eq{"is_admin": opts.IsAdmin.IsTrue()})
 | 
			
		||||
	if opts.IsAdmin.Has() {
 | 
			
		||||
		cond = cond.And(builder.Eq{"is_admin": opts.IsAdmin.Value()})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !opts.IsRestricted.IsNone() {
 | 
			
		||||
		cond = cond.And(builder.Eq{"is_restricted": opts.IsRestricted.IsTrue()})
 | 
			
		||||
	if opts.IsRestricted.Has() {
 | 
			
		||||
		cond = cond.And(builder.Eq{"is_restricted": opts.IsRestricted.Value()})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !opts.IsProhibitLogin.IsNone() {
 | 
			
		||||
		cond = cond.And(builder.Eq{"prohibit_login": opts.IsProhibitLogin.IsTrue()})
 | 
			
		||||
	if opts.IsProhibitLogin.Has() {
 | 
			
		||||
		cond = cond.And(builder.Eq{"prohibit_login": opts.IsProhibitLogin.Value()})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	e := db.GetEngine(ctx)
 | 
			
		||||
	if opts.IsTwoFactorEnabled.IsNone() {
 | 
			
		||||
	if !opts.IsTwoFactorEnabled.Has() {
 | 
			
		||||
		return e.Where(cond)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -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.
 | 
			
		||||
	// There are some possible methods to refactor this SQL in future when we really need to optimize the performance (but not now):
 | 
			
		||||
	// (1) add a column in user table (2) add a setting value in user_setting table (3) use search engines (bleve/elasticsearch)
 | 
			
		||||
	if opts.IsTwoFactorEnabled.IsTrue() {
 | 
			
		||||
	if opts.IsTwoFactorEnabled.Value() {
 | 
			
		||||
		cond = cond.And(builder.Expr("two_factor.uid IS NOT NULL"))
 | 
			
		||||
	} else {
 | 
			
		||||
		cond = cond.And(builder.Expr("two_factor.uid IS NULL"))
 | 
			
		||||
| 
						 | 
				
			
			@ -128,7 +131,7 @@ func SearchUsers(ctx context.Context, opts *SearchUserOptions) (users []*User, _
 | 
			
		|||
	defer sessCount.Close()
 | 
			
		||||
	count, err := sessCount.Count(new(User))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, 0, fmt.Errorf("Count: %w", err)
 | 
			
		||||
		return nil, 0, fmt.Errorf("count: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(opts.OrderBy) == 0 {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -727,7 +727,7 @@ func CreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOve
 | 
			
		|||
 | 
			
		||||
// IsLastAdminUser check whether user is the last admin
 | 
			
		||||
func IsLastAdminUser(ctx context.Context, user *User) bool {
 | 
			
		||||
	if user.IsAdmin && CountUsers(ctx, &CountUserFilter{IsAdmin: util.OptionalBoolTrue}) <= 1 {
 | 
			
		||||
	if user.IsAdmin && CountUsers(ctx, &CountUserFilter{IsAdmin: optional.Some(true)}) <= 1 {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
| 
						 | 
				
			
			@ -736,7 +736,7 @@ func IsLastAdminUser(ctx context.Context, user *User) bool {
 | 
			
		|||
// CountUserFilter represent optional filters for CountUsers
 | 
			
		||||
type CountUserFilter struct {
 | 
			
		||||
	LastLoginSince *int64
 | 
			
		||||
	IsAdmin        util.OptionalBool
 | 
			
		||||
	IsAdmin        optional.Option[bool]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CountUsers returns number of users.
 | 
			
		||||
| 
						 | 
				
			
			@ -754,8 +754,8 @@ func countUsers(ctx context.Context, opts *CountUserFilter) int64 {
 | 
			
		|||
			cond = cond.And(builder.Gte{"last_login_unix": *opts.LastLoginSince})
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !opts.IsAdmin.IsNone() {
 | 
			
		||||
			cond = cond.And(builder.Eq{"is_admin": opts.IsAdmin.IsTrue()})
 | 
			
		||||
		if opts.IsAdmin.Has() {
 | 
			
		||||
			cond = cond.And(builder.Eq{"is_admin": opts.IsAdmin.Value()})
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,10 +16,10 @@ import (
 | 
			
		|||
	"code.gitea.io/gitea/models/unittest"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/auth/password/hash"
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/structs"
 | 
			
		||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			@ -103,29 +103,29 @@ func TestSearchUsers(t *testing.T) {
 | 
			
		|||
	testUserSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}},
 | 
			
		||||
		[]int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37, 38, 39, 40})
 | 
			
		||||
 | 
			
		||||
	testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolFalse},
 | 
			
		||||
	testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(false)},
 | 
			
		||||
		[]int64{9})
 | 
			
		||||
 | 
			
		||||
	testUserSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolTrue},
 | 
			
		||||
	testUserSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)},
 | 
			
		||||
		[]int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37, 38, 39, 40})
 | 
			
		||||
 | 
			
		||||
	testUserSuccess(&user_model.SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolTrue},
 | 
			
		||||
	testUserSuccess(&user_model.SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)},
 | 
			
		||||
		[]int64{1, 10, 11, 12, 13, 14, 15, 16, 18})
 | 
			
		||||
 | 
			
		||||
	// order by name asc default
 | 
			
		||||
	testUserSuccess(&user_model.SearchUserOptions{Keyword: "user1", ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolTrue},
 | 
			
		||||
	testUserSuccess(&user_model.SearchUserOptions{Keyword: "user1", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)},
 | 
			
		||||
		[]int64{1, 10, 11, 12, 13, 14, 15, 16, 18})
 | 
			
		||||
 | 
			
		||||
	testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsAdmin: util.OptionalBoolTrue},
 | 
			
		||||
	testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsAdmin: optional.Some(true)},
 | 
			
		||||
		[]int64{1})
 | 
			
		||||
 | 
			
		||||
	testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsRestricted: util.OptionalBoolTrue},
 | 
			
		||||
	testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsRestricted: optional.Some(true)},
 | 
			
		||||
		[]int64{29})
 | 
			
		||||
 | 
			
		||||
	testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsProhibitLogin: util.OptionalBoolTrue},
 | 
			
		||||
	testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsProhibitLogin: optional.Some(true)},
 | 
			
		||||
		[]int64{37})
 | 
			
		||||
 | 
			
		||||
	testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsTwoFactorEnabled: util.OptionalBoolTrue},
 | 
			
		||||
	testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsTwoFactorEnabled: optional.Some(true)},
 | 
			
		||||
		[]int64{24})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,6 +12,7 @@ import (
 | 
			
		|||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/modules/json"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
	"code.gitea.io/gitea/modules/secret"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
			
		||||
| 
						 | 
				
			
			@ -433,7 +434,7 @@ type ListWebhookOptions struct {
 | 
			
		|||
	db.ListOptions
 | 
			
		||||
	RepoID   int64
 | 
			
		||||
	OwnerID  int64
 | 
			
		||||
	IsActive util.OptionalBool
 | 
			
		||||
	IsActive optional.Option[bool]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (opts ListWebhookOptions) ToConds() builder.Cond {
 | 
			
		||||
| 
						 | 
				
			
			@ -444,8 +445,8 @@ func (opts ListWebhookOptions) ToConds() builder.Cond {
 | 
			
		|||
	if opts.OwnerID != 0 {
 | 
			
		||||
		cond = cond.And(builder.Eq{"webhook.owner_id": opts.OwnerID})
 | 
			
		||||
	}
 | 
			
		||||
	if !opts.IsActive.IsNone() {
 | 
			
		||||
		cond = cond.And(builder.Eq{"webhook.is_active": opts.IsActive.IsTrue()})
 | 
			
		||||
	if opts.IsActive.Has() {
 | 
			
		||||
		cond = cond.And(builder.Eq{"webhook.is_active": opts.IsActive.Value()})
 | 
			
		||||
	}
 | 
			
		||||
	return cond
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,7 +8,7 @@ import (
 | 
			
		|||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// GetDefaultWebhooks returns all admin-default webhooks.
 | 
			
		||||
| 
						 | 
				
			
			@ -34,15 +34,15 @@ func GetSystemOrDefaultWebhook(ctx context.Context, id int64) (*Webhook, error)
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
// GetSystemWebhooks returns all admin system webhooks.
 | 
			
		||||
func GetSystemWebhooks(ctx context.Context, isActive util.OptionalBool) ([]*Webhook, error) {
 | 
			
		||||
func GetSystemWebhooks(ctx context.Context, isActive optional.Option[bool]) ([]*Webhook, error) {
 | 
			
		||||
	webhooks := make([]*Webhook, 0, 5)
 | 
			
		||||
	if isActive.IsNone() {
 | 
			
		||||
	if !isActive.Has() {
 | 
			
		||||
		return webhooks, db.GetEngine(ctx).
 | 
			
		||||
			Where("repo_id=? AND owner_id=? AND is_system_webhook=?", 0, 0, true).
 | 
			
		||||
			Find(&webhooks)
 | 
			
		||||
	}
 | 
			
		||||
	return webhooks, db.GetEngine(ctx).
 | 
			
		||||
		Where("repo_id=? AND owner_id=? AND is_system_webhook=? AND is_active = ?", 0, 0, true, isActive.IsTrue()).
 | 
			
		||||
		Where("repo_id=? AND owner_id=? AND is_system_webhook=? AND is_active = ?", 0, 0, true, isActive.Value()).
 | 
			
		||||
		Find(&webhooks)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,9 +11,9 @@ import (
 | 
			
		|||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/models/unittest"
 | 
			
		||||
	"code.gitea.io/gitea/modules/json"
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
	api "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	webhook_module "code.gitea.io/gitea/modules/webhook"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
| 
						 | 
				
			
			@ -123,7 +123,7 @@ func TestGetWebhookByOwnerID(t *testing.T) {
 | 
			
		|||
 | 
			
		||||
func TestGetActiveWebhooksByRepoID(t *testing.T) {
 | 
			
		||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
			
		||||
	hooks, err := db.Find[Webhook](db.DefaultContext, ListWebhookOptions{RepoID: 1, IsActive: util.OptionalBoolTrue})
 | 
			
		||||
	hooks, err := db.Find[Webhook](db.DefaultContext, ListWebhookOptions{RepoID: 1, IsActive: optional.Some(true)})
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	if assert.Len(t, hooks, 1) {
 | 
			
		||||
		assert.Equal(t, int64(1), hooks[0].ID)
 | 
			
		||||
| 
						 | 
				
			
			@ -143,7 +143,7 @@ func TestGetWebhooksByRepoID(t *testing.T) {
 | 
			
		|||
 | 
			
		||||
func TestGetActiveWebhooksByOwnerID(t *testing.T) {
 | 
			
		||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
			
		||||
	hooks, err := db.Find[Webhook](db.DefaultContext, ListWebhookOptions{OwnerID: 3, IsActive: util.OptionalBoolTrue})
 | 
			
		||||
	hooks, err := db.Find[Webhook](db.DefaultContext, ListWebhookOptions{OwnerID: 3, IsActive: optional.Some(true)})
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	if assert.Len(t, hooks, 1) {
 | 
			
		||||
		assert.Equal(t, int64(3), hooks[0].ID)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -35,6 +35,9 @@ func FullSteps(task *actions_model.ActionTask) []*actions_model.ActionTaskStep {
 | 
			
		|||
	} else if task.Status.IsDone() {
 | 
			
		||||
		preStep.Stopped = task.Stopped
 | 
			
		||||
		preStep.Status = actions_model.StatusFailure
 | 
			
		||||
		if task.Status.IsSkipped() {
 | 
			
		||||
			preStep.Status = actions_model.StatusSkipped
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	logIndex += preStep.LogLength
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -406,6 +406,9 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
 | 
			
		|||
	// all acts conditions should be satisfied
 | 
			
		||||
	for cond, vals := range acts {
 | 
			
		||||
		switch cond {
 | 
			
		||||
		case "types":
 | 
			
		||||
			// types have been checked
 | 
			
		||||
			continue
 | 
			
		||||
		case "branches":
 | 
			
		||||
			refName := git.RefName(prPayload.PullRequest.Base.Ref)
 | 
			
		||||
			patterns, err := workflowpattern.CompilePatterns(vals...)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,6 +12,7 @@ import (
 | 
			
		|||
	"io"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/exec"
 | 
			
		||||
	"runtime"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -344,6 +345,17 @@ func (c *Command) Run(opts *RunOpts) error {
 | 
			
		|||
		log.Debug("slow git.Command.Run: %s (%s)", c, elapsed)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// We need to check if the context is canceled by the program on Windows.
 | 
			
		||||
	// This is because Windows does not have signal checking when terminating the process.
 | 
			
		||||
	// It always returns exit code 1, unlike Linux, which has many exit codes for signals.
 | 
			
		||||
	if runtime.GOOS == "windows" &&
 | 
			
		||||
		err != nil &&
 | 
			
		||||
		err.Error() == "" &&
 | 
			
		||||
		cmd.ProcessState.ExitCode() == 1 &&
 | 
			
		||||
		ctx.Err() == context.Canceled {
 | 
			
		||||
		return ctx.Err()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err != nil && ctx.Err() != context.DeadlineExceeded {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -175,11 +175,11 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
 | 
			
		|||
		queries = append(queries, bleve.NewDisjunctionQuery(repoQueries...))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !options.IsPull.IsNone() {
 | 
			
		||||
		queries = append(queries, inner_bleve.BoolFieldQuery(options.IsPull.IsTrue(), "is_pull"))
 | 
			
		||||
	if options.IsPull.Has() {
 | 
			
		||||
		queries = append(queries, inner_bleve.BoolFieldQuery(options.IsPull.Value(), "is_pull"))
 | 
			
		||||
	}
 | 
			
		||||
	if !options.IsClosed.IsNone() {
 | 
			
		||||
		queries = append(queries, inner_bleve.BoolFieldQuery(options.IsClosed.IsTrue(), "is_closed"))
 | 
			
		||||
	if options.IsClosed.Has() {
 | 
			
		||||
		queries = append(queries, inner_bleve.BoolFieldQuery(options.IsClosed.Value(), "is_closed"))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if options.NoLabelOnly {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,6 +11,7 @@ import (
 | 
			
		|||
	issue_model "code.gitea.io/gitea/models/issues"
 | 
			
		||||
	"code.gitea.io/gitea/modules/container"
 | 
			
		||||
	"code.gitea.io/gitea/modules/indexer/issues/internal"
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_model.IssuesOptions, error) {
 | 
			
		||||
| 
						 | 
				
			
			@ -75,7 +76,7 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m
 | 
			
		|||
		UpdatedAfterUnix:   convertInt64(options.UpdatedAfterUnix),
 | 
			
		||||
		UpdatedBeforeUnix:  convertInt64(options.UpdatedBeforeUnix),
 | 
			
		||||
		PriorityRepoID:     0,
 | 
			
		||||
		IsArchived:         0,
 | 
			
		||||
		IsArchived:         optional.None[bool](),
 | 
			
		||||
		Org:                nil,
 | 
			
		||||
		Team:               nil,
 | 
			
		||||
		User:               nil,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -153,11 +153,11 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
 | 
			
		|||
		query.Must(q)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !options.IsPull.IsNone() {
 | 
			
		||||
		query.Must(elastic.NewTermQuery("is_pull", options.IsPull.IsTrue()))
 | 
			
		||||
	if options.IsPull.Has() {
 | 
			
		||||
		query.Must(elastic.NewTermQuery("is_pull", options.IsPull.Value()))
 | 
			
		||||
	}
 | 
			
		||||
	if !options.IsClosed.IsNone() {
 | 
			
		||||
		query.Must(elastic.NewTermQuery("is_closed", options.IsClosed.IsTrue()))
 | 
			
		||||
	if options.IsClosed.Has() {
 | 
			
		||||
		query.Must(elastic.NewTermQuery("is_closed", options.IsClosed.Value()))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if options.NoLabelOnly {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,10 +20,10 @@ import (
 | 
			
		|||
	"code.gitea.io/gitea/modules/indexer/issues/internal"
 | 
			
		||||
	"code.gitea.io/gitea/modules/indexer/issues/meilisearch"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
	"code.gitea.io/gitea/modules/process"
 | 
			
		||||
	"code.gitea.io/gitea/modules/queue"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// IndexerMetadata is used to send data to the queue, so it contains only the ids.
 | 
			
		||||
| 
						 | 
				
			
			@ -220,7 +220,7 @@ func PopulateIssueIndexer(ctx context.Context) error {
 | 
			
		|||
			ListOptions: db_model.ListOptions{Page: page, PageSize: repo_model.RepositoryListDefaultPageSize},
 | 
			
		||||
			OrderBy:     db_model.SearchOrderByID,
 | 
			
		||||
			Private:     true,
 | 
			
		||||
			Collaborate: util.OptionalBoolFalse,
 | 
			
		||||
			Collaborate: optional.Some(false),
 | 
			
		||||
		})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Error("SearchRepositoryByName: %v", err)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,8 +10,8 @@ import (
 | 
			
		|||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/models/unittest"
 | 
			
		||||
	"code.gitea.io/gitea/modules/indexer/issues/internal"
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
 | 
			
		||||
	_ "code.gitea.io/gitea/models"
 | 
			
		||||
	_ "code.gitea.io/gitea/models/actions"
 | 
			
		||||
| 
						 | 
				
			
			@ -210,13 +210,13 @@ func searchIssueIsPull(t *testing.T) {
 | 
			
		|||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			SearchOptions{
 | 
			
		||||
				IsPull: util.OptionalBoolFalse,
 | 
			
		||||
				IsPull: optional.Some(false),
 | 
			
		||||
			},
 | 
			
		||||
			[]int64{17, 16, 15, 14, 13, 6, 5, 18, 10, 7, 4, 1},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			SearchOptions{
 | 
			
		||||
				IsPull: util.OptionalBoolTrue,
 | 
			
		||||
				IsPull: optional.Some(true),
 | 
			
		||||
			},
 | 
			
		||||
			[]int64{22, 21, 12, 11, 20, 19, 9, 8, 3, 2},
 | 
			
		||||
		},
 | 
			
		||||
| 
						 | 
				
			
			@ -237,13 +237,13 @@ func searchIssueIsClosed(t *testing.T) {
 | 
			
		|||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			SearchOptions{
 | 
			
		||||
				IsClosed: util.OptionalBoolFalse,
 | 
			
		||||
				IsClosed: optional.Some(false),
 | 
			
		||||
			},
 | 
			
		||||
			[]int64{22, 21, 17, 16, 15, 14, 13, 12, 11, 20, 6, 19, 18, 10, 7, 9, 8, 3, 2, 1},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			SearchOptions{
 | 
			
		||||
				IsClosed: util.OptionalBoolTrue,
 | 
			
		||||
				IsClosed: optional.Some(true),
 | 
			
		||||
			},
 | 
			
		||||
			[]int64{5, 4},
 | 
			
		||||
		},
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,8 +5,8 @@ package internal
 | 
			
		|||
 | 
			
		||||
import (
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// IndexerData data stored in the issue indexer
 | 
			
		||||
| 
						 | 
				
			
			@ -77,8 +77,8 @@ type SearchOptions struct {
 | 
			
		|||
	RepoIDs   []int64 // repository IDs which the issues belong to
 | 
			
		||||
	AllPublic bool    // if include all public repositories
 | 
			
		||||
 | 
			
		||||
	IsPull   util.OptionalBool // if the issues is a pull request
 | 
			
		||||
	IsClosed util.OptionalBool // if the issues is closed
 | 
			
		||||
	IsPull   optional.Option[bool] // if the issues is a pull request
 | 
			
		||||
	IsClosed optional.Option[bool] // if the issues is closed
 | 
			
		||||
 | 
			
		||||
	IncludedLabelIDs    []int64 // labels the issues have
 | 
			
		||||
	ExcludedLabelIDs    []int64 // labels the issues don't have
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,8 +16,8 @@ import (
 | 
			
		|||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/modules/indexer/issues/internal"
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
| 
						 | 
				
			
			@ -166,7 +166,7 @@ var cases = []*testIndexerCase{
 | 
			
		|||
			Paginator: &db.ListOptions{
 | 
			
		||||
				PageSize: 5,
 | 
			
		||||
			},
 | 
			
		||||
			IsPull: util.OptionalBoolFalse,
 | 
			
		||||
			IsPull: optional.Some(false),
 | 
			
		||||
		},
 | 
			
		||||
		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 | 
			
		||||
			assert.Equal(t, 5, len(result.Hits))
 | 
			
		||||
| 
						 | 
				
			
			@ -182,7 +182,7 @@ var cases = []*testIndexerCase{
 | 
			
		|||
			Paginator: &db.ListOptions{
 | 
			
		||||
				PageSize: 5,
 | 
			
		||||
			},
 | 
			
		||||
			IsPull: util.OptionalBoolTrue,
 | 
			
		||||
			IsPull: optional.Some(true),
 | 
			
		||||
		},
 | 
			
		||||
		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 | 
			
		||||
			assert.Equal(t, 5, len(result.Hits))
 | 
			
		||||
| 
						 | 
				
			
			@ -198,7 +198,7 @@ var cases = []*testIndexerCase{
 | 
			
		|||
			Paginator: &db.ListOptions{
 | 
			
		||||
				PageSize: 5,
 | 
			
		||||
			},
 | 
			
		||||
			IsClosed: util.OptionalBoolFalse,
 | 
			
		||||
			IsClosed: optional.Some(false),
 | 
			
		||||
		},
 | 
			
		||||
		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 | 
			
		||||
			assert.Equal(t, 5, len(result.Hits))
 | 
			
		||||
| 
						 | 
				
			
			@ -214,7 +214,7 @@ var cases = []*testIndexerCase{
 | 
			
		|||
			Paginator: &db.ListOptions{
 | 
			
		||||
				PageSize: 5,
 | 
			
		||||
			},
 | 
			
		||||
			IsClosed: util.OptionalBoolTrue,
 | 
			
		||||
			IsClosed: optional.Some(true),
 | 
			
		||||
		},
 | 
			
		||||
		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 | 
			
		||||
			assert.Equal(t, 5, len(result.Hits))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -131,11 +131,11 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
 | 
			
		|||
		query.And(q)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !options.IsPull.IsNone() {
 | 
			
		||||
		query.And(inner_meilisearch.NewFilterEq("is_pull", options.IsPull.IsTrue()))
 | 
			
		||||
	if options.IsPull.Has() {
 | 
			
		||||
		query.And(inner_meilisearch.NewFilterEq("is_pull", options.IsPull.Value()))
 | 
			
		||||
	}
 | 
			
		||||
	if !options.IsClosed.IsNone() {
 | 
			
		||||
		query.And(inner_meilisearch.NewFilterEq("is_closed", options.IsClosed.IsTrue()))
 | 
			
		||||
	if options.IsClosed.Has() {
 | 
			
		||||
		query.And(inner_meilisearch.NewFilterEq("is_closed", options.IsClosed.Value()))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if options.NoLabelOnly {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -122,7 +122,13 @@ func validateRequired(field *api.IssueFormField, idx int) error {
 | 
			
		|||
		// The label is not required for a markdown or checkboxes field
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	return validateBoolItem(newErrorPosition(idx, field.Type), field.Validations, "required")
 | 
			
		||||
	if err := validateBoolItem(newErrorPosition(idx, field.Type), field.Validations, "required"); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if required, _ := field.Validations["required"].(bool); required && !field.VisibleOnForm() {
 | 
			
		||||
		return newErrorPosition(idx, field.Type).Errorf("can not require a hidden field")
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func validateID(field *api.IssueFormField, idx int, ids container.Set[string]) error {
 | 
			
		||||
| 
						 | 
				
			
			@ -172,10 +178,38 @@ func validateOptions(field *api.IssueFormField, idx int) error {
 | 
			
		|||
				return position.Errorf("'label' is required and should be a string")
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if visibility, ok := opt["visible"]; ok {
 | 
			
		||||
				visibilityList, ok := visibility.([]any)
 | 
			
		||||
				if !ok {
 | 
			
		||||
					return position.Errorf("'visible' should be list")
 | 
			
		||||
				}
 | 
			
		||||
				for _, visibleType := range visibilityList {
 | 
			
		||||
					visibleType, ok := visibleType.(string)
 | 
			
		||||
					if !ok || !(visibleType == "form" || visibleType == "content") {
 | 
			
		||||
						return position.Errorf("'visible' list can only contain strings of 'form' and 'content'")
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if required, ok := opt["required"]; ok {
 | 
			
		||||
				if _, ok := required.(bool); !ok {
 | 
			
		||||
					return position.Errorf("'required' should be a bool")
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// validate if hidden field is required
 | 
			
		||||
				if visibility, ok := opt["visible"]; ok {
 | 
			
		||||
					visibilityList, _ := visibility.([]any)
 | 
			
		||||
					isVisible := false
 | 
			
		||||
					for _, v := range visibilityList {
 | 
			
		||||
						if vv, _ := v.(string); vv == "form" {
 | 
			
		||||
							isVisible = true
 | 
			
		||||
							break
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
					if !isVisible {
 | 
			
		||||
						return position.Errorf("can not require a hidden checkbox")
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -238,7 +272,7 @@ func RenderToMarkdown(template *api.IssueTemplate, values url.Values) string {
 | 
			
		|||
			IssueFormField: field,
 | 
			
		||||
			Values:         values,
 | 
			
		||||
		}
 | 
			
		||||
		if f.ID == "" {
 | 
			
		||||
		if f.ID == "" || !f.VisibleInContent() {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		f.WriteTo(builder)
 | 
			
		||||
| 
						 | 
				
			
			@ -253,11 +287,6 @@ type valuedField struct {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
func (f *valuedField) WriteTo(builder *strings.Builder) {
 | 
			
		||||
	if f.Type == api.IssueFormFieldTypeMarkdown {
 | 
			
		||||
		// markdown blocks do not appear in output
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// write label
 | 
			
		||||
	if !f.HideLabel() {
 | 
			
		||||
		_, _ = fmt.Fprintf(builder, "### %s\n\n", f.Label())
 | 
			
		||||
| 
						 | 
				
			
			@ -269,6 +298,9 @@ func (f *valuedField) WriteTo(builder *strings.Builder) {
 | 
			
		|||
	switch f.Type {
 | 
			
		||||
	case api.IssueFormFieldTypeCheckboxes:
 | 
			
		||||
		for _, option := range f.Options() {
 | 
			
		||||
			if !option.VisibleInContent() {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			checked := " "
 | 
			
		||||
			if option.IsChecked() {
 | 
			
		||||
				checked = "x"
 | 
			
		||||
| 
						 | 
				
			
			@ -302,6 +334,10 @@ func (f *valuedField) WriteTo(builder *strings.Builder) {
 | 
			
		|||
		} else {
 | 
			
		||||
			_, _ = fmt.Fprintf(builder, "%s\n", value)
 | 
			
		||||
		}
 | 
			
		||||
	case api.IssueFormFieldTypeMarkdown:
 | 
			
		||||
		if value, ok := f.Attributes["value"].(string); ok {
 | 
			
		||||
			_, _ = fmt.Fprintf(builder, "%s\n", value)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	_, _ = fmt.Fprintln(builder)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -314,6 +350,9 @@ func (f *valuedField) Label() string {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
func (f *valuedField) HideLabel() bool {
 | 
			
		||||
	if f.Type == api.IssueFormFieldTypeMarkdown {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	if label, ok := f.Attributes["hide_label"].(bool); ok {
 | 
			
		||||
		return label
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -385,6 +424,22 @@ func (o *valuedOption) IsChecked() bool {
 | 
			
		|||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (o *valuedOption) VisibleInContent() bool {
 | 
			
		||||
	if o.field.Type == api.IssueFormFieldTypeCheckboxes {
 | 
			
		||||
		if vs, ok := o.data.(map[string]any); ok {
 | 
			
		||||
			if vl, ok := vs["visible"].([]any); ok {
 | 
			
		||||
				for _, v := range vl {
 | 
			
		||||
					if vv, _ := v.(string); vv == "content" {
 | 
			
		||||
						return true
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				return false
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var minQuotesRegex = regexp.MustCompilePOSIX("^`{3,}")
 | 
			
		||||
 | 
			
		||||
// minQuotes return 3 or more back-quotes.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,6 +10,7 @@ import (
 | 
			
		|||
	"code.gitea.io/gitea/modules/json"
 | 
			
		||||
	api "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -318,6 +319,42 @@ body:
 | 
			
		|||
`,
 | 
			
		||||
			wantErr: "body[0](checkboxes), option[0]: 'required' should be a bool",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "field is required but hidden",
 | 
			
		||||
			content: `
 | 
			
		||||
name: "test"
 | 
			
		||||
about: "this is about"
 | 
			
		||||
body:
 | 
			
		||||
  - type: "input"
 | 
			
		||||
    id: "1"
 | 
			
		||||
    attributes:
 | 
			
		||||
      label: "a"
 | 
			
		||||
    validations:
 | 
			
		||||
      required: true
 | 
			
		||||
    visible: [content]
 | 
			
		||||
`,
 | 
			
		||||
			wantErr: "body[0](input): can not require a hidden field",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "checkboxes is required but hidden",
 | 
			
		||||
			content: `
 | 
			
		||||
name: "test"
 | 
			
		||||
about: "this is about"
 | 
			
		||||
body:
 | 
			
		||||
  - type: checkboxes
 | 
			
		||||
    id: "1"
 | 
			
		||||
    attributes:
 | 
			
		||||
      label: Label of checkboxes
 | 
			
		||||
      description: Description of checkboxes
 | 
			
		||||
      options:
 | 
			
		||||
        - label: Option 1
 | 
			
		||||
          required: false
 | 
			
		||||
        - label: Required and hidden
 | 
			
		||||
          required: true
 | 
			
		||||
          visible: [content]
 | 
			
		||||
`,
 | 
			
		||||
			wantErr: "body[0](checkboxes), option[1]: can not require a hidden checkbox",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "valid",
 | 
			
		||||
			content: `
 | 
			
		||||
| 
						 | 
				
			
			@ -374,8 +411,11 @@ body:
 | 
			
		|||
          required: true
 | 
			
		||||
        - label: Option 2 of checkboxes
 | 
			
		||||
          required: false
 | 
			
		||||
        - label: Option 3 of checkboxes
 | 
			
		||||
        - label: Hidden Option 3 of checkboxes
 | 
			
		||||
          visible: [content]
 | 
			
		||||
        - label: Required but not submitted
 | 
			
		||||
          required: true
 | 
			
		||||
          visible: [form]
 | 
			
		||||
`,
 | 
			
		||||
			want: &api.IssueTemplate{
 | 
			
		||||
				Name:   "Name",
 | 
			
		||||
| 
						 | 
				
			
			@ -390,6 +430,7 @@ body:
 | 
			
		|||
						Attributes: map[string]any{
 | 
			
		||||
							"value": "Value of the markdown",
 | 
			
		||||
						},
 | 
			
		||||
						Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm},
 | 
			
		||||
					},
 | 
			
		||||
					{
 | 
			
		||||
						Type: "textarea",
 | 
			
		||||
| 
						 | 
				
			
			@ -404,6 +445,7 @@ body:
 | 
			
		|||
						Validations: map[string]any{
 | 
			
		||||
							"required": true,
 | 
			
		||||
						},
 | 
			
		||||
						Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent},
 | 
			
		||||
					},
 | 
			
		||||
					{
 | 
			
		||||
						Type: "input",
 | 
			
		||||
| 
						 | 
				
			
			@ -419,6 +461,7 @@ body:
 | 
			
		|||
							"is_number": true,
 | 
			
		||||
							"regex":     "[a-zA-Z0-9]+",
 | 
			
		||||
						},
 | 
			
		||||
						Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent},
 | 
			
		||||
					},
 | 
			
		||||
					{
 | 
			
		||||
						Type: "dropdown",
 | 
			
		||||
| 
						 | 
				
			
			@ -436,6 +479,7 @@ body:
 | 
			
		|||
						Validations: map[string]any{
 | 
			
		||||
							"required": true,
 | 
			
		||||
						},
 | 
			
		||||
						Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent},
 | 
			
		||||
					},
 | 
			
		||||
					{
 | 
			
		||||
						Type: "checkboxes",
 | 
			
		||||
| 
						 | 
				
			
			@ -446,9 +490,11 @@ body:
 | 
			
		|||
							"options": []any{
 | 
			
		||||
								map[string]any{"label": "Option 1 of checkboxes", "required": true},
 | 
			
		||||
								map[string]any{"label": "Option 2 of checkboxes", "required": false},
 | 
			
		||||
								map[string]any{"label": "Option 3 of checkboxes", "required": true},
 | 
			
		||||
								map[string]any{"label": "Hidden Option 3 of checkboxes", "visible": []string{"content"}},
 | 
			
		||||
								map[string]any{"label": "Required but not submitted", "required": true, "visible": []string{"form"}},
 | 
			
		||||
							},
 | 
			
		||||
						},
 | 
			
		||||
						Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				FileName: "test.yaml",
 | 
			
		||||
| 
						 | 
				
			
			@ -467,7 +513,12 @@ body:
 | 
			
		|||
  - type: markdown
 | 
			
		||||
    id: id1
 | 
			
		||||
    attributes:
 | 
			
		||||
      value: Value of the markdown
 | 
			
		||||
      value: Value of the markdown shown in form
 | 
			
		||||
  - type: markdown
 | 
			
		||||
    id: id2
 | 
			
		||||
    attributes:
 | 
			
		||||
      value: Value of the markdown shown in created issue
 | 
			
		||||
    visible: [content]
 | 
			
		||||
`,
 | 
			
		||||
			want: &api.IssueTemplate{
 | 
			
		||||
				Name:   "Name",
 | 
			
		||||
| 
						 | 
				
			
			@ -480,8 +531,17 @@ body:
 | 
			
		|||
						Type: "markdown",
 | 
			
		||||
						ID:   "id1",
 | 
			
		||||
						Attributes: map[string]any{
 | 
			
		||||
							"value": "Value of the markdown",
 | 
			
		||||
							"value": "Value of the markdown shown in form",
 | 
			
		||||
						},
 | 
			
		||||
						Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm},
 | 
			
		||||
					},
 | 
			
		||||
					{
 | 
			
		||||
						Type: "markdown",
 | 
			
		||||
						ID:   "id2",
 | 
			
		||||
						Attributes: map[string]any{
 | 
			
		||||
							"value": "Value of the markdown shown in created issue",
 | 
			
		||||
						},
 | 
			
		||||
						Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleContent},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				FileName: "test.yaml",
 | 
			
		||||
| 
						 | 
				
			
			@ -515,6 +575,7 @@ body:
 | 
			
		|||
						Attributes: map[string]any{
 | 
			
		||||
							"value": "Value of the markdown",
 | 
			
		||||
						},
 | 
			
		||||
						Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				FileName: "test.yaml",
 | 
			
		||||
| 
						 | 
				
			
			@ -548,6 +609,7 @@ body:
 | 
			
		|||
						Attributes: map[string]any{
 | 
			
		||||
							"value": "Value of the markdown",
 | 
			
		||||
						},
 | 
			
		||||
						Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				FileName: "test.yaml",
 | 
			
		||||
| 
						 | 
				
			
			@ -622,9 +684,14 @@ body:
 | 
			
		|||
  - type: markdown
 | 
			
		||||
    id: id1
 | 
			
		||||
    attributes:
 | 
			
		||||
      value: Value of the markdown
 | 
			
		||||
  - type: textarea
 | 
			
		||||
      value: Value of the markdown shown in form
 | 
			
		||||
  - type: markdown
 | 
			
		||||
    id: id2
 | 
			
		||||
    attributes:
 | 
			
		||||
      value: Value of the markdown shown in created issue
 | 
			
		||||
    visible: [content]
 | 
			
		||||
  - type: textarea
 | 
			
		||||
    id: id3
 | 
			
		||||
    attributes:
 | 
			
		||||
      label: Label of textarea
 | 
			
		||||
      description: Description of textarea
 | 
			
		||||
| 
						 | 
				
			
			@ -634,7 +701,7 @@ body:
 | 
			
		|||
    validations:
 | 
			
		||||
      required: true
 | 
			
		||||
  - type: input
 | 
			
		||||
    id: id3
 | 
			
		||||
    id: id4
 | 
			
		||||
    attributes:
 | 
			
		||||
      label: Label of input
 | 
			
		||||
      description: Description of input
 | 
			
		||||
| 
						 | 
				
			
			@ -646,7 +713,7 @@ body:
 | 
			
		|||
      is_number: true
 | 
			
		||||
      regex: "[a-zA-Z0-9]+"
 | 
			
		||||
  - type: dropdown
 | 
			
		||||
    id: id4
 | 
			
		||||
    id: id5
 | 
			
		||||
    attributes:
 | 
			
		||||
      label: Label of dropdown
 | 
			
		||||
      description: Description of dropdown
 | 
			
		||||
| 
						 | 
				
			
			@ -658,7 +725,7 @@ body:
 | 
			
		|||
    validations:
 | 
			
		||||
      required: true
 | 
			
		||||
  - type: checkboxes
 | 
			
		||||
    id: id5
 | 
			
		||||
    id: id6
 | 
			
		||||
    attributes:
 | 
			
		||||
      label: Label of checkboxes
 | 
			
		||||
      description: Description of checkboxes
 | 
			
		||||
| 
						 | 
				
			
			@ -669,20 +736,26 @@ body:
 | 
			
		|||
          required: false
 | 
			
		||||
        - label: Option 3 of checkboxes
 | 
			
		||||
          required: true
 | 
			
		||||
          visible: [form]
 | 
			
		||||
        - label: Hidden Option of checkboxes
 | 
			
		||||
          visible: [content]
 | 
			
		||||
`,
 | 
			
		||||
				values: map[string][]string{
 | 
			
		||||
					"form-field-id2":   {"Value of id2"},
 | 
			
		||||
					"form-field-id3":   {"Value of id3"},
 | 
			
		||||
					"form-field-id4":   {"0,1"},
 | 
			
		||||
					"form-field-id5-0": {"on"},
 | 
			
		||||
					"form-field-id5-2": {"on"},
 | 
			
		||||
					"form-field-id4":   {"Value of id4"},
 | 
			
		||||
					"form-field-id5":   {"0,1"},
 | 
			
		||||
					"form-field-id6-0": {"on"},
 | 
			
		||||
					"form-field-id6-2": {"on"},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			want: `### Label of textarea
 | 
			
		||||
 | 
			
		||||
` + "```bash\nValue of id2\n```" + `
 | 
			
		||||
			want: `Value of the markdown shown in created issue
 | 
			
		||||
 | 
			
		||||
Value of id3
 | 
			
		||||
### Label of textarea
 | 
			
		||||
 | 
			
		||||
` + "```bash\nValue of id3\n```" + `
 | 
			
		||||
 | 
			
		||||
Value of id4
 | 
			
		||||
 | 
			
		||||
### Label of dropdown
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -692,7 +765,7 @@ Option 1 of dropdown, Option 2 of dropdown
 | 
			
		|||
 | 
			
		||||
- [x] Option 1 of checkboxes
 | 
			
		||||
- [ ] Option 2 of checkboxes
 | 
			
		||||
- [x] Option 3 of checkboxes
 | 
			
		||||
- [ ] Hidden Option of checkboxes
 | 
			
		||||
 | 
			
		||||
`,
 | 
			
		||||
		},
 | 
			
		||||
| 
						 | 
				
			
			@ -704,7 +777,7 @@ Option 1 of dropdown, Option 2 of dropdown
 | 
			
		|||
				t.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
			if got := RenderToMarkdown(template, tt.args.values); got != tt.want {
 | 
			
		||||
				t.Errorf("RenderToMarkdown() = %v, want %v", got, tt.want)
 | 
			
		||||
				assert.EqualValues(t, tt.want, got)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -128,9 +128,18 @@ func unmarshal(filename string, content []byte) (*api.IssueTemplate, error) {
 | 
			
		|||
			}
 | 
			
		||||
		}
 | 
			
		||||
		for i, v := range it.Fields {
 | 
			
		||||
			// set default id value
 | 
			
		||||
			if v.ID == "" {
 | 
			
		||||
				v.ID = strconv.Itoa(i)
 | 
			
		||||
			}
 | 
			
		||||
			// set default visibility
 | 
			
		||||
			if v.Visible == nil {
 | 
			
		||||
				v.Visible = []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm}
 | 
			
		||||
				// markdown is not submitted by default
 | 
			
		||||
				if v.Type != api.IssueFormFieldTypeMarkdown {
 | 
			
		||||
					v.Visible = append(v.Visible, api.IssueFormFieldVisibleContent)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -388,7 +388,7 @@ func TestRender_ShortLinks(t *testing.T) {
 | 
			
		|||
			},
 | 
			
		||||
		}, input)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
 | 
			
		||||
		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
 | 
			
		||||
		buffer, err = markdown.RenderString(&markup.RenderContext{
 | 
			
		||||
			Ctx: git.DefaultContext,
 | 
			
		||||
			Links: markup.Links{
 | 
			
		||||
| 
						 | 
				
			
			@ -398,7 +398,7 @@ func TestRender_ShortLinks(t *testing.T) {
 | 
			
		|||
			IsWiki: true,
 | 
			
		||||
		}, input)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(buffer))
 | 
			
		||||
		assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer)))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	mediatree := util.URLJoin(markup.TestRepoURL, "media", "master")
 | 
			
		||||
| 
						 | 
				
			
			@ -501,7 +501,7 @@ func TestRender_RelativeImages(t *testing.T) {
 | 
			
		|||
			Metas: localMetas,
 | 
			
		||||
		}, input)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
 | 
			
		||||
		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
 | 
			
		||||
		buffer, err = markdown.RenderString(&markup.RenderContext{
 | 
			
		||||
			Ctx: git.DefaultContext,
 | 
			
		||||
			Links: markup.Links{
 | 
			
		||||
| 
						 | 
				
			
			@ -511,7 +511,7 @@ func TestRender_RelativeImages(t *testing.T) {
 | 
			
		|||
			IsWiki: true,
 | 
			
		||||
		}, input)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(buffer))
 | 
			
		||||
		assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer)))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rawwiki := util.URLJoin(markup.TestRepoURL, "wiki", "raw")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,7 @@ package markdown
 | 
			
		|||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"io"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"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
 | 
			
		||||
func RenderString(ctx *markup.RenderContext, content string) (string, error) {
 | 
			
		||||
func RenderString(ctx *markup.RenderContext, content string) (template.HTML, error) {
 | 
			
		||||
	var buf strings.Builder
 | 
			
		||||
	if err := Render(ctx, strings.NewReader(content), &buf); err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	return buf.String(), nil
 | 
			
		||||
	return template.HTML(buf.String()), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RenderRaw renders Markdown to HTML without handling special links.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,6 +5,7 @@ package markdown_test
 | 
			
		|||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
| 
						 | 
				
			
			@ -59,7 +60,7 @@ func TestRender_StandardLinks(t *testing.T) {
 | 
			
		|||
			},
 | 
			
		||||
		}, input)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
 | 
			
		||||
		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
 | 
			
		||||
 | 
			
		||||
		buffer, err = markdown.RenderString(&markup.RenderContext{
 | 
			
		||||
			Ctx: git.DefaultContext,
 | 
			
		||||
| 
						 | 
				
			
			@ -69,7 +70,7 @@ func TestRender_StandardLinks(t *testing.T) {
 | 
			
		|||
			IsWiki: true,
 | 
			
		||||
		}, input)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(buffer))
 | 
			
		||||
		assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer)))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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)
 | 
			
		||||
		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"
 | 
			
		||||
| 
						 | 
				
			
			@ -304,7 +305,7 @@ func TestTotal_RenderWiki(t *testing.T) {
 | 
			
		|||
			IsWiki: true,
 | 
			
		||||
		}, sameCases[i])
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.Equal(t, answers[i], line)
 | 
			
		||||
		assert.Equal(t, template.HTML(answers[i]), line)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	testCases := []string{
 | 
			
		||||
| 
						 | 
				
			
			@ -329,7 +330,7 @@ func TestTotal_RenderWiki(t *testing.T) {
 | 
			
		|||
			IsWiki: true,
 | 
			
		||||
		}, testCases[i])
 | 
			
		||||
		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,
 | 
			
		||||
		}, sameCases[i])
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.Equal(t, answers[i], line)
 | 
			
		||||
		assert.Equal(t, template.HTML(answers[i]), line)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	testCases := []string{}
 | 
			
		||||
| 
						 | 
				
			
			@ -362,7 +363,7 @@ func TestTotal_RenderString(t *testing.T) {
 | 
			
		|||
			},
 | 
			
		||||
		}, testCases[i])
 | 
			
		||||
		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)
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.Equal(t, expected, res)
 | 
			
		||||
	assert.Equal(t, template.HTML(expected), res)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestColorPreview(t *testing.T) {
 | 
			
		||||
| 
						 | 
				
			
			@ -463,7 +464,7 @@ func TestColorPreview(t *testing.T) {
 | 
			
		|||
	for _, test := range positiveTests {
 | 
			
		||||
		res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase)
 | 
			
		||||
		assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase)
 | 
			
		||||
		assert.Equal(t, test.expected, res, "Unexpected result in testcase %q", test.testcase)
 | 
			
		||||
		assert.Equal(t, template.HTML(test.expected), res, "Unexpected result in testcase %q", test.testcase)
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -542,7 +543,7 @@ func TestMathBlock(t *testing.T) {
 | 
			
		|||
	for _, test := range testcases {
 | 
			
		||||
		res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase)
 | 
			
		||||
		assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase)
 | 
			
		||||
		assert.Equal(t, test.expected, res, "Unexpected result in testcase %q", test.testcase)
 | 
			
		||||
		assert.Equal(t, template.HTML(test.expected), res, "Unexpected result in testcase %q", test.testcase)
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -741,7 +742,7 @@ Citation needed[^0].`,
 | 
			
		|||
	for _, test := range testcases {
 | 
			
		||||
		res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase)
 | 
			
		||||
		assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase)
 | 
			
		||||
		assert.Equal(t, test.expected, res, "Unexpected result in testcase %q", test.testcase)
 | 
			
		||||
		assert.Equal(t, test.expected, string(res), "Unexpected result in testcase %q", test.testcase)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -778,12 +779,12 @@ foo: bar
 | 
			
		|||
	for _, test := range testcases {
 | 
			
		||||
		res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase)
 | 
			
		||||
		assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase)
 | 
			
		||||
		assert.Equal(t, test.expected, res, "Unexpected result in testcase %q", test.testcase)
 | 
			
		||||
		assert.Equal(t, template.HTML(test.expected), res, "Unexpected result in testcase %q", test.testcase)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestRenderLinks(t *testing.T) {
 | 
			
		||||
	input := `  space @mention-user  
 | 
			
		||||
	input := `  space @mention-user${SPACE}${SPACE}
 | 
			
		||||
/just/a/path.bin
 | 
			
		||||
https://example.com/file.bin
 | 
			
		||||
[local link](file.bin)
 | 
			
		||||
| 
						 | 
				
			
			@ -804,8 +805,9 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
 | 
			
		|||
mail@domain.com
 | 
			
		||||
@mention-user test
 | 
			
		||||
#123
 | 
			
		||||
  space  
 | 
			
		||||
  space${SPACE}${SPACE}
 | 
			
		||||
`
 | 
			
		||||
	input = strings.ReplaceAll(input, "${SPACE}", " ") // replace ${SPACE} with " ", to avoid some editor's auto-trimming
 | 
			
		||||
	cases := []struct {
 | 
			
		||||
		Links    markup.Links
 | 
			
		||||
		IsWiki   bool
 | 
			
		||||
| 
						 | 
				
			
			@ -1168,7 +1170,7 @@ space</p>
 | 
			
		|||
	for i, c := range cases {
 | 
			
		||||
		result, err := markdown.RenderString(&markup.RenderContext{Ctx: context.Background(), Links: c.Links, IsWiki: c.IsWiki}, input)
 | 
			
		||||
		assert.NoError(t, err, "Unexpected error in testcase: %v", i)
 | 
			
		||||
		assert.Equal(t, c.Expected, result, "Unexpected result in testcase %v", i)
 | 
			
		||||
		assert.Equal(t, template.HTML(c.Expected), result, "Unexpected result in testcase %v", i)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1187,7 +1189,7 @@ func TestCustomMarkdownURL(t *testing.T) {
 | 
			
		|||
			},
 | 
			
		||||
		}, input)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
 | 
			
		||||
		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	test("[test](abp:subscribe?location=https://codeberg.org/filters.txt&title=joy)",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -27,6 +27,16 @@ func TestOption(t *testing.T) {
 | 
			
		|||
	assert.Equal(t, int(1), some.Value())
 | 
			
		||||
	assert.Equal(t, int(1), some.ValueOrDefault(2))
 | 
			
		||||
 | 
			
		||||
	noneBool := optional.None[bool]()
 | 
			
		||||
	assert.False(t, noneBool.Has())
 | 
			
		||||
	assert.False(t, noneBool.Value())
 | 
			
		||||
	assert.True(t, noneBool.ValueOrDefault(true))
 | 
			
		||||
 | 
			
		||||
	someBool := optional.Some(true)
 | 
			
		||||
	assert.True(t, someBool.Has())
 | 
			
		||||
	assert.True(t, someBool.Value())
 | 
			
		||||
	assert.True(t, someBool.ValueOrDefault(false))
 | 
			
		||||
 | 
			
		||||
	var ptr *int
 | 
			
		||||
	assert.False(t, optional.FromPtr(ptr).Has())
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -60,6 +60,9 @@ func (q *WorkerPoolQueue[T]) doDispatchBatchToWorker(wg *workerGroup[T], flushCh
 | 
			
		|||
		full = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// TODO: the logic could be improved in the future, to avoid a data-race between "doStartNewWorker" and "workerNum"
 | 
			
		||||
	// The root problem is that if we skip "doStartNewWorker" here, the "workerNum" might be decreased by other workers later
 | 
			
		||||
	// So ideally, it should check whether there are enough workers by some approaches, and start new workers if necessary.
 | 
			
		||||
	q.workerNumMu.Lock()
 | 
			
		||||
	noWorker := q.workerNum == 0
 | 
			
		||||
	if full || noWorker {
 | 
			
		||||
| 
						 | 
				
			
			@ -143,7 +146,11 @@ func (q *WorkerPoolQueue[T]) doStartNewWorker(wp *workerGroup[T]) {
 | 
			
		|||
		log.Debug("Queue %q starts new worker", q.GetName())
 | 
			
		||||
		defer log.Debug("Queue %q stops idle worker", q.GetName())
 | 
			
		||||
 | 
			
		||||
		atomic.AddInt32(&q.workerStartedCounter, 1) // Only increase counter, used for debugging
 | 
			
		||||
 | 
			
		||||
		t := time.NewTicker(workerIdleDuration)
 | 
			
		||||
		defer t.Stop()
 | 
			
		||||
 | 
			
		||||
		keepWorking := true
 | 
			
		||||
		stopWorking := func() {
 | 
			
		||||
			q.workerNumMu.Lock()
 | 
			
		||||
| 
						 | 
				
			
			@ -158,13 +165,18 @@ func (q *WorkerPoolQueue[T]) doStartNewWorker(wp *workerGroup[T]) {
 | 
			
		|||
			case batch, ok := <-q.batchChan:
 | 
			
		||||
				if !ok {
 | 
			
		||||
					stopWorking()
 | 
			
		||||
				} else {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				q.doWorkerHandle(batch)
 | 
			
		||||
				// reset the idle ticker, and drain the tick after reset in case a tick is already triggered
 | 
			
		||||
				t.Reset(workerIdleDuration)
 | 
			
		||||
				select {
 | 
			
		||||
				case <-t.C:
 | 
			
		||||
				default:
 | 
			
		||||
				}
 | 
			
		||||
			case <-t.C:
 | 
			
		||||
				q.workerNumMu.Lock()
 | 
			
		||||
				keepWorking = q.workerNum <= 1
 | 
			
		||||
				keepWorking = q.workerNum <= 1 // keep the last worker running
 | 
			
		||||
				if !keepWorking {
 | 
			
		||||
					q.workerNum--
 | 
			
		||||
				}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -40,6 +40,8 @@ type WorkerPoolQueue[T any] struct {
 | 
			
		|||
	workerMaxNum    int
 | 
			
		||||
	workerActiveNum int
 | 
			
		||||
	workerNumMu     sync.Mutex
 | 
			
		||||
 | 
			
		||||
	workerStartedCounter int32
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type flushType chan struct{}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,6 +11,7 @@ import (
 | 
			
		|||
	"time"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/test"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			@ -175,11 +176,7 @@ func testWorkerPoolQueuePersistence(t *testing.T, queueSetting setting.QueueSett
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
func TestWorkerPoolQueueActiveWorkers(t *testing.T) {
 | 
			
		||||
	oldWorkerIdleDuration := workerIdleDuration
 | 
			
		||||
	workerIdleDuration = 300 * time.Millisecond
 | 
			
		||||
	defer func() {
 | 
			
		||||
		workerIdleDuration = oldWorkerIdleDuration
 | 
			
		||||
	}()
 | 
			
		||||
	defer test.MockVariableValue(&workerIdleDuration, 300*time.Millisecond)()
 | 
			
		||||
 | 
			
		||||
	handler := func(items ...int) (unhandled []int) {
 | 
			
		||||
		time.Sleep(100 * time.Millisecond)
 | 
			
		||||
| 
						 | 
				
			
			@ -250,3 +247,25 @@ func TestWorkerPoolQueueShutdown(t *testing.T) {
 | 
			
		|||
	q, _ = newWorkerPoolQueueForTest("test-workpoolqueue", qs, handler, false)
 | 
			
		||||
	assert.EqualValues(t, 20, q.GetQueueItemNumber())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestWorkerPoolQueueWorkerIdleReset(t *testing.T) {
 | 
			
		||||
	defer test.MockVariableValue(&workerIdleDuration, 10*time.Millisecond)()
 | 
			
		||||
 | 
			
		||||
	handler := func(items ...int) (unhandled []int) {
 | 
			
		||||
		time.Sleep(50 * time.Millisecond)
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	q, _ := newWorkerPoolQueueForTest("test-workpoolqueue", setting.QueueSettings{Type: "channel", BatchLength: 1, MaxWorkers: 2, Length: 100}, handler, false)
 | 
			
		||||
	stop := runWorkerPoolQueue(q)
 | 
			
		||||
	for i := 0; i < 20; i++ {
 | 
			
		||||
		assert.NoError(t, q.Push(i))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	time.Sleep(500 * time.Millisecond)
 | 
			
		||||
	assert.EqualValues(t, 2, q.GetWorkerNumber())
 | 
			
		||||
	assert.EqualValues(t, 2, q.GetWorkerActiveNumber())
 | 
			
		||||
	// when the queue never becomes empty, the existing workers should keep working
 | 
			
		||||
	assert.EqualValues(t, 2, q.workerStartedCounter)
 | 
			
		||||
	stop()
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -31,9 +31,9 @@ var (
 | 
			
		|||
	// mentionPattern matches all mentions in the form of "@user" or "@org/team"
 | 
			
		||||
	mentionPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(@[0-9a-zA-Z-_]+|@[0-9a-zA-Z-_]+\/?[0-9a-zA-Z-_]+|@[0-9a-zA-Z-_][0-9a-zA-Z-_.]+\/?[0-9a-zA-Z-_.]+[0-9a-zA-Z-_])(?:'|\s|[:,;.?!]\s|[:,;.?!]?$|\)|\])`)
 | 
			
		||||
	// issueNumericPattern matches string that references to a numeric issue, e.g. #1287
 | 
			
		||||
	issueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[|\')([#!][0-9]+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`)
 | 
			
		||||
	issueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[|\'|\")([#!][0-9]+)(?:\s|$|\)|\]|\'|\"|[:;,.?!]\s|[:;,.?!]$)`)
 | 
			
		||||
	// issueAlphanumericPattern matches string that references to an alphanumeric issue, e.g. ABC-1234
 | 
			
		||||
	issueAlphanumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([A-Z]{1,10}-[1-9][0-9]*)(?:\s|$|\)|\]|:|\.(\s|$))`)
 | 
			
		||||
	issueAlphanumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[|\"|\')([A-Z]{1,10}-[1-9][0-9]*)(?:\s|$|\)|\]|:|\.(\s|$)|\"|\')`)
 | 
			
		||||
	// crossReferenceIssueNumericPattern matches string that references a numeric issue in a different repository
 | 
			
		||||
	// e.g. org/repo#12345
 | 
			
		||||
	crossReferenceIssueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-zA-Z-_\.]+/[0-9a-zA-Z-_\.]+[#!][0-9]+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -432,6 +432,8 @@ func TestRegExp_issueNumericPattern(t *testing.T) {
 | 
			
		|||
		"  #12",
 | 
			
		||||
		"#12:",
 | 
			
		||||
		"ref: #12: msg",
 | 
			
		||||
		"\"#1234\"",
 | 
			
		||||
		"'#1234'",
 | 
			
		||||
	}
 | 
			
		||||
	falseTestCases := []string{
 | 
			
		||||
		"# 1234",
 | 
			
		||||
| 
						 | 
				
			
			@ -462,6 +464,8 @@ func TestRegExp_issueAlphanumericPattern(t *testing.T) {
 | 
			
		|||
		"(ABC-123)",
 | 
			
		||||
		"[ABC-123]",
 | 
			
		||||
		"ABC-123:",
 | 
			
		||||
		"\"ABC-123\"",
 | 
			
		||||
		"'ABC-123'",
 | 
			
		||||
	}
 | 
			
		||||
	falseTestCases := []string{
 | 
			
		||||
		"RC-08",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,22 +6,18 @@ package repository
 | 
			
		|||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"sort"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	issues_model "code.gitea.io/gitea/models/issues"
 | 
			
		||||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
	"code.gitea.io/gitea/modules/label"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/options"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	asymkey_service "code.gitea.io/gitea/services/asymkey"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type OptionFile struct {
 | 
			
		||||
| 
						 | 
				
			
			@ -124,70 +120,6 @@ func LoadRepoConfig() error {
 | 
			
		|||
	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) {
 | 
			
		||||
	// Somehow the directory could exist.
 | 
			
		||||
	repoPath := repo_model.RepoPath(owner, name)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,4 +22,5 @@ func loadAdminFrom(rootCfg ConfigProvider) {
 | 
			
		|||
 | 
			
		||||
const (
 | 
			
		||||
	UserFeatureDeletion      = "deletion"
 | 
			
		||||
	UserFeatureManageGPGKeys = "manage_gpg_keys"
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,7 +21,7 @@ var SessionConfig = struct {
 | 
			
		|||
	ProviderConfig string
 | 
			
		||||
	// Cookie name to save session ID. Default is "MacaronSession".
 | 
			
		||||
	CookieName string
 | 
			
		||||
	// Cookie path to store. Default is "/". HINT: there was a bug, the old value doesn't have trailing slash, and could be empty "".
 | 
			
		||||
	// Cookie path to store. Default is "/".
 | 
			
		||||
	CookiePath string
 | 
			
		||||
	// GC interval time in seconds. Default is 3600.
 | 
			
		||||
	Gclifetime int64
 | 
			
		||||
| 
						 | 
				
			
			@ -49,7 +49,10 @@ func loadSessionFrom(rootCfg ConfigProvider) {
 | 
			
		|||
		SessionConfig.ProviderConfig = path.Join(AppWorkPath, SessionConfig.ProviderConfig)
 | 
			
		||||
	}
 | 
			
		||||
	SessionConfig.CookieName = sec.Key("COOKIE_NAME").MustString("i_like_gitea")
 | 
			
		||||
	SessionConfig.CookiePath = AppSubURL + "/" // there was a bug, old code only set CookePath=AppSubURL, no trailing slash
 | 
			
		||||
	SessionConfig.CookiePath = AppSubURL
 | 
			
		||||
	if SessionConfig.CookiePath == "" {
 | 
			
		||||
		SessionConfig.CookiePath = "/"
 | 
			
		||||
	}
 | 
			
		||||
	SessionConfig.Secure = sec.Key("COOKIE_SECURE").MustBool(strings.HasPrefix(strings.ToLower(AppURL), "https://"))
 | 
			
		||||
	SessionConfig.Gclifetime = sec.Key("GC_INTERVAL_TIME").MustInt64(86400)
 | 
			
		||||
	SessionConfig.Maxlifetime = sec.Key("SESSION_LIFE_TIME").MustInt64(86400)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,7 @@ package structs
 | 
			
		|||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"path"
 | 
			
		||||
	"slices"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -147,8 +148,33 @@ type IssueFormField struct {
 | 
			
		|||
	ID          string                  `json:"id" yaml:"id"`
 | 
			
		||||
	Attributes  map[string]any          `json:"attributes" yaml:"attributes"`
 | 
			
		||||
	Validations map[string]any          `json:"validations" yaml:"validations"`
 | 
			
		||||
	Visible     []IssueFormFieldVisible `json:"visible,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (iff IssueFormField) VisibleOnForm() bool {
 | 
			
		||||
	if len(iff.Visible) == 0 {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	return slices.Contains(iff.Visible, IssueFormFieldVisibleForm)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (iff IssueFormField) VisibleInContent() bool {
 | 
			
		||||
	if len(iff.Visible) == 0 {
 | 
			
		||||
		// we have our markdown exception
 | 
			
		||||
		return iff.Type != IssueFormFieldTypeMarkdown
 | 
			
		||||
	}
 | 
			
		||||
	return slices.Contains(iff.Visible, IssueFormFieldVisibleContent)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IssueFormFieldVisible defines issue form field visible
 | 
			
		||||
// swagger:model
 | 
			
		||||
type IssueFormFieldVisible string
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	IssueFormFieldVisibleForm    IssueFormFieldVisible = "form"
 | 
			
		||||
	IssueFormFieldVisibleContent IssueFormFieldVisible = "content"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// IssueTemplate represents an issue template for a repository
 | 
			
		||||
// swagger:model
 | 
			
		||||
type IssueTemplate struct {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -40,7 +40,7 @@ func NewFuncMap() template.FuncMap {
 | 
			
		|||
		"HTMLEscape":   HTMLEscape,
 | 
			
		||||
		"QueryEscape":  url.QueryEscape,
 | 
			
		||||
		"JSEscape":     JSEscapeSafe,
 | 
			
		||||
		"Str2html":    Str2html, // TODO: rename it to SanitizeHTML
 | 
			
		||||
		"SanitizeHTML": SanitizeHTML,
 | 
			
		||||
		"URLJoin":      util.URLJoin,
 | 
			
		||||
		"DotEscape":    DotEscape,
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -210,8 +210,8 @@ func SafeHTML(s any) template.HTML {
 | 
			
		|||
	panic(fmt.Sprintf("unexpected type %T", s))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Str2html sanitizes the input by pre-defined markdown rules
 | 
			
		||||
func Str2html(s any) template.HTML {
 | 
			
		||||
// SanitizeHTML sanitizes the input by pre-defined markdown rules
 | 
			
		||||
func SanitizeHTML(s any) template.HTML {
 | 
			
		||||
	switch v := s.(type) {
 | 
			
		||||
	case string:
 | 
			
		||||
		return template.HTML(markup.Sanitize(v))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -61,3 +61,8 @@ func TestJSEscapeSafe(t *testing.T) {
 | 
			
		|||
func TestHTMLFormat(t *testing.T) {
 | 
			
		||||
	assert.Equal(t, template.HTML("<a>< < 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>`)))
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,6 +5,7 @@ package templates
 | 
			
		|||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strings"
 | 
			
		||||
| 
						 | 
				
			
			@ -33,7 +34,7 @@ func mailSubjectTextFuncMap() texttmpl.FuncMap {
 | 
			
		|||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template, name string, content []byte) {
 | 
			
		||||
func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template, name string, content []byte) error {
 | 
			
		||||
	// Split template into subject and body
 | 
			
		||||
	var subjectContent []byte
 | 
			
		||||
	bodyContent := content
 | 
			
		||||
| 
						 | 
				
			
			@ -42,14 +43,13 @@ func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template,
 | 
			
		|||
		subjectContent = content[0:loc[0]]
 | 
			
		||||
		bodyContent = content[loc[1]:]
 | 
			
		||||
	}
 | 
			
		||||
	if _, err := stpl.New(name).
 | 
			
		||||
		Parse(string(subjectContent)); err != nil {
 | 
			
		||||
		log.Warn("Failed to parse template [%s/subject]: %v", name, err)
 | 
			
		||||
	if _, err := stpl.New(name).Parse(string(subjectContent)); err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to parse template [%s/subject]: %w", name, err)
 | 
			
		||||
	}
 | 
			
		||||
	if _, err := btpl.New(name).
 | 
			
		||||
		Parse(string(bodyContent)); err != nil {
 | 
			
		||||
		log.Warn("Failed to parse template [%s/body]: %v", name, err)
 | 
			
		||||
	if _, err := btpl.New(name).Parse(string(bodyContent)); err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to parse template [%s/body]: %w", name, err)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Mailer provides the templates required for sending notification mails.
 | 
			
		||||
| 
						 | 
				
			
			@ -81,7 +81,13 @@ func Mailer(ctx context.Context) (*texttmpl.Template, *template.Template) {
 | 
			
		|||
			if firstRun {
 | 
			
		||||
				log.Trace("Adding mail template %s: %s by %s", tmplName, assetPath, layerName)
 | 
			
		||||
			}
 | 
			
		||||
			buildSubjectBodyTemplate(subjectTemplates, bodyTemplates, tmplName, content)
 | 
			
		||||
			if err = buildSubjectBodyTemplate(subjectTemplates, bodyTemplates, tmplName, content); err != nil {
 | 
			
		||||
				if firstRun {
 | 
			
		||||
					log.Fatal("Failed to parse mail template, err: %v", err)
 | 
			
		||||
				} else {
 | 
			
		||||
					log.Error("Failed to parse mail template, err: %v", err)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -208,7 +208,7 @@ func RenderMarkdownToHtml(ctx context.Context, input string) template.HTML { //n
 | 
			
		|||
	if err != nil {
 | 
			
		||||
		log.Error("RenderString: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	return template.HTML(output)
 | 
			
		||||
	return output
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func RenderLabels(ctx context.Context, labels []*issues_model.Label, repoLink string) template.HTML {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,6 +4,7 @@
 | 
			
		|||
package templates
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -28,6 +29,19 @@ func (su *StringUtils) HasPrefix(s any, prefix string) bool {
 | 
			
		|||
	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 {
 | 
			
		||||
	return strings.Contains(s, substr)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,7 +9,9 @@ import (
 | 
			
		|||
)
 | 
			
		||||
 | 
			
		||||
// MockLocale provides a mocked locale without any translations
 | 
			
		||||
type MockLocale struct{}
 | 
			
		||||
type MockLocale struct {
 | 
			
		||||
	Lang, LangName string // these fields are used directly in templates: ctx.Locale.Lang
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ Locale = (*MockLocale)(nil)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -144,7 +144,7 @@ func Match(tags ...language.Tag) language.Tag {
 | 
			
		|||
// locale represents the information of localization.
 | 
			
		||||
type locale struct {
 | 
			
		||||
	i18n.Locale
 | 
			
		||||
	Lang, LangName string // these fields are used directly in templates: .i18n.Lang
 | 
			
		||||
	Lang, LangName string // these fields are used directly in templates: ctx.Locale.Lang
 | 
			
		||||
	msgPrinter     *message.Printer
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,64 +17,13 @@ import (
 | 
			
		|||
	"golang.org/x/text/language"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// OptionalBool a boolean that can be "null"
 | 
			
		||||
type OptionalBool byte
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	// OptionalBoolNone a "null" boolean value
 | 
			
		||||
	OptionalBoolNone OptionalBool = iota
 | 
			
		||||
	// OptionalBoolTrue a "true" boolean value
 | 
			
		||||
	OptionalBoolTrue
 | 
			
		||||
	// OptionalBoolFalse a "false" boolean value
 | 
			
		||||
	OptionalBoolFalse
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// IsTrue return true if equal to OptionalBoolTrue
 | 
			
		||||
func (o OptionalBool) IsTrue() bool {
 | 
			
		||||
	return o == OptionalBoolTrue
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsFalse return true if equal to OptionalBoolFalse
 | 
			
		||||
func (o OptionalBool) IsFalse() bool {
 | 
			
		||||
	return o == OptionalBoolFalse
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsNone return true if equal to OptionalBoolNone
 | 
			
		||||
func (o OptionalBool) IsNone() bool {
 | 
			
		||||
	return o == OptionalBoolNone
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ToGeneric converts OptionalBool to optional.Option[bool]
 | 
			
		||||
func (o OptionalBool) ToGeneric() optional.Option[bool] {
 | 
			
		||||
	if o.IsNone() {
 | 
			
		||||
// OptionalBoolParse get the corresponding optional.Option[bool] of a string using strconv.ParseBool
 | 
			
		||||
func OptionalBoolParse(s string) optional.Option[bool] {
 | 
			
		||||
	v, e := strconv.ParseBool(s)
 | 
			
		||||
	if e != nil {
 | 
			
		||||
		return optional.None[bool]()
 | 
			
		||||
	}
 | 
			
		||||
	return optional.Some[bool](o.IsTrue())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// OptionalBoolFromGeneric converts optional.Option[bool] to OptionalBool
 | 
			
		||||
func OptionalBoolFromGeneric(o optional.Option[bool]) OptionalBool {
 | 
			
		||||
	if o.Has() {
 | 
			
		||||
		return OptionalBoolOf(o.Value())
 | 
			
		||||
	}
 | 
			
		||||
	return OptionalBoolNone
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// OptionalBoolOf get the corresponding OptionalBool of a bool
 | 
			
		||||
func OptionalBoolOf(b bool) OptionalBool {
 | 
			
		||||
	if b {
 | 
			
		||||
		return OptionalBoolTrue
 | 
			
		||||
	}
 | 
			
		||||
	return OptionalBoolFalse
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// OptionalBoolParse get the corresponding OptionalBool of a string using strconv.ParseBool
 | 
			
		||||
func OptionalBoolParse(s string) OptionalBool {
 | 
			
		||||
	b, e := strconv.ParseBool(s)
 | 
			
		||||
	if e != nil {
 | 
			
		||||
		return OptionalBoolNone
 | 
			
		||||
	}
 | 
			
		||||
	return OptionalBoolOf(b)
 | 
			
		||||
	return optional.Some(v)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsEmptyString checks if the provided string is empty
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,6 +8,8 @@ import (
 | 
			
		|||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -173,17 +175,17 @@ func Test_RandomBytes(t *testing.T) {
 | 
			
		|||
	assert.NotEqual(t, bytes3, bytes4)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Test_OptionalBool(t *testing.T) {
 | 
			
		||||
	assert.Equal(t, OptionalBoolNone, OptionalBoolParse(""))
 | 
			
		||||
	assert.Equal(t, OptionalBoolNone, OptionalBoolParse("x"))
 | 
			
		||||
func TestOptionalBoolParse(t *testing.T) {
 | 
			
		||||
	assert.Equal(t, optional.None[bool](), OptionalBoolParse(""))
 | 
			
		||||
	assert.Equal(t, optional.None[bool](), OptionalBoolParse("x"))
 | 
			
		||||
 | 
			
		||||
	assert.Equal(t, OptionalBoolFalse, OptionalBoolParse("0"))
 | 
			
		||||
	assert.Equal(t, OptionalBoolFalse, OptionalBoolParse("f"))
 | 
			
		||||
	assert.Equal(t, OptionalBoolFalse, OptionalBoolParse("False"))
 | 
			
		||||
	assert.Equal(t, optional.Some(false), OptionalBoolParse("0"))
 | 
			
		||||
	assert.Equal(t, optional.Some(false), OptionalBoolParse("f"))
 | 
			
		||||
	assert.Equal(t, optional.Some(false), OptionalBoolParse("False"))
 | 
			
		||||
 | 
			
		||||
	assert.Equal(t, OptionalBoolTrue, OptionalBoolParse("1"))
 | 
			
		||||
	assert.Equal(t, OptionalBoolTrue, OptionalBoolParse("t"))
 | 
			
		||||
	assert.Equal(t, OptionalBoolTrue, OptionalBoolParse("True"))
 | 
			
		||||
	assert.Equal(t, optional.Some(true), OptionalBoolParse("1"))
 | 
			
		||||
	assert.Equal(t, optional.Some(true), OptionalBoolParse("t"))
 | 
			
		||||
	assert.Equal(t, optional.Some(true), OptionalBoolParse("True"))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Test case for any function which accepts and returns a single string.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										23
									
								
								options/license/MIT-Khronos-old
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								options/license/MIT-Khronos-old
									
										
									
									
									
										Normal 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.
 | 
			
		||||
| 
						 | 
				
			
			@ -124,6 +124,7 @@ pin=Připnout
 | 
			
		|||
unpin=Odepnout
 | 
			
		||||
 | 
			
		||||
artifacts=Artefakty
 | 
			
		||||
confirm_delete_artifact=Jste si jisti, že chcete odstranit artefakt „%s“?
 | 
			
		||||
 | 
			
		||||
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_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.
 | 
			
		||||
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]
 | 
			
		||||
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.
 | 
			
		||||
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]
 | 
			
		||||
change_avatar=Změnit váš avatar…
 | 
			
		||||
| 
						 | 
				
			
			@ -999,6 +1001,8 @@ issue_labels_helper=Vyberte sadu štítků úkolů.
 | 
			
		|||
license=Licence
 | 
			
		||||
license_helper=Vyberte licenční soubor.
 | 
			
		||||
license_helper_desc=Licence řídí, co ostatní mohou a nemohou dělat s vaším kódem. Nejste si jisti, která je pro váš projekt správná? Podívejte se na <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_helper=Vyberte šablonu souboru README.
 | 
			
		||||
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.internal=Interní
 | 
			
		||||
desc.archived=Archivováno
 | 
			
		||||
desc.sha256=SHA256
 | 
			
		||||
 | 
			
		||||
template.items=Položky šablony
 | 
			
		||||
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
 | 
			
		||||
symbolic_link=Symbolický odkaz
 | 
			
		||||
executable_file=Spustitelný soubor
 | 
			
		||||
vendored=Vendorováno
 | 
			
		||||
generated=Generováno
 | 
			
		||||
commit_graph=Graf commitů
 | 
			
		||||
commit_graph.select=Vybrat větve
 | 
			
		||||
commit_graph.hide_pr_refs=Skrýt požadavky na natažení
 | 
			
		||||
| 
						 | 
				
			
			@ -1550,7 +1557,11 @@ issues.label_title=Název štítku
 | 
			
		|||
issues.label_description=Popis štítku
 | 
			
		||||
issues.label_color=Barva štítku
 | 
			
		||||
issues.label_exclusive=Exkluzivní
 | 
			
		||||
issues.label_archive=Archivovat štítek
 | 
			
		||||
issues.label_archived_filter=Zobrazit archivované popisky
 | 
			
		||||
issues.label_archive_tooltip=Archivované štítky jsou ve výchozím nastavení vyloučeny z návrhů při hledání podle popisku.
 | 
			
		||||
issues.label_exclusive_desc=Pojmenujte štítek <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_open_issues=%d otevřených úkolů
 | 
			
		||||
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.pr_close_blocks=Tento požadavek na natažení blokuje uzavření následujících úkolů
 | 
			
		||||
issues.dependency.issue_close_blocked=Musíte zavřít všechny úkoly, které blokují tento úkol, aby jej bylo možné zavřít.
 | 
			
		||||
issues.dependency.issue_batch_close_blocked=Nelze uzavřít úkoly, které jste vybrali, protože úkol #%d má stále otevřené závislosti
 | 
			
		||||
issues.dependency.pr_close_blocked=Musíte zavřít všechny úkoly, které blokují tento požadavek na natažení, aby jej bylo možné sloučit.
 | 
			
		||||
issues.dependency.blocks_short=Blokuje
 | 
			
		||||
issues.dependency.blocked_by_short=Závisí na
 | 
			
		||||
| 
						 | 
				
			
			@ -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.filter_changes_by_commit=Filtrovat podle commitu
 | 
			
		||||
pulls.nothing_to_compare=Tyto větve jsou stejné. Není potřeba vytvářet požadavek na natažení.
 | 
			
		||||
pulls.nothing_to_compare_have_tag=Vybraná větev/značka je stejná.
 | 
			
		||||
pulls.nothing_to_compare_and_allow_empty_pr=Tyto větve jsou stejné. Tento požadavek na natažení bude prázdný.
 | 
			
		||||
pulls.has_pull_request=`Požadavek na natažení mezi těmito větvemi již existuje: <a href="%[1]s">%[2]s#%[3]d</a>`
 | 
			
		||||
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.open=Otevřít
 | 
			
		||||
milestones.close=Zavřít
 | 
			
		||||
milestones.new_subheader=Milníky vám pomohou organizovat úkoly a sledovat jejich pokrok.
 | 
			
		||||
milestones.completeness=%d%% Dokončeno
 | 
			
		||||
milestones.create=Vytvořit milník
 | 
			
		||||
milestones.title=Název
 | 
			
		||||
| 
						 | 
				
			
			@ -1987,6 +2001,7 @@ activity.git_stats_and_deletions=a
 | 
			
		|||
activity.git_stats_deletion_1=%d odebrání
 | 
			
		||||
activity.git_stats_deletion_n=%d odebrání
 | 
			
		||||
 | 
			
		||||
contributors.contribution_type.filter_label=Typ příspěvku:
 | 
			
		||||
contributors.contribution_type.commits=Commity
 | 
			
		||||
 | 
			
		||||
search=Vyhledat
 | 
			
		||||
| 
						 | 
				
			
			@ -2374,6 +2389,7 @@ settings.matrix.room_id=ID místnosti
 | 
			
		|||
settings.matrix.message_type=Typ zprávy
 | 
			
		||||
settings.archive.button=Archivovat repozitář
 | 
			
		||||
settings.archive.header=Archivovat tento repozitář
 | 
			
		||||
settings.archive.text=Archivace repozitáře způsobí, že bude zcela určen pouze pro čtení. Bude skryt z ovládacího panelu. Nikdo (ani vy!) nebude moci vytvářet nové revize ani otevírat nové úkoly nebo žádosti o natažení.
 | 
			
		||||
settings.archive.success=Repozitář byl úspěšně archivován.
 | 
			
		||||
settings.archive.error=Nastala chyba při archivování repozitáře. Prohlédněte si záznam pro více detailů.
 | 
			
		||||
settings.archive.error_ismirror=Nemůžete archivovat zrcadlený repozitář.
 | 
			
		||||
| 
						 | 
				
			
			@ -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.started=Spuštěna úloha mazání všech repozitářů, které nemají Git soubory.
 | 
			
		||||
dashboard.delete_generated_repository_avatars=Odstranit vygenerované avatary repozitářů
 | 
			
		||||
dashboard.sync_repo_tags=Synchronizovat značky z git dat do databáze
 | 
			
		||||
dashboard.update_mirrors=Aktualizovat zrcadla
 | 
			
		||||
dashboard.repo_health_check=Kontrola stavu všech repozitářů
 | 
			
		||||
dashboard.check_repo_stats=Zkontrolovat všechny statistiky repositáře
 | 
			
		||||
| 
						 | 
				
			
			@ -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.update_checker=Kontrola aktualizací
 | 
			
		||||
dashboard.delete_old_system_notices=Odstranit všechna stará systémová upozornění z databáze
 | 
			
		||||
dashboard.gc_lfs=Úklid LFS meta objektů
 | 
			
		||||
dashboard.stop_zombie_tasks=Zastavit zombie úlohy
 | 
			
		||||
dashboard.stop_endless_tasks=Zastavit nekonečné úlohy
 | 
			
		||||
dashboard.cancel_abandoned_jobs=Zrušit opuštěné úlohy
 | 
			
		||||
dashboard.start_schedule_tasks=Spustit naplánované úlohy
 | 
			
		||||
dashboard.sync_branch.started=Synchronizace větví byla spuštěna
 | 
			
		||||
dashboard.sync_tag.started=Synchronizace značek spuštěna
 | 
			
		||||
dashboard.rebuild_issue_indexer=Znovu sestavit index úkolů
 | 
			
		||||
 | 
			
		||||
users.user_manage_panel=Správa uživatelských účtů
 | 
			
		||||
users.new_account=Vytvořit uživatelský účet
 | 
			
		||||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
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]
 | 
			
		||||
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.repository=Informace o repozitáři
 | 
			
		||||
rpm.repository.architectures=Architektury
 | 
			
		||||
rpm.repository.multiple_groups=Tento balíček je k dispozici ve více skupinách.
 | 
			
		||||
rubygems.install=Pro instalaci balíčku pomocí gem spusťte následující příkaz:
 | 
			
		||||
rubygems.install2=nebo ho přidejte do Gemfie:
 | 
			
		||||
rubygems.dependencies.runtime=Běhové závislosti
 | 
			
		||||
| 
						 | 
				
			
			@ -3642,6 +3669,8 @@ runs.actors_no_select=Všichni aktéři
 | 
			
		|||
runs.status_no_select=Všechny stavy
 | 
			
		||||
runs.no_results=Nebyly nalezeny žádné výsledky.
 | 
			
		||||
runs.no_workflows=Zatím neexistují žádné pracovní postupy.
 | 
			
		||||
runs.no_workflows.quick_start=Nevíte jak začít s Gitea Actions? Podívejte se na <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.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.description=Odstranění proměnné je trvalé a nelze jej vrátit zpět. Pokračovat?
 | 
			
		||||
variables.description=Proměnné budou předány určitým akcím a nelze je přečíst jinak.
 | 
			
		||||
variables.id_not_exist=Proměnná s ID %d neexistuje.
 | 
			
		||||
variables.edit=Upravit proměnnou
 | 
			
		||||
variables.deletion.failed=Nepodařilo se odstranit proměnnou.
 | 
			
		||||
variables.deletion.success=Proměnná byla odstraněna.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -143,6 +143,19 @@ confirm_delete_selected = Confirm to delete all selected items?
 | 
			
		|||
name = Name
 | 
			
		||||
value = Value
 | 
			
		||||
 | 
			
		||||
filter = Filter
 | 
			
		||||
filter.clear = Clear Filter
 | 
			
		||||
filter.is_archived = Archived
 | 
			
		||||
filter.not_archived = Not Archived
 | 
			
		||||
filter.is_fork = Forked
 | 
			
		||||
filter.not_fork = Not Forked
 | 
			
		||||
filter.is_mirror = Mirrored
 | 
			
		||||
filter.not_mirror = Not Mirrored
 | 
			
		||||
filter.is_template = Template
 | 
			
		||||
filter.not_template = Not Template
 | 
			
		||||
filter.public = Public
 | 
			
		||||
filter.private = Private
 | 
			
		||||
 | 
			
		||||
[aria]
 | 
			
		||||
navbar = Navigation Bar
 | 
			
		||||
footer = Footer
 | 
			
		||||
| 
						 | 
				
			
			@ -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.head_out_of_date = Merge Failed: Whilst generating the merge, the head was updated. Hint: Try again.
 | 
			
		||||
pulls.has_merged = Failed: The pull request has been merged, you cannot merge again or change the target branch.
 | 
			
		||||
pulls.push_rejected = Merge Failed: The push was rejected. Review the Git Hooks for this repository.
 | 
			
		||||
pulls.push_rejected = Push Failed: The push was rejected. Review the Git Hooks for this repository.
 | 
			
		||||
pulls.push_rejected_summary = Full Rejection Message
 | 
			
		||||
pulls.push_rejected_no_message = Merge Failed: The push was rejected but there was no remote message.<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.status_checking = Some checks are pending
 | 
			
		||||
pulls.status_checks_success = All checks were successful
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -124,6 +124,7 @@ pin=Épingler
 | 
			
		|||
unpin=Désépingler
 | 
			
		||||
 | 
			
		||||
artifacts=Artefacts
 | 
			
		||||
confirm_delete_artifact=Êtes-vous sûr de vouloir supprimer l‘artefact « %s » ?
 | 
			
		||||
 | 
			
		||||
archived=Archivé
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -366,6 +367,7 @@ disable_register_prompt=Les inscriptions sont désactivées. Veuillez contacter
 | 
			
		|||
disable_register_mail=La confirmation par courriel à l’inscription est désactivée.
 | 
			
		||||
manual_activation_only=Contactez l'administrateur de votre site pour terminer l'activation.
 | 
			
		||||
remember_me=Mémoriser cet appareil
 | 
			
		||||
remember_me.compromised=Le jeton de connexion n’est 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=Mot de passe oublié ?
 | 
			
		||||
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.`
 | 
			
		||||
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 d’abord supprimer vos privilèges d’administrateur.
 | 
			
		||||
 | 
			
		||||
[user]
 | 
			
		||||
change_avatar=Changer votre avatar…
 | 
			
		||||
| 
						 | 
				
			
			@ -817,7 +820,7 @@ valid_until_date=Valable jusqu'au %s
 | 
			
		|||
valid_forever=Valide pour toujours
 | 
			
		||||
last_used=Dernière utilisation le
 | 
			
		||||
no_activity=Aucune activité récente
 | 
			
		||||
can_read_info=Lue(s)
 | 
			
		||||
can_read_info=Lecture
 | 
			
		||||
can_write_info=Écriture
 | 
			
		||||
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
 | 
			
		||||
| 
						 | 
				
			
			@ -850,7 +853,7 @@ permissions_public_only=Publique uniquement
 | 
			
		|||
permissions_access_all=Tout (public, privé et limité)
 | 
			
		||||
select_permissions=Sélectionner les autorisations
 | 
			
		||||
permission_no_access=Aucun accès
 | 
			
		||||
permission_read=Lue(s)
 | 
			
		||||
permission_read=Lecture
 | 
			
		||||
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 d’informations.
 | 
			
		||||
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_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_sync=synchronisé
 | 
			
		||||
mirror_sync_on_commit=Synchroniser quand les révisions sont soumis
 | 
			
		||||
mirror_address=Cloner depuis une URL
 | 
			
		||||
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.internal=Interne
 | 
			
		||||
desc.archived=Archivé
 | 
			
		||||
desc.sha256=SHA256
 | 
			
		||||
 | 
			
		||||
template.items=Élément du modèle
 | 
			
		||||
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
 | 
			
		||||
symbolic_link=Lien symbolique
 | 
			
		||||
executable_file=Fichiers exécutables
 | 
			
		||||
vendored=Externe
 | 
			
		||||
generated=Générée
 | 
			
		||||
commit_graph=Graphe des révisions
 | 
			
		||||
commit_graph.select=Sélectionner les branches
 | 
			
		||||
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_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.fast_forward_only_merge_pull_request=Avance rapide uniquement
 | 
			
		||||
pulls.merge_manually=Fusionner manuellement
 | 
			
		||||
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
 | 
			
		||||
| 
						 | 
				
			
			@ -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.
 | 
			
		||||
 | 
			
		||||
activity=Activité
 | 
			
		||||
activity.navbar.contributors=Contributeurs
 | 
			
		||||
activity.period.filter_label=Période :
 | 
			
		||||
activity.period.daily=1 jour
 | 
			
		||||
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_n=%d suppressions
 | 
			
		||||
 | 
			
		||||
contributors.contribution_type.filter_label=Type de contribution :
 | 
			
		||||
contributors.contribution_type.commits=Révisions
 | 
			
		||||
contributors.contribution_type.additions=Ajouts
 | 
			
		||||
contributors.contribution_type.deletions=Suppressions
 | 
			
		||||
 | 
			
		||||
search=Chercher
 | 
			
		||||
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.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 d’ajout, les approbations existantes sont révoquées.
 | 
			
		||||
settings.ignore_stale_approvals=Ignorer les approbations obsolètes
 | 
			
		||||
settings.ignore_stale_approvals_desc=Ignorer les approbations d’anciennes révisions (évaluations obsolètes) du décompte des approbations de la demande d’ajout. 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_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é
 | 
			
		||||
| 
						 | 
				
			
			@ -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.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.mirrors_unavailable=Les miroirs ne sont pas disponibles lorsque le dépôt est archivé.
 | 
			
		||||
settings.unarchive.button=Réhabiliter
 | 
			
		||||
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.
 | 
			
		||||
| 
						 | 
				
			
			@ -2652,6 +2667,11 @@ activity.navbar.code_frequency = Fréquence de code
 | 
			
		|||
activity.navbar.recent_commits = Commits récents
 | 
			
		||||
 | 
			
		||||
[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 s’est produite.
 | 
			
		||||
contributors.what=contributions
 | 
			
		||||
 | 
			
		||||
[org]
 | 
			
		||||
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]
 | 
			
		||||
dashboard=Tableau de bord
 | 
			
		||||
self_check=Autodiagnostique
 | 
			
		||||
identity_access=Identité et accès
 | 
			
		||||
users=Comptes utilisateurs
 | 
			
		||||
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_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_tags=Synchroniser les étiquettes git depuis les dépôts vers la base de données
 | 
			
		||||
dashboard.update_mirrors=Actualiser les miroirs
 | 
			
		||||
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
 | 
			
		||||
| 
						 | 
				
			
			@ -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.start_schedule_tasks=Démarrer les tâches planifiées
 | 
			
		||||
dashboard.sync_branch.started=Début de la synchronisation des branches
 | 
			
		||||
dashboard.sync_tag.started=Synchronisation des étiquettes
 | 
			
		||||
dashboard.rebuild_issue_indexer=Reconstruire l’indexeur des tickets
 | 
			
		||||
 | 
			
		||||
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_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 l’instant.
 | 
			
		||||
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 qu’en exécutant une requête SQL du type « ALTER … COLLATE … ».
 | 
			
		||||
 | 
			
		||||
[action]
 | 
			
		||||
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.repository=Informations sur le Dépôt
 | 
			
		||||
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.install2=ou ajoutez-le au Gemfile :
 | 
			
		||||
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.no_results=Aucun résultat correspondant.
 | 
			
		||||
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 d’informations 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.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.description=La suppression d’une 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.id_not_exist=La variable avec l’ID %d n’existe pas.
 | 
			
		||||
variables.edit=Modifier la variable
 | 
			
		||||
variables.deletion.failed=Impossible de retirer la variable.
 | 
			
		||||
variables.deletion.success=La variable a bien été retirée.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -124,6 +124,7 @@ pin=ピン留め
 | 
			
		|||
unpin=ピン留め解除
 | 
			
		||||
 | 
			
		||||
artifacts=成果物
 | 
			
		||||
confirm_delete_artifact=アーティファクト %s を削除してよろしいですか?
 | 
			
		||||
 | 
			
		||||
archived=アーカイブ
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -429,7 +430,7 @@ password_pwned_err=HaveIBeenPwnedへのリクエストを完了できません
 | 
			
		|||
change_unconfirmed_email = 登録時に間違ったメール アドレスを入力した場合は、以下で変更できます。代わりに確認メールが新しいアドレスに送信されます。
 | 
			
		||||
change_unconfirmed_email_error = メール アドレスを変更できません: %v
 | 
			
		||||
change_unconfirmed_email_summary = アクティベーションメールの送信先メールアドレスを変更します。
 | 
			
		||||
last_admin = 最後の管理者を削除することはできません。少なくとも 1 人の管理者が必要です。
 | 
			
		||||
last_admin=最後の管理者は削除できません。少なくとも一人の管理者が必要です。
 | 
			
		||||
 | 
			
		||||
[mail]
 | 
			
		||||
view_it_on=%s で見る
 | 
			
		||||
| 
						 | 
				
			
			@ -600,6 +601,7 @@ target_branch_not_exist=ターゲットのブランチが存在していませ
 | 
			
		|||
admin_cannot_delete_self = 管理者である場合、自分自身を削除することはできません。最初に管理者権限を削除してください。
 | 
			
		||||
username_error_no_dots = `英数字 (「0-9」、「a-z」、「A-Z」)、ダッシュ (「-」)、およびアンダースコア (「_」) のみを含めることができます。英数字以外の文字で開始または終了することはできず、連続した英数字以外の文字も禁止されています。`
 | 
			
		||||
 | 
			
		||||
admin_cannot_delete_self=あなたが管理者である場合、自分自身を削除することはできません。最初に管理者権限を削除してください。
 | 
			
		||||
 | 
			
		||||
[user]
 | 
			
		||||
change_avatar=アバターを変更…
 | 
			
		||||
| 
						 | 
				
			
			@ -993,6 +995,8 @@ issue_labels_helper=イシューのラベルセットを選択
 | 
			
		|||
license=ライセンス
 | 
			
		||||
license_helper=ライセンス ファイルを選択してください。
 | 
			
		||||
license_helper_desc=ライセンスにより、他人があなたのコードに対して何ができて何ができないのかを規定します。 どれがプロジェクトにふさわしいか迷っていますか? <a target="_blank" rel="noopener noreferrer" href="%s">ライセンス選択サイト</a> も確認してみてください。
 | 
			
		||||
object_format=オブジェクトのフォーマット
 | 
			
		||||
object_format_helper=リポジトリのオブジェクトフォーマット。後で変更することはできません。SHA1 は最も互換性があります。
 | 
			
		||||
readme=README
 | 
			
		||||
readme_helper=READMEファイル テンプレートを選択してください。
 | 
			
		||||
readme_helper_desc=プロジェクトについての説明をひととおり書く場所です。
 | 
			
		||||
| 
						 | 
				
			
			@ -1060,6 +1064,7 @@ desc.public=公開
 | 
			
		|||
desc.template=テンプレート
 | 
			
		||||
desc.internal=組織内
 | 
			
		||||
desc.archived=アーカイブ
 | 
			
		||||
desc.sha256=SHA256
 | 
			
		||||
 | 
			
		||||
template.items=テンプレート項目
 | 
			
		||||
template.git_content=Gitコンテンツ (デフォルトブランチ)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1036,6 +1036,7 @@ desc.public=Publisks
 | 
			
		|||
desc.template=Sagatave
 | 
			
		||||
desc.internal=Iekšējs
 | 
			
		||||
desc.archived=Arhivēts
 | 
			
		||||
desc.sha256=SHA256
 | 
			
		||||
 | 
			
		||||
template.items=Sagataves ieraksti
 | 
			
		||||
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ā.
 | 
			
		||||
 | 
			
		||||
[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_name_holder=Organizācijas nosaukums
 | 
			
		||||
| 
						 | 
				
			
			@ -2698,6 +2703,7 @@ teams.invite.description=Nospiediet pogu zemāk, lai pievienotos komandai.
 | 
			
		|||
 | 
			
		||||
[admin]
 | 
			
		||||
dashboard=Infopanelis
 | 
			
		||||
self_check=Pašpārbaude
 | 
			
		||||
identity_access=Identitāte un piekļuve
 | 
			
		||||
users=Lietotāju konti
 | 
			
		||||
organizations=Organizācijas
 | 
			
		||||
| 
						 | 
				
			
			@ -3223,6 +3229,7 @@ notices.desc=Apraksts
 | 
			
		|||
notices.op=Op.
 | 
			
		||||
notices.delete_success=Sistēmas paziņojumi ir dzēsti.
 | 
			
		||||
 | 
			
		||||
self_check.no_problem_found=Pašlaik nav atrasta neviena problēma.
 | 
			
		||||
 | 
			
		||||
[action]
 | 
			
		||||
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.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.id_not_exist=Mainīgais ar identifikatoru %d nepastāv.
 | 
			
		||||
variables.edit=Labot mainīgo
 | 
			
		||||
variables.deletion.failed=Neizdevās noņemt mainīgo.
 | 
			
		||||
variables.deletion.success=Mainīgais tika noņemts.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2140,7 +2140,7 @@ settings.trust_model.collaborator.long=协作者:信任协作者的签名
 | 
			
		|||
settings.trust_model.collaborator.desc=此仓库中协作者的有效签名将被标记为「可信」(无论它们是否是提交者),签名只符合提交者时将标记为「不可信」,都不匹配时标记为「不匹配」。
 | 
			
		||||
settings.trust_model.committer=提交者
 | 
			
		||||
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.long=协作者+提交者:信任协作者同时是提交者的签名
 | 
			
		||||
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.database_collation_mismatch=期望数据库使用的校验方式:%s
 | 
			
		||||
self_check.database_collation_case_insensitive=数据库正在使用一个校验 %s, 这是一个不敏感的校验. 虽然Gitea可以与它合作,但可能有一些罕见的情况不如预期的那样起作用。
 | 
			
		||||
self_check.database_inconsistent_collation_columns=数据库正在使用%s的排序规则,但是这些列使用了不匹配的排序规则。这可能会造成一些意外问题。
 | 
			
		||||
self_check.database_fix_mysql=对于MySQL/MariaDB用户,您可以使用“gitea doctor convert”命令来解决校验问题。 或者您也可以通过 "ALTER ... COLLATE ..." 这样的SQL 来手动解决这个问题。
 | 
			
		||||
self_check.database_fix_mssql=对于MSSQL用户,您现在只能通过"ALTER ... COLLATE ..."SQLs手动解决这个问题。
 | 
			
		||||
 | 
			
		||||
[action]
 | 
			
		||||
create_repo=创建了仓库 <a href="%s">%s</a>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										1026
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										1026
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										32
									
								
								package.json
									
										
									
									
									
								
							
							
						
						
									
										32
									
								
								package.json
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -9,7 +9,7 @@
 | 
			
		|||
    "@citation-js/plugin-csl": "0.7.6",
 | 
			
		||||
    "@citation-js/plugin-software-formats": "0.6.1",
 | 
			
		||||
    "@claviska/jquery-minicolors": "2.3.6",
 | 
			
		||||
    "@github/markdown-toolbar-element": "2.2.1",
 | 
			
		||||
    "@github/markdown-toolbar-element": "2.2.3",
 | 
			
		||||
    "@github/relative-time-element": "4.3.1",
 | 
			
		||||
    "@github/text-expander-element": "2.6.1",
 | 
			
		||||
    "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
 | 
			
		||||
| 
						 | 
				
			
			@ -17,11 +17,11 @@
 | 
			
		|||
    "@webcomponents/custom-elements": "1.6.0",
 | 
			
		||||
    "add-asset-webpack-plugin": "2.0.1",
 | 
			
		||||
    "ansi_up": "6.0.2",
 | 
			
		||||
    "asciinema-player": "3.6.4",
 | 
			
		||||
    "chart.js": "4.4.1",
 | 
			
		||||
    "asciinema-player": "3.7.0",
 | 
			
		||||
    "chart.js": "4.4.2",
 | 
			
		||||
    "chartjs-adapter-dayjs-4": "1.0.4",
 | 
			
		||||
    "chartjs-plugin-zoom": "2.0.1",
 | 
			
		||||
    "clippie": "4.0.6",
 | 
			
		||||
    "clippie": "4.0.7",
 | 
			
		||||
    "css-loader": "6.10.0",
 | 
			
		||||
    "css-variables-parser": "1.0.1",
 | 
			
		||||
    "dayjs": "1.11.10",
 | 
			
		||||
| 
						 | 
				
			
			@ -36,16 +36,16 @@
 | 
			
		|||
    "katex": "0.16.9",
 | 
			
		||||
    "license-checker-webpack-plugin": "0.2.1",
 | 
			
		||||
    "mermaid": "10.8.0",
 | 
			
		||||
    "mini-css-extract-plugin": "2.8.0",
 | 
			
		||||
    "mini-css-extract-plugin": "2.8.1",
 | 
			
		||||
    "minimatch": "9.0.3",
 | 
			
		||||
    "monaco-editor": "0.46.0",
 | 
			
		||||
    "monaco-editor-webpack-plugin": "7.1.0",
 | 
			
		||||
    "pdfobject": "2.3.0",
 | 
			
		||||
    "postcss": "8.4.35",
 | 
			
		||||
    "postcss-loader": "8.1.0",
 | 
			
		||||
    "postcss-loader": "8.1.1",
 | 
			
		||||
    "pretty-ms": "9.0.0",
 | 
			
		||||
    "sortablejs": "1.15.2",
 | 
			
		||||
    "swagger-ui-dist": "5.11.6",
 | 
			
		||||
    "swagger-ui-dist": "5.11.8",
 | 
			
		||||
    "tailwindcss": "3.4.1",
 | 
			
		||||
    "throttle-debounce": "5.0.0",
 | 
			
		||||
    "tinycolor2": "1.6.0",
 | 
			
		||||
| 
						 | 
				
			
			@ -53,25 +53,25 @@
 | 
			
		|||
    "toastify-js": "1.12.0",
 | 
			
		||||
    "tributejs": "5.1.3",
 | 
			
		||||
    "uint8-to-base64": "0.2.0",
 | 
			
		||||
    "vue": "3.4.19",
 | 
			
		||||
    "vue": "3.4.21",
 | 
			
		||||
    "vue-bar-graph": "2.0.0",
 | 
			
		||||
    "vue-chartjs": "5.3.0",
 | 
			
		||||
    "vue-loader": "17.4.2",
 | 
			
		||||
    "vue3-calendar-heatmap": "2.0.5",
 | 
			
		||||
    "webpack": "5.90.2",
 | 
			
		||||
    "webpack": "5.90.3",
 | 
			
		||||
    "webpack-cli": "5.1.4",
 | 
			
		||||
    "wrap-ansi": "9.0.0"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@eslint-community/eslint-plugin-eslint-comments": "4.1.0",
 | 
			
		||||
    "@playwright/test": "1.41.2",
 | 
			
		||||
    "@playwright/test": "1.42.1",
 | 
			
		||||
    "@stoplight/spectral-cli": "6.11.0",
 | 
			
		||||
    "@stylistic/eslint-plugin-js": "1.6.2",
 | 
			
		||||
    "@stylistic/stylelint-plugin": "2.0.0",
 | 
			
		||||
    "@stylistic/eslint-plugin-js": "1.6.3",
 | 
			
		||||
    "@stylistic/stylelint-plugin": "2.1.0",
 | 
			
		||||
    "@vitejs/plugin-vue": "5.0.4",
 | 
			
		||||
    "eslint": "8.56.0",
 | 
			
		||||
    "eslint": "8.57.0",
 | 
			
		||||
    "eslint-plugin-array-func": "4.0.0",
 | 
			
		||||
    "eslint-plugin-github": "4.10.1",
 | 
			
		||||
    "eslint-plugin-github": "4.10.2",
 | 
			
		||||
    "eslint-plugin-i": "2.29.1",
 | 
			
		||||
    "eslint-plugin-jquery": "1.5.1",
 | 
			
		||||
    "eslint-plugin-no-jquery": "2.7.0",
 | 
			
		||||
| 
						 | 
				
			
			@ -81,7 +81,7 @@
 | 
			
		|||
    "eslint-plugin-unicorn": "51.0.1",
 | 
			
		||||
    "eslint-plugin-vitest": "0.3.22",
 | 
			
		||||
    "eslint-plugin-vitest-globals": "1.4.0",
 | 
			
		||||
    "eslint-plugin-vue": "9.21.1",
 | 
			
		||||
    "eslint-plugin-vue": "9.22.0",
 | 
			
		||||
    "eslint-plugin-vue-scoped-css": "2.7.2",
 | 
			
		||||
    "eslint-plugin-wc": "2.0.4",
 | 
			
		||||
    "jsdom": "24.0.0",
 | 
			
		||||
| 
						 | 
				
			
			@ -93,7 +93,7 @@
 | 
			
		|||
    "svgo": "3.2.0",
 | 
			
		||||
    "updates": "15.1.2",
 | 
			
		||||
    "vite-string-plugin": "1.1.5",
 | 
			
		||||
    "vitest": "1.2.2"
 | 
			
		||||
    "vitest": "1.3.1"
 | 
			
		||||
  },
 | 
			
		||||
  "browserslist": [
 | 
			
		||||
    "defaults"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										39
									
								
								poetry.lock
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										39
									
								
								poetry.lock
									
										
									
										generated
									
									
									
								
							| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
 | 
			
		||||
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "click"
 | 
			
		||||
| 
						 | 
				
			
			@ -27,12 +27,12 @@ files = [
 | 
			
		|||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "cssbeautifier"
 | 
			
		||||
version = "1.14.11"
 | 
			
		||||
version = "1.15.1"
 | 
			
		||||
description = "CSS unobfuscator and beautifier."
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = "*"
 | 
			
		||||
files = [
 | 
			
		||||
    {file = "cssbeautifier-1.14.11.tar.gz", hash = "sha256:40544c2b62bbcb64caa5e7f37a02df95654e5ce1bcacadac4ca1f3dc89c31513"},
 | 
			
		||||
    {file = "cssbeautifier-1.15.1.tar.gz", hash = "sha256:9f7064362aedd559c55eeecf6b6bed65e05f33488dcbe39044f0403c26e1c006"},
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[package.dependencies]
 | 
			
		||||
| 
						 | 
				
			
			@ -67,13 +67,12 @@ tqdm = ">=4.62.2,<5.0.0"
 | 
			
		|||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "editorconfig"
 | 
			
		||||
version = "0.12.3"
 | 
			
		||||
version = "0.12.4"
 | 
			
		||||
description = "EditorConfig File Locator and Interpreter for Python"
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = "*"
 | 
			
		||||
files = [
 | 
			
		||||
    {file = "EditorConfig-0.12.3-py3-none-any.whl", hash = "sha256:6b0851425aa875b08b16789ee0eeadbd4ab59666e9ebe728e526314c4a2e52c1"},
 | 
			
		||||
    {file = "EditorConfig-0.12.3.tar.gz", hash = "sha256:57f8ce78afcba15c8b18d46b5170848c88d56fd38f05c2ec60dbbfcb8996e89e"},
 | 
			
		||||
    {file = "EditorConfig-0.12.4.tar.gz", hash = "sha256:24857fa1793917dd9ccf0c7810a07e05404ce9b823521c7dce22a4fb5d125f80"},
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
| 
						 | 
				
			
			@ -100,12 +99,12 @@ files = [
 | 
			
		|||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "jsbeautifier"
 | 
			
		||||
version = "1.14.11"
 | 
			
		||||
version = "1.15.1"
 | 
			
		||||
description = "JavaScript unobfuscator and beautifier."
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = "*"
 | 
			
		||||
files = [
 | 
			
		||||
    {file = "jsbeautifier-1.14.11.tar.gz", hash = "sha256:6b632581ea60dd1c133cd25a48ad187b4b91f526623c4b0fb5443ef805250505"},
 | 
			
		||||
    {file = "jsbeautifier-1.15.1.tar.gz", hash = "sha256:ebd733b560704c602d744eafc839db60a1ee9326e30a2a80c4adb8718adc1b24"},
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[package.dependencies]
 | 
			
		||||
| 
						 | 
				
			
			@ -114,13 +113,13 @@ six = ">=1.13.0"
 | 
			
		|||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "json5"
 | 
			
		||||
version = "0.9.14"
 | 
			
		||||
version = "0.9.18"
 | 
			
		||||
description = "A Python implementation of the JSON5 data format."
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = "*"
 | 
			
		||||
python-versions = ">=3.8"
 | 
			
		||||
files = [
 | 
			
		||||
    {file = "json5-0.9.14-py2.py3-none-any.whl", hash = "sha256:740c7f1b9e584a468dbb2939d8d458db3427f2c93ae2139d05f47e453eae964f"},
 | 
			
		||||
    {file = "json5-0.9.14.tar.gz", hash = "sha256:9ed66c3a6ca3510a976a9ef9b8c0787de24802724ab1860bc0153c7fdd589b02"},
 | 
			
		||||
    {file = "json5-0.9.18-py2.py3-none-any.whl", hash = "sha256:3f20193ff8dfdec6ab114b344e7ac5d76fac453c8bab9bdfe1460d1d528ec393"},
 | 
			
		||||
    {file = "json5-0.9.18.tar.gz", hash = "sha256:ecb8ac357004e3522fb989da1bf08b146011edbd14fdffae6caad3bd68493467"},
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[package.extras]
 | 
			
		||||
| 
						 | 
				
			
			@ -322,13 +321,13 @@ files = [
 | 
			
		|||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "tqdm"
 | 
			
		||||
version = "4.66.1"
 | 
			
		||||
version = "4.66.2"
 | 
			
		||||
description = "Fast, Extensible Progress Meter"
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = ">=3.7"
 | 
			
		||||
files = [
 | 
			
		||||
    {file = "tqdm-4.66.1-py3-none-any.whl", hash = "sha256:d302b3c5b53d47bce91fea46679d9c3c6508cf6332229aa1e7d8653723793386"},
 | 
			
		||||
    {file = "tqdm-4.66.1.tar.gz", hash = "sha256:d88e651f9db8d8551a62556d3cff9e3034274ca5d66e93197cf2490e2dcb69c7"},
 | 
			
		||||
    {file = "tqdm-4.66.2-py3-none-any.whl", hash = "sha256:1ee4f8a893eb9bef51c6e35730cebf234d5d0b6bd112b0271e10ed7c24a02bd9"},
 | 
			
		||||
    {file = "tqdm-4.66.2.tar.gz", hash = "sha256:6cd52cdf0fef0e0f543299cfc96fec90d7b8a7e88745f411ec33eb44d5ed3531"},
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[package.dependencies]
 | 
			
		||||
| 
						 | 
				
			
			@ -342,13 +341,13 @@ telegram = ["requests"]
 | 
			
		|||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "yamllint"
 | 
			
		||||
version = "1.35.0"
 | 
			
		||||
version = "1.35.1"
 | 
			
		||||
description = "A linter for YAML files."
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = ">=3.8"
 | 
			
		||||
files = [
 | 
			
		||||
    {file = "yamllint-1.35.0-py3-none-any.whl", hash = "sha256:601b0adaaac6d9bacb16a2e612e7ee8d23caf941ceebf9bfe2cff0f196266004"},
 | 
			
		||||
    {file = "yamllint-1.35.0.tar.gz", hash = "sha256:9bc99c3e9fe89b4c6ee26e17aa817cf2d14390de6577cb6e2e6ed5f72120c835"},
 | 
			
		||||
    {file = "yamllint-1.35.1-py3-none-any.whl", hash = "sha256:2e16e504bb129ff515b37823b472750b36b6de07963bd74b307341ef5ad8bdc3"},
 | 
			
		||||
    {file = "yamllint-1.35.1.tar.gz", hash = "sha256:7a003809f88324fd2c877734f2d575ee7881dd9043360657cc8049c809eba6cd"},
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[package.dependencies]
 | 
			
		||||
| 
						 | 
				
			
			@ -360,5 +359,5 @@ dev = ["doc8", "flake8", "flake8-import-order", "rstcheck[sphinx]", "sphinx"]
 | 
			
		|||
 | 
			
		||||
[metadata]
 | 
			
		||||
lock-version = "2.0"
 | 
			
		||||
python-versions = "^3.8"
 | 
			
		||||
content-hash = "ba1c2c4235872f67354b5f52aa5bf0cd616354961530d9dc907f9fba28cc1ece"
 | 
			
		||||
python-versions = "^3.10"
 | 
			
		||||
content-hash = "cd2ff218e9f27a464dfbc8ec2387824a90f4360e04c3f2e58cc375796b7df33a"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,11 +5,11 @@ description = ""
 | 
			
		|||
authors = []
 | 
			
		||||
 | 
			
		||||
[tool.poetry.dependencies]
 | 
			
		||||
python = "^3.8"
 | 
			
		||||
python = "^3.10"
 | 
			
		||||
 | 
			
		||||
[tool.poetry.group.dev.dependencies]
 | 
			
		||||
djlint = "1.34.1"
 | 
			
		||||
yamllint = "1.35.0"
 | 
			
		||||
yamllint = "1.35.1"
 | 
			
		||||
 | 
			
		||||
[tool.djlint]
 | 
			
		||||
profile="golang"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										1058
									
								
								routers/api/actions/artifact.pb.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1058
									
								
								routers/api/actions/artifact.pb.go
									
										
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										73
									
								
								routers/api/actions/artifact.proto
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								routers/api/actions/artifact.proto
									
										
									
									
									
										Normal 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;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -71,7 +71,6 @@ import (
 | 
			
		|||
 | 
			
		||||
	"code.gitea.io/gitea/models/actions"
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/modules/context"
 | 
			
		||||
	"code.gitea.io/gitea/modules/json"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
| 
						 | 
				
			
			@ -80,6 +79,7 @@ import (
 | 
			
		|||
	"code.gitea.io/gitea/modules/web"
 | 
			
		||||
	web_types "code.gitea.io/gitea/modules/web/types"
 | 
			
		||||
	actions_service "code.gitea.io/gitea/services/actions"
 | 
			
		||||
	"code.gitea.io/gitea/services/context"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const artifactRouteBase = "/_apis/pipelines/workflows/{run_id}/artifacts"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,11 +5,16 @@ package actions
 | 
			
		|||
 | 
			
		||||
import (
 | 
			
		||||
	"crypto/md5"
 | 
			
		||||
	"crypto/sha256"
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"encoding/hex"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"hash"
 | 
			
		||||
	"io"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"sort"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/actions"
 | 
			
		||||
| 
						 | 
				
			
			@ -18,6 +23,52 @@ import (
 | 
			
		|||
	"code.gitea.io/gitea/modules/storage"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func saveUploadChunkBase(st storage.ObjectStorage, ctx *ArtifactContext,
 | 
			
		||||
	artifact *actions.ActionArtifact,
 | 
			
		||||
	contentSize, runID, start, end, length int64, checkMd5 bool,
 | 
			
		||||
) (int64, error) {
 | 
			
		||||
	// build chunk store path
 | 
			
		||||
	storagePath := fmt.Sprintf("tmp%d/%d-%d-%d-%d.chunk", runID, runID, artifact.ID, start, end)
 | 
			
		||||
	var r io.Reader = ctx.Req.Body
 | 
			
		||||
	var hasher hash.Hash
 | 
			
		||||
	if checkMd5 {
 | 
			
		||||
		// use io.TeeReader to avoid reading all body to md5 sum.
 | 
			
		||||
		// it writes data to hasher after reading end
 | 
			
		||||
		// if hash is not matched, delete the read-end result
 | 
			
		||||
		hasher = md5.New()
 | 
			
		||||
		r = io.TeeReader(r, hasher)
 | 
			
		||||
	}
 | 
			
		||||
	// save chunk to storage
 | 
			
		||||
	writtenSize, err := st.Save(storagePath, r, -1)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return -1, fmt.Errorf("save chunk to storage error: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	var checkErr error
 | 
			
		||||
	if checkMd5 {
 | 
			
		||||
		// check md5
 | 
			
		||||
		reqMd5String := ctx.Req.Header.Get(artifactXActionsResultsMD5Header)
 | 
			
		||||
		chunkMd5String := base64.StdEncoding.EncodeToString(hasher.Sum(nil))
 | 
			
		||||
		log.Info("[artifact] check chunk md5, sum: %s, header: %s", chunkMd5String, reqMd5String)
 | 
			
		||||
		// if md5 not match, delete the chunk
 | 
			
		||||
		if reqMd5String != chunkMd5String {
 | 
			
		||||
			checkErr = fmt.Errorf("md5 not match")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if writtenSize != contentSize {
 | 
			
		||||
		checkErr = errors.Join(checkErr, fmt.Errorf("contentSize not match body size"))
 | 
			
		||||
	}
 | 
			
		||||
	if checkErr != nil {
 | 
			
		||||
		if err := st.Delete(storagePath); err != nil {
 | 
			
		||||
			log.Error("Error deleting chunk: %s, %v", storagePath, err)
 | 
			
		||||
		}
 | 
			
		||||
		return -1, checkErr
 | 
			
		||||
	}
 | 
			
		||||
	log.Info("[artifact] save chunk %s, size: %d, artifact id: %d, start: %d, end: %d",
 | 
			
		||||
		storagePath, contentSize, artifact.ID, start, end)
 | 
			
		||||
	// return chunk total size
 | 
			
		||||
	return length, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func saveUploadChunk(st storage.ObjectStorage, ctx *ArtifactContext,
 | 
			
		||||
	artifact *actions.ActionArtifact,
 | 
			
		||||
	contentSize, runID int64,
 | 
			
		||||
| 
						 | 
				
			
			@ -29,33 +80,15 @@ func saveUploadChunk(st storage.ObjectStorage, ctx *ArtifactContext,
 | 
			
		|||
		log.Warn("parse content range error: %v, content-range: %s", err, contentRange)
 | 
			
		||||
		return -1, fmt.Errorf("parse content range error: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	// build chunk store path
 | 
			
		||||
	storagePath := fmt.Sprintf("tmp%d/%d-%d-%d-%d.chunk", runID, runID, artifact.ID, start, end)
 | 
			
		||||
	// use io.TeeReader to avoid reading all body to md5 sum.
 | 
			
		||||
	// it writes data to hasher after reading end
 | 
			
		||||
	// if hash is not matched, delete the read-end result
 | 
			
		||||
	hasher := md5.New()
 | 
			
		||||
	r := io.TeeReader(ctx.Req.Body, hasher)
 | 
			
		||||
	// save chunk to storage
 | 
			
		||||
	writtenSize, err := st.Save(storagePath, r, -1)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return -1, fmt.Errorf("save chunk to storage error: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	// check md5
 | 
			
		||||
	reqMd5String := ctx.Req.Header.Get(artifactXActionsResultsMD5Header)
 | 
			
		||||
	chunkMd5String := base64.StdEncoding.EncodeToString(hasher.Sum(nil))
 | 
			
		||||
	log.Info("[artifact] check chunk md5, sum: %s, header: %s", chunkMd5String, reqMd5String)
 | 
			
		||||
	// if md5 not match, delete the chunk
 | 
			
		||||
	if reqMd5String != chunkMd5String || writtenSize != contentSize {
 | 
			
		||||
		if err := st.Delete(storagePath); err != nil {
 | 
			
		||||
			log.Error("Error deleting chunk: %s, %v", storagePath, err)
 | 
			
		||||
		}
 | 
			
		||||
		return -1, fmt.Errorf("md5 not match")
 | 
			
		||||
	}
 | 
			
		||||
	log.Info("[artifact] save chunk %s, size: %d, artifact id: %d, start: %d, end: %d",
 | 
			
		||||
		storagePath, contentSize, artifact.ID, start, end)
 | 
			
		||||
	// return chunk total size
 | 
			
		||||
	return length, nil
 | 
			
		||||
	return saveUploadChunkBase(st, ctx, artifact, contentSize, runID, start, end, length, true)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func appendUploadChunk(st storage.ObjectStorage, ctx *ArtifactContext,
 | 
			
		||||
	artifact *actions.ActionArtifact,
 | 
			
		||||
	start, contentSize, runID int64,
 | 
			
		||||
) (int64, error) {
 | 
			
		||||
	end := start + contentSize - 1
 | 
			
		||||
	return saveUploadChunkBase(st, ctx, artifact, contentSize, runID, start, end, contentSize, false)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type chunkFileItem struct {
 | 
			
		||||
| 
						 | 
				
			
			@ -111,14 +144,14 @@ func mergeChunksForRun(ctx *ArtifactContext, st storage.ObjectStorage, runID int
 | 
			
		|||
			log.Debug("artifact %d chunks not found", art.ID)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		if err := mergeChunksForArtifact(ctx, chunks, st, art); err != nil {
 | 
			
		||||
		if err := mergeChunksForArtifact(ctx, chunks, st, art, ""); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st storage.ObjectStorage, artifact *actions.ActionArtifact) error {
 | 
			
		||||
func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st storage.ObjectStorage, artifact *actions.ActionArtifact, checksum string) error {
 | 
			
		||||
	sort.Slice(chunks, func(i, j int) bool {
 | 
			
		||||
		return chunks[i].Start < chunks[j].Start
 | 
			
		||||
	})
 | 
			
		||||
| 
						 | 
				
			
			@ -157,6 +190,14 @@ func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st st
 | 
			
		|||
		readers = append(readers, readCloser)
 | 
			
		||||
	}
 | 
			
		||||
	mergedReader := io.MultiReader(readers...)
 | 
			
		||||
	shaPrefix := "sha256:"
 | 
			
		||||
	var hash hash.Hash
 | 
			
		||||
	if strings.HasPrefix(checksum, shaPrefix) {
 | 
			
		||||
		hash = sha256.New()
 | 
			
		||||
	}
 | 
			
		||||
	if hash != nil {
 | 
			
		||||
		mergedReader = io.TeeReader(mergedReader, hash)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// if chunk is gzip, use gz as extension
 | 
			
		||||
	// download-artifact action will use content-encoding header to decide if it should decompress the file
 | 
			
		||||
| 
						 | 
				
			
			@ -185,6 +226,14 @@ func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st st
 | 
			
		|||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	if hash != nil {
 | 
			
		||||
		rawChecksum := hash.Sum(nil)
 | 
			
		||||
		actualChecksum := hex.EncodeToString(rawChecksum)
 | 
			
		||||
		if !strings.HasSuffix(checksum, actualChecksum) {
 | 
			
		||||
			return fmt.Errorf("update artifact error checksum is invalid")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// save storage path to artifact
 | 
			
		||||
	log.Debug("[artifact] merge chunks to artifact: %d, %s, old:%s", artifact.ID, storagePath, artifact.StoragePath)
 | 
			
		||||
	// if artifact is already uploaded, delete the old file
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -43,6 +43,17 @@ func validateRunID(ctx *ArtifactContext) (*actions.ActionTask, int64, bool) {
 | 
			
		|||
	return task, runID, true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func validateRunIDV4(ctx *ArtifactContext, rawRunID string) (*actions.ActionTask, int64, bool) {
 | 
			
		||||
	task := ctx.ActionTask
 | 
			
		||||
	runID, err := strconv.ParseInt(rawRunID, 10, 64)
 | 
			
		||||
	if err != nil || task.Job.RunID != runID {
 | 
			
		||||
		log.Error("Error runID not match")
 | 
			
		||||
		ctx.Error(http.StatusBadRequest, "run-id does not match")
 | 
			
		||||
		return nil, 0, false
 | 
			
		||||
	}
 | 
			
		||||
	return task, runID, true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func validateArtifactHash(ctx *ArtifactContext, artifactName string) bool {
 | 
			
		||||
	paramHash := ctx.Params("artifact_hash")
 | 
			
		||||
	// use artifact name to create upload url
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										512
									
								
								routers/api/actions/artifactsv4.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										512
									
								
								routers/api/actions/artifactsv4.go
									
										
									
									
									
										Normal 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)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -15,12 +15,12 @@ import (
 | 
			
		|||
 | 
			
		||||
	packages_model "code.gitea.io/gitea/models/packages"
 | 
			
		||||
	alpine_model "code.gitea.io/gitea/models/packages/alpine"
 | 
			
		||||
	"code.gitea.io/gitea/modules/context"
 | 
			
		||||
	"code.gitea.io/gitea/modules/json"
 | 
			
		||||
	packages_module "code.gitea.io/gitea/modules/packages"
 | 
			
		||||
	alpine_module "code.gitea.io/gitea/modules/packages/alpine"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	"code.gitea.io/gitea/routers/api/packages/helper"
 | 
			
		||||
	"code.gitea.io/gitea/services/context"
 | 
			
		||||
	packages_service "code.gitea.io/gitea/services/packages"
 | 
			
		||||
	alpine_service "code.gitea.io/gitea/services/packages/alpine"
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,7 +10,6 @@ import (
 | 
			
		|||
 | 
			
		||||
	auth_model "code.gitea.io/gitea/models/auth"
 | 
			
		||||
	"code.gitea.io/gitea/models/perm"
 | 
			
		||||
	"code.gitea.io/gitea/modules/context"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"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/vagrant"
 | 
			
		||||
	"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) {
 | 
			
		||||
| 
						 | 
				
			
			@ -642,7 +641,7 @@ func CommonRoutes() *web.Route {
 | 
			
		|||
				})
 | 
			
		||||
			})
 | 
			
		||||
		}, reqPackageAccess(perm.AccessModeRead))
 | 
			
		||||
	}, context_service.UserAssignmentWeb(), context.PackageAssignment())
 | 
			
		||||
	}, context.UserAssignmentWeb(), context.PackageAssignment())
 | 
			
		||||
 | 
			
		||||
	return r
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -812,7 +811,7 @@ func ContainerRoutes() *web.Route {
 | 
			
		|||
 | 
			
		||||
			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
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,14 +12,15 @@ import (
 | 
			
		|||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	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/optional"
 | 
			
		||||
	packages_module "code.gitea.io/gitea/modules/packages"
 | 
			
		||||
	cargo_module "code.gitea.io/gitea/modules/packages/cargo"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/structs"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	"code.gitea.io/gitea/routers/api/packages/helper"
 | 
			
		||||
	"code.gitea.io/gitea/services/context"
 | 
			
		||||
	"code.gitea.io/gitea/services/convert"
 | 
			
		||||
	packages_service "code.gitea.io/gitea/services/packages"
 | 
			
		||||
	cargo_service "code.gitea.io/gitea/services/packages/cargo"
 | 
			
		||||
| 
						 | 
				
			
			@ -110,7 +111,7 @@ func SearchPackages(ctx *context.Context) {
 | 
			
		|||
			OwnerID:    ctx.Package.Owner.ID,
 | 
			
		||||
			Type:       packages_model.TypeCargo,
 | 
			
		||||
			Name:       packages_model.SearchValue{Value: ctx.FormTrim("q")},
 | 
			
		||||
			IsInternal: util.OptionalBoolFalse,
 | 
			
		||||
			IsInternal: optional.Some(false),
 | 
			
		||||
			Paginator:  &paginator,
 | 
			
		||||
		},
 | 
			
		||||
	)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,12 +15,13 @@ import (
 | 
			
		|||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	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"
 | 
			
		||||
	chef_module "code.gitea.io/gitea/modules/packages/chef"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	"code.gitea.io/gitea/routers/api/packages/helper"
 | 
			
		||||
	"code.gitea.io/gitea/services/context"
 | 
			
		||||
	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{
 | 
			
		||||
		OwnerID:    ctx.Package.Owner.ID,
 | 
			
		||||
		Type:       packages_model.TypeChef,
 | 
			
		||||
		IsInternal: util.OptionalBoolFalse,
 | 
			
		||||
		IsInternal: optional.Some(false),
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
| 
						 | 
				
			
			@ -85,7 +86,7 @@ func EnumeratePackages(ctx *context.Context) {
 | 
			
		|||
		OwnerID:    ctx.Package.Owner.ID,
 | 
			
		||||
		Type:       packages_model.TypeChef,
 | 
			
		||||
		Name:       packages_model.SearchValue{Value: ctx.FormTrim("q")},
 | 
			
		||||
		IsInternal: util.OptionalBoolFalse,
 | 
			
		||||
		IsInternal: optional.Some(false),
 | 
			
		||||
		Paginator: db.NewAbsoluteListOptions(
 | 
			
		||||
			ctx.FormInt("start"),
 | 
			
		||||
			ctx.FormInt("items"),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
Some files were not shown because too many files have changed in this diff Show more
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue