mirror of
https://github.com/go-gitea/gitea
synced 2024-10-19 20:10:10 +02:00
Merge branch 'main' into add-repo-license-display
This commit is contained in:
commit
7582a662c7
8
.github/workflows/pull-compliance.yml
vendored
8
.github/workflows/pull-compliance.yml
vendored
@ -32,9 +32,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
python-version: "3.12"
|
||||
- run: pip install poetry
|
||||
- run: make deps-py
|
||||
- run: make lint-templates
|
||||
@ -45,9 +45,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
python-version: "3.12"
|
||||
- run: pip install poetry
|
||||
- run: make deps-py
|
||||
- run: make lint-yaml
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -15,7 +15,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
|
||||
|
@ -1,6 +1,5 @@
|
||||
[Unit]
|
||||
Description=Gitea (Git with a cup of tea)
|
||||
After=syslog.target
|
||||
After=network.target
|
||||
###
|
||||
# Don't forget to add the database service dependencies
|
||||
|
@ -1480,8 +1480,9 @@ LEVEL = Info
|
||||
;;
|
||||
;; Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled
|
||||
;DEFAULT_EMAIL_NOTIFICATIONS = enabled
|
||||
;; Disabled features for users, could be "deletion", more features can be disabled in future
|
||||
;; Disabled features for users, could be "deletion","manage_gpg_keys" 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,8 +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.
|
||||
- `USER_DISABLED_FEATURES`: **_empty_** Disabled features for users, could be `deletion` and more features can be added in future.
|
||||
- `USER_DISABLED_FEATURES`: **_empty_** Disabled features for users, could be `deletion`, `manage_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,8 +497,9 @@ Gitea 创建以下非唯一队列:
|
||||
|
||||
- `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**:用户电子邮件通知的默认配置(用户可配置)。选项:enabled、onmention、disabled
|
||||
- `DISABLE_REGULAR_ORG_CREATION`: **false**:禁止普通(非管理员)用户创建组织。
|
||||
- `USER_DISABLED_FEATURES`:**_empty_** 禁用的用户特性,当前允许为空或者 `deletion`, 未来可以增加更多设置。
|
||||
- `USER_DISABLED_FEATURES`:**_empty_** 禁用的用户特性,当前允许为空或者 `deletion`,`manage_gpg_keys` 未来可以增加更多设置。
|
||||
- `deletion`: 用户不能通过界面或者API删除他自己。
|
||||
- `manage_gpg_keys`: 用户不能配置 GPG 密钥
|
||||
|
||||
## 安全性 (`security`)
|
||||
|
||||
|
@ -259,14 +259,14 @@ This template produces something along these lines:
|
||||
The template system contains several functions that can be used to further process and format
|
||||
the messages. Here's a list of some of them:
|
||||
|
||||
| Name | Parameters | Available | Usage |
|
||||
| ---------------- | ----------- | --------- |-----------------------------------------------------------------------------|
|
||||
| `AppUrl` | - | Any | Gitea's URL |
|
||||
| `AppName` | - | Any | Set from `app.ini`, usually "Gitea" |
|
||||
| `AppDomain` | - | Any | Gitea's host name |
|
||||
| `EllipsisString` | string, int | Any | Truncates a string to the specified length; adds ellipsis as needed |
|
||||
| `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 `.ReviewComments.RenderedContent`. |
|
||||
| Name | Parameters | Available | Usage |
|
||||
| ---------------- | ----------- | --------- | ------------------------------------------------------------------- |
|
||||
| `AppUrl` | - | Any | Gitea's URL |
|
||||
| `AppName` | - | Any | Set from `app.ini`, usually "Gitea" |
|
||||
| `AppDomain` | - | Any | Gitea's host name |
|
||||
| `EllipsisString` | string, int | Any | Truncates a string to the specified length; adds ellipsis as needed |
|
||||
| `SanitizeHTML` | string | Body only | Sanitizes text by removing any dangerous HTML tags from it |
|
||||
| `SafeHTML` | string | Body only | Takes the input as HTML, can be used for outputing raw HTML content |
|
||||
|
||||
These are _functions_, not metadata, so they have to be used:
|
||||
|
||||
|
@ -242,14 +242,14 @@ _主题_ 和 _邮件正文_ 由 [Golang的模板引擎](https://go.dev/pkg/text/
|
||||
|
||||
模板系统包含一些函数,可用于进一步处理和格式化消息。以下是其中一些函数的列表:
|
||||
|
||||
| 函数名 | 参数 | 可用于 | 用法 |
|
||||
|------------------| ----------- | ------------ |---------------------------------------------------------|
|
||||
| `AppUrl` | - | 任何地方 | Gitea 的 URL |
|
||||
| `AppName` | - | 任何地方 | 从 `app.ini` 中设置,通常为 "Gitea" |
|
||||
| `AppDomain` | - | 任何地方 | Gitea 的主机名 |
|
||||
| `EllipsisString` | string, int | 任何地方 | 将字符串截断为指定长度;根据需要添加省略号 |
|
||||
| `SanitizeHTML` | string | 仅正文部分 | 通过删除其中的危险 HTML 标签对文本进行清理 |
|
||||
| `SafeHTML` | string | 仅正文部分 | 将输入作为 HTML 处理;可用于 `.ReviewComments.RenderedContent` 等字段 |
|
||||
| 函数名 | 参数 | 可用于 | 用法 |
|
||||
|------------------| ----------- | ------------ | ------------------------------ |
|
||||
| `AppUrl` | - | 任何地方 | Gitea 的 URL |
|
||||
| `AppName` | - | 任何地方 | 从 `app.ini` 中设置,通常为 "Gitea" |
|
||||
| `AppDomain` | - | 任何地方 | Gitea 的主机名 |
|
||||
| `EllipsisString` | string, int | 任何地方 | 将字符串截断为指定长度;根据需要添加省略号 |
|
||||
| `SanitizeHTML` | string | 仅正文部分 | 通过删除其中的危险 HTML 标签对文本进行清理 |
|
||||
| `SafeHTML` | string | 仅正文部分 | 将输入作为 HTML 处理;可用于输出原始的 HTML 内容 |
|
||||
|
||||
这些都是 _函数_,而不是元数据,因此必须按以下方式使用:
|
||||
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/shared/types"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
@ -159,7 +160,7 @@ type FindRunnerOptions struct {
|
||||
OwnerID int64
|
||||
Sort string
|
||||
Filter string
|
||||
IsOnline util.OptionalBool
|
||||
IsOnline optional.Option[bool]
|
||||
WithAvailable bool // not only runners belong to, but also runners can be used
|
||||
}
|
||||
|
||||
@ -186,10 +187,12 @@ func (opts FindRunnerOptions) ToConds() builder.Cond {
|
||||
cond = cond.And(builder.Like{"name", opts.Filter})
|
||||
}
|
||||
|
||||
if opts.IsOnline.IsTrue() {
|
||||
cond = cond.And(builder.Gt{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()})
|
||||
} else if opts.IsOnline.IsFalse() {
|
||||
cond = cond.And(builder.Lte{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()})
|
||||
if opts.IsOnline.Has() {
|
||||
if opts.IsOnline.Value() {
|
||||
cond = cond.And(builder.Gt{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()})
|
||||
} else {
|
||||
cond = cond.And(builder.Lte{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()})
|
||||
}
|
||||
}
|
||||
return cond
|
||||
}
|
||||
|
@ -225,8 +225,8 @@ func (a *Action) ShortActUserName(ctx context.Context) string {
|
||||
return base.EllipsisString(a.GetActUserName(ctx), 20)
|
||||
}
|
||||
|
||||
// GetDisplayName gets the action's display name based on DEFAULT_SHOW_FULL_NAME, or falls back to the username if it is blank.
|
||||
func (a *Action) GetDisplayName(ctx context.Context) string {
|
||||
// GetActDisplayName gets the action's display name based on DEFAULT_SHOW_FULL_NAME, or falls back to the username if it is blank.
|
||||
func (a *Action) GetActDisplayName(ctx context.Context) string {
|
||||
if setting.UI.DefaultShowFullName {
|
||||
trimmedFullName := strings.TrimSpace(a.GetActFullName(ctx))
|
||||
if len(trimmedFullName) > 0 {
|
||||
@ -236,8 +236,8 @@ func (a *Action) GetDisplayName(ctx context.Context) string {
|
||||
return a.ShortActUserName(ctx)
|
||||
}
|
||||
|
||||
// GetDisplayNameTitle gets the action's display name used for the title (tooltip) based on DEFAULT_SHOW_FULL_NAME
|
||||
func (a *Action) GetDisplayNameTitle(ctx context.Context) string {
|
||||
// GetActDisplayNameTitle gets the action's display name used for the title (tooltip) based on DEFAULT_SHOW_FULL_NAME
|
||||
func (a *Action) GetActDisplayNameTitle(ctx context.Context) string {
|
||||
if setting.UI.DefaultShowFullName {
|
||||
return a.ShortActUserName(ctx)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -22,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"
|
||||
@ -1036,8 +1037,8 @@ type FindCommentsOptions struct {
|
||||
TreePath string
|
||||
Type CommentType
|
||||
IssueIDs []int64
|
||||
Invalidated util.OptionalBool
|
||||
IsPull util.OptionalBool
|
||||
Invalidated optional.Option[bool]
|
||||
IsPull optional.Option[bool]
|
||||
}
|
||||
|
||||
// ToConds implements FindOptions interface
|
||||
@ -1069,11 +1070,11 @@ func (opts FindCommentsOptions) ToConds() builder.Cond {
|
||||
if len(opts.TreePath) > 0 {
|
||||
cond = cond.And(builder.Eq{"comment.tree_path": opts.TreePath})
|
||||
}
|
||||
if !opts.Invalidated.IsNone() {
|
||||
cond = cond.And(builder.Eq{"comment.invalidated": opts.Invalidated.IsTrue()})
|
||||
if opts.Invalidated.Has() {
|
||||
cond = cond.And(builder.Eq{"comment.invalidated": opts.Invalidated.Value()})
|
||||
}
|
||||
if opts.IsPull != util.OptionalBoolNone {
|
||||
cond = cond.And(builder.Eq{"issue.is_pull": opts.IsPull.IsTrue()})
|
||||
if opts.IsPull.Has() {
|
||||
cond = cond.And(builder.Eq{"issue.is_pull": opts.IsPull.Value()})
|
||||
}
|
||||
return cond
|
||||
}
|
||||
@ -1082,7 +1083,7 @@ func (opts FindCommentsOptions) ToConds() builder.Cond {
|
||||
func FindComments(ctx context.Context, opts *FindCommentsOptions) (CommentList, error) {
|
||||
comments := make([]*Comment, 0, 10)
|
||||
sess := db.GetEngine(ctx).Where(opts.ToConds())
|
||||
if opts.RepoID > 0 || opts.IsPull != util.OptionalBoolNone {
|
||||
if opts.RepoID > 0 || opts.IsPull.Has() {
|
||||
sess.Join("INNER", "issue", "issue.id = comment.issue_id")
|
||||
}
|
||||
|
||||
|
@ -172,13 +172,9 @@ func FetchIssueContentHistoryList(dbCtx context.Context, issueID, commentID int6
|
||||
|
||||
// HasIssueContentHistory check if a ContentHistory entry exists
|
||||
func HasIssueContentHistory(dbCtx context.Context, issueID, commentID int64) (bool, error) {
|
||||
exists, err := db.GetEngine(dbCtx).Cols("id").Exist(&ContentHistory{
|
||||
IssueID: issueID,
|
||||
CommentID: commentID,
|
||||
})
|
||||
exists, err := db.GetEngine(dbCtx).Where(builder.Eq{"issue_id": issueID, "comment_id": commentID}).Exist(&ContentHistory{})
|
||||
if err != nil {
|
||||
log.Error("can not fetch issue content history. err=%v", err)
|
||||
return false, err
|
||||
return false, fmt.Errorf("can not check issue content history. err: %w", err)
|
||||
}
|
||||
return exists, err
|
||||
}
|
||||
|
@ -78,3 +78,22 @@ func TestContentHistory(t *testing.T) {
|
||||
assert.EqualValues(t, 7, list2[1].HistoryID)
|
||||
assert.EqualValues(t, 4, list2[2].HistoryID)
|
||||
}
|
||||
|
||||
func TestHasIssueContentHistoryForCommentOnly(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
_ = db.TruncateBeans(db.DefaultContext, &issues_model.ContentHistory{})
|
||||
|
||||
hasHistory1, _ := issues_model.HasIssueContentHistory(db.DefaultContext, 10, 0)
|
||||
assert.False(t, hasHistory1)
|
||||
hasHistory2, _ := issues_model.HasIssueContentHistory(db.DefaultContext, 10, 100)
|
||||
assert.False(t, hasHistory2)
|
||||
|
||||
_ = issues_model.SaveIssueContentHistory(db.DefaultContext, 1, 10, 100, timeutil.TimeStampNow(), "c-a", true)
|
||||
_ = issues_model.SaveIssueContentHistory(db.DefaultContext, 1, 10, 100, timeutil.TimeStampNow().Add(5), "c-b", false)
|
||||
|
||||
hasHistory1, _ = issues_model.HasIssueContentHistory(db.DefaultContext, 10, 0)
|
||||
assert.False(t, hasHistory1)
|
||||
hasHistory2, _ = issues_model.HasIssueContentHistory(db.DefaultContext, 10, 100)
|
||||
assert.True(t, hasHistory2)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
|
||||
"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"
|
||||
@ -302,7 +303,7 @@ func DeleteMilestoneByRepoID(ctx context.Context, repoID, id int64) error {
|
||||
}
|
||||
numClosedMilestones, err := db.Count[Milestone](ctx, FindMilestoneOptions{
|
||||
RepoID: repo.ID,
|
||||
IsClosed: util.OptionalBoolTrue,
|
||||
IsClosed: optional.Some(true),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -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
|
||||
HasFileWithName string // only results are found which are associated with a file with the specific name
|
||||
HasFiles util.OptionalBool // only results are found which have associated files
|
||||
IsInternal optional.Option[bool]
|
||||
HasFileWithName string // only results are found which are associated with a file with the specific name
|
||||
HasFiles optional.Option[bool] // only results are found which have associated files
|
||||
Sort VersionSort
|
||||
db.Paginator
|
||||
}
|
||||
|
||||
func (opts *PackageSearchOptions) ToConds() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
if !opts.IsInternal.IsNone() {
|
||||
if opts.IsInternal.Has() {
|
||||
cond = builder.Eq{
|
||||
"package_version.is_internal": opts.IsInternal.IsTrue(),
|
||||
"package_version.is_internal": opts.IsInternal.Value(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -250,10 +251,10 @@ func (opts *PackageSearchOptions) ToConds() builder.Cond {
|
||||
cond = cond.And(builder.Exists(builder.Select("package_file.id").From("package_file").Where(fileCond)))
|
||||
}
|
||||
|
||||
if !opts.HasFiles.IsNone() {
|
||||
if opts.HasFiles.Has() {
|
||||
filesCond := builder.Exists(builder.Select("package_file.id").From("package_file").Where(builder.Expr("package_file.version_id = package_version.id")))
|
||||
|
||||
if opts.HasFiles.IsFalse() {
|
||||
if !opts.HasFiles.Value() {
|
||||
filesCond = builder.Not{filesCond}
|
||||
}
|
||||
|
||||
@ -307,8 +308,8 @@ func SearchLatestVersions(ctx context.Context, opts *PackageSearchOptions) ([]*P
|
||||
And(builder.Expr("pv2.id IS NULL"))
|
||||
|
||||
joinCond := builder.Expr("package_version.package_id = pv2.package_id AND (package_version.created_unix < pv2.created_unix OR (package_version.created_unix = pv2.created_unix AND package_version.id < pv2.id))")
|
||||
if !opts.IsInternal.IsNone() {
|
||||
joinCond = joinCond.And(builder.Eq{"pv2.is_internal": opts.IsInternal.IsTrue()})
|
||||
if opts.IsInternal.Has() {
|
||||
joinCond = joinCond.And(builder.Eq{"pv2.is_internal": opts.IsInternal.Value()})
|
||||
}
|
||||
|
||||
sess := db.GetEngine(ctx).
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/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"
|
||||
@ -196,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
|
||||
@ -207,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 {
|
||||
|
@ -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"
|
||||
@ -840,7 +841,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.
|
||||
@ -852,8 +853,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))
|
||||
|
@ -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) {
|
||||
|
@ -715,7 +715,7 @@ func CreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOve
|
||||
|
||||
// IsLastAdminUser check whether user is the last admin
|
||||
func IsLastAdminUser(ctx context.Context, user *User) bool {
|
||||
if user.IsAdmin && CountUsers(ctx, &CountUserFilter{IsAdmin: util.OptionalBoolTrue}) <= 1 {
|
||||
if user.IsAdmin && CountUsers(ctx, &CountUserFilter{IsAdmin: optional.Some(true)}) <= 1 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@ -724,7 +724,7 @@ func IsLastAdminUser(ctx context.Context, user *User) bool {
|
||||
// CountUserFilter represent optional filters for CountUsers
|
||||
type CountUserFilter struct {
|
||||
LastLoginSince *int64
|
||||
IsAdmin util.OptionalBool
|
||||
IsAdmin optional.Option[bool]
|
||||
}
|
||||
|
||||
// CountUsers returns number of users.
|
||||
@ -742,8 +742,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()})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -60,6 +60,9 @@ func (q *WorkerPoolQueue[T]) doDispatchBatchToWorker(wg *workerGroup[T], flushCh
|
||||
full = true
|
||||
}
|
||||
|
||||
// TODO: the logic could be improved in the future, to avoid a data-race between "doStartNewWorker" and "workerNum"
|
||||
// The root problem is that if we skip "doStartNewWorker" here, the "workerNum" might be decreased by other workers later
|
||||
// So ideally, it should check whether there are enough workers by some approaches, and start new workers if necessary.
|
||||
q.workerNumMu.Lock()
|
||||
noWorker := q.workerNum == 0
|
||||
if full || noWorker {
|
||||
@ -143,7 +146,11 @@ func (q *WorkerPoolQueue[T]) doStartNewWorker(wp *workerGroup[T]) {
|
||||
log.Debug("Queue %q starts new worker", q.GetName())
|
||||
defer log.Debug("Queue %q stops idle worker", q.GetName())
|
||||
|
||||
atomic.AddInt32(&q.workerStartedCounter, 1) // Only increase counter, used for debugging
|
||||
|
||||
t := time.NewTicker(workerIdleDuration)
|
||||
defer t.Stop()
|
||||
|
||||
keepWorking := true
|
||||
stopWorking := func() {
|
||||
q.workerNumMu.Lock()
|
||||
@ -158,13 +165,18 @@ func (q *WorkerPoolQueue[T]) doStartNewWorker(wp *workerGroup[T]) {
|
||||
case batch, ok := <-q.batchChan:
|
||||
if !ok {
|
||||
stopWorking()
|
||||
} else {
|
||||
q.doWorkerHandle(batch)
|
||||
t.Reset(workerIdleDuration)
|
||||
continue
|
||||
}
|
||||
q.doWorkerHandle(batch)
|
||||
// reset the idle ticker, and drain the tick after reset in case a tick is already triggered
|
||||
t.Reset(workerIdleDuration)
|
||||
select {
|
||||
case <-t.C:
|
||||
default:
|
||||
}
|
||||
case <-t.C:
|
||||
q.workerNumMu.Lock()
|
||||
keepWorking = q.workerNum <= 1
|
||||
keepWorking = q.workerNum <= 1 // keep the last worker running
|
||||
if !keepWorking {
|
||||
q.workerNum--
|
||||
}
|
||||
|
@ -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|[:;,.?!]$)`)
|
||||
|
@ -429,6 +429,8 @@ func TestRegExp_issueNumericPattern(t *testing.T) {
|
||||
" #12",
|
||||
"#12:",
|
||||
"ref: #12: msg",
|
||||
"\"#1234\"",
|
||||
"'#1234'",
|
||||
}
|
||||
falseTestCases := []string{
|
||||
"# 1234",
|
||||
@ -459,6 +461,8 @@ func TestRegExp_issueAlphanumericPattern(t *testing.T) {
|
||||
"(ABC-123)",
|
||||
"[ABC-123]",
|
||||
"ABC-123:",
|
||||
"\"ABC-123\"",
|
||||
"'ABC-123'",
|
||||
}
|
||||
falseTestCases := []string{
|
||||
"RC-08",
|
||||
|
@ -20,5 +20,6 @@ func loadAdminFrom(rootCfg ConfigProvider) {
|
||||
}
|
||||
|
||||
const (
|
||||
UserFeatureDeletion = "deletion"
|
||||
UserFeatureDeletion = "deletion"
|
||||
UserFeatureManageGPGKeys = "manage_gpg_keys"
|
||||
)
|
||||
|
@ -20,7 +20,7 @@ var SessionConfig = struct {
|
||||
ProviderConfig string
|
||||
// Cookie name to save session ID. Default is "MacaronSession".
|
||||
CookieName string
|
||||
// Cookie path to store. Default is "/". HINT: there was a bug, the old value doesn't have trailing slash, and could be empty "".
|
||||
// Cookie path to store. Default is "/".
|
||||
CookiePath string
|
||||
// GC interval time in seconds. Default is 3600.
|
||||
Gclifetime int64
|
||||
@ -49,7 +49,10 @@ func loadSessionFrom(rootCfg ConfigProvider) {
|
||||
fatalDuplicatedPath("session", SessionConfig.ProviderConfig)
|
||||
}
|
||||
SessionConfig.CookieName = sec.Key("COOKIE_NAME").MustString("i_like_gitea")
|
||||
SessionConfig.CookiePath = AppSubURL + "/" // there was a bug, old code only set CookePath=AppSubURL, no trailing slash
|
||||
SessionConfig.CookiePath = AppSubURL
|
||||
if SessionConfig.CookiePath == "" {
|
||||
SessionConfig.CookiePath = "/"
|
||||
}
|
||||
SessionConfig.Secure = sec.Key("COOKIE_SECURE").MustBool(strings.HasPrefix(strings.ToLower(AppURL), "https://"))
|
||||
SessionConfig.Gclifetime = sec.Key("GC_INTERVAL_TIME").MustInt64(86400)
|
||||
SessionConfig.Maxlifetime = sec.Key("SESSION_LIFE_TIME").MustInt64(86400)
|
||||
|
@ -5,6 +5,7 @@ package templates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"regexp"
|
||||
"strings"
|
||||
@ -33,7 +34,7 @@ func mailSubjectTextFuncMap() texttmpl.FuncMap {
|
||||
}
|
||||
}
|
||||
|
||||
func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template, name string, content []byte) {
|
||||
func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template, name string, content []byte) error {
|
||||
// Split template into subject and body
|
||||
var subjectContent []byte
|
||||
bodyContent := content
|
||||
@ -42,20 +43,13 @@ func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template,
|
||||
subjectContent = content[0:loc[0]]
|
||||
bodyContent = content[loc[1]:]
|
||||
}
|
||||
if _, err := stpl.New(name).
|
||||
Parse(string(subjectContent)); err != nil {
|
||||
log.Error("Failed to parse template [%s/subject]: %v", name, err)
|
||||
if !setting.IsProd {
|
||||
log.Fatal("Please fix the mail template error")
|
||||
}
|
||||
if _, err := stpl.New(name).Parse(string(subjectContent)); err != nil {
|
||||
return fmt.Errorf("failed to parse template [%s/subject]: %w", name, err)
|
||||
}
|
||||
if _, err := btpl.New(name).
|
||||
Parse(string(bodyContent)); err != nil {
|
||||
log.Error("Failed to parse template [%s/body]: %v", name, err)
|
||||
if !setting.IsProd {
|
||||
log.Fatal("Please fix the mail template error")
|
||||
}
|
||||
if _, err := btpl.New(name).Parse(string(bodyContent)); err != nil {
|
||||
return fmt.Errorf("failed to parse template [%s/body]: %w", name, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Mailer provides the templates required for sending notification mails.
|
||||
@ -87,7 +81,13 @@ func Mailer(ctx context.Context) (*texttmpl.Template, *template.Template) {
|
||||
if firstRun {
|
||||
log.Trace("Adding mail template %s: %s by %s", tmplName, assetPath, layerName)
|
||||
}
|
||||
buildSubjectBodyTemplate(subjectTemplates, bodyTemplates, tmplName, content)
|
||||
if err = buildSubjectBodyTemplate(subjectTemplates, bodyTemplates, tmplName, content); err != nil {
|
||||
if firstRun {
|
||||
log.Fatal("Failed to parse mail template, err: %v", err)
|
||||
} else {
|
||||
log.Error("Failed to parse mail template, err: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,57 +17,6 @@ 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() {
|
||||
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 optional.Option[bool] of a string using strconv.ParseBool
|
||||
func OptionalBoolParse(s string) optional.Option[bool] {
|
||||
v, e := strconv.ParseBool(s)
|
||||
|
@ -142,6 +142,19 @@ confirm_delete_selected = Confirm to delete all selected items?
|
||||
name = Name
|
||||
value = Value
|
||||
|
||||
filter = Filter
|
||||
filter.clear = Clear Filter
|
||||
filter.is_archived = Archived
|
||||
filter.not_archived = Not Archived
|
||||
filter.is_fork = Forked
|
||||
filter.not_fork = Not Forked
|
||||
filter.is_mirror = Mirrored
|
||||
filter.not_mirror = Not Mirrored
|
||||
filter.is_template = Template
|
||||
filter.not_template = Not Template
|
||||
filter.public = Public
|
||||
filter.private = Private
|
||||
|
||||
[aria]
|
||||
navbar = Navigation Bar
|
||||
footer = Footer
|
||||
@ -1793,9 +1806,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
|
||||
|
@ -424,6 +424,7 @@ authorization_failed_desc=L'autorisation a échoué car nous avons détecté une
|
||||
sspi_auth_failed=Échec de l'authentification SSPI
|
||||
password_pwned=Le mot de passe que vous avez choisi se trouve sur la liste <a target="_blank" rel="noopener noreferrer" href="https://haveibeenpwned.com/Passwords">des mots de passe ayant fuité</a> sur internet. Veuillez réessayer avec un mot de passe différent et considérer remplacer ce mot de passe si vous l'utilisez ailleurs.
|
||||
password_pwned_err=Impossible d'envoyer la demande à HaveIBeenPwned
|
||||
last_admin=Vous ne pouvez pas supprimer ce compte car au moins un administrateur est requis.
|
||||
|
||||
[mail]
|
||||
view_it_on=Voir sur %s
|
||||
@ -1714,6 +1715,7 @@ pulls.select_commit_hold_shift_for_range=Maintenir Maj et cliquer sur des révis
|
||||
pulls.review_only_possible_for_full_diff=Une évaluation n'est possible que lorsque vous affichez le différentiel complet.
|
||||
pulls.filter_changes_by_commit=Filtrer par révision
|
||||
pulls.nothing_to_compare=Ces branches sont identiques. Il n’y a pas besoin de créer une demande d'ajout.
|
||||
pulls.nothing_to_compare_have_tag=Les branches/étiquettes sélectionnées sont équivalentes.
|
||||
pulls.nothing_to_compare_and_allow_empty_pr=Ces branches sont égales. Cette demande d'ajout sera vide.
|
||||
pulls.has_pull_request='Il existe déjà une demande d'ajout entre ces deux branches : <a href="%[1]s">%[2]s#%[3]d</a>'
|
||||
pulls.create=Créer une demande d'ajout
|
||||
|
@ -123,6 +123,7 @@ pin=ピン留め
|
||||
unpin=ピン留め解除
|
||||
|
||||
artifacts=成果物
|
||||
confirm_delete_artifact=アーティファクト %s を削除してよろしいですか?
|
||||
|
||||
archived=アーカイブ
|
||||
|
||||
@ -423,6 +424,7 @@ authorization_failed_desc=無効なリクエストを検出したため認可が
|
||||
sspi_auth_failed=SSPI認証に失敗しました
|
||||
password_pwned=あなたが選択したパスワードは、過去の情報漏洩事件で流出した<a target="_blank" rel="noopener noreferrer" href="https://haveibeenpwned.com/Passwords">盗まれたパスワードのリスト</a>に含まれています。 別のパスワードでもう一度試してください。 また他の登録でもこのパスワードからの変更を検討してください。
|
||||
password_pwned_err=HaveIBeenPwnedへのリクエストを完了できませんでした
|
||||
last_admin=最後の管理者は削除できません。少なくとも一人の管理者が必要です。
|
||||
|
||||
[mail]
|
||||
view_it_on=%s で見る
|
||||
@ -588,6 +590,7 @@ org_still_own_packages=組織はまだ1つ以上のパッケージを所有し
|
||||
|
||||
target_branch_not_exist=ターゲットのブランチが存在していません。
|
||||
|
||||
admin_cannot_delete_self=あなたが管理者である場合、自分自身を削除することはできません。最初に管理者権限を削除してください。
|
||||
|
||||
[user]
|
||||
change_avatar=アバターを変更…
|
||||
@ -968,6 +971,8 @@ issue_labels_helper=イシューのラベルセットを選択
|
||||
license=ライセンス
|
||||
license_helper=ライセンス ファイルを選択してください。
|
||||
license_helper_desc=ライセンスにより、他人があなたのコードに対して何ができて何ができないのかを規定します。 どれがプロジェクトにふさわしいか迷っていますか? <a target="_blank" rel="noopener noreferrer" href="%s">ライセンス選択サイト</a> も確認してみてください。
|
||||
object_format=オブジェクトのフォーマット
|
||||
object_format_helper=リポジトリのオブジェクトフォーマット。後で変更することはできません。SHA1 は最も互換性があります。
|
||||
readme=README
|
||||
readme_helper=READMEファイル テンプレートを選択してください。
|
||||
readme_helper_desc=プロジェクトについての説明をひととおり書く場所です。
|
||||
@ -1035,6 +1040,7 @@ desc.public=公開
|
||||
desc.template=テンプレート
|
||||
desc.internal=組織内
|
||||
desc.archived=アーカイブ
|
||||
desc.sha256=SHA256
|
||||
|
||||
template.items=テンプレート項目
|
||||
template.git_content=Gitコンテンツ (デフォルトブランチ)
|
||||
|
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;
|
||||
}
|
@ -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)
|
||||
}
|
@ -13,6 +13,7 @@ import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
cargo_module "code.gitea.io/gitea/modules/packages/cargo"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@ -110,7 +111,7 @@ func SearchPackages(ctx *context.Context) {
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages_model.TypeCargo,
|
||||
Name: packages_model.SearchValue{Value: ctx.FormTrim("q")},
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
Paginator: &paginator,
|
||||
},
|
||||
)
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
chef_module "code.gitea.io/gitea/modules/packages/chef"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@ -40,7 +41,7 @@ func PackagesUniverse(ctx *context.Context) {
|
||||
pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages_model.TypeChef,
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
})
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
@ -85,7 +86,7 @@ func EnumeratePackages(ctx *context.Context) {
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages_model.TypeChef,
|
||||
Name: packages_model.SearchValue{Value: ctx.FormTrim("q")},
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
Paginator: db.NewAbsoluteListOptions(
|
||||
ctx.FormInt("start"),
|
||||
ctx.FormInt("items"),
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
composer_module "code.gitea.io/gitea/modules/packages/composer"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@ -66,7 +67,7 @@ func SearchPackages(ctx *context.Context) {
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages_model.TypeComposer,
|
||||
Name: packages_model.SearchValue{Value: ctx.FormTrim("q")},
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
Paginator: &paginator,
|
||||
}
|
||||
if ctx.FormTrim("type") != "" {
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
goproxy_module "code.gitea.io/gitea/modules/packages/goproxy"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
@ -129,7 +130,7 @@ func resolvePackage(ctx *context.Context, ownerID int64, name, version string) (
|
||||
Value: name,
|
||||
ExactMatch: true,
|
||||
},
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
Sort: packages_model.SortCreatedDesc,
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
helm_module "code.gitea.io/gitea/modules/packages/helm"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@ -42,7 +43,7 @@ func Index(ctx *context.Context) {
|
||||
pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages_model.TypeHelm,
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
})
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
@ -110,7 +111,7 @@ func DownloadPackageFile(ctx *context.Context) {
|
||||
Value: ctx.Params("package"),
|
||||
},
|
||||
HasFileWithName: filename,
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
})
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
npm_module "code.gitea.io/gitea/modules/packages/npm"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
func createPackageMetadataResponse(registryURL string, pds []*packages_model.PackageDescriptor) *npm_module.PackageMetadata {
|
||||
@ -98,7 +99,7 @@ func createPackageSearchResponse(pds []*packages_model.PackageDescriptor, total
|
||||
Maintainers: []npm_module.User{}, // npm cli needs this field
|
||||
Keywords: metadata.Keywords,
|
||||
Links: &npm_module.PackageSearchPackageLinks{
|
||||
Registry: pd.FullWebLink(),
|
||||
Registry: setting.AppURL + "api/packages/" + pd.Owner.Name + "/npm",
|
||||
Homepage: metadata.ProjectURL,
|
||||
},
|
||||
},
|
||||
|
@ -17,6 +17,7 @@ import (
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
npm_module "code.gitea.io/gitea/modules/packages/npm"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@ -120,7 +121,7 @@ func DownloadPackageFileByName(ctx *context.Context) {
|
||||
Value: packageNameFromParams(ctx),
|
||||
},
|
||||
HasFileWithName: filename,
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
})
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
@ -395,7 +396,7 @@ func setPackageTag(ctx std_ctx.Context, tag string, pv *packages_model.PackageVe
|
||||
Properties: map[string]string{
|
||||
npm_module.TagProperty: tag,
|
||||
},
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@ -431,7 +432,7 @@ func PackageSearch(ctx *context.Context) {
|
||||
pvs, total, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages_model.TypeNpm,
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
Name: packages_model.SearchValue{
|
||||
ExactMatch: false,
|
||||
Value: ctx.FormTrim("text"),
|
||||
|
@ -18,6 +18,7 @@ import (
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
nuget_model "code.gitea.io/gitea/models/packages/nuget"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
nuget_module "code.gitea.io/gitea/modules/packages/nuget"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@ -122,7 +123,7 @@ func SearchServiceV2(ctx *context.Context) {
|
||||
Name: packages_model.SearchValue{
|
||||
Value: getSearchTerm(ctx),
|
||||
},
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
Paginator: paginator,
|
||||
})
|
||||
if err != nil {
|
||||
@ -172,7 +173,7 @@ func SearchServiceV2Count(ctx *context.Context) {
|
||||
Name: packages_model.SearchValue{
|
||||
Value: getSearchTerm(ctx),
|
||||
},
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
})
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
@ -187,7 +188,7 @@ func SearchServiceV3(ctx *context.Context) {
|
||||
pvs, count, err := nuget_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Name: packages_model.SearchValue{Value: ctx.FormTrim("q")},
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
Paginator: db.NewAbsoluteListOptions(
|
||||
ctx.FormInt("skip"),
|
||||
ctx.FormInt("take"),
|
||||
@ -313,7 +314,7 @@ func EnumeratePackageVersionsV2(ctx *context.Context) {
|
||||
ExactMatch: true,
|
||||
Value: packageName,
|
||||
},
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
Paginator: paginator,
|
||||
})
|
||||
if err != nil {
|
||||
@ -358,7 +359,7 @@ func EnumeratePackageVersionsV2Count(ctx *context.Context) {
|
||||
ExactMatch: true,
|
||||
Value: strings.Trim(ctx.FormTrim("id"), "'"),
|
||||
},
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
})
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
"strings"
|
||||
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
rubygems_module "code.gitea.io/gitea/modules/packages/rubygems"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
@ -43,7 +44,7 @@ func EnumeratePackagesLatest(ctx *context.Context) {
|
||||
pvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages_model.TypeRubyGems,
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
})
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
@ -304,7 +305,7 @@ func getVersionsByFilename(ctx *context.Context, filename string) ([]*packages_m
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages_model.TypeRubyGems,
|
||||
HasFileWithName: filename,
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
})
|
||||
return pvs, err
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
swift_module "code.gitea.io/gitea/modules/packages/swift"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@ -433,7 +434,7 @@ func LookupPackageIdentifiers(ctx *context.Context) {
|
||||
Properties: map[string]string{
|
||||
swift_module.PropertyRepositoryURL: url,
|
||||
},
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
})
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models/webhook"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
@ -37,7 +38,7 @@ func ListHooks(ctx *context.APIContext) {
|
||||
// "200":
|
||||
// "$ref": "#/responses/HookList"
|
||||
|
||||
sysHooks, err := webhook.GetSystemWebhooks(ctx, util.OptionalBoolNone)
|
||||
sysHooks, err := webhook.GetSystemWebhooks(ctx, optional.None[bool]())
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetSystemWebhooks", err)
|
||||
return
|
||||
|
@ -7,8 +7,8 @@ import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
@ -60,7 +60,7 @@ func ListPackages(ctx *context.APIContext) {
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages.Type(packageType),
|
||||
Name: packages.SearchValue{Value: query},
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
Paginator: &listOptions,
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -23,7 +23,6 @@ import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
@ -123,14 +122,14 @@ func SearchIssues(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
var isClosed util.OptionalBool
|
||||
var isClosed optional.Option[bool]
|
||||
switch ctx.FormString("state") {
|
||||
case "closed":
|
||||
isClosed = util.OptionalBoolTrue
|
||||
isClosed = optional.Some(true)
|
||||
case "all":
|
||||
isClosed = util.OptionalBoolNone
|
||||
isClosed = optional.None[bool]()
|
||||
default:
|
||||
isClosed = util.OptionalBoolFalse
|
||||
isClosed = optional.Some(false)
|
||||
}
|
||||
|
||||
var (
|
||||
@ -205,14 +204,14 @@ func SearchIssues(ctx *context.APIContext) {
|
||||
keyword = ""
|
||||
}
|
||||
|
||||
var isPull util.OptionalBool
|
||||
var isPull optional.Option[bool]
|
||||
switch ctx.FormString("type") {
|
||||
case "pulls":
|
||||
isPull = util.OptionalBoolTrue
|
||||
isPull = optional.Some(true)
|
||||
case "issues":
|
||||
isPull = util.OptionalBoolFalse
|
||||
isPull = optional.Some(false)
|
||||
default:
|
||||
isPull = util.OptionalBoolNone
|
||||
isPull = optional.None[bool]()
|
||||
}
|
||||
|
||||
var includedAnyLabels []int64
|
||||
@ -397,14 +396,14 @@ func ListIssues(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
var isClosed util.OptionalBool
|
||||
var isClosed optional.Option[bool]
|
||||
switch ctx.FormString("state") {
|
||||
case "closed":
|
||||
isClosed = util.OptionalBoolTrue
|
||||
isClosed = optional.Some(true)
|
||||
case "all":
|
||||
isClosed = util.OptionalBoolNone
|
||||
isClosed = optional.None[bool]()
|
||||
default:
|
||||
isClosed = util.OptionalBoolFalse
|
||||
isClosed = optional.Some(false)
|
||||
}
|
||||
|
||||
keyword := ctx.FormTrim("q")
|
||||
@ -453,31 +452,29 @@ func ListIssues(ctx *context.APIContext) {
|
||||
|
||||
listOptions := utils.GetListOptions(ctx)
|
||||
|
||||
var isPull util.OptionalBool
|
||||
isPull := optional.None[bool]()
|
||||
switch ctx.FormString("type") {
|
||||
case "pulls":
|
||||
isPull = util.OptionalBoolTrue
|
||||
isPull = optional.Some(true)
|
||||
case "issues":
|
||||
isPull = util.OptionalBoolFalse
|
||||
default:
|
||||
isPull = util.OptionalBoolNone
|
||||
isPull = optional.Some(false)
|
||||
}
|
||||
|
||||
if isPull != util.OptionalBoolNone && !ctx.Repo.CanReadIssuesOrPulls(isPull.IsTrue()) {
|
||||
if isPull.Has() && !ctx.Repo.CanReadIssuesOrPulls(isPull.Value()) {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if isPull == util.OptionalBoolNone {
|
||||
if !isPull.Has() {
|
||||
canReadIssues := ctx.Repo.CanRead(unit.TypeIssues)
|
||||
canReadPulls := ctx.Repo.CanRead(unit.TypePullRequests)
|
||||
if !canReadIssues && !canReadPulls {
|
||||
ctx.NotFound()
|
||||
return
|
||||
} else if !canReadIssues {
|
||||
isPull = util.OptionalBoolTrue
|
||||
isPull = optional.Some(true)
|
||||
} else if !canReadPulls {
|
||||
isPull = util.OptionalBoolFalse
|
||||
isPull = optional.Some(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -14,8 +14,8 @@ import (
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
@ -278,15 +278,15 @@ func ListRepoIssueComments(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
var isPull util.OptionalBool
|
||||
var isPull optional.Option[bool]
|
||||
canReadIssue := ctx.Repo.CanRead(unit.TypeIssues)
|
||||
canReadPull := ctx.Repo.CanRead(unit.TypePullRequests)
|
||||
if canReadIssue && canReadPull {
|
||||
isPull = util.OptionalBoolNone
|
||||
isPull = optional.None[bool]()
|
||||
} else if canReadIssue {
|
||||
isPull = util.OptionalBoolFalse
|
||||
isPull = optional.Some(false)
|
||||
} else if canReadPull {
|
||||
isPull = util.OptionalBoolTrue
|
||||
isPull = optional.Some(true)
|
||||
} else {
|
||||
ctx.NotFound()
|
||||
return
|
||||
|
@ -11,9 +11,9 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
@ -61,10 +61,10 @@ func ListMilestones(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
state := api.StateType(ctx.FormString("state"))
|
||||
var isClosed util.OptionalBool
|
||||
var isClosed optional.Option[bool]
|
||||
switch state {
|
||||
case api.StateClosed, api.StateOpen:
|
||||
isClosed = util.OptionalBoolOf(state == api.StateClosed)
|
||||
isClosed = optional.Some(state == api.StateClosed)
|
||||
}
|
||||
|
||||
milestones, total, err := db.FindAndCount[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
|
||||
|
@ -4,6 +4,7 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
@ -215,6 +216,10 @@ func CreateRelease(ctx *context.APIContext) {
|
||||
// "409":
|
||||
// "$ref": "#/responses/error"
|
||||
form := web.GetForm(ctx).(*api.CreateReleaseOption)
|
||||
if ctx.Repo.Repository.IsEmpty {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "RepoIsEmpty", fmt.Errorf("repo is empty"))
|
||||
return
|
||||
}
|
||||
rel, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, form.TagName)
|
||||
if err != nil {
|
||||
if !repo_model.IsErrReleaseNotExist(err) {
|
||||
|
@ -4,7 +4,9 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@ -154,6 +156,7 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
|
||||
// - application/json
|
||||
// consumes:
|
||||
// - multipart/form-data
|
||||
// - application/octet-stream
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
@ -180,7 +183,7 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
|
||||
// in: formData
|
||||
// description: attachment to upload
|
||||
// type: file
|
||||
// required: true
|
||||
// required: false
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/Attachment"
|
||||
@ -202,20 +205,36 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
// Get uploaded file from request
|
||||
file, header, err := ctx.Req.FormFile("attachment")
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetFile", err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
var content io.ReadCloser
|
||||
var filename string
|
||||
var size int64 = -1
|
||||
|
||||
filename := header.Filename
|
||||
if query := ctx.FormString("name"); query != "" {
|
||||
filename = query
|
||||
if strings.HasPrefix(strings.ToLower(ctx.Req.Header.Get("Content-Type")), "multipart/form-data") {
|
||||
file, header, err := ctx.Req.FormFile("attachment")
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetFile", err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
content = file
|
||||
size = header.Size
|
||||
filename = header.Filename
|
||||
if name := ctx.FormString("name"); name != "" {
|
||||
filename = name
|
||||
}
|
||||
} else {
|
||||
content = ctx.Req.Body
|
||||
filename = ctx.FormString("name")
|
||||
}
|
||||
|
||||
if filename == "" {
|
||||
ctx.Error(http.StatusBadRequest, "CreateReleaseAttachment", "Could not determine name of attachment.")
|
||||
return
|
||||
}
|
||||
|
||||
// Create a new attachment and save the file
|
||||
attach, err := attachment.UploadAttachment(ctx, file, setting.Repository.Release.AllowedTypes, header.Size, &repo_model.Attachment{
|
||||
attach, err := attachment.UploadAttachment(ctx, content, setting.Repository.Release.AllowedTypes, size, &repo_model.Attachment{
|
||||
Name: filename,
|
||||
UploaderID: ctx.Doer.ID,
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
|
||||
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
@ -132,6 +133,11 @@ func GetGPGKey(ctx *context.APIContext) {
|
||||
|
||||
// CreateUserGPGKey creates new GPG key to given user by ID.
|
||||
func CreateUserGPGKey(ctx *context.APIContext, form api.CreateGPGKeyOption, uid int64) {
|
||||
if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageGPGKeys) {
|
||||
ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited"))
|
||||
return
|
||||
}
|
||||
|
||||
token := asymkey_model.VerificationToken(ctx.Doer, 1)
|
||||
lastToken := asymkey_model.VerificationToken(ctx.Doer, 0)
|
||||
|
||||
@ -268,6 +274,11 @@ func DeleteGPGKey(ctx *context.APIContext) {
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageGPGKeys) {
|
||||
ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := asymkey_model.DeleteGPGKey(ctx, ctx.Doer, ctx.ParamsInt64(":id")); err != nil {
|
||||
if asymkey_model.IsErrGPGKeyAccessDenied(err) {
|
||||
ctx.Error(http.StatusForbidden, "", "You do not have access to this key")
|
||||
|
@ -38,6 +38,7 @@ func ProtocolMiddlewares() (handlers []any) {
|
||||
})
|
||||
})
|
||||
|
||||
// wrap the request and response, use the process context and add it to the process manager
|
||||
handlers = append(handlers, func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||
ctx, _, finished := process.GetManager().AddTypedContext(req.Context(), fmt.Sprintf("%s: %s", req.Method, req.RequestURI), process.RequestProcessType, true)
|
||||
|
@ -201,6 +201,8 @@ func NormalRoutes() *web.Route {
|
||||
// TODO: this prefix should be generated with a token string with runner ?
|
||||
prefix = "/api/actions_pipeline"
|
||||
r.Mount(prefix, actions_router.ArtifactsRoutes(prefix))
|
||||
prefix = actions_router.ArtifactV4RouteBase
|
||||
r.Mount(prefix, actions_router.ArtifactsV4Routes(prefix))
|
||||
}
|
||||
|
||||
return r
|
||||
|
@ -8,8 +8,8 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/models/webhook"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
@ -35,7 +35,7 @@ func DefaultOrSystemWebhooks(ctx *context.Context) {
|
||||
|
||||
sys["Title"] = ctx.Tr("admin.systemhooks")
|
||||
sys["Description"] = ctx.Tr("admin.systemhooks.desc")
|
||||
sys["Webhooks"], err = webhook.GetSystemWebhooks(ctx, util.OptionalBoolNone)
|
||||
sys["Webhooks"], err = webhook.GetSystemWebhooks(ctx, optional.None[bool]())
|
||||
sys["BaseLink"] = setting.AppSubURL + "/admin/hooks"
|
||||
sys["BaseLinkNew"] = setting.AppSubURL + "/admin/system-hooks"
|
||||
if err != nil {
|
||||
|
@ -11,8 +11,8 @@ import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
packages_cleanup_service "code.gitea.io/gitea/services/packages/cleanup"
|
||||
@ -36,7 +36,7 @@ func Packages(ctx *context.Context) {
|
||||
Type: packages_model.Type(packageType),
|
||||
Name: packages_model.SearchValue{Value: query},
|
||||
Sort: sort,
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
Paginator: &db.ListOptions{
|
||||
PageSize: setting.UI.PackagesPagingNum,
|
||||
Page: page,
|
||||
|
@ -96,7 +96,7 @@ func NewUser(ctx *context.Context) {
|
||||
ctx.Data["login_type"] = "0-0"
|
||||
|
||||
sources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{
|
||||
IsActive: util.OptionalBoolTrue,
|
||||
IsActive: optional.Some(true),
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("auth.Sources", err)
|
||||
@ -117,7 +117,7 @@ func NewUserPost(ctx *context.Context) {
|
||||
ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()
|
||||
|
||||
sources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{
|
||||
IsActive: util.OptionalBoolTrue,
|
||||
IsActive: optional.Some(true),
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("auth.Sources", err)
|
||||
@ -439,6 +439,7 @@ func EditUserPost(ctx *context.Context) {
|
||||
AllowCreateOrganization: optional.Some(form.AllowCreateOrganization),
|
||||
IsRestricted: optional.Some(form.Restricted),
|
||||
Visibility: optional.Some(form.Visibility),
|
||||
Language: optional.Some(form.Language),
|
||||
}
|
||||
|
||||
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
|
||||
|
@ -163,7 +163,7 @@ func SignIn(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, util.OptionalBoolTrue)
|
||||
oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
|
||||
if err != nil {
|
||||
ctx.ServerError("UserSignIn", err)
|
||||
return
|
||||
@ -186,7 +186,7 @@ func SignIn(ctx *context.Context) {
|
||||
func SignInPost(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("sign_in")
|
||||
|
||||
oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, util.OptionalBoolTrue)
|
||||
oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
|
||||
if err != nil {
|
||||
ctx.ServerError("UserSignIn", err)
|
||||
return
|
||||
@ -410,7 +410,7 @@ func SignUp(ctx *context.Context) {
|
||||
|
||||
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up"
|
||||
|
||||
oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, util.OptionalBoolTrue)
|
||||
oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
|
||||
if err != nil {
|
||||
ctx.ServerError("UserSignUp", err)
|
||||
return
|
||||
@ -439,7 +439,7 @@ func SignUpPost(ctx *context.Context) {
|
||||
|
||||
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up"
|
||||
|
||||
oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, util.OptionalBoolTrue)
|
||||
oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
|
||||
if err != nil {
|
||||
ctx.ServerError("UserSignUp", err)
|
||||
return
|
||||
|
@ -109,6 +109,21 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
|
||||
language := ctx.FormTrim("language")
|
||||
ctx.Data["Language"] = language
|
||||
|
||||
archived := ctx.FormOptionalBool("archived")
|
||||
ctx.Data["IsArchived"] = archived
|
||||
|
||||
fork := ctx.FormOptionalBool("fork")
|
||||
ctx.Data["IsFork"] = fork
|
||||
|
||||
mirror := ctx.FormOptionalBool("mirror")
|
||||
ctx.Data["IsMirror"] = mirror
|
||||
|
||||
template := ctx.FormOptionalBool("template")
|
||||
ctx.Data["IsTemplate"] = template
|
||||
|
||||
private := ctx.FormOptionalBool("private")
|
||||
ctx.Data["IsPrivate"] = private
|
||||
|
||||
repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
Page: page,
|
||||
@ -125,6 +140,11 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
|
||||
Language: language,
|
||||
IncludeDescription: setting.UI.SearchRepoDescription,
|
||||
OnlyShowRelevant: opts.OnlyShowRelevant,
|
||||
Archived: archived,
|
||||
Fork: fork,
|
||||
Mirror: mirror,
|
||||
Template: template,
|
||||
IsPrivate: private,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("SearchRepository", err)
|
||||
|
@ -85,6 +85,21 @@ func Home(ctx *context.Context) {
|
||||
page = 1
|
||||
}
|
||||
|
||||
archived := ctx.FormOptionalBool("archived")
|
||||
ctx.Data["IsArchived"] = archived
|
||||
|
||||
fork := ctx.FormOptionalBool("fork")
|
||||
ctx.Data["IsFork"] = fork
|
||||
|
||||
mirror := ctx.FormOptionalBool("mirror")
|
||||
ctx.Data["IsMirror"] = mirror
|
||||
|
||||
template := ctx.FormOptionalBool("template")
|
||||
ctx.Data["IsTemplate"] = template
|
||||
|
||||
private := ctx.FormOptionalBool("private")
|
||||
ctx.Data["IsPrivate"] = private
|
||||
|
||||
var (
|
||||
repos []*repo_model.Repository
|
||||
count int64
|
||||
@ -102,6 +117,11 @@ func Home(ctx *context.Context) {
|
||||
Actor: ctx.Doer,
|
||||
Language: language,
|
||||
IncludeDescription: setting.UI.SearchRepoDescription,
|
||||
Archived: archived,
|
||||
Fork: fork,
|
||||
Mirror: mirror,
|
||||
Template: template,
|
||||
IsPrivate: private,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("SearchRepository", err)
|
||||
|
@ -18,9 +18,9 @@ import (
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
@ -67,7 +67,7 @@ func Projects(ctx *context.Context) {
|
||||
PageSize: setting.UI.IssuePagingNum,
|
||||
},
|
||||
OwnerID: ctx.ContextUser.ID,
|
||||
IsClosed: util.OptionalBoolOf(isShowClosed),
|
||||
IsClosed: optional.Some(isShowClosed),
|
||||
OrderBy: project_model.GetSearchOrderByBySortType(sortType),
|
||||
Type: projectType,
|
||||
Title: keyword,
|
||||
@ -79,7 +79,7 @@ func Projects(ctx *context.Context) {
|
||||
|
||||
opTotal, err := db.Count[project_model.Project](ctx, project_model.SearchOptions{
|
||||
OwnerID: ctx.ContextUser.ID,
|
||||
IsClosed: util.OptionalBoolOf(!isShowClosed),
|
||||
IsClosed: optional.Some(!isShowClosed),
|
||||
Type: projectType,
|
||||
})
|
||||
if err != nil {
|
||||
@ -388,7 +388,7 @@ func ViewProject(ctx *context.Context) {
|
||||
if len(referencedIDs) > 0 {
|
||||
if linkedPrs, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{
|
||||
IssueIDs: referencedIDs,
|
||||
IsPull: util.OptionalBoolTrue,
|
||||
IsPull: optional.Some(true),
|
||||
}); err == nil {
|
||||
linkedPrsMap[issue.ID] = linkedPrs
|
||||
}
|
||||
|
@ -16,8 +16,8 @@ import (
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/web/repo"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
@ -78,7 +78,7 @@ func List(ctx *context.Context) {
|
||||
// Get all runner labels
|
||||
runners, err := db.Find[actions_model.ActionRunner](ctx, actions_model.FindRunnerOptions{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
IsOnline: util.OptionalBoolTrue,
|
||||
IsOnline: optional.Some(true),
|
||||
WithAvailable: true,
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -22,6 +22,7 @@ import (
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/actions"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
@ -602,6 +603,28 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
|
||||
|
||||
ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(artifactName), artifactName))
|
||||
|
||||
// Artifacts using the v4 backend are stored as a single combined zip file per artifact on the backend
|
||||
// The v4 backend enshures ContentEncoding is set to "application/zip", which is not the case for the old backend
|
||||
if len(artifacts) == 1 && artifacts[0].ArtifactName+".zip" == artifacts[0].ArtifactPath && artifacts[0].ContentEncoding == "application/zip" {
|
||||
art := artifacts[0]
|
||||
if setting.Actions.ArtifactStorage.MinioConfig.ServeDirect {
|
||||
u, err := storage.ActionsArtifacts.URL(art.StoragePath, art.ArtifactPath)
|
||||
if u != nil && err == nil {
|
||||
ctx.Redirect(u.String())
|
||||
return
|
||||
}
|
||||
}
|
||||
f, err := storage.ActionsArtifacts.Open(art.StoragePath)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
_, _ = io.Copy(ctx.Resp, f)
|
||||
return
|
||||
}
|
||||
|
||||
// Artifacts using the v1-v3 backend are stored as multiple individual files per artifact on the backend
|
||||
// Those need to be zipped for download
|
||||
writer := zip.NewWriter(ctx.Resp)
|
||||
defer writer.Close()
|
||||
for _, art := range artifacts {
|
||||
|
@ -18,6 +18,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
@ -53,7 +54,7 @@ func Branches(ctx *context.Context) {
|
||||
|
||||
kw := ctx.FormString("q")
|
||||
|
||||
defaultBranch, branches, branchesCount, err := repo_service.LoadBranches(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, util.OptionalBoolNone, kw, page, pageSize)
|
||||
defaultBranch, branches, branchesCount, err := repo_service.LoadBranches(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, optional.None[bool](), kw, page, pageSize)
|
||||
if err != nil {
|
||||
ctx.ServerError("LoadBranches", err)
|
||||
return
|
||||
@ -236,7 +237,7 @@ func CreateBranch(ctx *context.Context) {
|
||||
if len(e.Message) == 0 {
|
||||
ctx.Flash.Error(ctx.Tr("repo.editor.push_rejected_no_message"))
|
||||
} else {
|
||||
flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
|
||||
flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
|
||||
"Message": ctx.Tr("repo.editor.push_rejected"),
|
||||
"Summary": ctx.Tr("repo.editor.push_rejected_summary"),
|
||||
"Details": utils.SanitizeFlashErrorString(e.Message),
|
||||
|
@ -341,7 +341,7 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
|
||||
if len(errPushRej.Message) == 0 {
|
||||
ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplEditFile, &form)
|
||||
} else {
|
||||
flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
|
||||
flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
|
||||
"Message": ctx.Tr("repo.editor.push_rejected"),
|
||||
"Summary": ctx.Tr("repo.editor.push_rejected_summary"),
|
||||
"Details": utils.SanitizeFlashErrorString(errPushRej.Message),
|
||||
@ -353,7 +353,7 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
|
||||
ctx.RenderWithErr(flashError, tplEditFile, &form)
|
||||
}
|
||||
} else {
|
||||
flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
|
||||
flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
|
||||
"Message": ctx.Tr("repo.editor.fail_to_update_file", form.TreePath),
|
||||
"Summary": ctx.Tr("repo.editor.fail_to_update_file_summary"),
|
||||
"Details": utils.SanitizeFlashErrorString(err.Error()),
|
||||
@ -542,7 +542,7 @@ func DeleteFilePost(ctx *context.Context) {
|
||||
if len(errPushRej.Message) == 0 {
|
||||
ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplDeleteFile, &form)
|
||||
} else {
|
||||
flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
|
||||
flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
|
||||
"Message": ctx.Tr("repo.editor.push_rejected"),
|
||||
"Summary": ctx.Tr("repo.editor.push_rejected_summary"),
|
||||
"Details": utils.SanitizeFlashErrorString(errPushRej.Message),
|
||||
@ -742,7 +742,7 @@ func UploadFilePost(ctx *context.Context) {
|
||||
if len(errPushRej.Message) == 0 {
|
||||
ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplUploadFile, &form)
|
||||
} else {
|
||||
flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
|
||||
flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
|
||||
"Message": ctx.Tr("repo.editor.push_rejected"),
|
||||
"Summary": ctx.Tr("repo.editor.push_rejected_summary"),
|
||||
"Details": utils.SanitizeFlashErrorString(errPushRej.Message),
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
stdCtx "context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@ -139,7 +140,7 @@ func MustAllowPulls(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption util.OptionalBool) {
|
||||
func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption optional.Option[bool]) {
|
||||
var err error
|
||||
viewType := ctx.FormString("type")
|
||||
sortType := ctx.FormString("sort")
|
||||
@ -240,18 +241,18 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
|
||||
}
|
||||
}
|
||||
|
||||
var isShowClosed util.OptionalBool
|
||||
var isShowClosed optional.Option[bool]
|
||||
switch ctx.FormString("state") {
|
||||
case "closed":
|
||||
isShowClosed = util.OptionalBoolTrue
|
||||
isShowClosed = optional.Some(true)
|
||||
case "all":
|
||||
isShowClosed = util.OptionalBoolNone
|
||||
isShowClosed = optional.None[bool]()
|
||||
default:
|
||||
isShowClosed = util.OptionalBoolFalse
|
||||
isShowClosed = optional.Some(false)
|
||||
}
|
||||
// if there are closed issues and no open issues, default to showing all issues
|
||||
if len(ctx.FormString("state")) == 0 && issueStats.OpenCount == 0 && issueStats.ClosedCount != 0 {
|
||||
isShowClosed = util.OptionalBoolNone
|
||||
isShowClosed = optional.None[bool]()
|
||||
}
|
||||
|
||||
if repo.IsTimetrackerEnabled(ctx) {
|
||||
@ -271,10 +272,10 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
|
||||
}
|
||||
|
||||
var total int
|
||||
switch isShowClosed {
|
||||
case util.OptionalBoolTrue:
|
||||
switch {
|
||||
case isShowClosed.Value():
|
||||
total = int(issueStats.ClosedCount)
|
||||
case util.OptionalBoolNone:
|
||||
case !isShowClosed.Has():
|
||||
total = int(issueStats.OpenCount + issueStats.ClosedCount)
|
||||
default:
|
||||
total = int(issueStats.OpenCount)
|
||||
@ -430,7 +431,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
|
||||
return
|
||||
}
|
||||
|
||||
pinned, err := issues_model.GetPinnedIssues(ctx, repo.ID, isPullOption.IsTrue())
|
||||
pinned, err := issues_model.GetPinnedIssues(ctx, repo.ID, isPullOption.Value())
|
||||
if err != nil {
|
||||
ctx.ServerError("GetPinnedIssues", err)
|
||||
return
|
||||
@ -460,10 +461,10 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
|
||||
ctx.Data["AssigneeID"] = assigneeID
|
||||
ctx.Data["PosterID"] = posterID
|
||||
ctx.Data["Keyword"] = keyword
|
||||
switch isShowClosed {
|
||||
case util.OptionalBoolTrue:
|
||||
switch {
|
||||
case isShowClosed.Value():
|
||||
ctx.Data["State"] = "closed"
|
||||
case util.OptionalBoolNone:
|
||||
case !isShowClosed.Has():
|
||||
ctx.Data["State"] = "all"
|
||||
default:
|
||||
ctx.Data["State"] = "open"
|
||||
@ -512,7 +513,7 @@ func Issues(ctx *context.Context) {
|
||||
ctx.Data["NewIssueChooseTemplate"] = issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo)
|
||||
}
|
||||
|
||||
issues(ctx, ctx.FormInt64("milestone"), ctx.FormInt64("project"), util.OptionalBoolOf(isPullList))
|
||||
issues(ctx, ctx.FormInt64("milestone"), ctx.FormInt64("project"), optional.Some(isPullList))
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
@ -554,7 +555,7 @@ func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *repo_model.R
|
||||
var err error
|
||||
ctx.Data["OpenMilestones"], err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
|
||||
RepoID: repo.ID,
|
||||
IsClosed: util.OptionalBoolFalse,
|
||||
IsClosed: optional.Some(false),
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetMilestones", err)
|
||||
@ -562,7 +563,7 @@ func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *repo_model.R
|
||||
}
|
||||
ctx.Data["ClosedMilestones"], err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
|
||||
RepoID: repo.ID,
|
||||
IsClosed: util.OptionalBoolTrue,
|
||||
IsClosed: optional.Some(true),
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetMilestones", err)
|
||||
@ -590,7 +591,7 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
|
||||
projects, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
|
||||
ListOptions: db.ListOptionsAll,
|
||||
RepoID: repo.ID,
|
||||
IsClosed: util.OptionalBoolFalse,
|
||||
IsClosed: optional.Some(false),
|
||||
Type: project_model.TypeRepository,
|
||||
})
|
||||
if err != nil {
|
||||
@ -600,7 +601,7 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
|
||||
projects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
|
||||
ListOptions: db.ListOptionsAll,
|
||||
OwnerID: repo.OwnerID,
|
||||
IsClosed: util.OptionalBoolFalse,
|
||||
IsClosed: optional.Some(false),
|
||||
Type: repoOwnerType,
|
||||
})
|
||||
if err != nil {
|
||||
@ -613,7 +614,7 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
|
||||
projects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
|
||||
ListOptions: db.ListOptionsAll,
|
||||
RepoID: repo.ID,
|
||||
IsClosed: util.OptionalBoolTrue,
|
||||
IsClosed: optional.Some(true),
|
||||
Type: project_model.TypeRepository,
|
||||
})
|
||||
if err != nil {
|
||||
@ -623,7 +624,7 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
|
||||
projects2, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
|
||||
ListOptions: db.ListOptionsAll,
|
||||
OwnerID: repo.OwnerID,
|
||||
IsClosed: util.OptionalBoolTrue,
|
||||
IsClosed: optional.Some(true),
|
||||
Type: repoOwnerType,
|
||||
})
|
||||
if err != nil {
|
||||
@ -1016,7 +1017,7 @@ func NewIssue(ctx *context.Context) {
|
||||
ctx.HTML(http.StatusOK, tplIssueNew)
|
||||
}
|
||||
|
||||
func renderErrorOfTemplates(ctx *context.Context, errs map[string]error) string {
|
||||
func renderErrorOfTemplates(ctx *context.Context, errs map[string]error) template.HTML {
|
||||
var files []string
|
||||
for k := range errs {
|
||||
files = append(files, k)
|
||||
@ -1028,14 +1029,14 @@ func renderErrorOfTemplates(ctx *context.Context, errs map[string]error) string
|
||||
lines = append(lines, fmt.Sprintf("%s: %v", file, errs[file]))
|
||||
}
|
||||
|
||||
flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
|
||||
flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
|
||||
"Message": ctx.Tr("repo.issues.choose.ignore_invalid_templates"),
|
||||
"Summary": ctx.Tr("repo.issues.choose.invalid_templates", len(errs)),
|
||||
"Details": utils.SanitizeFlashErrorString(strings.Join(lines, "\n")),
|
||||
})
|
||||
if err != nil {
|
||||
log.Debug("render flash error: %v", err)
|
||||
flashError = ctx.Locale.TrString("repo.issues.choose.ignore_invalid_templates")
|
||||
flashError = ctx.Locale.Tr("repo.issues.choose.ignore_invalid_templates")
|
||||
}
|
||||
return flashError
|
||||
}
|
||||
@ -2501,14 +2502,14 @@ func SearchIssues(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var isClosed util.OptionalBool
|
||||
var isClosed optional.Option[bool]
|
||||
switch ctx.FormString("state") {
|
||||
case "closed":
|
||||
isClosed = util.OptionalBoolTrue
|
||||
isClosed = optional.Some(true)
|
||||
case "all":
|
||||
isClosed = util.OptionalBoolNone
|
||||
isClosed = optional.None[bool]()
|
||||
default:
|
||||
isClosed = util.OptionalBoolFalse
|
||||
isClosed = optional.Some(false)
|
||||
}
|
||||
|
||||
var (
|
||||
@ -2583,14 +2584,12 @@ func SearchIssues(ctx *context.Context) {
|
||||
keyword = ""
|
||||
}
|
||||
|
||||
var isPull util.OptionalBool
|
||||
isPull := optional.None[bool]()
|
||||
switch ctx.FormString("type") {
|
||||
case "pulls":
|
||||
isPull = util.OptionalBoolTrue
|
||||
isPull = optional.Some(true)
|
||||
case "issues":
|
||||
isPull = util.OptionalBoolFalse
|
||||
default:
|
||||
isPull = util.OptionalBoolNone
|
||||
isPull = optional.Some(false)
|
||||
}
|
||||
|
||||
var includedAnyLabels []int64
|
||||
@ -2725,14 +2724,14 @@ func ListIssues(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var isClosed util.OptionalBool
|
||||
var isClosed optional.Option[bool]
|
||||
switch ctx.FormString("state") {
|
||||
case "closed":
|
||||
isClosed = util.OptionalBoolTrue
|
||||
isClosed = optional.Some(true)
|
||||
case "all":
|
||||
isClosed = util.OptionalBoolNone
|
||||
isClosed = optional.None[bool]()
|
||||
default:
|
||||
isClosed = util.OptionalBoolFalse
|
||||
isClosed = optional.Some(false)
|
||||
}
|
||||
|
||||
keyword := ctx.FormTrim("q")
|
||||
@ -2784,14 +2783,12 @@ func ListIssues(ctx *context.Context) {
|
||||
projectID = &v
|
||||
}
|
||||
|
||||
var isPull util.OptionalBool
|
||||
isPull := optional.None[bool]()
|
||||
switch ctx.FormString("type") {
|
||||
case "pulls":
|
||||
isPull = util.OptionalBoolTrue
|
||||
isPull = optional.Some(true)
|
||||
case "issues":
|
||||
isPull = util.OptionalBoolFalse
|
||||
default:
|
||||
isPull = util.OptionalBoolNone
|
||||
isPull = optional.Some(false)
|
||||
}
|
||||
|
||||
// FIXME: we should be more efficient here
|
||||
@ -3296,7 +3293,7 @@ func ChangeIssueReaction(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
html, err := ctx.RenderToString(tplReactions, map[string]any{
|
||||
html, err := ctx.RenderToHTML(tplReactions, map[string]any{
|
||||
"ctxData": ctx.Data,
|
||||
"ActionURL": fmt.Sprintf("%s/issues/%d/reactions", ctx.Repo.RepoLink, issue.Index),
|
||||
"Reactions": issue.Reactions.GroupByType(),
|
||||
@ -3403,7 +3400,7 @@ func ChangeCommentReaction(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
html, err := ctx.RenderToString(tplReactions, map[string]any{
|
||||
html, err := ctx.RenderToHTML(tplReactions, map[string]any{
|
||||
"ctxData": ctx.Data,
|
||||
"ActionURL": fmt.Sprintf("%s/comments/%d/reactions", ctx.Repo.RepoLink, comment.ID),
|
||||
"Reactions": comment.Reactions.GroupByType(),
|
||||
@ -3546,8 +3543,8 @@ func updateAttachments(ctx *context.Context, item any, files []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func attachmentsHTML(ctx *context.Context, attachments []*repo_model.Attachment, content string) string {
|
||||
attachHTML, err := ctx.RenderToString(tplAttachment, map[string]any{
|
||||
func attachmentsHTML(ctx *context.Context, attachments []*repo_model.Attachment, content string) template.HTML {
|
||||
attachHTML, err := ctx.RenderToHTML(tplAttachment, map[string]any{
|
||||
"ctxData": ctx.Data,
|
||||
"Attachments": attachments,
|
||||
"Content": content,
|
||||
|
@ -94,7 +94,7 @@ func canSoftDeleteContentHistory(ctx *context.Context, issue *issues_model.Issue
|
||||
// CanWrite means the doer can manage the issue/PR list
|
||||
if ctx.Repo.IsOwner() || ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
|
||||
canSoftDelete = true
|
||||
} else {
|
||||
} else if ctx.Doer != nil {
|
||||
// for read-only users, they could still post issues or comments,
|
||||
// they should be able to delete the history related to their own issue/comment, a case is:
|
||||
// 1. the user posts some sensitive data
|
||||
@ -186,6 +186,10 @@ func SoftDeleteContentHistory(ctx *context.Context) {
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
if ctx.Doer == nil {
|
||||
ctx.NotFound("Require SignIn", nil)
|
||||
return
|
||||
}
|
||||
|
||||
commentID := ctx.FormInt64("comment_id")
|
||||
historyID := ctx.FormInt64("history_id")
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user