diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 1051b0f2a2..a4a6ce8fcf 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -8,7 +8,9 @@
},
"ghcr.io/devcontainers/features/git-lfs:1.1.0": {},
"ghcr.io/devcontainers-contrib/features/poetry:2": {},
- "ghcr.io/devcontainers/features/python:1": {}
+ "ghcr.io/devcontainers/features/python:1": {
+ "version": "3.12"
+ }
},
"customizations": {
"vscode": {
diff --git a/.github/workflows/files-changed.yml b/.github/workflows/files-changed.yml
index c909f78597..f9b6b1ec49 100644
--- a/.github/workflows/files-changed.yml
+++ b/.github/workflows/files-changed.yml
@@ -48,6 +48,7 @@ jobs:
- "Makefile"
- ".golangci.yml"
- ".editorconfig"
+ - "options/locale/locale_en-US.ini"
frontend:
- "**/*.js"
diff --git a/.github/workflows/pull-compliance.yml b/.github/workflows/pull-compliance.yml
index 391137f015..02a265b1ff 100644
--- a/.github/workflows/pull-compliance.yml
+++ b/.github/workflows/pull-compliance.yml
@@ -32,9 +32,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - uses: actions/setup-python@v4
+ - uses: actions/setup-python@v5
with:
- python-version: "3.11"
+ python-version: "3.12"
- run: pip install poetry
- run: make deps-py
- run: make lint-templates
@@ -45,9 +45,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - uses: actions/setup-python@v4
+ - uses: actions/setup-python@v5
with:
- python-version: "3.11"
+ python-version: "3.12"
- run: pip install poetry
- run: make deps-py
- run: make lint-yaml
diff --git a/.github/workflows/pull-db-tests.yml b/.github/workflows/pull-db-tests.yml
index a3886bf618..61c0391509 100644
--- a/.github/workflows/pull-db-tests.yml
+++ b/.github/workflows/pull-db-tests.yml
@@ -49,7 +49,10 @@ jobs:
- run: make backend
env:
TAGS: bindata
- - run: make test-pgsql-migration test-pgsql
+ - name: run migration tests
+ run: make test-pgsql-migration
+ - name: run tests
+ run: make test-pgsql
timeout-minutes: 50
env:
TAGS: bindata gogit
@@ -72,7 +75,10 @@ jobs:
- run: make backend
env:
TAGS: bindata gogit sqlite sqlite_unlock_notify
- - run: make test-sqlite-migration test-sqlite
+ - name: run migration tests
+ run: make test-sqlite-migration
+ - name: run tests
+ run: make test-sqlite
timeout-minutes: 50
env:
TAGS: bindata gogit sqlite sqlite_unlock_notify
@@ -175,8 +181,10 @@ jobs:
- run: make backend
env:
TAGS: bindata
+ - name: run migration tests
+ run: make test-mysql-migration
- name: run tests
- run: make test-mysql-migration integration-test-coverage
+ run: make integration-test-coverage
env:
TAGS: bindata
RACE_ENABLED: true
@@ -208,7 +216,9 @@ jobs:
- run: make backend
env:
TAGS: bindata
- - run: make test-mssql-migration test-mssql
+ - run: make test-mssql-migration
+ - name: run tests
+ run: make test-mssql
timeout-minutes: 50
env:
TAGS: bindata
diff --git a/.stylelintrc.yaml b/.stylelintrc.yaml
index 7dd0a566f2..c7725159f1 100644
--- a/.stylelintrc.yaml
+++ b/.stylelintrc.yaml
@@ -64,6 +64,7 @@ rules:
"@stylistic/media-query-list-comma-newline-before": null
"@stylistic/media-query-list-comma-space-after": null
"@stylistic/media-query-list-comma-space-before": null
+ "@stylistic/named-grid-areas-alignment": null
"@stylistic/no-empty-first-line": null
"@stylistic/no-eol-whitespace": true
"@stylistic/no-extra-semicolons": true
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index dc90c6905b..5d20bc2589 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -464,7 +464,7 @@ We assume in good faith that the information you provide is legally binding.
We adopted a release schedule to streamline the process of working on, finishing, and issuing releases. \
The overall goal is to make a major release every three or four months, which breaks down into two or three months of general development followed by one month of testing and polishing known as the release freeze. \
All the feature pull requests should be
-merged before feature freeze. And, during the frozen period, a corresponding
+merged before feature freeze. All feature pull requests haven't been merged before this feature freeze will be moved to next milestone, please notice our feature freeze announcement on discord. And, during the frozen period, a corresponding
release branch is open for fixes backported from main branch. Release candidates
are made during this period for user testing to
obtain a final version that is maintained in this branch.
diff --git a/Makefile b/Makefile
index 0e9e792053..5ab8655c2f 100644
--- a/Makefile
+++ b/Makefile
@@ -115,6 +115,7 @@ LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64
GO_PACKAGES ?= $(filter-out code.gitea.io/gitea/tests/integration/migration-test code.gitea.io/gitea/tests code.gitea.io/gitea/tests/integration code.gitea.io/gitea/tests/e2e,$(shell $(GO) list ./... | grep -v /vendor/))
GO_TEST_PACKAGES ?= $(filter-out $(shell $(GO) list code.gitea.io/gitea/models/migrations/...) code.gitea.io/gitea/tests/integration/migration-test code.gitea.io/gitea/tests code.gitea.io/gitea/tests/integration code.gitea.io/gitea/tests/e2e,$(shell $(GO) list ./... | grep -v /vendor/))
+MIGRATE_TEST_PACKAGES ?= $(shell $(GO) list code.gitea.io/gitea/models/migrations/...)
FOMANTIC_WORK_DIR := web_src/fomantic
@@ -147,6 +148,7 @@ GO_DIRS := build cmd models modules routers services tests
WEB_DIRS := web_src/js web_src/css
SPELLCHECK_FILES := $(GO_DIRS) $(WEB_DIRS) docs/content templates options/locale/locale_en-US.ini .github
+EDITORCONFIG_FILES := templates .github/workflows options/locale/locale_en-US.ini
GO_SOURCES := $(wildcard *.go)
GO_SOURCES += $(shell find $(GO_DIRS) -type f -name "*.go" ! -path modules/options/bindata.go ! -path modules/public/bindata.go ! -path modules/templates/bindata.go)
@@ -426,7 +428,7 @@ lint-go-vet:
.PHONY: lint-editorconfig
lint-editorconfig:
- $(GO) run $(EDITORCONFIG_CHECKER_PACKAGE) templates .github/workflows
+ @$(GO) run $(EDITORCONFIG_CHECKER_PACKAGE) $(EDITORCONFIG_FILES)
.PHONY: lint-actions
lint-actions:
@@ -709,9 +711,7 @@ migrations.sqlite.test: $(GO_SOURCES) generate-ini-sqlite
.PHONY: migrations.individual.mysql.test
migrations.individual.mysql.test: $(GO_SOURCES)
- for pkg in $(shell $(GO) list code.gitea.io/gitea/models/migrations/...); do \
- GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' $$pkg; \
- done
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES)
.PHONY: migrations.individual.sqlite.test\#%
migrations.individual.sqlite.test\#%: $(GO_SOURCES) generate-ini-sqlite
@@ -719,20 +719,15 @@ migrations.individual.sqlite.test\#%: $(GO_SOURCES) generate-ini-sqlite
.PHONY: migrations.individual.pgsql.test
migrations.individual.pgsql.test: $(GO_SOURCES)
- for pkg in $(shell $(GO) list code.gitea.io/gitea/models/migrations/...); do \
- GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' $$pkg; \
- done
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES)
.PHONY: migrations.individual.pgsql.test\#%
migrations.individual.pgsql.test\#%: $(GO_SOURCES) generate-ini-pgsql
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$*
-
.PHONY: migrations.individual.mssql.test
migrations.individual.mssql.test: $(GO_SOURCES) generate-ini-mssql
- for pkg in $(shell $(GO) list code.gitea.io/gitea/models/migrations/...); do \
- GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' $$pkg -test.failfast; \
- done
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES)
.PHONY: migrations.individual.mssql.test\#%
migrations.individual.mssql.test\#%: $(GO_SOURCES) generate-ini-mssql
@@ -740,9 +735,7 @@ migrations.individual.mssql.test\#%: $(GO_SOURCES) generate-ini-mssql
.PHONY: migrations.individual.sqlite.test
migrations.individual.sqlite.test: $(GO_SOURCES) generate-ini-sqlite
- for pkg in $(shell $(GO) list code.gitea.io/gitea/models/migrations/...); do \
- GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' $$pkg; \
- done
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES)
.PHONY: migrations.individual.sqlite.test\#%
migrations.individual.sqlite.test\#%: $(GO_SOURCES) generate-ini-sqlite
@@ -908,6 +901,7 @@ fomantic:
cd $(FOMANTIC_WORK_DIR) && npm install --no-save
cp -f $(FOMANTIC_WORK_DIR)/theme.config.less $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/src/theme.config
cp -rf $(FOMANTIC_WORK_DIR)/_site $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/src/
+ $(SED_INPLACE) -e 's/ overrideBrowserslist\r/ overrideBrowserslist: ["defaults"]\r/g' $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/tasks/config/tasks.js
cd $(FOMANTIC_WORK_DIR) && npx gulp -f node_modules/fomantic-ui/gulpfile.js build
# fomantic uses "touchstart" as click event for some browsers, it's not ideal, so we force fomantic to always use "click" as click event
$(SED_INPLACE) -e 's/clickEvent[ \t]*=/clickEvent = "click", unstableClickEvent =/g' $(FOMANTIC_WORK_DIR)/build/semantic.js
diff --git a/cmd/admin_regenerate.go b/cmd/admin_regenerate.go
index 0db505ff9c..ab769f6d0c 100644
--- a/cmd/admin_regenerate.go
+++ b/cmd/admin_regenerate.go
@@ -4,8 +4,8 @@
package cmd
import (
- asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/modules/graceful"
+ asymkey_service "code.gitea.io/gitea/services/asymkey"
repo_service "code.gitea.io/gitea/services/repository"
"github.com/urfave/cli/v2"
@@ -42,5 +42,5 @@ func runRegenerateKeys(_ *cli.Context) error {
if err := initDB(ctx); err != nil {
return err
}
- return asymkey_model.RewriteAllPublicKeys(ctx)
+ return asymkey_service.RewriteAllPublicKeys(ctx)
}
diff --git a/contrib/systemd/gitea.service b/contrib/systemd/gitea.service
index d205c6ee8b..c091722a74 100644
--- a/contrib/systemd/gitea.service
+++ b/contrib/systemd/gitea.service
@@ -1,6 +1,5 @@
[Unit]
Description=Gitea (Git with a cup of tea)
-After=syslog.target
After=network.target
###
# Don't forget to add the database service dependencies
diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index dc5aa691ee..17d6cd3a35 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -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","manage_gpg_keys" more features can be disabled in future
+;; Disabled features for users, could be "deletion", "manage_ssh_keys","manage_gpg_keys" more features can be disabled in future
;; - deletion: a user cannot delete their own account
+;; - manage_ssh_keys: a user cannot configure ssh keys
;; - manage_gpg_keys: a user cannot configure gpg keys
;USER_DISABLED_FEATURES =
diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md
index ea6e1eb1a4..8a01711949 100644
--- a/docs/content/administration/config-cheat-sheet.en-us.md
+++ b/docs/content/administration/config-cheat-sheet.en-us.md
@@ -518,9 +518,10 @@ And the following unique queues:
- `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**: Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled
- `DISABLE_REGULAR_ORG_CREATION`: **false**: Disallow regular (non-admin) users from creating organizations.
-- `USER_DISABLED_FEATURES`: **_empty_** Disabled features for users, could be `deletion`, `manage_gpg_keys` and more features can be added in future.
+- `USER_DISABLED_FEATURES`: **_empty_** Disabled features for users, could be `deletion`, `manage_ssh_keys`, `manage_gpg_keys` and more features can be added in future.
- `deletion`: User cannot delete their own account.
- - `manage_gpg_keys`: User cannot configure gpg keys
+ - `manage_ssh_keys`: User cannot configure ssh keys.
+ - `manage_gpg_keys`: User cannot configure gpg keys.
## Security (`security`)
diff --git a/docs/content/administration/config-cheat-sheet.zh-cn.md b/docs/content/administration/config-cheat-sheet.zh-cn.md
index 5cc5734359..7b102eda8e 100644
--- a/docs/content/administration/config-cheat-sheet.zh-cn.md
+++ b/docs/content/administration/config-cheat-sheet.zh-cn.md
@@ -497,9 +497,10 @@ Gitea 创建以下非唯一队列:
- `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**:用户电子邮件通知的默认配置(用户可配置)。选项:enabled、onmention、disabled
- `DISABLE_REGULAR_ORG_CREATION`: **false**:禁止普通(非管理员)用户创建组织。
-- `USER_DISABLED_FEATURES`:**_empty_** 禁用的用户特性,当前允许为空或者 `deletion`,`manage_gpg_keys` 未来可以增加更多设置。
+- `USER_DISABLED_FEATURES`:**_empty_** 禁用的用户特性,当前允许为空或者 `deletion`,`manage_ssh_keys`, `manage_gpg_keys` 未来可以增加更多设置。
- `deletion`: 用户不能通过界面或者API删除他自己。
- - `manage_gpg_keys`: 用户不能配置 GPG 密钥
+ - `manage_ssh_keys`: 用户不能通过界面或者API配置SSH Keys。
+ - `manage_gpg_keys`: 用户不能配置 GPG 密钥。
## 安全性 (`security`)
diff --git a/docs/content/administration/mail-templates.en-us.md b/docs/content/administration/mail-templates.en-us.md
index 9077f97aea..4026b89975 100644
--- a/docs/content/administration/mail-templates.en-us.md
+++ b/docs/content/administration/mail-templates.en-us.md
@@ -224,7 +224,7 @@ Please check [Gitea's logs](administration/logging-config.md) for error messages
{{if not (eq .Body "")}}
Message content
- {{.Body | SanitizeHTML}}
+ {{.Body}}
{{end}}
@@ -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:
diff --git a/docs/content/administration/mail-templates.zh-cn.md b/docs/content/administration/mail-templates.zh-cn.md
index d58f9dc176..3c7c2a9397 100644
--- a/docs/content/administration/mail-templates.zh-cn.md
+++ b/docs/content/administration/mail-templates.zh-cn.md
@@ -207,7 +207,7 @@ _主题_ 和 _邮件正文_ 由 [Golang的模板引擎](https://go.dev/pkg/text/
{{if not (eq .Body "")}}
消息内容:
- {{.Body | SanitizeHTML}}
+ {{.Body}}
{{end}}
@@ -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 内容 |
这些都是 _函数_,而不是元数据,因此必须按以下方式使用:
diff --git a/docs/content/contributing/guidelines-frontend.en-us.md b/docs/content/contributing/guidelines-frontend.en-us.md
index a33a38a6f9..2c0aaaed4a 100644
--- a/docs/content/contributing/guidelines-frontend.en-us.md
+++ b/docs/content/contributing/guidelines-frontend.en-us.md
@@ -47,7 +47,7 @@ We recommend [Google HTML/CSS Style Guide](https://google.github.io/styleguide/h
9. Avoid unnecessary `!important` in CSS, add comments to explain why it's necessary if it can't be avoided.
10. Avoid mixing different events in one event listener, prefer to use individual event listeners for every event.
11. Custom event names are recommended to use `ce-` prefix.
-12. Gitea's tailwind-style CSS classes use `gt-` prefix (`gt-relative`), while Gitea's own private framework-level CSS classes use `g-` prefix (`g-modal-confirm`).
+12. Prefer using Tailwind CSS which is available via `tw-` prefix, e.g. `tw-relative`. Gitea's helper CSS classes use `gt-` prefix (`gt-df`), while Gitea's own private framework-level CSS classes use `g-` prefix (`g-modal-confirm`).
13. Avoid inline scripts & styles as much as possible, it's recommended to put JS code into JS files and use CSS classes. If inline scripts & styles are unavoidable, explain the reason why it can't be avoided.
### Accessibility / ARIA
diff --git a/docs/content/contributing/guidelines-frontend.zh-cn.md b/docs/content/contributing/guidelines-frontend.zh-cn.md
index 43f72b4808..ace0d97f49 100644
--- a/docs/content/contributing/guidelines-frontend.zh-cn.md
+++ b/docs/content/contributing/guidelines-frontend.zh-cn.md
@@ -47,7 +47,7 @@ HTML 页面由[Go HTML Template](https://pkg.go.dev/html/template)渲染。
9. 避免在 CSS 中使用不必要的`!important`,如果无法避免,添加注释解释为什么需要它。
10. 避免在一个事件监听器中混合不同的事件,优先为每个事件使用独立的事件监听器。
11. 推荐使用自定义事件名称前缀`ce-`。
-12. Gitea 的 tailwind-style CSS 类使用`gt-`前缀(`gt-relative`),而 Gitea 自身的私有框架级 CSS 类使用`g-`前缀(`g-modal-confirm`)。
+12. 建议使用 Tailwind CSS,它可以通过 `tw-` 前缀获得,例如 `tw-relative`. Gitea 自身的助手类 CSS 使用 `gt-` 前缀(`gt-df`),Gitea 自身的私有框架级 CSS 类使用 `g-` 前缀(`g-modal-confirm`)。
13. 尽量避免内联脚本和样式,建议将JS代码放入JS文件中并使用CSS类。如果内联脚本和样式不可避免,请解释无法避免的原因。
### 可访问性 / ARIA
diff --git a/docs/content/usage/blocking-users.en-us.md b/docs/content/usage/blocking-users.en-us.md
new file mode 100644
index 0000000000..b59bbe4d62
--- /dev/null
+++ b/docs/content/usage/blocking-users.en-us.md
@@ -0,0 +1,56 @@
+---
+date: "2024-01-31T00:00:00+00:00"
+title: "Blocking a user"
+slug: "blocking-user"
+sidebar_position: 25
+toc: false
+draft: false
+aliases:
+ - /en-us/webhooks
+menu:
+ sidebar:
+ parent: "usage"
+ name: "Blocking a user"
+ sidebar_position: 30
+ identifier: "blocking-user"
+---
+
+# Blocking a user
+
+Gitea supports blocking of users to restrict how they can interact with you and your content.
+
+You can block a user in your account settings, from the user's profile or from comments created by the user.
+The user is not directly notified about the block, but they can notice they are blocked when they attempt to interact with you.
+Organization owners can block anyone who is not a member of the organization too.
+If a blocked user has admin permissions, they can still perform all actions even if blocked.
+
+### When you block a user
+
+- the user stops following you
+- you stop following the user
+- the user's stars are removed from your repositories
+- your stars are removed from their repositories
+- the user stops watching your repositories
+- you stop watching their repositories
+- the user's issue assignments are removed from your repositories
+- your issue assignments are removed from their repositories
+- the user is removed as a collaborator on your repositories
+- you are removed as a collaborator on their repositories
+- any pending repository transfers to or from the blocked user are canceled
+
+### When you block a user, the user cannot
+
+- follow you
+- watch your repositories
+- star your repositories
+- fork your repositories
+- transfer repositories to you
+- open issues or pull requests on your repositories
+- comment on issues or pull requests you've created
+- comment on issues or pull requests on your repositories
+- react to your comments on issues or pull requests
+- react to comments on issues or pull requests on your repositories
+- assign you to issues or pull requests
+- add you as a collaborator on their repositories
+- send you notifications by @mentioning your username
+- be added as team member (if blocked by an organization)
diff --git a/docs/content/usage/issue-pull-request-templates.en-us.md b/docs/content/usage/issue-pull-request-templates.en-us.md
index b031b262fb..e203c0d379 100644
--- a/docs/content/usage/issue-pull-request-templates.en-us.md
+++ b/docs/content/usage/issue-pull-request-templates.en-us.md
@@ -136,6 +136,12 @@ body:
attributes:
value: |
Thanks for taking the time to fill out this bug report!
+ # some markdown that will only be visible once the issue has been created
+ - type: markdown
+ attributes:
+ value: |
+ This issue was created by an issue **template** :)
+ visible: [content]
- type: input
id: contact
attributes:
@@ -187,11 +193,16 @@ body:
options:
- label: I agree to follow this project's Code of Conduct
required: true
+ - label: I have also read the CONTRIBUTION.MD
+ required: true
+ visible: [form]
+ - label: This is a TODO only visible after issue creation
+ visible: [content]
```
### Markdown
-You can use a `markdown` element to display Markdown in your form that provides extra context to the user, but is not submitted.
+You can use a `markdown` element to display Markdown in your form that provides extra context to the user, but is not submitted by default.
Attributes:
@@ -199,6 +210,8 @@ Attributes:
|-------|--------------------------------------------------------------|----------|--------|---------|--------------|
| value | The text that is rendered. Markdown formatting is supported. | Required | String | - | - |
+visible: Default is **[form]**
+
### Textarea
You can use a `textarea` element to add a multi-line text field to your form. Contributors can also attach files in `textarea` fields.
@@ -219,6 +232,8 @@ Validations:
|----------|------------------------------------------------------|----------|---------|---------|--------------|
| required | Prevents form submission until element is completed. | Optional | Boolean | false | - |
+visible: Default is **[form, content]**
+
### Input
You can use an `input` element to add a single-line text field to your form.
@@ -240,6 +255,8 @@ Validations:
| is_number | Prevents form submission until element is filled with a number. | Optional | Boolean | false | - |
| regex | Prevents form submission until element is filled with a value that match the regular expression. | Optional | String | - | a [regular expression](https://en.wikipedia.org/wiki/Regular_expression) |
+visible: Default is **[form, content]**
+
### Dropdown
You can use a `dropdown` element to add a dropdown menu in your form.
@@ -259,6 +276,8 @@ Validations:
|----------|------------------------------------------------------|----------|---------|---------|--------------|
| required | Prevents form submission until element is completed. | Optional | Boolean | false | - |
+visible: Default is **[form, content]**
+
### Checkboxes
You can use the `checkboxes` element to add a set of checkboxes to your form.
@@ -266,17 +285,20 @@ You can use the `checkboxes` element to add a set of checkboxes to your form.
Attributes:
| Key | Description | Required | Type | Default | Valid values |
-|-------------|-------------------------------------------------------------------------------------------------------|----------|--------|--------------|--------------|
+| ----------- | ----------------------------------------------------------------------------------------------------- | -------- | ------ | ------------ | ------------ |
| label | A brief description of the expected user input, which is displayed in the form. | Required | String | - | - |
| description | A description of the set of checkboxes, which is displayed in the form. Supports Markdown formatting. | Optional | String | Empty String | - |
| options | An array of checkboxes that the user can select. For syntax, see below. | Required | Array | - | - |
For each value in the options array, you can set the following keys.
-| Key | Description | Required | Type | Default | Options |
-|----------|------------------------------------------------------------------------------------------------------------------------------------------|----------|---------|---------|---------|
-| label | The identifier for the option, which is displayed in the form. Markdown is supported for bold or italic text formatting, and hyperlinks. | Required | String | - | - |
-| required | Prevents form submission until element is completed. | Optional | Boolean | false | - |
+| Key | Description | Required | Type | Default | Options |
+|--------------|------------------------------------------------------------------------------------------------------------------------------------------|----------|--------------|---------|---------|
+| label | The identifier for the option, which is displayed in the form. Markdown is supported for bold or italic text formatting, and hyperlinks. | Required | String | - | - |
+| required | Prevents form submission until element is completed. | Optional | Boolean | false | - |
+| visible | Whether a specific checkbox appears in the form only, in the created issue only, or both. Valid options are "form" and "content". | Optional | String array | false | - |
+
+visible: Default is **[form, content]**
## Syntax for issue config
@@ -292,15 +314,15 @@ contact_links:
### Possible Options
-| Key | Description | Type | Default |
-|----------------------|-------------------------------------------------------------------------------------------------------|--------------------|----------------|
-| blank_issues_enabled | If set to false, the User is forced to use a Template | Boolean | true |
-| contact_links | Custom Links to show in the Choose Box | Contact Link Array | Empty Array |
+| Key | Description | Type | Default |
+|----------------------|-------------------------------------------------------|--------------------|-------------|
+| blank_issues_enabled | If set to false, the User is forced to use a Template | Boolean | true |
+| contact_links | Custom Links to show in the Choose Box | Contact Link Array | Empty Array |
### Contact Link
-| Key | Description | Type | Required |
-|----------------------|-------------------------------------------------------------------------------------------------------|---------|----------|
-| name | the name of your link | String | true |
-| url | The URL of your Link | String | true |
-| about | A short description of your Link | String | true |
+| Key | Description | Type | Required |
+|-------|----------------------------------|--------|----------|
+| name | the name of your link | String | true |
+| url | The URL of your Link | String | true |
+| about | A short description of your Link | String | true |
diff --git a/go.mod b/go.mod
index 03f6ad1215..9b70a191ce 100644
--- a/go.mod
+++ b/go.mod
@@ -49,7 +49,7 @@ require (
github.com/go-ldap/ldap/v3 v3.4.6
github.com/go-sql-driver/mysql v1.7.1
github.com/go-swagger/go-swagger v0.30.5
- github.com/go-testfixtures/testfixtures/v3 v3.9.0
+ github.com/go-testfixtures/testfixtures/v3 v3.10.0
github.com/go-webauthn/webauthn v0.10.0
github.com/gobwas/glob v0.2.3
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f
@@ -57,7 +57,7 @@ require (
github.com/golang-jwt/jwt/v5 v5.2.0
github.com/google/go-github/v57 v57.0.0
github.com/google/pprof v0.0.0-20240117000934-35fc243c5815
- github.com/google/uuid v1.5.0
+ github.com/google/uuid v1.6.0
github.com/gorilla/feeds v1.1.2
github.com/gorilla/sessions v1.2.2
github.com/hashicorp/go-version v1.6.0
@@ -73,7 +73,7 @@ require (
github.com/lib/pq v1.10.9
github.com/markbates/goth v1.78.0
github.com/mattn/go-isatty v0.0.20
- github.com/mattn/go-sqlite3 v1.14.19
+ github.com/mattn/go-sqlite3 v1.14.22
github.com/meilisearch/meilisearch-go v0.26.1
github.com/mholt/archiver/v3 v3.5.1
github.com/microcosm-cc/bluemonday v1.0.26
@@ -113,7 +113,7 @@ require (
golang.org/x/text v0.14.0
golang.org/x/tools v0.17.0
google.golang.org/grpc v1.60.1
- google.golang.org/protobuf v1.32.0
+ google.golang.org/protobuf v1.33.0
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/ini.v1 v1.67.0
gopkg.in/yaml.v3 v3.0.1
@@ -129,7 +129,7 @@ require (
dario.cat/mergo v1.0.0 // indirect
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect
github.com/ClickHouse/ch-go v0.61.1 // indirect
- github.com/ClickHouse/clickhouse-go/v2 v2.17.1 // indirect
+ github.com/ClickHouse/clickhouse-go/v2 v2.18.0 // indirect
github.com/DataDog/zstd v1.5.5 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.1 // indirect
@@ -241,7 +241,7 @@ require (
github.com/oklog/ulid v1.3.1 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
- github.com/paulmach/orb v0.11.0 // indirect
+ github.com/paulmach/orb v0.11.1 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
@@ -302,7 +302,7 @@ replace github.com/hashicorp/go-version => github.com/6543/go-version v1.3.1
replace github.com/shurcooL/vfsgen => github.com/lunny/vfsgen v0.0.0-20220105142115-2c99e1ffdfa0
-replace github.com/nektos/act => gitea.com/gitea/act v0.2.51
+replace github.com/nektos/act => gitea.com/gitea/act v0.259.1
replace github.com/gorilla/feeds => github.com/yardenshoham/feeds v0.0.0-20240110072658-f3d0c21c0bd5
diff --git a/go.sum b/go.sum
index b3b8ad8ce4..a44809dde5 100644
--- a/go.sum
+++ b/go.sum
@@ -48,8 +48,8 @@ dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:cliQ4HHsCo6xi2oWZYKWW4bly/Ory9FuTpFPRxj/mAg=
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs=
-gitea.com/gitea/act v0.2.51 h1:gXc/B4OlTciTTzAx9cmNyw04n2SDO7exPjAsR5Idu+c=
-gitea.com/gitea/act v0.2.51/go.mod h1:CoaX2053jqBlD6JMgu4d4UgFL/rp2I14Kt5mMqcs0Z0=
+gitea.com/gitea/act v0.259.1 h1:8GG1o/xtUHl3qjn5f0h/2FXrT5ubBn05TJOM5ry+FBw=
+gitea.com/gitea/act v0.259.1/go.mod h1:UxZWRYqQG2Yj4+4OqfGWW5a3HELwejyWFQyU7F1jUD8=
gitea.com/go-chi/binding v0.0.0-20230415142243-04b515c6d669 h1:RUBX+MK/TsDxpHmymaOaydfigEbbzqUnG1OTZU/HAeo=
gitea.com/go-chi/binding v0.0.0-20230415142243-04b515c6d669/go.mod h1:77TZu701zMXWJFvB8gvTbQ92zQ3DQq/H7l5wAEjQRKc=
gitea.com/go-chi/cache v0.0.0-20210110083709-82c4c9ce2d5e/go.mod h1:k2V/gPDEtXGjjMGuBJiapffAXTv76H4snSmlJRLUhH0=
@@ -78,8 +78,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/ClickHouse/ch-go v0.61.1 h1:j5rx3qnvcnYjhnP1IdXE/vdIRQiqgwAzyqOaasA6QCw=
github.com/ClickHouse/ch-go v0.61.1/go.mod h1:myxt/JZgy2BYHFGQqzmaIpbfr5CMbs3YHVULaWQj5YU=
-github.com/ClickHouse/clickhouse-go/v2 v2.17.1 h1:ZCmAYWpu75IyEi7+Yrs/uaAjiCGY5wfW5kXo64exkX4=
-github.com/ClickHouse/clickhouse-go/v2 v2.17.1/go.mod h1:rkGTvFDTLqLIm0ma+13xmcCfr/08Gvs7KmFt1tgiWHQ=
+github.com/ClickHouse/clickhouse-go/v2 v2.18.0 h1:O1LicIeg2JS2V29fKRH4+yT3f6jvvcJBm506dpVQ4mQ=
+github.com/ClickHouse/clickhouse-go/v2 v2.18.0/go.mod h1:ztQvX6wm7kAbhJslS87EXEhOVNY/TObXwyURnGju5FQ=
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ=
github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
@@ -384,8 +384,8 @@ github.com/go-swagger/scan-repo-boundary v0.0.0-20180623220736-973b3573c013/go.m
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
-github.com/go-testfixtures/testfixtures/v3 v3.9.0 h1:938g5V+GWLVejm3Hc+nWCuEXRlcglZDDlN/t1gWzcSY=
-github.com/go-testfixtures/testfixtures/v3 v3.9.0/go.mod h1:cdsKD2ApFBjdog9jRsz6EJqF+LClq/hrwE9K/1Dzo4s=
+github.com/go-testfixtures/testfixtures/v3 v3.10.0 h1:BrBwN7AuC+74g5qtk9D59TLGOaEa8Bw1WmIsf+SyzWc=
+github.com/go-testfixtures/testfixtures/v3 v3.10.0/go.mod h1:z8RoleoNtibi6Ar8ziCW7e6PQ+jWiqbUWvuv8AMe4lo=
github.com/go-webauthn/webauthn v0.10.0 h1:yuW2e1tXnRAwAvKrR4q4LQmc6XtCMH639/ypZGhZCwk=
github.com/go-webauthn/webauthn v0.10.0/go.mod h1:l0NiauXhL6usIKqNLCUM3Qir43GK7ORg8ggold0Uv/Y=
github.com/go-webauthn/x v0.1.6 h1:QNAX+AWeqRt9loE8mULeWJCqhVG5D/jvdmJ47fIWCkQ=
@@ -488,8 +488,8 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
-github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
@@ -643,8 +643,8 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
-github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI=
-github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
+github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
+github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/meilisearch/meilisearch-go v0.26.1 h1:3bmo2uLijX7kvBmiZ9LupVfC95TFcRJDgrRTzbOoE4A=
github.com/meilisearch/meilisearch-go v0.26.1/go.mod h1:SxuSqDcPBIykjWz1PX+KzsYzArNLSCadQodWs8extS0=
github.com/mholt/acmez v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30=
@@ -718,8 +718,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0-rc6 h1:XDqvyKsJEbRtATzkgItUqBA7QHk58yxX1Ov9HERHNqU=
github.com/opencontainers/image-spec v1.1.0-rc6/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
-github.com/paulmach/orb v0.11.0 h1:JfVXJUBeH9ifc/OrhBY0lL16QsmPgpCHMlqSSYhcgAA=
-github.com/paulmach/orb v0.11.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
+github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU=
+github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
@@ -1308,8 +1308,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
-google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
+google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/models/actions/runner.go b/models/actions/runner.go
index b646146ee6..67f003387b 100644
--- a/models/actions/runner.go
+++ b/models/actions/runner.go
@@ -13,6 +13,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/shared/types"
user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/util"
@@ -159,7 +160,7 @@ type FindRunnerOptions struct {
OwnerID int64
Sort string
Filter string
- IsOnline util.OptionalBool
+ IsOnline optional.Option[bool]
WithAvailable bool // not only runners belong to, but also runners can be used
}
@@ -186,10 +187,12 @@ func (opts FindRunnerOptions) ToConds() builder.Cond {
cond = cond.And(builder.Like{"name", opts.Filter})
}
- if opts.IsOnline.IsTrue() {
- cond = cond.And(builder.Gt{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()})
- } else if opts.IsOnline.IsFalse() {
- cond = cond.And(builder.Lte{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()})
+ if opts.IsOnline.Has() {
+ if opts.IsOnline.Value() {
+ cond = cond.And(builder.Gt{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()})
+ } else {
+ cond = cond.And(builder.Lte{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()})
+ }
}
return cond
}
diff --git a/models/actions/variable.go b/models/actions/variable.go
index 12717e0ae4..14ded60fac 100644
--- a/models/actions/variable.go
+++ b/models/actions/variable.go
@@ -10,6 +10,7 @@ import (
"strings"
"code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
@@ -82,3 +83,35 @@ func UpdateVariable(ctx context.Context, variable *ActionVariable) (bool, error)
})
return count != 0, err
}
+
+func GetVariablesOfRun(ctx context.Context, run *ActionRun) (map[string]string, error) {
+ variables := map[string]string{}
+
+ // Global
+ globalVariables, err := db.Find[ActionVariable](ctx, FindVariablesOpts{})
+ if err != nil {
+ log.Error("find global variables: %v", err)
+ return nil, err
+ }
+
+ // Org / User level
+ ownerVariables, err := db.Find[ActionVariable](ctx, FindVariablesOpts{OwnerID: run.Repo.OwnerID})
+ if err != nil {
+ log.Error("find variables of org: %d, error: %v", run.Repo.OwnerID, err)
+ return nil, err
+ }
+
+ // Repo level
+ repoVariables, err := db.Find[ActionVariable](ctx, FindVariablesOpts{RepoID: run.RepoID})
+ if err != nil {
+ log.Error("find variables of repo: %d, error: %v", run.RepoID, err)
+ return nil, err
+ }
+
+ // Level precedence: Repo > Org / User > Global
+ for _, v := range append(globalVariables, append(ownerVariables, repoVariables...)...) {
+ variables[v.Name] = v.Data
+ }
+
+ return variables, nil
+}
diff --git a/models/activities/action.go b/models/activities/action.go
index 15bd9a52ac..36205eedd1 100644
--- a/models/activities/action.go
+++ b/models/activities/action.go
@@ -225,8 +225,8 @@ func (a *Action) ShortActUserName(ctx context.Context) string {
return base.EllipsisString(a.GetActUserName(ctx), 20)
}
-// GetDisplayName gets the action's display name based on DEFAULT_SHOW_FULL_NAME, or falls back to the username if it is blank.
-func (a *Action) GetDisplayName(ctx context.Context) string {
+// GetActDisplayName gets the action's display name based on DEFAULT_SHOW_FULL_NAME, or falls back to the username if it is blank.
+func (a *Action) GetActDisplayName(ctx context.Context) string {
if setting.UI.DefaultShowFullName {
trimmedFullName := strings.TrimSpace(a.GetActFullName(ctx))
if len(trimmedFullName) > 0 {
@@ -236,8 +236,8 @@ func (a *Action) GetDisplayName(ctx context.Context) string {
return a.ShortActUserName(ctx)
}
-// GetDisplayNameTitle gets the action's display name used for the title (tooltip) based on DEFAULT_SHOW_FULL_NAME
-func (a *Action) GetDisplayNameTitle(ctx context.Context) string {
+// GetActDisplayNameTitle gets the action's display name used for the title (tooltip) based on DEFAULT_SHOW_FULL_NAME
+func (a *Action) GetActDisplayNameTitle(ctx context.Context) string {
if setting.UI.DefaultShowFullName {
return a.ShortActUserName(ctx)
}
@@ -393,10 +393,14 @@ func (a *Action) GetCreate() time.Time {
return a.CreatedUnix.AsTime()
}
-// GetIssueInfos returns a list of issues associated with
-// the action.
+// GetIssueInfos returns a list of associated information with the action.
func (a *Action) GetIssueInfos() []string {
- return strings.SplitN(a.Content, "|", 3)
+ // make sure it always returns 3 elements, because there are some access to the a[1] and a[2] without checking the length
+ ret := strings.SplitN(a.Content, "|", 3)
+ for len(ret) < 3 {
+ ret = append(ret, "")
+ }
+ return ret
}
// GetIssueTitle returns the title of first issue associated with the action.
diff --git a/models/asymkey/ssh_key_authorized_keys.go b/models/asymkey/ssh_key_authorized_keys.go
index 267ab252c8..9279db2020 100644
--- a/models/asymkey/ssh_key_authorized_keys.go
+++ b/models/asymkey/ssh_key_authorized_keys.go
@@ -12,7 +12,6 @@ import (
"path/filepath"
"strings"
"sync"
- "time"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
@@ -44,6 +43,12 @@ const (
var sshOpLocker sync.Mutex
+func WithSSHOpLocker(f func() error) error {
+ sshOpLocker.Lock()
+ defer sshOpLocker.Unlock()
+ return f()
+}
+
// AuthorizedStringForKey creates the authorized keys string appropriate for the provided key
func AuthorizedStringForKey(key *PublicKey) string {
sb := &strings.Builder{}
@@ -114,65 +119,6 @@ func appendAuthorizedKeysToFile(keys ...*PublicKey) error {
return nil
}
-// RewriteAllPublicKeys removes any authorized key and rewrite all keys from database again.
-// Note: db.GetEngine(ctx).Iterate does not get latest data after insert/delete, so we have to call this function
-// outside any session scope independently.
-func RewriteAllPublicKeys(ctx context.Context) error {
- // Don't rewrite key if internal server
- if setting.SSH.StartBuiltinServer || !setting.SSH.CreateAuthorizedKeysFile {
- return nil
- }
-
- sshOpLocker.Lock()
- defer sshOpLocker.Unlock()
-
- if setting.SSH.RootPath != "" {
- // First of ensure that the RootPath is present, and if not make it with 0700 permissions
- // This of course doesn't guarantee that this is the right directory for authorized_keys
- // but at least if it's supposed to be this directory and it doesn't exist and we're the
- // right user it will at least be created properly.
- err := os.MkdirAll(setting.SSH.RootPath, 0o700)
- if err != nil {
- log.Error("Unable to MkdirAll(%s): %v", setting.SSH.RootPath, err)
- return err
- }
- }
-
- fPath := filepath.Join(setting.SSH.RootPath, "authorized_keys")
- tmpPath := fPath + ".tmp"
- t, err := os.OpenFile(tmpPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
- if err != nil {
- return err
- }
- defer func() {
- t.Close()
- if err := util.Remove(tmpPath); err != nil {
- log.Warn("Unable to remove temporary authorized keys file: %s: Error: %v", tmpPath, err)
- }
- }()
-
- if setting.SSH.AuthorizedKeysBackup {
- isExist, err := util.IsExist(fPath)
- if err != nil {
- log.Error("Unable to check if %s exists. Error: %v", fPath, err)
- return err
- }
- if isExist {
- bakPath := fmt.Sprintf("%s_%d.gitea_bak", fPath, time.Now().Unix())
- if err = util.CopyFile(fPath, bakPath); err != nil {
- return err
- }
- }
- }
-
- if err := RegeneratePublicKeys(ctx, t); err != nil {
- return err
- }
-
- t.Close()
- return util.Rename(tmpPath, fPath)
-}
-
// RegeneratePublicKeys regenerates the authorized_keys file
func RegeneratePublicKeys(ctx context.Context, t io.StringWriter) error {
if err := db.GetEngine(ctx).Where("type != ?", KeyTypePrincipal).Iterate(new(PublicKey), func(idx int, bean any) (err error) {
diff --git a/models/asymkey/ssh_key_principals.go b/models/asymkey/ssh_key_principals.go
index 4e7dee2c91..e8b97d306e 100644
--- a/models/asymkey/ssh_key_principals.go
+++ b/models/asymkey/ssh_key_principals.go
@@ -9,51 +9,11 @@ import (
"strings"
"code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/perm"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
)
-// AddPrincipalKey adds new principal to database and authorized_principals file.
-func AddPrincipalKey(ctx context.Context, ownerID int64, content string, authSourceID int64) (*PublicKey, error) {
- dbCtx, committer, err := db.TxContext(ctx)
- if err != nil {
- return nil, err
- }
- defer committer.Close()
-
- // Principals cannot be duplicated.
- has, err := db.GetEngine(dbCtx).
- Where("content = ? AND type = ?", content, KeyTypePrincipal).
- Get(new(PublicKey))
- if err != nil {
- return nil, err
- } else if has {
- return nil, ErrKeyAlreadyExist{0, "", content}
- }
-
- key := &PublicKey{
- OwnerID: ownerID,
- Name: content,
- Content: content,
- Mode: perm.AccessModeWrite,
- Type: KeyTypePrincipal,
- LoginSourceID: authSourceID,
- }
- if err = db.Insert(dbCtx, key); err != nil {
- return nil, fmt.Errorf("addKey: %w", err)
- }
-
- if err = committer.Commit(); err != nil {
- return nil, err
- }
-
- committer.Close()
-
- return key, RewriteAllPrincipalKeys(ctx)
-}
-
// CheckPrincipalKeyString strips spaces and returns an error if the given principal contains newlines
func CheckPrincipalKeyString(ctx context.Context, user *user_model.User, content string) (_ string, err error) {
if setting.SSH.Disabled {
diff --git a/models/auth/source.go b/models/auth/source.go
index 1bdde8235c..f360ca9801 100644
--- a/models/auth/source.go
+++ b/models/auth/source.go
@@ -11,6 +11,7 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
@@ -243,14 +244,14 @@ func CreateSource(ctx context.Context, source *Source) error {
type FindSourcesOptions struct {
db.ListOptions
- IsActive util.OptionalBool
+ IsActive optional.Option[bool]
LoginType Type
}
func (opts FindSourcesOptions) ToConds() builder.Cond {
conds := builder.NewCond()
- if !opts.IsActive.IsNone() {
- conds = conds.And(builder.Eq{"is_active": opts.IsActive.IsTrue()})
+ if opts.IsActive.Has() {
+ conds = conds.And(builder.Eq{"is_active": opts.IsActive.Value()})
}
if opts.LoginType != NoType {
conds = conds.And(builder.Eq{"`type`": opts.LoginType})
@@ -262,7 +263,7 @@ func (opts FindSourcesOptions) ToConds() builder.Cond {
// source of type LoginSSPI
func IsSSPIEnabled(ctx context.Context) bool {
exist, err := db.Exist[Source](ctx, FindSourcesOptions{
- IsActive: util.OptionalBoolTrue,
+ IsActive: optional.Some(true),
LoginType: SSPI,
}.ToConds())
if err != nil {
diff --git a/models/db/collation.go b/models/db/collation.go
index 2f5ff2bf05..c128cf5029 100644
--- a/models/db/collation.go
+++ b/models/db/collation.go
@@ -166,8 +166,7 @@ func preprocessDatabaseCollation(x *xorm.Engine) {
// try to alter database collation to expected if the database is empty, it might fail in some cases (and it isn't necessary to succeed)
// at the moment, there is no "altering" solution for MSSQL, site admin should manually change the database collation
- // and there is a bug https://github.com/go-testfixtures/testfixtures/pull/182 mssql: Invalid object name 'information_schema.tables'.
- if !r.CollationEquals(r.DatabaseCollation, r.ExpectedCollation) && r.ExistingTableNumber == 0 && x.Dialect().URI().DBType == schemas.MYSQL {
+ if !r.CollationEquals(r.DatabaseCollation, r.ExpectedCollation) && r.ExistingTableNumber == 0 {
if err = alterDatabaseCollation(x, r.ExpectedCollation); err != nil {
log.Error("Failed to change database collation to %q: %v", r.ExpectedCollation, err)
} else {
diff --git a/models/fixtures/access.yml b/models/fixtures/access.yml
index 641c453eb7..4171e31fef 100644
--- a/models/fixtures/access.yml
+++ b/models/fixtures/access.yml
@@ -42,120 +42,132 @@
-
id: 8
- user_id: 15
+ user_id: 10
repo_id: 21
mode: 2
-
id: 9
- user_id: 15
- repo_id: 22
+ user_id: 10
+ repo_id: 32
mode: 2
-
id: 10
user_id: 15
+ repo_id: 21
+ mode: 2
+
+-
+ id: 11
+ user_id: 15
+ repo_id: 22
+ mode: 2
+
+-
+ id: 12
+ user_id: 15
repo_id: 23
mode: 4
-
- id: 11
+ id: 13
user_id: 15
repo_id: 24
mode: 4
-
- id: 12
+ id: 14
user_id: 15
repo_id: 32
mode: 2
-
- id: 13
+ id: 15
user_id: 18
repo_id: 21
mode: 2
-
- id: 14
+ id: 16
user_id: 18
repo_id: 22
mode: 2
-
- id: 15
+ id: 17
user_id: 18
repo_id: 23
mode: 4
-
- id: 16
+ id: 18
user_id: 18
repo_id: 24
mode: 4
-
- id: 17
+ id: 19
user_id: 20
repo_id: 24
mode: 1
-
- id: 18
+ id: 20
user_id: 20
repo_id: 27
mode: 4
-
- id: 19
+ id: 21
user_id: 20
repo_id: 28
mode: 4
-
- id: 20
+ id: 22
user_id: 29
repo_id: 4
mode: 2
-
- id: 21
+ id: 23
user_id: 29
repo_id: 24
mode: 1
-
- id: 22
+ id: 24
user_id: 31
repo_id: 27
mode: 4
-
- id: 23
+ id: 25
user_id: 31
repo_id: 28
mode: 4
-
- id: 24
+ id: 26
user_id: 38
repo_id: 60
mode: 2
-
- id: 25
+ id: 27
user_id: 38
repo_id: 61
mode: 1
-
- id: 26
+ id: 28
user_id: 39
repo_id: 61
mode: 1
-
- id: 27
+ id: 29
user_id: 40
repo_id: 61
mode: 4
diff --git a/models/fixtures/collaboration.yml b/models/fixtures/collaboration.yml
index 7603bdad32..4c3ac367f6 100644
--- a/models/fixtures/collaboration.yml
+++ b/models/fixtures/collaboration.yml
@@ -51,3 +51,15 @@
repo_id: 60
user_id: 38
mode: 2 # write
+
+-
+ id: 10
+ repo_id: 21
+ user_id: 10
+ mode: 2 # write
+
+-
+ id: 11
+ repo_id: 32
+ user_id: 10
+ mode: 2 # write
diff --git a/models/fixtures/hook_task.yml b/models/fixtures/hook_task.yml
index 6dbb10151a..d573406b36 100644
--- a/models/fixtures/hook_task.yml
+++ b/models/fixtures/hook_task.yml
@@ -3,3 +3,35 @@
hook_id: 1
uuid: uuid1
is_delivered: true
+ is_succeed: false
+ request_content: >
+ {
+ "url": "/matrix-delivered",
+ "http_method":"PUT",
+ "headers": {
+ "X-Head": "42"
+ },
+ "body": "{}"
+ }
+
+-
+ id: 2
+ hook_id: 1
+ uuid: uuid2
+ is_delivered: false
+
+-
+ id: 3
+ hook_id: 1
+ uuid: uuid3
+ is_delivered: true
+ is_succeed: true
+ payload_content: '{"key":"value"}' # legacy task, payload saved in payload_content (and not in request_content)
+ request_content: >
+ {
+ "url": "/matrix-success",
+ "http_method":"PUT",
+ "headers": {
+ "X-Head": "42"
+ }
+ }
diff --git a/models/fixtures/issue_assignees.yml b/models/fixtures/issue_assignees.yml
index e5d36f921a..c40ecad676 100644
--- a/models/fixtures/issue_assignees.yml
+++ b/models/fixtures/issue_assignees.yml
@@ -14,3 +14,7 @@
id: 4
assignee_id: 2
issue_id: 17
+-
+ id: 5
+ assignee_id: 10
+ issue_id: 6
diff --git a/models/fixtures/repo_transfer.yml b/models/fixtures/repo_transfer.yml
index b841b5e983..db92c95248 100644
--- a/models/fixtures/repo_transfer.yml
+++ b/models/fixtures/repo_transfer.yml
@@ -5,3 +5,19 @@
repo_id: 3
created_unix: 1553610671
updated_unix: 1553610671
+
+-
+ id: 2
+ doer_id: 16
+ recipient_id: 10
+ repo_id: 21
+ created_unix: 1553610671
+ updated_unix: 1553610671
+
+-
+ id: 3
+ doer_id: 3
+ recipient_id: 10
+ repo_id: 32
+ created_unix: 1553610671
+ updated_unix: 1553610671
diff --git a/models/fixtures/repo_unit.yml b/models/fixtures/repo_unit.yml
index 4b26674990..8a22db0445 100644
--- a/models/fixtures/repo_unit.yml
+++ b/models/fixtures/repo_unit.yml
@@ -650,12 +650,6 @@
type: 2
created_unix: 946684810
--
- id: 98
- repo_id: 1
- type: 8
- created_unix: 946684810
-
-
id: 99
repo_id: 1
diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml
index d094fe82d8..e5c6224c96 100644
--- a/models/fixtures/repository.yml
+++ b/models/fixtures/repository.yml
@@ -614,8 +614,8 @@
owner_name: user16
lower_name: big_test_public_3
name: big_test_public_3
- num_watches: 0
- num_stars: 0
+ num_watches: 1
+ num_stars: 1
num_forks: 0
num_issues: 0
num_closed_issues: 0
@@ -945,8 +945,8 @@
owner_name: org3
lower_name: repo21
name: repo21
- num_watches: 0
- num_stars: 0
+ num_watches: 1
+ num_stars: 1
num_forks: 0
num_issues: 2
num_closed_issues: 0
diff --git a/models/fixtures/star.yml b/models/fixtures/star.yml
index 860f26b8e2..39b51b3736 100644
--- a/models/fixtures/star.yml
+++ b/models/fixtures/star.yml
@@ -7,3 +7,13 @@
id: 2
uid: 2
repo_id: 4
+
+-
+ id: 3
+ uid: 10
+ repo_id: 21
+
+-
+ id: 4
+ uid: 10
+ repo_id: 32
diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml
index 16b687ae04..a3de535508 100644
--- a/models/fixtures/user.yml
+++ b/models/fixtures/user.yml
@@ -361,7 +361,7 @@
use_custom_avatar: false
num_followers: 0
num_following: 0
- num_stars: 0
+ num_stars: 2
num_repos: 3
num_teams: 0
num_members: 0
diff --git a/models/fixtures/user_blocking.yml b/models/fixtures/user_blocking.yml
new file mode 100644
index 0000000000..2ec9d99df5
--- /dev/null
+++ b/models/fixtures/user_blocking.yml
@@ -0,0 +1,19 @@
+-
+ id: 1
+ blocker_id: 2
+ blockee_id: 29
+
+-
+ id: 2
+ blocker_id: 17
+ blockee_id: 28
+
+-
+ id: 3
+ blocker_id: 2
+ blockee_id: 34
+
+-
+ id: 4
+ blocker_id: 50
+ blockee_id: 34
diff --git a/models/fixtures/watch.yml b/models/fixtures/watch.yml
index 1950ac99e7..18bcd2ed2b 100644
--- a/models/fixtures/watch.yml
+++ b/models/fixtures/watch.yml
@@ -27,3 +27,15 @@
user_id: 11
repo_id: 1
mode: 3 # auto
+
+-
+ id: 6
+ user_id: 10
+ repo_id: 21
+ mode: 1 # normal
+
+-
+ id: 7
+ user_id: 10
+ repo_id: 32
+ mode: 1 # normal
diff --git a/models/git/branch.go b/models/git/branch.go
index db02fc9b28..fa0781fed1 100644
--- a/models/git/branch.go
+++ b/models/git/branch.go
@@ -158,6 +158,11 @@ func GetBranch(ctx context.Context, repoID int64, branchName string) (*Branch, e
return &branch, nil
}
+func GetBranches(ctx context.Context, repoID int64, branchNames []string) ([]*Branch, error) {
+ branches := make([]*Branch, 0, len(branchNames))
+ return branches, db.GetEngine(ctx).Where("repo_id=?", repoID).In("name", branchNames).Find(&branches)
+}
+
func AddBranches(ctx context.Context, branches []*Branch) error {
for _, branch := range branches {
if _, err := db.GetEngine(ctx).Insert(branch); err != nil {
diff --git a/models/issues/assignees.go b/models/issues/assignees.go
index 60f32d9557..30234be07a 100644
--- a/models/issues/assignees.go
+++ b/models/issues/assignees.go
@@ -64,6 +64,27 @@ func IsUserAssignedToIssue(ctx context.Context, issue *Issue, user *user_model.U
return db.Exist[IssueAssignees](ctx, builder.Eq{"assignee_id": user.ID, "issue_id": issue.ID})
}
+type AssignedIssuesOptions struct {
+ db.ListOptions
+ AssigneeID int64
+ RepoOwnerID int64
+}
+
+func (opts *AssignedIssuesOptions) ToConds() builder.Cond {
+ cond := builder.NewCond()
+ if opts.AssigneeID != 0 {
+ cond = cond.And(builder.In("issue.id", builder.Select("issue_id").From("issue_assignees").Where(builder.Eq{"assignee_id": opts.AssigneeID})))
+ }
+ if opts.RepoOwnerID != 0 {
+ cond = cond.And(builder.In("issue.repo_id", builder.Select("id").From("repository").Where(builder.Eq{"owner_id": opts.RepoOwnerID})))
+ }
+ return cond
+}
+
+func GetAssignedIssues(ctx context.Context, opts *AssignedIssuesOptions) ([]*Issue, int64, error) {
+ return db.FindAndCount[Issue](ctx, opts)
+}
+
// ToggleIssueAssignee changes a user between assigned and not assigned for this issue, and make issue comment for it.
func ToggleIssueAssignee(ctx context.Context, issue *Issue, doer *user_model.User, assigneeID int64) (removed bool, comment *Comment, err error) {
ctx, committer, err := db.TxContext(ctx)
diff --git a/models/issues/comment.go b/models/issues/comment.go
index 038f1562a1..8f5a868b64 100644
--- a/models/issues/comment.go
+++ b/models/issues/comment.go
@@ -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"
@@ -1037,8 +1038,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
@@ -1070,11 +1071,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
}
@@ -1083,7 +1084,7 @@ func (opts FindCommentsOptions) ToConds() builder.Cond {
func FindComments(ctx context.Context, opts *FindCommentsOptions) (CommentList, error) {
comments := make([]*Comment, 0, 10)
sess := db.GetEngine(ctx).Where(opts.ToConds())
- if opts.RepoID > 0 || opts.IsPull != util.OptionalBoolNone {
+ if opts.RepoID > 0 || opts.IsPull.Has() {
sess.Join("INNER", "issue", "issue.id = comment.issue_id")
}
diff --git a/models/issues/issue_search.go b/models/issues/issue_search.go
index 7dc277327a..c5c9cecdb9 100644
--- a/models/issues/issue_search.go
+++ b/models/issues/issue_search.go
@@ -13,7 +13,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/modules/optional"
"xorm.io/builder"
"xorm.io/xorm"
@@ -34,8 +34,8 @@ type IssuesOptions struct { //nolint
MilestoneIDs []int64
ProjectID int64
ProjectBoardID int64
- IsClosed util.OptionalBool
- IsPull util.OptionalBool
+ IsClosed optional.Option[bool]
+ IsPull optional.Option[bool]
LabelIDs []int64
IncludedLabelNames []string
ExcludedLabelNames []string
@@ -46,7 +46,7 @@ type IssuesOptions struct { //nolint
UpdatedBeforeUnix int64
// prioritize issues from this repo
PriorityRepoID int64
- IsArchived util.OptionalBool
+ IsArchived optional.Option[bool]
Org *organization.Organization // issues permission scope
Team *organization.Team // issues permission scope
User *user_model.User // issues permission scope
@@ -217,8 +217,8 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
applyRepoConditions(sess, opts)
- if !opts.IsClosed.IsNone() {
- sess.And("issue.is_closed=?", opts.IsClosed.IsTrue())
+ if opts.IsClosed.Has() {
+ sess.And("issue.is_closed=?", opts.IsClosed.Value())
}
if opts.AssigneeID > 0 {
@@ -260,21 +260,18 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
applyProjectBoardCondition(sess, opts)
- switch opts.IsPull {
- case util.OptionalBoolTrue:
- sess.And("issue.is_pull=?", true)
- case util.OptionalBoolFalse:
- sess.And("issue.is_pull=?", false)
+ if opts.IsPull.Has() {
+ sess.And("issue.is_pull=?", opts.IsPull.Value())
}
- if opts.IsArchived != util.OptionalBoolNone {
- sess.And(builder.Eq{"repository.is_archived": opts.IsArchived.IsTrue()})
+ if opts.IsArchived.Has() {
+ sess.And(builder.Eq{"repository.is_archived": opts.IsArchived.Value()})
}
applyLabelsCondition(sess, opts)
if opts.User != nil {
- sess.And(issuePullAccessibleRepoCond("issue.repo_id", opts.User.ID, opts.Org, opts.Team, opts.IsPull.IsTrue()))
+ sess.And(issuePullAccessibleRepoCond("issue.repo_id", opts.User.ID, opts.Org, opts.Team, opts.IsPull.Value()))
}
return sess
diff --git a/models/issues/issue_stats.go b/models/issues/issue_stats.go
index 99ca19f804..32c5674fc9 100644
--- a/models/issues/issue_stats.go
+++ b/models/issues/issue_stats.go
@@ -8,7 +8,6 @@ import (
"fmt"
"code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/modules/util"
"xorm.io/builder"
"xorm.io/xorm"
@@ -170,11 +169,8 @@ func applyIssuesOptions(sess *xorm.Session, opts *IssuesOptions, issueIDs []int6
applyReviewedCondition(sess, opts.ReviewedID)
}
- switch opts.IsPull {
- case util.OptionalBoolTrue:
- sess.And("issue.is_pull=?", true)
- case util.OptionalBoolFalse:
- sess.And("issue.is_pull=?", false)
+ if opts.IsPull.Has() {
+ sess.And("issue.is_pull=?", opts.IsPull.Value())
}
return sess
diff --git a/models/issues/issue_update.go b/models/issues/issue_update.go
index b258dc882c..ef96e1ee50 100644
--- a/models/issues/issue_update.go
+++ b/models/issues/issue_update.go
@@ -517,6 +517,15 @@ func FindAndUpdateIssueMentions(ctx context.Context, issue *Issue, doer *user_mo
if err != nil {
return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err)
}
+
+ notBlocked := make([]*user_model.User, 0, len(mentions))
+ for _, user := range mentions {
+ if !user_model.IsUserBlockedBy(ctx, doer, user.ID) {
+ notBlocked = append(notBlocked, user)
+ }
+ }
+ mentions = notBlocked
+
if err = UpdateIssueMentions(ctx, issue.ID, mentions); err != nil {
return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err)
}
diff --git a/models/issues/issue_xref.go b/models/issues/issue_xref.go
index cfc3c1683c..e2e35859df 100644
--- a/models/issues/issue_xref.go
+++ b/models/issues/issue_xref.go
@@ -214,6 +214,10 @@ func (issue *Issue) verifyReferencedIssue(stdCtx context.Context, ctx *crossRefe
if !perm.CanReadIssuesOrPulls(refIssue.IsPull) {
return nil, references.XRefActionNone, nil
}
+ if user_model.IsUserBlockedBy(stdCtx, ctx.Doer, refIssue.PosterID, refIssue.Repo.OwnerID) {
+ return nil, references.XRefActionNone, nil
+ }
+
// Accept close/reopening actions only if the poster is able to close the
// referenced issue manually at this moment. The only exception is
// the poster of a new PR referencing an issue on the same repo: then the merger
diff --git a/models/issues/label.go b/models/issues/label.go
index e7f4255bba..a79c22575b 100644
--- a/models/issues/label.go
+++ b/models/issues/label.go
@@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/label"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
@@ -128,7 +129,7 @@ func (l *Label) CalOpenOrgIssues(ctx context.Context, repoID, labelID int64) {
counts, _ := CountIssuesByRepo(ctx, &IssuesOptions{
RepoIDs: []int64{repoID},
LabelIDs: []int64{labelID},
- IsClosed: util.OptionalBoolFalse,
+ IsClosed: optional.Some(false),
})
for _, count := range counts {
diff --git a/models/issues/milestone.go b/models/issues/milestone.go
index b44580039d..a54792a881 100644
--- a/models/issues/milestone.go
+++ b/models/issues/milestone.go
@@ -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"
@@ -304,7 +305,7 @@ func DeleteMilestoneByRepoID(ctx context.Context, repoID, id int64) error {
}
numClosedMilestones, err := db.Count[Milestone](ctx, FindMilestoneOptions{
RepoID: repo.ID,
- IsClosed: util.OptionalBoolTrue,
+ IsClosed: optional.Some(true),
})
if err != nil {
return err
diff --git a/models/issues/milestone_list.go b/models/issues/milestone_list.go
index a73bf73c17..d1b3f0301b 100644
--- a/models/issues/milestone_list.go
+++ b/models/issues/milestone_list.go
@@ -8,7 +8,7 @@ import (
"strings"
"code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/modules/optional"
"xorm.io/builder"
)
@@ -28,7 +28,7 @@ func (milestones MilestoneList) getMilestoneIDs() []int64 {
type FindMilestoneOptions struct {
db.ListOptions
RepoID int64
- IsClosed util.OptionalBool
+ IsClosed optional.Option[bool]
Name string
SortType string
RepoCond builder.Cond
@@ -40,8 +40,8 @@ func (opts FindMilestoneOptions) ToConds() builder.Cond {
if opts.RepoID != 0 {
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
}
- if opts.IsClosed != util.OptionalBoolNone {
- cond = cond.And(builder.Eq{"is_closed": opts.IsClosed.IsTrue()})
+ if opts.IsClosed.Has() {
+ cond = cond.And(builder.Eq{"is_closed": opts.IsClosed.Value()})
}
if opts.RepoCond != nil && opts.RepoCond.IsValid() {
cond = cond.And(builder.In("repo_id", builder.Select("id").From("repository").Where(opts.RepoCond)))
diff --git a/models/issues/milestone_test.go b/models/issues/milestone_test.go
index 7477af92c8..e5f6f15ca2 100644
--- a/models/issues/milestone_test.go
+++ b/models/issues/milestone_test.go
@@ -11,10 +11,10 @@ import (
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
- "code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert"
)
@@ -39,10 +39,10 @@ func TestGetMilestoneByRepoID(t *testing.T) {
func TestGetMilestonesByRepoID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
test := func(repoID int64, state api.StateType) {
- var isClosed util.OptionalBool
+ var isClosed optional.Option[bool]
switch state {
case api.StateClosed, api.StateOpen:
- isClosed = util.OptionalBoolOf(state == api.StateClosed)
+ isClosed = optional.Some(state == api.StateClosed)
}
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
milestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
@@ -84,7 +84,7 @@ func TestGetMilestonesByRepoID(t *testing.T) {
milestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
RepoID: unittest.NonexistentID,
- IsClosed: util.OptionalBoolFalse,
+ IsClosed: optional.Some(false),
})
assert.NoError(t, err)
assert.Len(t, milestones, 0)
@@ -101,7 +101,7 @@ func TestGetMilestones(t *testing.T) {
PageSize: setting.UI.IssuePagingNum,
},
RepoID: repo.ID,
- IsClosed: util.OptionalBoolFalse,
+ IsClosed: optional.Some(false),
SortType: sortType,
})
assert.NoError(t, err)
@@ -118,7 +118,7 @@ func TestGetMilestones(t *testing.T) {
PageSize: setting.UI.IssuePagingNum,
},
RepoID: repo.ID,
- IsClosed: util.OptionalBoolTrue,
+ IsClosed: optional.Some(true),
Name: "",
SortType: sortType,
})
@@ -178,7 +178,7 @@ func TestCountRepoClosedMilestones(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
count, err := db.Count[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
RepoID: repoID,
- IsClosed: util.OptionalBoolTrue,
+ IsClosed: optional.Some(true),
})
assert.NoError(t, err)
assert.EqualValues(t, repo.NumClosedMilestones, count)
@@ -189,7 +189,7 @@ func TestCountRepoClosedMilestones(t *testing.T) {
count, err := db.Count[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
RepoID: unittest.NonexistentID,
- IsClosed: util.OptionalBoolTrue,
+ IsClosed: optional.Some(true),
})
assert.NoError(t, err)
assert.EqualValues(t, 0, count)
@@ -206,7 +206,7 @@ func TestCountMilestonesByRepoIDs(t *testing.T) {
openCounts, err := issues_model.CountMilestonesMap(db.DefaultContext, issues_model.FindMilestoneOptions{
RepoIDs: []int64{1, 2},
- IsClosed: util.OptionalBoolFalse,
+ IsClosed: optional.Some(false),
})
assert.NoError(t, err)
assert.EqualValues(t, repo1OpenCount, openCounts[1])
@@ -215,7 +215,7 @@ func TestCountMilestonesByRepoIDs(t *testing.T) {
closedCounts, err := issues_model.CountMilestonesMap(db.DefaultContext,
issues_model.FindMilestoneOptions{
RepoIDs: []int64{1, 2},
- IsClosed: util.OptionalBoolTrue,
+ IsClosed: optional.Some(true),
})
assert.NoError(t, err)
assert.EqualValues(t, repo1ClosedCount, closedCounts[1])
@@ -234,7 +234,7 @@ func TestGetMilestonesByRepoIDs(t *testing.T) {
PageSize: setting.UI.IssuePagingNum,
},
RepoIDs: []int64{repo1.ID, repo2.ID},
- IsClosed: util.OptionalBoolFalse,
+ IsClosed: optional.Some(false),
SortType: sortType,
})
assert.NoError(t, err)
@@ -252,7 +252,7 @@ func TestGetMilestonesByRepoIDs(t *testing.T) {
PageSize: setting.UI.IssuePagingNum,
},
RepoIDs: []int64{repo1.ID, repo2.ID},
- IsClosed: util.OptionalBoolTrue,
+ IsClosed: optional.Some(true),
SortType: sortType,
})
assert.NoError(t, err)
diff --git a/models/issues/pull.go b/models/issues/pull.go
index eae51a7a46..4021e10b7b 100644
--- a/models/issues/pull.go
+++ b/models/issues/pull.go
@@ -901,12 +901,7 @@ func PullRequestCodeOwnersReview(ctx context.Context, pull *Issue, pr *PullReque
}
defer repo.Close()
- branch, err := repo.GetDefaultBranch()
- if err != nil {
- return err
- }
-
- commit, err := repo.GetBranchCommit(branch)
+ commit, err := repo.GetBranchCommit(pr.BaseRepo.DefaultBranch)
if err != nil {
return err
}
diff --git a/models/issues/reaction.go b/models/issues/reaction.go
index bb47cf24ca..d5448636fe 100644
--- a/models/issues/reaction.go
+++ b/models/issues/reaction.go
@@ -240,25 +240,6 @@ func CreateReaction(ctx context.Context, opts *ReactionOptions) (*Reaction, erro
return reaction, nil
}
-// CreateIssueReaction creates a reaction on issue.
-func CreateIssueReaction(ctx context.Context, doerID, issueID int64, content string) (*Reaction, error) {
- return CreateReaction(ctx, &ReactionOptions{
- Type: content,
- DoerID: doerID,
- IssueID: issueID,
- })
-}
-
-// CreateCommentReaction creates a reaction on comment.
-func CreateCommentReaction(ctx context.Context, doerID, issueID, commentID int64, content string) (*Reaction, error) {
- return CreateReaction(ctx, &ReactionOptions{
- Type: content,
- DoerID: doerID,
- IssueID: issueID,
- CommentID: commentID,
- })
-}
-
// DeleteReaction deletes reaction for issue or comment.
func DeleteReaction(ctx context.Context, opts *ReactionOptions) error {
reaction := &Reaction{
diff --git a/models/issues/review_list.go b/models/issues/review_list.go
index 282f18b4f7..ec6cb07988 100644
--- a/models/issues/review_list.go
+++ b/models/issues/review_list.go
@@ -9,7 +9,7 @@ import (
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container"
- "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/modules/optional"
"xorm.io/builder"
)
@@ -68,7 +68,7 @@ type FindReviewOptions struct {
IssueID int64
ReviewerID int64
OfficialOnly bool
- Dismissed util.OptionalBool
+ Dismissed optional.Option[bool]
}
func (opts *FindReviewOptions) toCond() builder.Cond {
@@ -85,8 +85,8 @@ func (opts *FindReviewOptions) toCond() builder.Cond {
if opts.OfficialOnly {
cond = cond.And(builder.Eq{"official": true})
}
- if !opts.Dismissed.IsNone() {
- cond = cond.And(builder.Eq{"dismissed": opts.Dismissed.IsTrue()})
+ if opts.Dismissed.Has() {
+ cond = cond.And(builder.Eq{"dismissed": opts.Dismissed.Value()})
}
return cond
}
diff --git a/models/issues/tracked_time.go b/models/issues/tracked_time.go
index 91c4832e49..4063ca043b 100644
--- a/models/issues/tracked_time.go
+++ b/models/issues/tracked_time.go
@@ -11,6 +11,7 @@ import (
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
@@ -340,7 +341,7 @@ func GetTrackedTimeByID(ctx context.Context, id int64) (*TrackedTime, error) {
}
// GetIssueTotalTrackedTime returns the total tracked time for issues by given conditions.
-func GetIssueTotalTrackedTime(ctx context.Context, opts *IssuesOptions, isClosed util.OptionalBool) (int64, error) {
+func GetIssueTotalTrackedTime(ctx context.Context, opts *IssuesOptions, isClosed optional.Option[bool]) (int64, error) {
if len(opts.IssueIDs) <= MaxQueryParameters {
return getIssueTotalTrackedTimeChunk(ctx, opts, isClosed, opts.IssueIDs)
}
@@ -363,7 +364,7 @@ func GetIssueTotalTrackedTime(ctx context.Context, opts *IssuesOptions, isClosed
return accum, nil
}
-func getIssueTotalTrackedTimeChunk(ctx context.Context, opts *IssuesOptions, isClosed util.OptionalBool, issueIDs []int64) (int64, error) {
+func getIssueTotalTrackedTimeChunk(ctx context.Context, opts *IssuesOptions, isClosed optional.Option[bool], issueIDs []int64) (int64, error) {
sumSession := func(opts *IssuesOptions, issueIDs []int64) *xorm.Session {
sess := db.GetEngine(ctx).
Table("tracked_time").
@@ -378,8 +379,8 @@ func getIssueTotalTrackedTimeChunk(ctx context.Context, opts *IssuesOptions, isC
}
session := sumSession(opts, issueIDs)
- if !isClosed.IsNone() {
- session = session.And("issue.is_closed = ?", isClosed.IsTrue())
+ if isClosed.Has() {
+ session = session.And("issue.is_closed = ?", isClosed.Value())
}
return session.SumInt(new(trackedTime), "tracked_time.time")
}
diff --git a/models/issues/tracked_time_test.go b/models/issues/tracked_time_test.go
index 9beb862ffb..d82bff967a 100644
--- a/models/issues/tracked_time_test.go
+++ b/models/issues/tracked_time_test.go
@@ -11,7 +11,7 @@ import (
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/modules/optional"
"github.com/stretchr/testify/assert"
)
@@ -120,15 +120,15 @@ func TestTotalTimesForEachUser(t *testing.T) {
func TestGetIssueTotalTrackedTime(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- ttt, err := issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, util.OptionalBoolFalse)
+ ttt, err := issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, optional.Some(false))
assert.NoError(t, err)
assert.EqualValues(t, 3682, ttt)
- ttt, err = issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, util.OptionalBoolTrue)
+ ttt, err = issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, optional.Some(true))
assert.NoError(t, err)
assert.EqualValues(t, 0, ttt)
- ttt, err = issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, util.OptionalBoolNone)
+ ttt, err = issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, optional.None[bool]())
assert.NoError(t, err)
assert.EqualValues(t, 3682, ttt)
}
diff --git a/models/migrations/base/db_test.go b/models/migrations/base/db_test.go
index 4d61b758cf..80bf00b22a 100644
--- a/models/migrations/base/db_test.go
+++ b/models/migrations/base/db_test.go
@@ -36,12 +36,14 @@ func Test_DropTableColumns(t *testing.T) {
"updated_unix",
}
+ x.SetMapper(names.GonicMapper{})
+
for i := range columns {
- x.SetMapper(names.GonicMapper{})
if err := x.Sync(new(DropTest)); err != nil {
t.Errorf("unable to create DropTest table: %v", err)
return
}
+
sess := x.NewSession()
if err := sess.Begin(); err != nil {
sess.Close()
@@ -64,7 +66,6 @@ func Test_DropTableColumns(t *testing.T) {
return
}
for j := range columns[i+1:] {
- x.SetMapper(names.GonicMapper{})
if err := x.Sync(new(DropTest)); err != nil {
t.Errorf("unable to create DropTest table: %v", err)
return
diff --git a/models/migrations/fixtures/Test_AddIssueResourceIndexTable/issue.yml b/models/migrations/fixtures/Test_AddIssueResourceIndexTable/issue.yml
new file mode 100644
index 0000000000..f95d47916b
--- /dev/null
+++ b/models/migrations/fixtures/Test_AddIssueResourceIndexTable/issue.yml
@@ -0,0 +1,4 @@
+-
+ id: 1
+ repo_id: 1
+ index: 1
diff --git a/models/migrations/fixtures/Test_AddRepoIDForAttachment/attachment.yml b/models/migrations/fixtures/Test_AddRepoIDForAttachment/attachment.yml
new file mode 100644
index 0000000000..056236ba9e
--- /dev/null
+++ b/models/migrations/fixtures/Test_AddRepoIDForAttachment/attachment.yml
@@ -0,0 +1,11 @@
+-
+ id: 1
+ uuid: a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11
+ issue_id: 1
+ release_id: 0
+
+-
+ id: 2
+ uuid: a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12
+ issue_id: 0
+ release_id: 1
diff --git a/models/migrations/fixtures/Test_AddRepoIDForAttachment/issue.yml b/models/migrations/fixtures/Test_AddRepoIDForAttachment/issue.yml
new file mode 100644
index 0000000000..7f3255096d
--- /dev/null
+++ b/models/migrations/fixtures/Test_AddRepoIDForAttachment/issue.yml
@@ -0,0 +1,3 @@
+-
+ id: 1
+ repo_id: 1
diff --git a/models/migrations/fixtures/Test_AddRepoIDForAttachment/release.yml b/models/migrations/fixtures/Test_AddRepoIDForAttachment/release.yml
new file mode 100644
index 0000000000..7f3255096d
--- /dev/null
+++ b/models/migrations/fixtures/Test_AddRepoIDForAttachment/release.yml
@@ -0,0 +1,3 @@
+-
+ id: 1
+ repo_id: 1
diff --git a/models/migrations/fixtures/Test_RepositoryFormat/comment.yml b/models/migrations/fixtures/Test_RepositoryFormat/comment.yml
new file mode 100644
index 0000000000..1197b086e3
--- /dev/null
+++ b/models/migrations/fixtures/Test_RepositoryFormat/comment.yml
@@ -0,0 +1,3 @@
+-
+ id: 1
+ commit_sha: 19fe5caf872476db265596eaac1dc35ad1c6422d
diff --git a/models/migrations/fixtures/Test_RepositoryFormat/commit_status.yml b/models/migrations/fixtures/Test_RepositoryFormat/commit_status.yml
new file mode 100644
index 0000000000..ca0aaec4cc
--- /dev/null
+++ b/models/migrations/fixtures/Test_RepositoryFormat/commit_status.yml
@@ -0,0 +1,3 @@
+-
+ id: 1
+ context_hash: 19fe5caf872476db265596eaac1dc35ad1c6422d
diff --git a/models/migrations/fixtures/Test_RepositoryFormat/pull_request.yml b/models/migrations/fixtures/Test_RepositoryFormat/pull_request.yml
new file mode 100644
index 0000000000..380cc079ee
--- /dev/null
+++ b/models/migrations/fixtures/Test_RepositoryFormat/pull_request.yml
@@ -0,0 +1,5 @@
+-
+ id: 1
+ commit_sha: 19fe5caf872476db265596eaac1dc35ad1c6422d
+ merge_base: 19fe5caf872476db265596eaac1dc35ad1c6422d
+ merged_commit_id: 19fe5caf872476db265596eaac1dc35ad1c6422d
diff --git a/models/migrations/fixtures/Test_RepositoryFormat/release.yml b/models/migrations/fixtures/Test_RepositoryFormat/release.yml
new file mode 100644
index 0000000000..ffabe4ab9e
--- /dev/null
+++ b/models/migrations/fixtures/Test_RepositoryFormat/release.yml
@@ -0,0 +1,3 @@
+-
+ id: 1
+ sha1: 19fe5caf872476db265596eaac1dc35ad1c6422d
diff --git a/models/migrations/fixtures/Test_RepositoryFormat/repo_archiver.yml b/models/migrations/fixtures/Test_RepositoryFormat/repo_archiver.yml
new file mode 100644
index 0000000000..f04cb3b340
--- /dev/null
+++ b/models/migrations/fixtures/Test_RepositoryFormat/repo_archiver.yml
@@ -0,0 +1,3 @@
+-
+ id: 1
+ commit_id: 19fe5caf872476db265596eaac1dc35ad1c6422d
diff --git a/models/migrations/fixtures/Test_RepositoryFormat/repo_indexer_status.yml b/models/migrations/fixtures/Test_RepositoryFormat/repo_indexer_status.yml
new file mode 100644
index 0000000000..1197b086e3
--- /dev/null
+++ b/models/migrations/fixtures/Test_RepositoryFormat/repo_indexer_status.yml
@@ -0,0 +1,3 @@
+-
+ id: 1
+ commit_sha: 19fe5caf872476db265596eaac1dc35ad1c6422d
diff --git a/models/migrations/fixtures/Test_RepositoryFormat/review_state.yml b/models/migrations/fixtures/Test_RepositoryFormat/review_state.yml
new file mode 100644
index 0000000000..1197b086e3
--- /dev/null
+++ b/models/migrations/fixtures/Test_RepositoryFormat/review_state.yml
@@ -0,0 +1,3 @@
+-
+ id: 1
+ commit_sha: 19fe5caf872476db265596eaac1dc35ad1c6422d
diff --git a/models/migrations/fixtures/Test_UpdateBadgeColName/badge.yml b/models/migrations/fixtures/Test_UpdateBadgeColName/badge.yml
new file mode 100644
index 0000000000..7025144106
--- /dev/null
+++ b/models/migrations/fixtures/Test_UpdateBadgeColName/badge.yml
@@ -0,0 +1,4 @@
+-
+ id: 1
+ description: the badge
+ image_url: https://gitea.com/myimage.png
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 516eb53f62..ce77432db4 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -560,6 +560,12 @@ var migrations = []Migration{
NewMigration("Add support for SHA256 git repositories", v1_22.AdjustDBForSha256),
// v287 -> v288
NewMigration("Use Slug instead of ID for Badges", v1_22.UseSlugInsteadOfIDForBadges),
+ // v288 -> v289
+ NewMigration("Add user_blocking table", v1_22.AddUserBlockingTable),
+ // v289 -> v290
+ NewMigration("Add default_wiki_branch to repository table", v1_22.AddDefaultWikiBranch),
+ // v290 -> v291
+ NewMigration("Add PayloadVersion to HookTask", v1_22.AddPayloadVersionToHookTaskTable),
}
// GetCurrentDBVersion returns the current db version
diff --git a/models/migrations/v1_16/v193_test.go b/models/migrations/v1_16/v193_test.go
index 17669a012e..d99bbc2962 100644
--- a/models/migrations/v1_16/v193_test.go
+++ b/models/migrations/v1_16/v193_test.go
@@ -15,7 +15,6 @@ func Test_AddRepoIDForAttachment(t *testing.T) {
type Attachment struct {
ID int64 `xorm:"pk autoincr"`
UUID string `xorm:"uuid UNIQUE"`
- RepoID int64 `xorm:"INDEX"` // this should not be zero
IssueID int64 `xorm:"INDEX"` // maybe zero when creating
ReleaseID int64 `xorm:"INDEX"` // maybe zero when creating
UploaderID int64 `xorm:"INDEX DEFAULT 0"`
@@ -44,12 +43,21 @@ func Test_AddRepoIDForAttachment(t *testing.T) {
return
}
- var issueAttachments []*Attachment
- err := x.Where("issue_id > 0").Find(&issueAttachments)
+ type NewAttachment struct {
+ ID int64 `xorm:"pk autoincr"`
+ UUID string `xorm:"uuid UNIQUE"`
+ RepoID int64 `xorm:"INDEX"` // this should not be zero
+ IssueID int64 `xorm:"INDEX"` // maybe zero when creating
+ ReleaseID int64 `xorm:"INDEX"` // maybe zero when creating
+ UploaderID int64 `xorm:"INDEX DEFAULT 0"`
+ }
+
+ var issueAttachments []*NewAttachment
+ err := x.Table("attachment").Where("issue_id > 0").Find(&issueAttachments)
assert.NoError(t, err)
for _, attach := range issueAttachments {
- assert.Greater(t, attach.RepoID, 0)
- assert.Greater(t, attach.IssueID, 0)
+ assert.Greater(t, attach.RepoID, int64(0))
+ assert.Greater(t, attach.IssueID, int64(0))
var issue Issue
has, err := x.ID(attach.IssueID).Get(&issue)
assert.NoError(t, err)
@@ -57,12 +65,12 @@ func Test_AddRepoIDForAttachment(t *testing.T) {
assert.EqualValues(t, attach.RepoID, issue.RepoID)
}
- var releaseAttachments []*Attachment
- err = x.Where("release_id > 0").Find(&releaseAttachments)
+ var releaseAttachments []*NewAttachment
+ err = x.Table("attachment").Where("release_id > 0").Find(&releaseAttachments)
assert.NoError(t, err)
for _, attach := range releaseAttachments {
- assert.Greater(t, attach.RepoID, 0)
- assert.Greater(t, attach.IssueID, 0)
+ assert.Greater(t, attach.RepoID, int64(0))
+ assert.Greater(t, attach.ReleaseID, int64(0))
var release Release
has, err := x.ID(attach.ReleaseID).Get(&release)
assert.NoError(t, err)
diff --git a/models/migrations/v1_22/v283.go b/models/migrations/v1_22/v283.go
index b2b94845d9..0a45c51245 100644
--- a/models/migrations/v1_22/v283.go
+++ b/models/migrations/v1_22/v283.go
@@ -4,7 +4,10 @@
package v1_22 //nolint
import (
+ "fmt"
+
"xorm.io/xorm"
+ "xorm.io/xorm/schemas"
)
func AddCombinedIndexToIssueUser(x *xorm.Engine) error {
@@ -20,8 +23,18 @@ func AddCombinedIndexToIssueUser(x *xorm.Engine) error {
return err
}
for _, issueUser := range duplicatedIssueUsers {
- if _, err := x.Exec("delete from issue_user where id in (SELECT id FROM issue_user WHERE issue_id = ? and uid = ? limit ?)", issueUser.IssueID, issueUser.UID, issueUser.Cnt-1); err != nil {
- return err
+ if x.Dialect().URI().DBType == schemas.MSSQL {
+ if _, err := x.Exec(fmt.Sprintf("delete from issue_user where id in (SELECT top %d id FROM issue_user WHERE issue_id = ? and uid = ?)", issueUser.Cnt-1), issueUser.IssueID, issueUser.UID); err != nil {
+ return err
+ }
+ } else {
+ var ids []int64
+ if err := x.SQL("SELECT id FROM issue_user WHERE issue_id = ? and uid = ? limit ?", issueUser.IssueID, issueUser.UID, issueUser.Cnt-1).Find(&ids); err != nil {
+ return err
+ }
+ if _, err := x.Table("issue_user").In("id", ids).Delete(); err != nil {
+ return err
+ }
}
}
diff --git a/models/migrations/v1_22/v286.go b/models/migrations/v1_22/v286.go
index ef19f64221..fbbd87344f 100644
--- a/models/migrations/v1_22/v286.go
+++ b/models/migrations/v1_22/v286.go
@@ -36,9 +36,9 @@ func expandHashReferencesToSha256(x *xorm.Engine) error {
if setting.Database.Type.IsMSSQL() {
// drop indexes that need to be re-created afterwards
droppedIndexes := []string{
- "DROP INDEX commit_status.IDX_commit_status_context_hash",
- "DROP INDEX review_state.UQE_review_state_pull_commit_user",
- "DROP INDEX repo_archiver.UQE_repo_archiver_s",
+ "DROP INDEX IF EXISTS [IDX_commit_status_context_hash] ON [commit_status]",
+ "DROP INDEX IF EXISTS [UQE_review_state_pull_commit_user] ON [review_state]",
+ "DROP INDEX IF EXISTS [UQE_repo_archiver_s] ON [repo_archiver]",
}
for _, s := range droppedIndexes {
_, err := db.Exec(s)
@@ -53,7 +53,7 @@ func expandHashReferencesToSha256(x *xorm.Engine) error {
if setting.Database.Type.IsMySQL() {
_, err = db.Exec(fmt.Sprintf("ALTER TABLE `%s` MODIFY COLUMN `%s` VARCHAR(64)", alts[0], alts[1]))
} else if setting.Database.Type.IsMSSQL() {
- _, err = db.Exec(fmt.Sprintf("ALTER TABLE `%s` ALTER COLUMN `%s` VARCHAR(64)", alts[0], alts[1]))
+ _, err = db.Exec(fmt.Sprintf("ALTER TABLE [%s] ALTER COLUMN [%s] VARCHAR(64)", alts[0], alts[1]))
} else {
_, err = db.Exec(fmt.Sprintf("ALTER TABLE `%s` ALTER COLUMN `%s` TYPE VARCHAR(64)", alts[0], alts[1]))
}
diff --git a/models/migrations/v1_22/v286_test.go b/models/migrations/v1_22/v286_test.go
index e36a18a116..7c353747e3 100644
--- a/models/migrations/v1_22/v286_test.go
+++ b/models/migrations/v1_22/v286_test.go
@@ -17,14 +17,72 @@ func PrepareOldRepository(t *testing.T) (*xorm.Engine, func()) {
ID int64 `xorm:"pk autoincr"`
}
+ type CommitStatus struct {
+ ID int64
+ ContextHash string
+ }
+
+ type RepoArchiver struct {
+ ID int64
+ RepoID int64
+ Type int
+ CommitID string
+ }
+
+ type ReviewState struct {
+ ID int64
+ CommitSHA string
+ UserID int64
+ PullID int64
+ }
+
+ type Comment struct {
+ ID int64
+ CommitSHA string
+ }
+
+ type PullRequest struct {
+ ID int64
+ CommitSHA string
+ MergeBase string
+ MergedCommitID string
+ }
+
+ type Release struct {
+ ID int64
+ Sha1 string
+ }
+
+ type RepoIndexerStatus struct {
+ ID int64
+ CommitSHA string
+ }
+
+ type Review struct {
+ ID int64
+ CommitID string
+ }
+
// Prepare and load the testing database
- return base.PrepareTestEnv(t, 0, new(Repository))
+ return base.PrepareTestEnv(t, 0,
+ new(Repository),
+ new(CommitStatus),
+ new(RepoArchiver),
+ new(ReviewState),
+ new(Review),
+ new(Comment),
+ new(PullRequest),
+ new(Release),
+ new(RepoIndexerStatus),
+ )
}
func Test_RepositoryFormat(t *testing.T) {
x, deferable := PrepareOldRepository(t)
defer deferable()
+ assert.NoError(t, AdjustDBForSha256(x))
+
type Repository struct {
ID int64 `xorm:"pk autoincr"`
ObjectFormatName string `xorg:"not null default('sha1')"`
@@ -37,12 +95,10 @@ func Test_RepositoryFormat(t *testing.T) {
assert.NoError(t, err)
assert.EqualValues(t, 4, count)
- assert.NoError(t, AdjustDBForSha256(x))
-
- repo.ID = 20
repo.ObjectFormatName = "sha256"
_, err = x.Insert(repo)
assert.NoError(t, err)
+ id := repo.ID
count, err = x.Count(new(Repository))
assert.NoError(t, err)
@@ -55,7 +111,7 @@ func Test_RepositoryFormat(t *testing.T) {
assert.EqualValues(t, "sha1", repo.ObjectFormatName)
repo = new(Repository)
- ok, err = x.ID(20).Get(repo)
+ ok, err = x.ID(id).Get(repo)
assert.NoError(t, err)
assert.EqualValues(t, true, ok)
assert.EqualValues(t, "sha256", repo.ObjectFormatName)
diff --git a/models/migrations/v1_22/v287_test.go b/models/migrations/v1_22/v287_test.go
index 19c7ae3b91..9c7b10947d 100644
--- a/models/migrations/v1_22/v287_test.go
+++ b/models/migrations/v1_22/v287_test.go
@@ -20,20 +20,20 @@ func Test_UpdateBadgeColName(t *testing.T) {
}
// Prepare and load the testing database
- x, deferable := base.PrepareTestEnv(t, 0, new(BadgeUnique), new(Badge))
+ x, deferable := base.PrepareTestEnv(t, 0, new(Badge))
defer deferable()
if x == nil || t.Failed() {
return
}
- oldBadges := []Badge{
- {ID: 1, Description: "Test Badge 1", ImageURL: "https://example.com/badge1.png"},
- {ID: 2, Description: "Test Badge 2", ImageURL: "https://example.com/badge2.png"},
- {ID: 3, Description: "Test Badge 3", ImageURL: "https://example.com/badge3.png"},
+ oldBadges := []*Badge{
+ {Description: "Test Badge 1", ImageURL: "https://example.com/badge1.png"},
+ {Description: "Test Badge 2", ImageURL: "https://example.com/badge2.png"},
+ {Description: "Test Badge 3", ImageURL: "https://example.com/badge3.png"},
}
for _, badge := range oldBadges {
- _, err := x.Insert(&badge)
+ _, err := x.Insert(badge)
assert.NoError(t, err)
}
@@ -48,7 +48,7 @@ func Test_UpdateBadgeColName(t *testing.T) {
}
for i, e := range oldBadges {
- got := got[i]
+ got := got[i+1] // 1 is in the badge.yml
assert.Equal(t, e.ID, got.ID)
assert.Equal(t, fmt.Sprintf("%d", e.ID), got.Slug)
}
diff --git a/models/migrations/v1_22/v288.go b/models/migrations/v1_22/v288.go
new file mode 100644
index 0000000000..7c93bfcc66
--- /dev/null
+++ b/models/migrations/v1_22/v288.go
@@ -0,0 +1,26 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_22 //nolint
+
+import (
+ "code.gitea.io/gitea/modules/timeutil"
+
+ "xorm.io/xorm"
+)
+
+type Blocking struct {
+ ID int64 `xorm:"pk autoincr"`
+ BlockerID int64 `xorm:"UNIQUE(block)"`
+ BlockeeID int64 `xorm:"UNIQUE(block)"`
+ Note string
+ CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
+}
+
+func (*Blocking) TableName() string {
+ return "user_blocking"
+}
+
+func AddUserBlockingTable(x *xorm.Engine) error {
+ return x.Sync(&Blocking{})
+}
diff --git a/models/migrations/v1_22/v289.go b/models/migrations/v1_22/v289.go
new file mode 100644
index 0000000000..e2dfc48715
--- /dev/null
+++ b/models/migrations/v1_22/v289.go
@@ -0,0 +1,18 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_22 //nolint
+
+import "xorm.io/xorm"
+
+func AddDefaultWikiBranch(x *xorm.Engine) error {
+ type Repository struct {
+ ID int64
+ DefaultWikiBranch string
+ }
+ if err := x.Sync(&Repository{}); err != nil {
+ return err
+ }
+ _, err := x.Exec("UPDATE `repository` SET default_wiki_branch = 'master' WHERE (default_wiki_branch IS NULL) OR (default_wiki_branch = '')")
+ return err
+}
diff --git a/models/migrations/v1_22/v290.go b/models/migrations/v1_22/v290.go
new file mode 100644
index 0000000000..e9b7f504ba
--- /dev/null
+++ b/models/migrations/v1_22/v290.go
@@ -0,0 +1,17 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_22 //nolint
+
+import (
+ "xorm.io/xorm"
+)
+
+type HookTask struct {
+ PayloadVersion int `xorm:"DEFAULT 1"`
+}
+
+func AddPayloadVersionToHookTaskTable(x *xorm.Engine) error {
+ // create missing column
+ return x.Sync(new(HookTask))
+}
diff --git a/models/org.go b/models/org.go
index 5f61f05b16..69cc47137e 100644
--- a/models/org.go
+++ b/models/org.go
@@ -12,15 +12,16 @@ import (
"code.gitea.io/gitea/models/organization"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
+ user_model "code.gitea.io/gitea/models/user"
)
// RemoveOrgUser removes user from given organization.
-func RemoveOrgUser(ctx context.Context, orgID, userID int64) error {
+func RemoveOrgUser(ctx context.Context, org *organization.Organization, user *user_model.User) error {
ou := new(organization.OrgUser)
has, err := db.GetEngine(ctx).
- Where("uid=?", userID).
- And("org_id=?", orgID).
+ Where("uid=?", user.ID).
+ And("org_id=?", org.ID).
Get(ou)
if err != nil {
return fmt.Errorf("get org-user: %w", err)
@@ -28,13 +29,8 @@ func RemoveOrgUser(ctx context.Context, orgID, userID int64) error {
return nil
}
- org, err := organization.GetOrgByID(ctx, orgID)
- if err != nil {
- return fmt.Errorf("GetUserByID [%d]: %w", orgID, err)
- }
-
// Check if the user to delete is the last member in owner team.
- if isOwner, err := organization.IsOrganizationOwner(ctx, orgID, userID); err != nil {
+ if isOwner, err := organization.IsOrganizationOwner(ctx, org.ID, user.ID); err != nil {
return err
} else if isOwner {
t, err := organization.GetOwnerTeam(ctx, org.ID)
@@ -45,8 +41,8 @@ func RemoveOrgUser(ctx context.Context, orgID, userID int64) error {
if err := t.LoadMembers(ctx); err != nil {
return err
}
- if t.Members[0].ID == userID {
- return organization.ErrLastOrgOwner{UID: userID}
+ if t.Members[0].ID == user.ID {
+ return organization.ErrLastOrgOwner{UID: user.ID}
}
}
}
@@ -59,28 +55,32 @@ func RemoveOrgUser(ctx context.Context, orgID, userID int64) error {
if _, err := db.DeleteByID[organization.OrgUser](ctx, ou.ID); err != nil {
return err
- } else if _, err = db.Exec(ctx, "UPDATE `user` SET num_members=num_members-1 WHERE id=?", orgID); err != nil {
+ } else if _, err = db.Exec(ctx, "UPDATE `user` SET num_members=num_members-1 WHERE id=?", org.ID); err != nil {
return err
}
// Delete all repository accesses and unwatch them.
- env, err := organization.AccessibleReposEnv(ctx, org, userID)
+ env, err := organization.AccessibleReposEnv(ctx, org, user.ID)
if err != nil {
return fmt.Errorf("AccessibleReposEnv: %w", err)
}
repoIDs, err := env.RepoIDs(1, org.NumRepos)
if err != nil {
- return fmt.Errorf("GetUserRepositories [%d]: %w", userID, err)
+ return fmt.Errorf("GetUserRepositories [%d]: %w", user.ID, err)
}
for _, repoID := range repoIDs {
- if err = repo_model.WatchRepo(ctx, userID, repoID, false); err != nil {
+ repo, err := repo_model.GetRepositoryByID(ctx, repoID)
+ if err != nil {
+ return err
+ }
+ if err = repo_model.WatchRepo(ctx, user, repo, false); err != nil {
return err
}
}
if len(repoIDs) > 0 {
if _, err = db.GetEngine(ctx).
- Where("user_id = ?", userID).
+ Where("user_id = ?", user.ID).
In("repo_id", repoIDs).
Delete(new(access_model.Access)); err != nil {
return err
@@ -88,12 +88,12 @@ func RemoveOrgUser(ctx context.Context, orgID, userID int64) error {
}
// Delete member in their teams.
- teams, err := organization.GetUserOrgTeams(ctx, org.ID, userID)
+ teams, err := organization.GetUserOrgTeams(ctx, org.ID, user.ID)
if err != nil {
return err
}
for _, t := range teams {
- if err = removeTeamMember(ctx, t, userID); err != nil {
+ if err = removeTeamMember(ctx, t, user); err != nil {
return err
}
}
diff --git a/models/org_team.go b/models/org_team.go
index 1a452436c3..aecf0d80fd 100644
--- a/models/org_team.go
+++ b/models/org_team.go
@@ -44,7 +44,7 @@ func AddRepository(ctx context.Context, t *organization.Team, repo *repo_model.R
return fmt.Errorf("getMembers: %w", err)
}
for _, u := range t.Members {
- if err = repo_model.WatchRepo(ctx, u.ID, repo.ID, true); err != nil {
+ if err = repo_model.WatchRepo(ctx, u, repo, true); err != nil {
return fmt.Errorf("watchRepo: %w", err)
}
}
@@ -125,7 +125,7 @@ func removeAllRepositories(ctx context.Context, t *organization.Team) (err error
continue
}
- if err = repo_model.WatchRepo(ctx, user.ID, repo.ID, false); err != nil {
+ if err = repo_model.WatchRepo(ctx, user, repo, false); err != nil {
return err
}
@@ -341,7 +341,7 @@ func DeleteTeam(ctx context.Context, t *organization.Team) error {
}
for _, tm := range t.Members {
- if err := removeInvalidOrgUser(ctx, tm.ID, t.OrgID); err != nil {
+ if err := removeInvalidOrgUser(ctx, t.OrgID, tm); err != nil {
return err
}
}
@@ -356,19 +356,23 @@ func DeleteTeam(ctx context.Context, t *organization.Team) error {
// AddTeamMember adds new membership of given team to given organization,
// the user will have membership to given organization automatically when needed.
-func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) error {
- isAlreadyMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, userID)
+func AddTeamMember(ctx context.Context, team *organization.Team, user *user_model.User) error {
+ if user_model.IsUserBlockedBy(ctx, user, team.OrgID) {
+ return user_model.ErrBlockedUser
+ }
+
+ isAlreadyMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, user.ID)
if err != nil || isAlreadyMember {
return err
}
- if err := organization.AddOrgUser(ctx, team.OrgID, userID); err != nil {
+ if err := organization.AddOrgUser(ctx, team.OrgID, user.ID); err != nil {
return err
}
err = db.WithTx(ctx, func(ctx context.Context) error {
// check in transaction
- isAlreadyMember, err = organization.IsTeamMember(ctx, team.OrgID, team.ID, userID)
+ isAlreadyMember, err = organization.IsTeamMember(ctx, team.OrgID, team.ID, user.ID)
if err != nil || isAlreadyMember {
return err
}
@@ -376,7 +380,7 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e
sess := db.GetEngine(ctx)
if err := db.Insert(ctx, &organization.TeamUser{
- UID: userID,
+ UID: user.ID,
OrgID: team.OrgID,
TeamID: team.ID,
}); err != nil {
@@ -392,7 +396,7 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e
subQuery := builder.Select("repo_id").From("team_repo").
Where(builder.Eq{"team_id": team.ID})
- if _, err := sess.Where("user_id=?", userID).
+ if _, err := sess.Where("user_id=?", user.ID).
In("repo_id", subQuery).
And("mode < ?", team.AccessMode).
SetExpr("mode", team.AccessMode).
@@ -402,14 +406,14 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e
// for not exist access
var repoIDs []int64
- accessSubQuery := builder.Select("repo_id").From("access").Where(builder.Eq{"user_id": userID})
+ accessSubQuery := builder.Select("repo_id").From("access").Where(builder.Eq{"user_id": user.ID})
if err := sess.SQL(subQuery.And(builder.NotIn("repo_id", accessSubQuery))).Find(&repoIDs); err != nil {
return fmt.Errorf("select id accesses: %w", err)
}
accesses := make([]*access_model.Access, 0, 100)
for i, repoID := range repoIDs {
- accesses = append(accesses, &access_model.Access{RepoID: repoID, UserID: userID, Mode: team.AccessMode})
+ accesses = append(accesses, &access_model.Access{RepoID: repoID, UserID: user.ID, Mode: team.AccessMode})
if (i%100 == 0 || i == len(repoIDs)-1) && len(accesses) > 0 {
if err = db.Insert(ctx, accesses); err != nil {
return fmt.Errorf("insert new user accesses: %w", err)
@@ -430,10 +434,11 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e
if err := team.LoadRepositories(ctx); err != nil {
log.Error("team.LoadRepositories failed: %v", err)
}
+
// FIXME: in the goroutine, it can't access the "ctx", it could only use db.DefaultContext at the moment
go func(repos []*repo_model.Repository) {
for _, repo := range repos {
- if err = repo_model.WatchRepo(db.DefaultContext, userID, repo.ID, true); err != nil {
+ if err = repo_model.WatchRepo(db.DefaultContext, user, repo, true); err != nil {
log.Error("watch repo failed: %v", err)
}
}
@@ -443,16 +448,16 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e
return nil
}
-func removeTeamMember(ctx context.Context, team *organization.Team, userID int64) error {
+func removeTeamMember(ctx context.Context, team *organization.Team, user *user_model.User) error {
e := db.GetEngine(ctx)
- isMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, userID)
+ isMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, user.ID)
if err != nil || !isMember {
return err
}
// Check if the user to delete is the last member in owner team.
if team.IsOwnerTeam() && team.NumMembers == 1 {
- return organization.ErrLastOrgOwner{UID: userID}
+ return organization.ErrLastOrgOwner{UID: user.ID}
}
team.NumMembers--
@@ -462,7 +467,7 @@ func removeTeamMember(ctx context.Context, team *organization.Team, userID int64
}
if _, err := e.Delete(&organization.TeamUser{
- UID: userID,
+ UID: user.ID,
OrgID: team.OrgID,
TeamID: team.ID,
}); err != nil {
@@ -476,76 +481,76 @@ func removeTeamMember(ctx context.Context, team *organization.Team, userID int64
// Delete access to team repositories.
for _, repo := range team.Repos {
- if err := access_model.RecalculateUserAccess(ctx, repo, userID); err != nil {
+ if err := access_model.RecalculateUserAccess(ctx, repo, user.ID); err != nil {
return err
}
// Remove watches from now unaccessible
- if err := ReconsiderWatches(ctx, repo, userID); err != nil {
+ if err := ReconsiderWatches(ctx, repo, user); err != nil {
return err
}
// Remove issue assignments from now unaccessible
- if err := ReconsiderRepoIssuesAssignee(ctx, repo, userID); err != nil {
+ if err := ReconsiderRepoIssuesAssignee(ctx, repo, user); err != nil {
return err
}
}
- return removeInvalidOrgUser(ctx, userID, team.OrgID)
+ return removeInvalidOrgUser(ctx, team.OrgID, user)
}
-func removeInvalidOrgUser(ctx context.Context, userID, orgID int64) error {
+func removeInvalidOrgUser(ctx context.Context, orgID int64, user *user_model.User) error {
// Check if the user is a member of any team in the organization.
if count, err := db.GetEngine(ctx).Count(&organization.TeamUser{
- UID: userID,
+ UID: user.ID,
OrgID: orgID,
}); err != nil {
return err
} else if count == 0 {
- return RemoveOrgUser(ctx, orgID, userID)
+ org, err := organization.GetOrgByID(ctx, orgID)
+ if err != nil {
+ return err
+ }
+
+ return RemoveOrgUser(ctx, org, user)
}
return nil
}
// RemoveTeamMember removes member from given team of given organization.
-func RemoveTeamMember(ctx context.Context, team *organization.Team, userID int64) error {
+func RemoveTeamMember(ctx context.Context, team *organization.Team, user *user_model.User) error {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return err
}
defer committer.Close()
- if err := removeTeamMember(ctx, team, userID); err != nil {
+ if err := removeTeamMember(ctx, team, user); err != nil {
return err
}
return committer.Commit()
}
-func ReconsiderRepoIssuesAssignee(ctx context.Context, repo *repo_model.Repository, uid int64) error {
- user, err := user_model.GetUserByID(ctx, uid)
- if err != nil {
- return err
- }
-
+func ReconsiderRepoIssuesAssignee(ctx context.Context, repo *repo_model.Repository, user *user_model.User) error {
if canAssigned, err := access_model.CanBeAssigned(ctx, user, repo, true); err != nil || canAssigned {
return err
}
- if _, err := db.GetEngine(ctx).Where(builder.Eq{"assignee_id": uid}).
+ if _, err := db.GetEngine(ctx).Where(builder.Eq{"assignee_id": user.ID}).
In("issue_id", builder.Select("id").From("issue").Where(builder.Eq{"repo_id": repo.ID})).
Delete(&issues_model.IssueAssignees{}); err != nil {
- return fmt.Errorf("Could not delete assignee[%d] %w", uid, err)
+ return fmt.Errorf("Could not delete assignee[%d] %w", user.ID, err)
}
return nil
}
-func ReconsiderWatches(ctx context.Context, repo *repo_model.Repository, uid int64) error {
- if has, err := access_model.HasAccess(ctx, uid, repo); err != nil || has {
+func ReconsiderWatches(ctx context.Context, repo *repo_model.Repository, user *user_model.User) error {
+ if has, err := access_model.HasAccess(ctx, user.ID, repo); err != nil || has {
return err
}
- if err := repo_model.WatchRepo(ctx, uid, repo.ID, false); err != nil {
+ if err := repo_model.WatchRepo(ctx, user, repo, false); err != nil {
return err
}
// Remove all IssueWatches a user has subscribed to in the repository
- return issues_model.RemoveIssueWatchersByRepoID(ctx, uid, repo.ID)
+ return issues_model.RemoveIssueWatchersByRepoID(ctx, user.ID, repo.ID)
}
diff --git a/models/org_team_test.go b/models/org_team_test.go
index e4b7b917e8..cf2c8be536 100644
--- a/models/org_team_test.go
+++ b/models/org_team_test.go
@@ -21,33 +21,42 @@ import (
func TestTeam_AddMember(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- test := func(teamID, userID int64) {
- team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
- assert.NoError(t, AddTeamMember(db.DefaultContext, team, userID))
- unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: userID, TeamID: teamID})
- unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID}, &user_model.User{ID: team.OrgID})
+ test := func(team *organization.Team, user *user_model.User) {
+ assert.NoError(t, AddTeamMember(db.DefaultContext, team, user))
+ unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID})
+ unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID}, &user_model.User{ID: team.OrgID})
}
- test(1, 2)
- test(1, 4)
- test(3, 2)
+
+ team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
+ team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3})
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+
+ test(team1, user2)
+ test(team1, user4)
+ test(team3, user2)
}
func TestTeam_RemoveMember(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- testSuccess := func(teamID, userID int64) {
- team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
- assert.NoError(t, RemoveTeamMember(db.DefaultContext, team, userID))
- unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: userID, TeamID: teamID})
- unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID})
+ testSuccess := func(team *organization.Team, user *user_model.User) {
+ assert.NoError(t, RemoveTeamMember(db.DefaultContext, team, user))
+ unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID})
+ unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID})
}
- testSuccess(1, 4)
- testSuccess(2, 2)
- testSuccess(3, 2)
- testSuccess(3, unittest.NonexistentID)
- team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
- err := RemoveTeamMember(db.DefaultContext, team, 2)
+ team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
+ team2 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
+ team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3})
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+
+ testSuccess(team1, user4)
+ testSuccess(team2, user2)
+ testSuccess(team3, user2)
+
+ err := RemoveTeamMember(db.DefaultContext, team1, user2)
assert.True(t, organization.IsErrLastOrgOwner(err))
}
@@ -120,33 +129,42 @@ func TestDeleteTeam(t *testing.T) {
func TestAddTeamMember(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- test := func(teamID, userID int64) {
- team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
- assert.NoError(t, AddTeamMember(db.DefaultContext, team, userID))
- unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: userID, TeamID: teamID})
- unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID}, &user_model.User{ID: team.OrgID})
+ test := func(team *organization.Team, user *user_model.User) {
+ assert.NoError(t, AddTeamMember(db.DefaultContext, team, user))
+ unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID})
+ unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID}, &user_model.User{ID: team.OrgID})
}
- test(1, 2)
- test(1, 4)
- test(3, 2)
+
+ team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
+ team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3})
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+
+ test(team1, user2)
+ test(team1, user4)
+ test(team3, user2)
}
func TestRemoveTeamMember(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- testSuccess := func(teamID, userID int64) {
- team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
- assert.NoError(t, RemoveTeamMember(db.DefaultContext, team, userID))
- unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: userID, TeamID: teamID})
- unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID})
+ testSuccess := func(team *organization.Team, user *user_model.User) {
+ assert.NoError(t, RemoveTeamMember(db.DefaultContext, team, user))
+ unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID})
+ unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID})
}
- testSuccess(1, 4)
- testSuccess(2, 2)
- testSuccess(3, 2)
- testSuccess(3, unittest.NonexistentID)
- team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
- err := RemoveTeamMember(db.DefaultContext, team, 2)
+ team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
+ team2 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
+ team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3})
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+
+ testSuccess(team1, user4)
+ testSuccess(team2, user2)
+ testSuccess(team3, user2)
+
+ err := RemoveTeamMember(db.DefaultContext, team1, user2)
assert.True(t, organization.IsErrLastOrgOwner(err))
}
@@ -155,15 +173,15 @@ func TestRepository_RecalculateAccesses3(t *testing.T) {
team5 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 5})
user29 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29})
- has, err := db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: 29, RepoID: 23})
+ has, err := db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: user29.ID, RepoID: 23})
assert.NoError(t, err)
assert.False(t, has)
// adding user29 to team5 should add an explicit access row for repo 23
// even though repo 23 is public
- assert.NoError(t, AddTeamMember(db.DefaultContext, team5, user29.ID))
+ assert.NoError(t, AddTeamMember(db.DefaultContext, team5, user29))
- has, err = db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: 29, RepoID: 23})
+ has, err = db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: user29.ID, RepoID: 23})
assert.NoError(t, err)
assert.True(t, has)
}
diff --git a/models/org_test.go b/models/org_test.go
index d10a1dc218..247530406d 100644
--- a/models/org_test.go
+++ b/models/org_test.go
@@ -16,22 +16,27 @@ import (
func TestUser_RemoveMember(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
+
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+ user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
// remove a user that is a member
- unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{UID: 4, OrgID: 3})
+ unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{UID: user4.ID, OrgID: org.ID})
prevNumMembers := org.NumMembers
- assert.NoError(t, RemoveOrgUser(db.DefaultContext, org.ID, 4))
- unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: 4, OrgID: 3})
- org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
+ assert.NoError(t, RemoveOrgUser(db.DefaultContext, org, user4))
+ unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: user4.ID, OrgID: org.ID})
+
+ org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: org.ID})
assert.Equal(t, prevNumMembers-1, org.NumMembers)
// remove a user that is not a member
- unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: 5, OrgID: 3})
+ unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: user5.ID, OrgID: org.ID})
prevNumMembers = org.NumMembers
- assert.NoError(t, RemoveOrgUser(db.DefaultContext, org.ID, 5))
- unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: 5, OrgID: 3})
- org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
+ assert.NoError(t, RemoveOrgUser(db.DefaultContext, org, user5))
+ unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: user5.ID, OrgID: org.ID})
+
+ org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: org.ID})
assert.Equal(t, prevNumMembers, org.NumMembers)
unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{})
@@ -39,23 +44,31 @@ func TestUser_RemoveMember(t *testing.T) {
func TestRemoveOrgUser(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- testSuccess := func(orgID, userID int64) {
- org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: orgID})
+
+ testSuccess := func(org *organization.Organization, user *user_model.User) {
expectedNumMembers := org.NumMembers
- if unittest.BeanExists(t, &organization.OrgUser{OrgID: orgID, UID: userID}) {
+ if unittest.BeanExists(t, &organization.OrgUser{OrgID: org.ID, UID: user.ID}) {
expectedNumMembers--
}
- assert.NoError(t, RemoveOrgUser(db.DefaultContext, orgID, userID))
- unittest.AssertNotExistsBean(t, &organization.OrgUser{OrgID: orgID, UID: userID})
- org = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: orgID})
+ assert.NoError(t, RemoveOrgUser(db.DefaultContext, org, user))
+ unittest.AssertNotExistsBean(t, &organization.OrgUser{OrgID: org.ID, UID: user.ID})
+ org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: org.ID})
assert.EqualValues(t, expectedNumMembers, org.NumMembers)
}
- testSuccess(3, 4)
- testSuccess(3, 4)
- err := RemoveOrgUser(db.DefaultContext, 7, 5)
+ org3 := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
+ org7 := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 7})
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+ user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
+
+ testSuccess(org3, user4)
+
+ org3 = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
+ testSuccess(org3, user4)
+
+ err := RemoveOrgUser(db.DefaultContext, org7, user5)
assert.Error(t, err)
assert.True(t, organization.IsErrLastOrgOwner(err))
- unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{OrgID: 7, UID: 5})
+ unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{OrgID: org7.ID, UID: user5.ID})
unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{})
}
diff --git a/models/organization/org.go b/models/organization/org.go
index b4919defb4..a3082e9ac7 100644
--- a/models/organization/org.go
+++ b/models/organization/org.go
@@ -400,6 +400,7 @@ func DeleteOrganization(ctx context.Context, org *Organization) error {
&TeamUnit{OrgID: org.ID},
&TeamInvite{OrgID: org.ID},
&secret_model.Secret{OwnerID: org.ID},
+ &user_model.Blocking{BlockerID: org.ID},
); err != nil {
return fmt.Errorf("DeleteBeans: %w", err)
}
diff --git a/models/organization/team_user.go b/models/organization/team_user.go
index ab767db200..d6d0a5054d 100644
--- a/models/organization/team_user.go
+++ b/models/organization/team_user.go
@@ -30,14 +30,6 @@ func IsTeamMember(ctx context.Context, orgID, teamID, userID int64) (bool, error
Exist()
}
-// GetTeamUsersByTeamID returns team users for a team
-func GetTeamUsersByTeamID(ctx context.Context, teamID int64) ([]*TeamUser, error) {
- teamUsers := make([]*TeamUser, 0, 10)
- return teamUsers, db.GetEngine(ctx).
- Where("team_id=?", teamID).
- Find(&teamUsers)
-}
-
// SearchMembersOptions holds the search options
type SearchMembersOptions struct {
db.ListOptions
diff --git a/models/packages/descriptor.go b/models/packages/descriptor.go
index f849ab5c04..b8ef698d38 100644
--- a/models/packages/descriptor.go
+++ b/models/packages/descriptor.go
@@ -70,16 +70,26 @@ type PackageFileDescriptor struct {
Properties PackagePropertyList
}
-// PackageWebLink returns the package web link
+// PackageWebLink returns the relative package web link
func (pd *PackageDescriptor) PackageWebLink() string {
return fmt.Sprintf("%s/-/packages/%s/%s", pd.Owner.HomeLink(), string(pd.Package.Type), url.PathEscape(pd.Package.LowerName))
}
-// FullWebLink returns the package version web link
-func (pd *PackageDescriptor) FullWebLink() string {
+// VersionWebLink returns the relative package version web link
+func (pd *PackageDescriptor) VersionWebLink() string {
return fmt.Sprintf("%s/%s", pd.PackageWebLink(), url.PathEscape(pd.Version.LowerVersion))
}
+// PackageHTMLURL returns the absolute package HTML URL
+func (pd *PackageDescriptor) PackageHTMLURL() string {
+ return fmt.Sprintf("%s/-/packages/%s/%s", pd.Owner.HTMLURL(), string(pd.Package.Type), url.PathEscape(pd.Package.LowerName))
+}
+
+// VersionHTMLURL returns the absolute package version HTML URL
+func (pd *PackageDescriptor) VersionHTMLURL() string {
+ return fmt.Sprintf("%s/%s", pd.PackageHTMLURL(), url.PathEscape(pd.Version.LowerVersion))
+}
+
// CalculateBlobSize returns the total blobs size in bytes
func (pd *PackageDescriptor) CalculateBlobSize() int64 {
size := int64(0)
diff --git a/models/packages/nuget/search.go b/models/packages/nuget/search.go
index 53cdf2d4ad..7a505ff08f 100644
--- a/models/packages/nuget/search.go
+++ b/models/packages/nuget/search.go
@@ -55,7 +55,7 @@ func CountPackages(ctx context.Context, opts *packages_model.PackageSearchOption
func toConds(opts *packages_model.PackageSearchOptions) builder.Cond {
var cond builder.Cond = builder.Eq{
- "package.is_internal": opts.IsInternal.IsTrue(),
+ "package.is_internal": opts.IsInternal.Value(),
"package.owner_id": opts.OwnerID,
"package.type": packages_model.TypeNuGet,
}
diff --git a/models/packages/package_version.go b/models/packages/package_version.go
index 8fc475691b..505dbaa0a5 100644
--- a/models/packages/package_version.go
+++ b/models/packages/package_version.go
@@ -9,6 +9,7 @@ import (
"strings"
"code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
@@ -105,7 +106,7 @@ func getVersionByNameAndVersion(ctx context.Context, ownerID int64, packageType
ExactMatch: true,
Value: version,
},
- IsInternal: util.OptionalBoolOf(isInternal),
+ IsInternal: optional.Some(isInternal),
Paginator: db.NewAbsoluteListOptions(0, 1),
})
if err != nil {
@@ -122,7 +123,7 @@ func GetVersionsByPackageType(ctx context.Context, ownerID int64, packageType Ty
pvs, _, err := SearchVersions(ctx, &PackageSearchOptions{
OwnerID: ownerID,
Type: packageType,
- IsInternal: util.OptionalBoolFalse,
+ IsInternal: optional.Some(false),
})
return pvs, err
}
@@ -136,7 +137,7 @@ func GetVersionsByPackageName(ctx context.Context, ownerID int64, packageType Ty
ExactMatch: true,
Value: name,
},
- IsInternal: util.OptionalBoolFalse,
+ IsInternal: optional.Some(false),
})
return pvs, err
}
@@ -182,18 +183,18 @@ type PackageSearchOptions struct {
Name SearchValue // only results with the specific name are found
Version SearchValue // only results with the specific version are found
Properties map[string]string // only results are found which contain all listed version properties with the specific value
- IsInternal util.OptionalBool
- HasFileWithName string // only results are found which are associated with a file with the specific name
- HasFiles util.OptionalBool // only results are found which have associated files
+ IsInternal optional.Option[bool]
+ HasFileWithName string // only results are found which are associated with a file with the specific name
+ HasFiles optional.Option[bool] // only results are found which have associated files
Sort VersionSort
db.Paginator
}
func (opts *PackageSearchOptions) ToConds() builder.Cond {
cond := builder.NewCond()
- if !opts.IsInternal.IsNone() {
+ if opts.IsInternal.Has() {
cond = builder.Eq{
- "package_version.is_internal": opts.IsInternal.IsTrue(),
+ "package_version.is_internal": opts.IsInternal.Value(),
}
}
@@ -250,10 +251,10 @@ func (opts *PackageSearchOptions) ToConds() builder.Cond {
cond = cond.And(builder.Exists(builder.Select("package_file.id").From("package_file").Where(fileCond)))
}
- if !opts.HasFiles.IsNone() {
+ if opts.HasFiles.Has() {
filesCond := builder.Exists(builder.Select("package_file.id").From("package_file").Where(builder.Expr("package_file.version_id = package_version.id")))
- if opts.HasFiles.IsFalse() {
+ if !opts.HasFiles.Value() {
filesCond = builder.Not{filesCond}
}
@@ -307,8 +308,8 @@ func SearchLatestVersions(ctx context.Context, opts *PackageSearchOptions) ([]*P
And(builder.Expr("pv2.id IS NULL"))
joinCond := builder.Expr("package_version.package_id = pv2.package_id AND (package_version.created_unix < pv2.created_unix OR (package_version.created_unix = pv2.created_unix AND package_version.id < pv2.id))")
- if !opts.IsInternal.IsNone() {
- joinCond = joinCond.And(builder.Eq{"pv2.is_internal": opts.IsInternal.IsTrue()})
+ if opts.IsInternal.Has() {
+ joinCond = joinCond.And(builder.Eq{"pv2.is_internal": opts.IsInternal.Value()})
}
sess := db.GetEngine(ctx).
diff --git a/models/perm/access/access.go b/models/perm/access/access.go
index 3e2568b4b4..b422a08614 100644
--- a/models/perm/access/access.go
+++ b/models/perm/access/access.go
@@ -128,9 +128,9 @@ func refreshAccesses(ctx context.Context, repo *repo_model.Repository, accessMap
// refreshCollaboratorAccesses retrieves repository collaborations with their access modes.
func refreshCollaboratorAccesses(ctx context.Context, repoID int64, accessMap map[int64]*userAccess) error {
- collaborators, err := repo_model.GetCollaborators(ctx, repoID, db.ListOptions{})
+ collaborators, _, err := repo_model.GetCollaborators(ctx, &repo_model.FindCollaborationOptions{RepoID: repoID})
if err != nil {
- return fmt.Errorf("getCollaborations: %w", err)
+ return fmt.Errorf("GetCollaborators: %w", err)
}
for _, c := range collaborators {
if c.User.IsGhost() {
diff --git a/models/project/board.go b/models/project/board.go
index 3e2d8e0472..c0e6529880 100644
--- a/models/project/board.go
+++ b/models/project/board.go
@@ -232,7 +232,7 @@ func UpdateBoard(ctx context.Context, board *Board) error {
func (p *Project) GetBoards(ctx context.Context) (BoardList, error) {
boards := make([]*Board, 0, 5)
- if err := db.GetEngine(ctx).Where("project_id=? AND `default`=?", p.ID, false).OrderBy("Sorting").Find(&boards); err != nil {
+ if err := db.GetEngine(ctx).Where("project_id=? AND `default`=?", p.ID, false).OrderBy("sorting").Find(&boards); err != nil {
return nil, err
}
diff --git a/models/project/project.go b/models/project/project.go
index 42b06e58c9..8f9ee2a99e 100644
--- a/models/project/project.go
+++ b/models/project/project.go
@@ -12,6 +12,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/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 {
diff --git a/models/repo/collaboration.go b/models/repo/collaboration.go
index 7288082614..272c6ac05b 100644
--- a/models/repo/collaboration.go
+++ b/models/repo/collaboration.go
@@ -36,14 +36,44 @@ type Collaborator struct {
Collaboration *Collaboration
}
+type FindCollaborationOptions struct {
+ db.ListOptions
+ RepoID int64
+ RepoOwnerID int64
+ CollaboratorID int64
+}
+
+func (opts *FindCollaborationOptions) ToConds() builder.Cond {
+ cond := builder.NewCond()
+ if opts.RepoID != 0 {
+ cond = cond.And(builder.Eq{"collaboration.repo_id": opts.RepoID})
+ }
+ if opts.RepoOwnerID != 0 {
+ cond = cond.And(builder.Eq{"repository.owner_id": opts.RepoOwnerID})
+ }
+ if opts.CollaboratorID != 0 {
+ cond = cond.And(builder.Eq{"collaboration.user_id": opts.CollaboratorID})
+ }
+ return cond
+}
+
+func (opts *FindCollaborationOptions) ToJoins() []db.JoinFunc {
+ if opts.RepoOwnerID != 0 {
+ return []db.JoinFunc{
+ func(e db.Engine) error {
+ e.Join("INNER", "repository", "repository.id = collaboration.repo_id")
+ return nil
+ },
+ }
+ }
+ return nil
+}
+
// GetCollaborators returns the collaborators for a repository
-func GetCollaborators(ctx context.Context, repoID int64, listOptions db.ListOptions) ([]*Collaborator, error) {
- collaborations, err := db.Find[Collaboration](ctx, FindCollaborationOptions{
- ListOptions: listOptions,
- RepoID: repoID,
- })
+func GetCollaborators(ctx context.Context, opts *FindCollaborationOptions) ([]*Collaborator, int64, error) {
+ collaborations, total, err := db.FindAndCount[Collaboration](ctx, opts)
if err != nil {
- return nil, fmt.Errorf("db.Find[Collaboration]: %w", err)
+ return nil, 0, fmt.Errorf("db.FindAndCount[Collaboration]: %w", err)
}
collaborators := make([]*Collaborator, 0, len(collaborations))
@@ -54,7 +84,7 @@ func GetCollaborators(ctx context.Context, repoID int64, listOptions db.ListOpti
usersMap := make(map[int64]*user_model.User)
if err := db.GetEngine(ctx).In("id", userIDs).Find(&usersMap); err != nil {
- return nil, fmt.Errorf("Find users map by user ids: %w", err)
+ return nil, 0, fmt.Errorf("Find users map by user ids: %w", err)
}
for _, c := range collaborations {
@@ -67,7 +97,7 @@ func GetCollaborators(ctx context.Context, repoID int64, listOptions db.ListOpti
Collaboration: c,
})
}
- return collaborators, nil
+ return collaborators, total, nil
}
// GetCollaboration get collaboration for a repository id with a user id
@@ -88,15 +118,6 @@ func IsCollaborator(ctx context.Context, repoID, userID int64) (bool, error) {
return db.GetEngine(ctx).Get(&Collaboration{RepoID: repoID, UserID: userID})
}
-type FindCollaborationOptions struct {
- db.ListOptions
- RepoID int64
-}
-
-func (opts FindCollaborationOptions) ToConds() builder.Cond {
- return builder.And(builder.Eq{"repo_id": opts.RepoID})
-}
-
// ChangeCollaborationAccessMode sets new access mode for the collaboration.
func ChangeCollaborationAccessMode(ctx context.Context, repo *Repository, uid int64, mode perm.AccessMode) error {
// Discard invalid input
diff --git a/models/repo/collaboration_test.go b/models/repo/collaboration_test.go
index 21a99dd557..639050f5fd 100644
--- a/models/repo/collaboration_test.go
+++ b/models/repo/collaboration_test.go
@@ -19,7 +19,7 @@ func TestRepository_GetCollaborators(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
test := func(repoID int64) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
- collaborators, err := repo_model.GetCollaborators(db.DefaultContext, repo.ID, db.ListOptions{})
+ collaborators, _, err := repo_model.GetCollaborators(db.DefaultContext, &repo_model.FindCollaborationOptions{RepoID: repo.ID})
assert.NoError(t, err)
expectedLen, err := db.GetEngine(db.DefaultContext).Count(&repo_model.Collaboration{RepoID: repoID})
assert.NoError(t, err)
@@ -37,11 +37,17 @@ func TestRepository_GetCollaborators(t *testing.T) {
// Test db.ListOptions
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 22})
- collaborators1, err := repo_model.GetCollaborators(db.DefaultContext, repo.ID, db.ListOptions{PageSize: 1, Page: 1})
+ collaborators1, _, err := repo_model.GetCollaborators(db.DefaultContext, &repo_model.FindCollaborationOptions{
+ ListOptions: db.ListOptions{PageSize: 1, Page: 1},
+ RepoID: repo.ID,
+ })
assert.NoError(t, err)
assert.Len(t, collaborators1, 1)
- collaborators2, err := repo_model.GetCollaborators(db.DefaultContext, repo.ID, db.ListOptions{PageSize: 1, Page: 2})
+ collaborators2, _, err := repo_model.GetCollaborators(db.DefaultContext, &repo_model.FindCollaborationOptions{
+ ListOptions: db.ListOptions{PageSize: 1, Page: 2},
+ RepoID: repo.ID,
+ })
assert.NoError(t, err)
assert.Len(t, collaborators2, 1)
@@ -85,31 +91,6 @@ func TestRepository_ChangeCollaborationAccessMode(t *testing.T) {
unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID})
}
-func TestRepository_CountCollaborators(t *testing.T) {
- assert.NoError(t, unittest.PrepareTestDatabase())
-
- repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
- count, err := db.Count[repo_model.Collaboration](db.DefaultContext, repo_model.FindCollaborationOptions{
- RepoID: repo1.ID,
- })
- assert.NoError(t, err)
- assert.EqualValues(t, 2, count)
-
- repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 22})
- count, err = db.Count[repo_model.Collaboration](db.DefaultContext, repo_model.FindCollaborationOptions{
- RepoID: repo2.ID,
- })
- assert.NoError(t, err)
- assert.EqualValues(t, 2, count)
-
- // Non-existent repository.
- count, err = db.Count[repo_model.Collaboration](db.DefaultContext, repo_model.FindCollaborationOptions{
- RepoID: unittest.NonexistentID,
- })
- assert.NoError(t, err)
- assert.EqualValues(t, 0, count)
-}
-
func TestRepository_IsOwnerMemberCollaborator(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
diff --git a/models/repo/repo.go b/models/repo/repo.go
index 0eb7b1e0c9..e830b736fd 100644
--- a/models/repo/repo.go
+++ b/models/repo/repo.go
@@ -20,6 +20,7 @@ import (
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
@@ -135,6 +136,7 @@ type Repository struct {
OriginalServiceType api.GitServiceType `xorm:"index"`
OriginalURL string `xorm:"VARCHAR(2048)"`
DefaultBranch string
+ DefaultWikiBranch string
NumWatches int
NumStars int
@@ -284,6 +286,9 @@ func (repo *Repository) AfterLoad() {
repo.NumOpenMilestones = repo.NumMilestones - repo.NumClosedMilestones
repo.NumOpenProjects = repo.NumProjects - repo.NumClosedProjects
repo.NumOpenActionRuns = repo.NumActionRuns - repo.NumClosedActionRuns
+ if repo.DefaultWikiBranch == "" {
+ repo.DefaultWikiBranch = setting.Repository.DefaultBranch
+ }
}
// LoadAttributes loads attributes of the repository.
@@ -410,6 +415,13 @@ func (repo *Repository) MustGetUnit(ctx context.Context, tp unit.Type) *RepoUnit
Type: tp,
Config: new(ActionsConfig),
}
+ } else if tp == unit.TypeProjects {
+ cfg := new(ProjectsConfig)
+ cfg.ProjectsMode = ProjectsModeNone
+ return &RepoUnit{
+ Type: tp,
+ Config: cfg,
+ }
}
return &RepoUnit{
@@ -846,7 +858,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.
@@ -858,8 +870,8 @@ func CountRepositories(ctx context.Context, opts CountRepositoryOptions) (int64,
if opts.OwnerID > 0 {
sess.And("owner_id = ?", opts.OwnerID)
}
- if !opts.Private.IsNone() {
- sess.And("is_private=?", opts.Private.IsTrue())
+ if opts.Private.Has() {
+ sess.And("is_private=?", opts.Private.Value())
}
count, err := sess.Count(new(Repository))
diff --git a/models/repo/repo_test.go b/models/repo/repo_test.go
index ca9209d751..c13b698abf 100644
--- a/models/repo/repo_test.go
+++ b/models/repo/repo_test.go
@@ -12,17 +12,17 @@ import (
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/markup"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
- "code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert"
)
var (
countRepospts = repo_model.CountRepositoryOptions{OwnerID: 10}
- countReposptsPublic = repo_model.CountRepositoryOptions{OwnerID: 10, Private: util.OptionalBoolFalse}
- countReposptsPrivate = repo_model.CountRepositoryOptions{OwnerID: 10, Private: util.OptionalBoolTrue}
+ countReposptsPublic = repo_model.CountRepositoryOptions{OwnerID: 10, Private: optional.Some(false)}
+ countReposptsPrivate = repo_model.CountRepositoryOptions{OwnerID: 10, Private: optional.Some(true)}
)
func TestGetRepositoryCount(t *testing.T) {
@@ -64,16 +64,17 @@ func TestRepoAPIURL(t *testing.T) {
func TestWatchRepo(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- const repoID = 3
- const userID = 2
- assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, userID, repoID, true))
- unittest.AssertExistsAndLoadBean(t, &repo_model.Watch{RepoID: repoID, UserID: userID})
- unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repoID})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
- assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, userID, repoID, false))
- unittest.AssertNotExistsBean(t, &repo_model.Watch{RepoID: repoID, UserID: userID})
- unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repoID})
+ assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, user, repo, true))
+ unittest.AssertExistsAndLoadBean(t, &repo_model.Watch{RepoID: repo.ID, UserID: user.ID})
+ unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID})
+
+ assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, user, repo, false))
+ unittest.AssertNotExistsBean(t, &repo_model.Watch{RepoID: repo.ID, UserID: user.ID})
+ unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID})
}
func TestMetas(t *testing.T) {
diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go
index 31a2a2e248..5a841f4d31 100644
--- a/models/repo/repo_unit.go
+++ b/models/repo/repo_unit.go
@@ -202,6 +202,53 @@ func (cfg *ActionsConfig) ToDB() ([]byte, error) {
return json.Marshal(cfg)
}
+// ProjectsMode represents the projects enabled for a repository
+type ProjectsMode string
+
+const (
+ // ProjectsModeRepo allows only repo-level projects
+ ProjectsModeRepo ProjectsMode = "repo"
+ // ProjectsModeOwner allows only owner-level projects
+ ProjectsModeOwner ProjectsMode = "owner"
+ // ProjectsModeAll allows both kinds of projects
+ ProjectsModeAll ProjectsMode = "all"
+ // ProjectsModeNone doesn't allow projects
+ ProjectsModeNone ProjectsMode = "none"
+)
+
+// ProjectsConfig describes projects config
+type ProjectsConfig struct {
+ ProjectsMode ProjectsMode
+}
+
+// FromDB fills up a ProjectsConfig from serialized format.
+func (cfg *ProjectsConfig) FromDB(bs []byte) error {
+ return json.UnmarshalHandleDoubleEncode(bs, &cfg)
+}
+
+// ToDB exports a ProjectsConfig to a serialized format.
+func (cfg *ProjectsConfig) ToDB() ([]byte, error) {
+ return json.Marshal(cfg)
+}
+
+func (cfg *ProjectsConfig) GetProjectsMode() ProjectsMode {
+ if cfg.ProjectsMode != "" {
+ return cfg.ProjectsMode
+ }
+
+ return ProjectsModeAll
+}
+
+func (cfg *ProjectsConfig) IsProjectsAllowed(m ProjectsMode) bool {
+ projectsMode := cfg.GetProjectsMode()
+
+ if m == ProjectsModeNone {
+ return true
+ }
+
+ return projectsMode == m || projectsMode == ProjectsModeAll
+}
+
// BeforeSet is invoked from XORM before setting the value of a field of this object.
func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) {
switch colName {
@@ -217,7 +264,9 @@ func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) {
r.Config = new(IssuesConfig)
case unit.TypeActions:
r.Config = new(ActionsConfig)
- case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypeProjects, unit.TypePackages:
+ case unit.TypeProjects:
+ r.Config = new(ProjectsConfig)
+ case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypePackages:
fallthrough
default:
r.Config = new(UnitConfig)
@@ -265,6 +314,11 @@ func (r *RepoUnit) ActionsConfig() *ActionsConfig {
return r.Config.(*ActionsConfig)
}
+// ProjectsConfig returns config for unit.ProjectsConfig
+func (r *RepoUnit) ProjectsConfig() *ProjectsConfig {
+ return r.Config.(*ProjectsConfig)
+}
+
func getUnitsByRepoID(ctx context.Context, repoID int64) (units []*RepoUnit, err error) {
var tmpUnits []*RepoUnit
if err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Find(&tmpUnits); err != nil {
diff --git a/models/repo/star.go b/models/repo/star.go
index 60737149da..4c66855525 100644
--- a/models/repo/star.go
+++ b/models/repo/star.go
@@ -24,26 +24,30 @@ func init() {
}
// StarRepo or unstar repository.
-func StarRepo(ctx context.Context, userID, repoID int64, star bool) error {
+func StarRepo(ctx context.Context, doer *user_model.User, repo *Repository, star bool) error {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return err
}
defer committer.Close()
- staring := IsStaring(ctx, userID, repoID)
+ staring := IsStaring(ctx, doer.ID, repo.ID)
if star {
+ if user_model.IsUserBlockedBy(ctx, doer, repo.OwnerID) {
+ return user_model.ErrBlockedUser
+ }
+
if staring {
return nil
}
- if err := db.Insert(ctx, &Star{UID: userID, RepoID: repoID}); err != nil {
+ if err := db.Insert(ctx, &Star{UID: doer.ID, RepoID: repo.ID}); err != nil {
return err
}
- if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars + 1 WHERE id = ?", repoID); err != nil {
+ if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars + 1 WHERE id = ?", repo.ID); err != nil {
return err
}
- if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars + 1 WHERE id = ?", userID); err != nil {
+ if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars + 1 WHERE id = ?", doer.ID); err != nil {
return err
}
} else {
@@ -51,13 +55,13 @@ func StarRepo(ctx context.Context, userID, repoID int64, star bool) error {
return nil
}
- if _, err := db.DeleteByBean(ctx, &Star{UID: userID, RepoID: repoID}); err != nil {
+ if _, err := db.DeleteByBean(ctx, &Star{UID: doer.ID, RepoID: repo.ID}); err != nil {
return err
}
- if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars - 1 WHERE id = ?", repoID); err != nil {
+ if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars - 1 WHERE id = ?", repo.ID); err != nil {
return err
}
- if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars - 1 WHERE id = ?", userID); err != nil {
+ if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars - 1 WHERE id = ?", doer.ID); err != nil {
return err
}
}
diff --git a/models/repo/star_test.go b/models/repo/star_test.go
index 62eac4e29a..aaac89d975 100644
--- a/models/repo/star_test.go
+++ b/models/repo/star_test.go
@@ -9,21 +9,24 @@ import (
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
"github.com/stretchr/testify/assert"
)
func TestStarRepo(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- const userID = 2
- const repoID = 1
- unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
- assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, true))
- unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
- assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, true))
- unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
- assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, false))
- unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
+
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+
+ unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
+ assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, true))
+ unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
+ assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, true))
+ unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
+ assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, false))
+ unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
}
func TestIsStaring(t *testing.T) {
@@ -54,17 +57,18 @@ func TestRepository_GetStargazers2(t *testing.T) {
func TestClearRepoStars(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- const userID = 2
- const repoID = 1
- unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
- assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, true))
- unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
- assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, false))
- unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
- assert.NoError(t, repo_model.ClearRepoStars(db.DefaultContext, repoID))
- unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+
+ unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
+ assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, true))
+ unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
+ assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, false))
+ unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
+ assert.NoError(t, repo_model.ClearRepoStars(db.DefaultContext, repo.ID))
+ unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
+
gazers, err := repo_model.GetStargazers(db.DefaultContext, repo, db.ListOptions{Page: 0})
assert.NoError(t, err)
assert.Len(t, gazers, 0)
diff --git a/models/repo/user_repo.go b/models/repo/user_repo.go
index 30c9db7474..6862247657 100644
--- a/models/repo/user_repo.go
+++ b/models/repo/user_repo.go
@@ -16,47 +16,82 @@ import (
"xorm.io/builder"
)
+type StarredReposOptions struct {
+ db.ListOptions
+ StarrerID int64
+ RepoOwnerID int64
+ IncludePrivate bool
+}
+
+func (opts *StarredReposOptions) ToConds() builder.Cond {
+ var cond builder.Cond = builder.Eq{
+ "star.uid": opts.StarrerID,
+ }
+ if opts.RepoOwnerID != 0 {
+ cond = cond.And(builder.Eq{
+ "repository.owner_id": opts.RepoOwnerID,
+ })
+ }
+ if !opts.IncludePrivate {
+ cond = cond.And(builder.Eq{
+ "repository.is_private": false,
+ })
+ }
+ return cond
+}
+
+func (opts *StarredReposOptions) ToJoins() []db.JoinFunc {
+ return []db.JoinFunc{
+ func(e db.Engine) error {
+ e.Join("INNER", "star", "`repository`.id=`star`.repo_id")
+ return nil
+ },
+ }
+}
+
// GetStarredRepos returns the repos starred by a particular user
-func GetStarredRepos(ctx context.Context, userID int64, private bool, listOptions db.ListOptions) ([]*Repository, error) {
- sess := db.GetEngine(ctx).
- Where("star.uid=?", userID).
- Join("LEFT", "star", "`repository`.id=`star`.repo_id")
- if !private {
- sess = sess.And("is_private=?", false)
+func GetStarredRepos(ctx context.Context, opts *StarredReposOptions) ([]*Repository, error) {
+ return db.Find[Repository](ctx, opts)
+}
+
+type WatchedReposOptions struct {
+ db.ListOptions
+ WatcherID int64
+ RepoOwnerID int64
+ IncludePrivate bool
+}
+
+func (opts *WatchedReposOptions) ToConds() builder.Cond {
+ var cond builder.Cond = builder.Eq{
+ "watch.user_id": opts.WatcherID,
}
-
- if listOptions.Page != 0 {
- sess = db.SetSessionPagination(sess, &listOptions)
-
- repos := make([]*Repository, 0, listOptions.PageSize)
- return repos, sess.Find(&repos)
+ if opts.RepoOwnerID != 0 {
+ cond = cond.And(builder.Eq{
+ "repository.owner_id": opts.RepoOwnerID,
+ })
}
+ if !opts.IncludePrivate {
+ cond = cond.And(builder.Eq{
+ "repository.is_private": false,
+ })
+ }
+ return cond.And(builder.Neq{
+ "watch.mode": WatchModeDont,
+ })
+}
- repos := make([]*Repository, 0, 10)
- return repos, sess.Find(&repos)
+func (opts *WatchedReposOptions) ToJoins() []db.JoinFunc {
+ return []db.JoinFunc{
+ func(e db.Engine) error {
+ e.Join("INNER", "watch", "`repository`.id=`watch`.repo_id")
+ return nil
+ },
+ }
}
// GetWatchedRepos returns the repos watched by a particular user
-func GetWatchedRepos(ctx context.Context, userID int64, private bool, listOptions db.ListOptions) ([]*Repository, int64, error) {
- sess := db.GetEngine(ctx).
- Where("watch.user_id=?", userID).
- And("`watch`.mode<>?", WatchModeDont).
- Join("LEFT", "watch", "`repository`.id=`watch`.repo_id")
- if !private {
- sess = sess.And("is_private=?", false)
- }
-
- if listOptions.Page != 0 {
- sess = db.SetSessionPagination(sess, &listOptions)
-
- repos := make([]*Repository, 0, listOptions.PageSize)
- total, err := sess.FindAndCount(&repos)
- return repos, total, err
- }
-
- repos := make([]*Repository, 0, 10)
- total, err := sess.FindAndCount(&repos)
- return repos, total, err
+func GetWatchedRepos(ctx context.Context, opts *WatchedReposOptions) ([]*Repository, int64, error) {
+ return db.FindAndCount[Repository](ctx, opts)
}
// GetRepoAssignees returns all users that have write access and can be assigned to issues
diff --git a/models/repo/user_repo_test.go b/models/repo/user_repo_test.go
index 7816b0262a..591dcea5b5 100644
--- a/models/repo/user_repo_test.go
+++ b/models/repo/user_repo_test.go
@@ -25,10 +25,8 @@ func TestRepoAssignees(t *testing.T) {
repo21 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 21})
users, err = repo_model.GetRepoAssignees(db.DefaultContext, repo21)
assert.NoError(t, err)
- assert.Len(t, users, 3)
- assert.Equal(t, users[0].ID, int64(15))
- assert.Equal(t, users[1].ID, int64(18))
- assert.Equal(t, users[2].ID, int64(16))
+ assert.Len(t, users, 4)
+ assert.ElementsMatch(t, []int64{10, 15, 16, 18}, []int64{users[0].ID, users[1].ID, users[2].ID, users[3].ID})
}
func TestRepoGetReviewers(t *testing.T) {
diff --git a/models/repo/watch.go b/models/repo/watch.go
index 80da4030cb..a616544cae 100644
--- a/models/repo/watch.go
+++ b/models/repo/watch.go
@@ -104,29 +104,23 @@ func watchRepoMode(ctx context.Context, watch Watch, mode WatchMode) (err error)
return err
}
-// WatchRepoMode watch repository in specific mode.
-func WatchRepoMode(ctx context.Context, userID, repoID int64, mode WatchMode) (err error) {
- var watch Watch
- if watch, err = GetWatch(ctx, userID, repoID); err != nil {
- return err
- }
- return watchRepoMode(ctx, watch, mode)
-}
-
// WatchRepo watch or unwatch repository.
-func WatchRepo(ctx context.Context, userID, repoID int64, doWatch bool) (err error) {
- var watch Watch
- if watch, err = GetWatch(ctx, userID, repoID); err != nil {
+func WatchRepo(ctx context.Context, doer *user_model.User, repo *Repository, doWatch bool) error {
+ watch, err := GetWatch(ctx, doer.ID, repo.ID)
+ if err != nil {
return err
}
if !doWatch && watch.Mode == WatchModeAuto {
- err = watchRepoMode(ctx, watch, WatchModeDont)
+ return watchRepoMode(ctx, watch, WatchModeDont)
} else if !doWatch {
- err = watchRepoMode(ctx, watch, WatchModeNone)
- } else {
- err = watchRepoMode(ctx, watch, WatchModeNormal)
+ return watchRepoMode(ctx, watch, WatchModeNone)
}
- return err
+
+ if user_model.IsUserBlockedBy(ctx, doer, repo.OwnerID) {
+ return user_model.ErrBlockedUser
+ }
+
+ return watchRepoMode(ctx, watch, WatchModeNormal)
}
// GetWatchers returns all watchers of given repository.
diff --git a/models/repo/watch_test.go b/models/repo/watch_test.go
index 7aa899291c..a95a267961 100644
--- a/models/repo/watch_test.go
+++ b/models/repo/watch_test.go
@@ -9,6 +9,7 @@ import (
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
@@ -64,6 +65,8 @@ func TestWatchIfAuto(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ user12 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 12})
+
watchers, err := repo_model.GetRepoWatchers(db.DefaultContext, repo.ID, db.ListOptions{Page: 1})
assert.NoError(t, err)
assert.Len(t, watchers, repo.NumWatches)
@@ -105,7 +108,7 @@ func TestWatchIfAuto(t *testing.T) {
assert.Len(t, watchers, prevCount+1)
// Should remove watch, inhibit from adding auto
- assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, 12, 1, false))
+ assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, user12, repo, false))
watchers, err = repo_model.GetRepoWatchers(db.DefaultContext, repo.ID, db.ListOptions{Page: 1})
assert.NoError(t, err)
assert.Len(t, watchers, prevCount)
@@ -116,24 +119,3 @@ func TestWatchIfAuto(t *testing.T) {
assert.NoError(t, err)
assert.Len(t, watchers, prevCount)
}
-
-func TestWatchRepoMode(t *testing.T) {
- assert.NoError(t, unittest.PrepareTestDatabase())
-
- unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 0)
-
- assert.NoError(t, repo_model.WatchRepoMode(db.DefaultContext, 12, 1, repo_model.WatchModeAuto))
- unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 1)
- unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1, Mode: repo_model.WatchModeAuto}, 1)
-
- assert.NoError(t, repo_model.WatchRepoMode(db.DefaultContext, 12, 1, repo_model.WatchModeNormal))
- unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 1)
- unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1, Mode: repo_model.WatchModeNormal}, 1)
-
- assert.NoError(t, repo_model.WatchRepoMode(db.DefaultContext, 12, 1, repo_model.WatchModeDont))
- unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 1)
- unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1, Mode: repo_model.WatchModeDont}, 1)
-
- assert.NoError(t, repo_model.WatchRepoMode(db.DefaultContext, 12, 1, repo_model.WatchModeNone))
- unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 0)
-}
diff --git a/models/repo_transfer.go b/models/repo_transfer.go
index 676e2dbb63..747ec2f248 100644
--- a/models/repo_transfer.go
+++ b/models/repo_transfer.go
@@ -13,6 +13,8 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/timeutil"
+
+ "xorm.io/builder"
)
// RepoTransfer is used to manage repository transfers
@@ -94,21 +96,46 @@ func (r *RepoTransfer) CanUserAcceptTransfer(ctx context.Context, u *user_model.
return allowed
}
+type PendingRepositoryTransferOptions struct {
+ RepoID int64
+ SenderID int64
+ RecipientID int64
+}
+
+func (opts *PendingRepositoryTransferOptions) ToConds() builder.Cond {
+ cond := builder.NewCond()
+ if opts.RepoID != 0 {
+ cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
+ }
+ if opts.SenderID != 0 {
+ cond = cond.And(builder.Eq{"doer_id": opts.SenderID})
+ }
+ if opts.RecipientID != 0 {
+ cond = cond.And(builder.Eq{"recipient_id": opts.RecipientID})
+ }
+ return cond
+}
+
+func GetPendingRepositoryTransfers(ctx context.Context, opts *PendingRepositoryTransferOptions) ([]*RepoTransfer, error) {
+ transfers := make([]*RepoTransfer, 0, 10)
+ return transfers, db.GetEngine(ctx).
+ Where(opts.ToConds()).
+ Find(&transfers)
+}
+
// GetPendingRepositoryTransfer fetches the most recent and ongoing transfer
// process for the repository
func GetPendingRepositoryTransfer(ctx context.Context, repo *repo_model.Repository) (*RepoTransfer, error) {
- transfer := new(RepoTransfer)
-
- has, err := db.GetEngine(ctx).Where("repo_id = ? ", repo.ID).Get(transfer)
+ transfers, err := GetPendingRepositoryTransfers(ctx, &PendingRepositoryTransferOptions{RepoID: repo.ID})
if err != nil {
return nil, err
}
- if !has {
+ if len(transfers) != 1 {
return nil, ErrNoPendingRepoTransfer{RepoID: repo.ID}
}
- return transfer, nil
+ return transfers[0], nil
}
func DeleteRepositoryTransfer(ctx context.Context, repoID int64) error {
diff --git a/models/secret/secret.go b/models/secret/secret.go
index 41e860d7f6..35bed500b9 100644
--- a/models/secret/secret.go
+++ b/models/secret/secret.go
@@ -9,7 +9,10 @@ import (
"fmt"
"strings"
+ actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
+ actions_module "code.gitea.io/gitea/modules/actions"
+ "code.gitea.io/gitea/modules/log"
secret_module "code.gitea.io/gitea/modules/secret"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
@@ -112,3 +115,39 @@ func UpdateSecret(ctx context.Context, secretID int64, data string) error {
}
return err
}
+
+func GetSecretsOfTask(ctx context.Context, task *actions_model.ActionTask) (map[string]string, error) {
+ secrets := map[string]string{}
+
+ secrets["GITHUB_TOKEN"] = task.Token
+ secrets["GITEA_TOKEN"] = task.Token
+
+ if task.Job.Run.IsForkPullRequest && task.Job.Run.TriggerEvent != actions_module.GithubEventPullRequestTarget {
+ // ignore secrets for fork pull request, except GITHUB_TOKEN and GITEA_TOKEN which are automatically generated.
+ // for the tasks triggered by pull_request_target event, they could access the secrets because they will run in the context of the base branch
+ // see the documentation: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target
+ return secrets, nil
+ }
+
+ ownerSecrets, err := db.Find[Secret](ctx, FindSecretsOptions{OwnerID: task.Job.Run.Repo.OwnerID})
+ if err != nil {
+ log.Error("find secrets of owner %v: %v", task.Job.Run.Repo.OwnerID, err)
+ return nil, err
+ }
+ repoSecrets, err := db.Find[Secret](ctx, FindSecretsOptions{RepoID: task.Job.Run.RepoID})
+ if err != nil {
+ log.Error("find secrets of repo %v: %v", task.Job.Run.RepoID, err)
+ return nil, err
+ }
+
+ for _, secret := range append(ownerSecrets, repoSecrets...) {
+ v, err := secret_module.DecryptSecret(setting.SecretKey, secret.Data)
+ if err != nil {
+ log.Error("decrypt secret %v %q: %v", secret.ID, secret.Name, err)
+ return nil, err
+ }
+ secrets[secret.Name] = v
+ }
+
+ return secrets, nil
+}
diff --git a/models/user/block.go b/models/user/block.go
new file mode 100644
index 0000000000..5f2b65a199
--- /dev/null
+++ b/models/user/block.go
@@ -0,0 +1,123 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+ "context"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/modules/container"
+ "code.gitea.io/gitea/modules/timeutil"
+ "code.gitea.io/gitea/modules/util"
+
+ "xorm.io/builder"
+)
+
+var (
+ ErrBlockOrganization = util.NewInvalidArgumentErrorf("cannot block an organization")
+ ErrCanNotBlock = util.NewInvalidArgumentErrorf("cannot block the user")
+ ErrCanNotUnblock = util.NewInvalidArgumentErrorf("cannot unblock the user")
+ ErrBlockedUser = util.NewPermissionDeniedErrorf("user is blocked")
+)
+
+type Blocking struct {
+ ID int64 `xorm:"pk autoincr"`
+ BlockerID int64 `xorm:"UNIQUE(block)"`
+ Blocker *User `xorm:"-"`
+ BlockeeID int64 `xorm:"UNIQUE(block)"`
+ Blockee *User `xorm:"-"`
+ Note string
+ CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
+}
+
+func (*Blocking) TableName() string {
+ return "user_blocking"
+}
+
+func init() {
+ db.RegisterModel(new(Blocking))
+}
+
+func UpdateBlockingNote(ctx context.Context, id int64, note string) error {
+ _, err := db.GetEngine(ctx).ID(id).Cols("note").Update(&Blocking{Note: note})
+ return err
+}
+
+func IsUserBlockedBy(ctx context.Context, blockee *User, blockerIDs ...int64) bool {
+ if len(blockerIDs) == 0 {
+ return false
+ }
+
+ if blockee.IsAdmin {
+ return false
+ }
+
+ cond := builder.Eq{"user_blocking.blockee_id": blockee.ID}.
+ And(builder.In("user_blocking.blocker_id", blockerIDs))
+
+ has, _ := db.GetEngine(ctx).Where(cond).Exist(&Blocking{})
+ return has
+}
+
+type FindBlockingOptions struct {
+ db.ListOptions
+ BlockerID int64
+ BlockeeID int64
+}
+
+func (opts *FindBlockingOptions) ToConds() builder.Cond {
+ cond := builder.NewCond()
+ if opts.BlockerID != 0 {
+ cond = cond.And(builder.Eq{"user_blocking.blocker_id": opts.BlockerID})
+ }
+ if opts.BlockeeID != 0 {
+ cond = cond.And(builder.Eq{"user_blocking.blockee_id": opts.BlockeeID})
+ }
+ return cond
+}
+
+func FindBlockings(ctx context.Context, opts *FindBlockingOptions) ([]*Blocking, int64, error) {
+ return db.FindAndCount[Blocking](ctx, opts)
+}
+
+func GetBlocking(ctx context.Context, blockerID, blockeeID int64) (*Blocking, error) {
+ blocks, _, err := FindBlockings(ctx, &FindBlockingOptions{
+ BlockerID: blockerID,
+ BlockeeID: blockeeID,
+ })
+ if err != nil {
+ return nil, err
+ }
+ if len(blocks) == 0 {
+ return nil, nil
+ }
+ return blocks[0], nil
+}
+
+type BlockingList []*Blocking
+
+func (blocks BlockingList) LoadAttributes(ctx context.Context) error {
+ ids := make(container.Set[int64], len(blocks)*2)
+ for _, b := range blocks {
+ ids.Add(b.BlockerID)
+ ids.Add(b.BlockeeID)
+ }
+
+ userList, err := GetUsersByIDs(ctx, ids.Values())
+ if err != nil {
+ return err
+ }
+
+ userMap := make(map[int64]*User, len(userList))
+ for _, u := range userList {
+ userMap[u.ID] = u
+ }
+
+ for _, b := range blocks {
+ b.Blocker = userMap[b.BlockerID]
+ b.Blockee = userMap[b.BlockeeID]
+ }
+
+ return nil
+}
diff --git a/models/user/email_address.go b/models/user/email_address.go
index 5d67304691..11700a0129 100644
--- a/models/user/email_address.go
+++ b/models/user/email_address.go
@@ -154,37 +154,18 @@ func UpdateEmailAddress(ctx context.Context, email *EmailAddress) error {
var emailRegexp = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
-// ValidateEmail check if email is a allowed address
+// ValidateEmail check if email is a valid & allowed address
func ValidateEmail(email string) error {
- if len(email) == 0 {
- return ErrEmailInvalid{email}
+ if err := validateEmailBasic(email); err != nil {
+ return err
}
+ return validateEmailDomain(email)
+}
- if !emailRegexp.MatchString(email) {
- return ErrEmailCharIsNotSupported{email}
- }
-
- if email[0] == '-' {
- return ErrEmailInvalid{email}
- }
-
- if _, err := mail.ParseAddress(email); err != nil {
- return ErrEmailInvalid{email}
- }
-
- // if there is no allow list, then check email against block list
- if len(setting.Service.EmailDomainAllowList) == 0 &&
- validation.IsEmailDomainListed(setting.Service.EmailDomainBlockList, email) {
- return ErrEmailInvalid{email}
- }
-
- // if there is an allow list, then check email against allow list
- if len(setting.Service.EmailDomainAllowList) > 0 &&
- !validation.IsEmailDomainListed(setting.Service.EmailDomainAllowList, email) {
- return ErrEmailInvalid{email}
- }
-
- return nil
+// ValidateEmailForAdmin check if email is a valid address when admins manually add or edit users
+func ValidateEmailForAdmin(email string) error {
+ return validateEmailBasic(email)
+ // In this case we do not need to check the email domain
}
func GetEmailAddressByEmail(ctx context.Context, email string) (*EmailAddress, error) {
@@ -534,3 +515,41 @@ func ActivateUserEmail(ctx context.Context, userID int64, email string, activate
return committer.Commit()
}
+
+// validateEmailBasic checks whether the email complies with the rules
+func validateEmailBasic(email string) error {
+ if len(email) == 0 {
+ return ErrEmailInvalid{email}
+ }
+
+ if !emailRegexp.MatchString(email) {
+ return ErrEmailCharIsNotSupported{email}
+ }
+
+ if email[0] == '-' {
+ return ErrEmailInvalid{email}
+ }
+
+ if _, err := mail.ParseAddress(email); err != nil {
+ return ErrEmailInvalid{email}
+ }
+
+ return nil
+}
+
+// validateEmailDomain checks whether the email domain is allowed or blocked
+func validateEmailDomain(email string) error {
+ // if there is no allow list, then check email against block list
+ if len(setting.Service.EmailDomainAllowList) == 0 &&
+ validation.IsEmailDomainListed(setting.Service.EmailDomainBlockList, email) {
+ return ErrEmailInvalid{email}
+ }
+
+ // if there is an allow list, then check email against allow list
+ if len(setting.Service.EmailDomainAllowList) > 0 &&
+ !validation.IsEmailDomainListed(setting.Service.EmailDomainAllowList, email) {
+ return ErrEmailInvalid{email}
+ }
+
+ return nil
+}
diff --git a/models/user/follow.go b/models/user/follow.go
index f4dd2891ff..cf9672109a 100644
--- a/models/user/follow.go
+++ b/models/user/follow.go
@@ -29,26 +29,30 @@ func IsFollowing(ctx context.Context, userID, followID int64) bool {
}
// FollowUser marks someone be another's follower.
-func FollowUser(ctx context.Context, userID, followID int64) (err error) {
- if userID == followID || IsFollowing(ctx, userID, followID) {
+func FollowUser(ctx context.Context, user, follow *User) (err error) {
+ if user.ID == follow.ID || IsFollowing(ctx, user.ID, follow.ID) {
return nil
}
+ if IsUserBlockedBy(ctx, user, follow.ID) || IsUserBlockedBy(ctx, follow, user.ID) {
+ return ErrBlockedUser
+ }
+
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return err
}
defer committer.Close()
- if err = db.Insert(ctx, &Follow{UserID: userID, FollowID: followID}); err != nil {
+ if err = db.Insert(ctx, &Follow{UserID: user.ID, FollowID: follow.ID}); err != nil {
return err
}
- if _, err = db.Exec(ctx, "UPDATE `user` SET num_followers = num_followers + 1 WHERE id = ?", followID); err != nil {
+ if _, err = db.Exec(ctx, "UPDATE `user` SET num_followers = num_followers + 1 WHERE id = ?", follow.ID); err != nil {
return err
}
- if _, err = db.Exec(ctx, "UPDATE `user` SET num_following = num_following + 1 WHERE id = ?", userID); err != nil {
+ if _, err = db.Exec(ctx, "UPDATE `user` SET num_following = num_following + 1 WHERE id = ?", user.ID); err != nil {
return err
}
return committer.Commit()
diff --git a/models/user/user.go b/models/user/user.go
index e92bbd4d0b..0bdda8655f 100644
--- a/models/user/user.go
+++ b/models/user/user.go
@@ -586,6 +586,16 @@ type CreateUserOverwriteOptions struct {
// CreateUser creates record of a new user.
func CreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err error) {
+ return createUser(ctx, u, false, overwriteDefault...)
+}
+
+// AdminCreateUser is used by admins to manually create users
+func AdminCreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err error) {
+ return createUser(ctx, u, true, overwriteDefault...)
+}
+
+// createUser creates record of a new user.
+func createUser(ctx context.Context, u *User, createdByAdmin bool, overwriteDefault ...*CreateUserOverwriteOptions) (err error) {
if err = IsUsableUsername(u.Name); err != nil {
return err
}
@@ -639,8 +649,14 @@ func CreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOve
return err
}
- if err := ValidateEmail(u.Email); err != nil {
- return err
+ if createdByAdmin {
+ if err := ValidateEmailForAdmin(u.Email); err != nil {
+ return err
+ }
+ } else {
+ if err := ValidateEmail(u.Email); err != nil {
+ return err
+ }
}
ctx, committer, err := db.TxContext(ctx)
@@ -715,7 +731,7 @@ func CreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOve
// IsLastAdminUser check whether user is the last admin
func IsLastAdminUser(ctx context.Context, user *User) bool {
- if user.IsAdmin && CountUsers(ctx, &CountUserFilter{IsAdmin: util.OptionalBoolTrue}) <= 1 {
+ if user.IsAdmin && CountUsers(ctx, &CountUserFilter{IsAdmin: optional.Some(true)}) <= 1 {
return true
}
return false
@@ -724,7 +740,7 @@ func IsLastAdminUser(ctx context.Context, user *User) bool {
// CountUserFilter represent optional filters for CountUsers
type CountUserFilter struct {
LastLoginSince *int64
- IsAdmin util.OptionalBool
+ IsAdmin optional.Option[bool]
}
// CountUsers returns number of users.
@@ -742,8 +758,8 @@ func countUsers(ctx context.Context, opts *CountUserFilter) int64 {
cond = cond.And(builder.Gte{"last_login_unix": *opts.LastLoginSince})
}
- if !opts.IsAdmin.IsNone() {
- cond = cond.And(builder.Eq{"is_admin": opts.IsAdmin.IsTrue()})
+ if opts.IsAdmin.Has() {
+ cond = cond.And(builder.Eq{"is_admin": opts.IsAdmin.Value()})
}
}
@@ -1167,7 +1183,7 @@ func IsUserVisibleToViewer(ctx context.Context, u, viewer *User) bool {
return false
}
- // If they follow - they see each over
+ // If they follow - they see each other
follower := IsFollowing(ctx, u.ID, viewer.ID)
if follower {
return true
diff --git a/models/user/user_test.go b/models/user/user_test.go
index f522f743d5..f4efd071ea 100644
--- a/models/user/user_test.go
+++ b/models/user/user_test.go
@@ -399,14 +399,19 @@ func TestGetUserByOpenID(t *testing.T) {
func TestFollowUser(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- testSuccess := func(followerID, followedID int64) {
- assert.NoError(t, user_model.FollowUser(db.DefaultContext, followerID, followedID))
- unittest.AssertExistsAndLoadBean(t, &user_model.Follow{UserID: followerID, FollowID: followedID})
+ testSuccess := func(follower, followed *user_model.User) {
+ assert.NoError(t, user_model.FollowUser(db.DefaultContext, follower, followed))
+ unittest.AssertExistsAndLoadBean(t, &user_model.Follow{UserID: follower.ID, FollowID: followed.ID})
}
- testSuccess(4, 2)
- testSuccess(5, 2)
- assert.NoError(t, user_model.FollowUser(db.DefaultContext, 2, 2))
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+ user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
+
+ testSuccess(user4, user2)
+ testSuccess(user5, user2)
+
+ assert.NoError(t, user_model.FollowUser(db.DefaultContext, user2, user2))
unittest.CheckConsistencyFor(t, &user_model.User{})
}
diff --git a/models/webhook/hooktask.go b/models/webhook/hooktask.go
index 2fb655ebca..ff3fdbadb2 100644
--- a/models/webhook/hooktask.go
+++ b/models/webhook/hooktask.go
@@ -5,13 +5,13 @@ package webhook
import (
"context"
+ "errors"
"time"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
- api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
webhook_module "code.gitea.io/gitea/modules/webhook"
@@ -31,6 +31,7 @@ type HookRequest struct {
URL string `json:"url"`
HTTPMethod string `json:"http_method"`
Headers map[string]string `json:"headers"`
+ Body string `json:"body"`
}
// HookResponse represents hook task response information.
@@ -45,11 +46,15 @@ type HookTask struct {
ID int64 `xorm:"pk autoincr"`
HookID int64 `xorm:"index"`
UUID string `xorm:"unique"`
- api.Payloader `xorm:"-"`
PayloadContent string `xorm:"LONGTEXT"`
- EventType webhook_module.HookEventType
- IsDelivered bool
- Delivered timeutil.TimeStampNano
+ // PayloadVersion number to allow for smooth version upgrades:
+ // - PayloadVersion 1: PayloadContent contains the JSON as sent to the URL
+ // - PayloadVersion 2: PayloadContent contains the original event
+ PayloadVersion int `xorm:"DEFAULT 1"`
+
+ EventType webhook_module.HookEventType
+ IsDelivered bool
+ Delivered timeutil.TimeStampNano
// History info.
IsSucceed bool
@@ -115,16 +120,12 @@ func HookTasks(ctx context.Context, hookID int64, page int) ([]*HookTask, error)
// it handles conversion from Payload to PayloadContent.
func CreateHookTask(ctx context.Context, t *HookTask) (*HookTask, error) {
t.UUID = gouuid.New().String()
- if t.Payloader != nil {
- data, err := t.Payloader.JSONPayload()
- if err != nil {
- return nil, err
- }
- t.PayloadContent = string(data)
- }
if t.Delivered == 0 {
t.Delivered = timeutil.TimeStampNanoNow()
}
+ if t.PayloadVersion == 0 {
+ return nil, errors.New("missing HookTask.PayloadVersion")
+ }
return t, db.Insert(ctx, t)
}
@@ -165,6 +166,7 @@ func ReplayHookTask(ctx context.Context, hookID int64, uuid string) (*HookTask,
HookID: task.HookID,
PayloadContent: task.PayloadContent,
EventType: task.EventType,
+ PayloadVersion: task.PayloadVersion,
})
}
diff --git a/models/webhook/webhook.go b/models/webhook/webhook.go
index 4a84a3d411..894357e36a 100644
--- a/models/webhook/webhook.go
+++ b/models/webhook/webhook.go
@@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/secret"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
@@ -433,7 +434,7 @@ type ListWebhookOptions struct {
db.ListOptions
RepoID int64
OwnerID int64
- IsActive util.OptionalBool
+ IsActive optional.Option[bool]
}
func (opts ListWebhookOptions) ToConds() builder.Cond {
@@ -444,8 +445,8 @@ func (opts ListWebhookOptions) ToConds() builder.Cond {
if opts.OwnerID != 0 {
cond = cond.And(builder.Eq{"webhook.owner_id": opts.OwnerID})
}
- if !opts.IsActive.IsNone() {
- cond = cond.And(builder.Eq{"webhook.is_active": opts.IsActive.IsTrue()})
+ if opts.IsActive.Has() {
+ cond = cond.And(builder.Eq{"webhook.is_active": opts.IsActive.Value()})
}
return cond
}
diff --git a/models/webhook/webhook_system.go b/models/webhook/webhook_system.go
index 2e89f9547b..a2a9ee321a 100644
--- a/models/webhook/webhook_system.go
+++ b/models/webhook/webhook_system.go
@@ -8,7 +8,7 @@ import (
"fmt"
"code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/modules/optional"
)
// GetDefaultWebhooks returns all admin-default webhooks.
@@ -34,15 +34,15 @@ func GetSystemOrDefaultWebhook(ctx context.Context, id int64) (*Webhook, error)
}
// GetSystemWebhooks returns all admin system webhooks.
-func GetSystemWebhooks(ctx context.Context, isActive util.OptionalBool) ([]*Webhook, error) {
+func GetSystemWebhooks(ctx context.Context, isActive optional.Option[bool]) ([]*Webhook, error) {
webhooks := make([]*Webhook, 0, 5)
- if isActive.IsNone() {
+ if !isActive.Has() {
return webhooks, db.GetEngine(ctx).
Where("repo_id=? AND owner_id=? AND is_system_webhook=?", 0, 0, true).
Find(&webhooks)
}
return webhooks, db.GetEngine(ctx).
- Where("repo_id=? AND owner_id=? AND is_system_webhook=? AND is_active = ?", 0, 0, true, isActive.IsTrue()).
+ Where("repo_id=? AND owner_id=? AND is_system_webhook=? AND is_active = ?", 0, 0, true, isActive.Value()).
Find(&webhooks)
}
diff --git a/models/webhook/webhook_test.go b/models/webhook/webhook_test.go
index 694fd7a873..f4403776ce 100644
--- a/models/webhook/webhook_test.go
+++ b/models/webhook/webhook_test.go
@@ -11,9 +11,8 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/json"
- api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/optional"
"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"
@@ -35,8 +34,10 @@ func TestWebhook_History(t *testing.T) {
webhook := unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 1})
tasks, err := webhook.History(db.DefaultContext, 0)
assert.NoError(t, err)
- if assert.Len(t, tasks, 1) {
- assert.Equal(t, int64(1), tasks[0].ID)
+ if assert.Len(t, tasks, 3) {
+ assert.Equal(t, int64(3), tasks[0].ID)
+ assert.Equal(t, int64(2), tasks[1].ID)
+ assert.Equal(t, int64(1), tasks[2].ID)
}
webhook = unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 2})
@@ -123,7 +124,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 +144,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)
@@ -197,8 +198,10 @@ func TestHookTasks(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
hookTasks, err := HookTasks(db.DefaultContext, 1, 1)
assert.NoError(t, err)
- if assert.Len(t, hookTasks, 1) {
- assert.Equal(t, int64(1), hookTasks[0].ID)
+ if assert.Len(t, hookTasks, 3) {
+ assert.Equal(t, int64(3), hookTasks[0].ID)
+ assert.Equal(t, int64(2), hookTasks[1].ID)
+ assert.Equal(t, int64(1), hookTasks[2].ID)
}
hookTasks, err = HookTasks(db.DefaultContext, unittest.NonexistentID, 1)
@@ -209,8 +212,8 @@ func TestHookTasks(t *testing.T) {
func TestCreateHookTask(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
hookTask := &HookTask{
- HookID: 3,
- Payloader: &api.PushPayload{},
+ HookID: 3,
+ PayloadVersion: 2,
}
unittest.AssertNotExistsBean(t, hookTask)
_, err := CreateHookTask(db.DefaultContext, hookTask)
@@ -232,10 +235,10 @@ func TestUpdateHookTask(t *testing.T) {
func TestCleanupHookTaskTable_PerWebhook_DeletesDelivered(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
hookTask := &HookTask{
- HookID: 3,
- Payloader: &api.PushPayload{},
- IsDelivered: true,
- Delivered: timeutil.TimeStampNanoNow(),
+ HookID: 3,
+ IsDelivered: true,
+ Delivered: timeutil.TimeStampNanoNow(),
+ PayloadVersion: 2,
}
unittest.AssertNotExistsBean(t, hookTask)
_, err := CreateHookTask(db.DefaultContext, hookTask)
@@ -249,9 +252,9 @@ func TestCleanupHookTaskTable_PerWebhook_DeletesDelivered(t *testing.T) {
func TestCleanupHookTaskTable_PerWebhook_LeavesUndelivered(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
hookTask := &HookTask{
- HookID: 4,
- Payloader: &api.PushPayload{},
- IsDelivered: false,
+ HookID: 4,
+ IsDelivered: false,
+ PayloadVersion: 2,
}
unittest.AssertNotExistsBean(t, hookTask)
_, err := CreateHookTask(db.DefaultContext, hookTask)
@@ -265,10 +268,10 @@ func TestCleanupHookTaskTable_PerWebhook_LeavesUndelivered(t *testing.T) {
func TestCleanupHookTaskTable_PerWebhook_LeavesMostRecentTask(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
hookTask := &HookTask{
- HookID: 4,
- Payloader: &api.PushPayload{},
- IsDelivered: true,
- Delivered: timeutil.TimeStampNanoNow(),
+ HookID: 4,
+ IsDelivered: true,
+ Delivered: timeutil.TimeStampNanoNow(),
+ PayloadVersion: 2,
}
unittest.AssertNotExistsBean(t, hookTask)
_, err := CreateHookTask(db.DefaultContext, hookTask)
@@ -282,10 +285,10 @@ func TestCleanupHookTaskTable_PerWebhook_LeavesMostRecentTask(t *testing.T) {
func TestCleanupHookTaskTable_OlderThan_DeletesDelivered(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
hookTask := &HookTask{
- HookID: 3,
- Payloader: &api.PushPayload{},
- IsDelivered: true,
- Delivered: timeutil.TimeStampNano(time.Now().AddDate(0, 0, -8).UnixNano()),
+ HookID: 3,
+ IsDelivered: true,
+ Delivered: timeutil.TimeStampNano(time.Now().AddDate(0, 0, -8).UnixNano()),
+ PayloadVersion: 2,
}
unittest.AssertNotExistsBean(t, hookTask)
_, err := CreateHookTask(db.DefaultContext, hookTask)
@@ -299,9 +302,9 @@ func TestCleanupHookTaskTable_OlderThan_DeletesDelivered(t *testing.T) {
func TestCleanupHookTaskTable_OlderThan_LeavesUndelivered(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
hookTask := &HookTask{
- HookID: 4,
- Payloader: &api.PushPayload{},
- IsDelivered: false,
+ HookID: 4,
+ IsDelivered: false,
+ PayloadVersion: 2,
}
unittest.AssertNotExistsBean(t, hookTask)
_, err := CreateHookTask(db.DefaultContext, hookTask)
@@ -315,10 +318,10 @@ func TestCleanupHookTaskTable_OlderThan_LeavesUndelivered(t *testing.T) {
func TestCleanupHookTaskTable_OlderThan_LeavesTaskEarlierThanAgeToDelete(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
hookTask := &HookTask{
- HookID: 4,
- Payloader: &api.PushPayload{},
- IsDelivered: true,
- Delivered: timeutil.TimeStampNano(time.Now().AddDate(0, 0, -6).UnixNano()),
+ HookID: 4,
+ IsDelivered: true,
+ Delivered: timeutil.TimeStampNano(time.Now().AddDate(0, 0, -6).UnixNano()),
+ PayloadVersion: 2,
}
unittest.AssertNotExistsBean(t, hookTask)
_, err := CreateHookTask(db.DefaultContext, hookTask)
diff --git a/modules/base/natural_sort.go b/modules/base/natural_sort.go
index e920177f89..0f90ec70ce 100644
--- a/modules/base/natural_sort.go
+++ b/modules/base/natural_sort.go
@@ -4,85 +4,12 @@
package base
import (
- "math/big"
- "unicode/utf8"
+ "golang.org/x/text/collate"
+ "golang.org/x/text/language"
)
// NaturalSortLess compares two strings so that they could be sorted in natural order
func NaturalSortLess(s1, s2 string) bool {
- var i1, i2 int
- for {
- rune1, j1, end1 := getNextRune(s1, i1)
- rune2, j2, end2 := getNextRune(s2, i2)
- if end1 || end2 {
- return end1 != end2 && end1
- }
- dec1 := isDecimal(rune1)
- dec2 := isDecimal(rune2)
- var less, equal bool
- if dec1 && dec2 {
- i1, i2, less, equal = compareByNumbers(s1, i1, s2, i2)
- } else if !dec1 && !dec2 {
- equal = rune1 == rune2
- less = rune1 < rune2
- i1 = j1
- i2 = j2
- } else {
- return rune1 < rune2
- }
- if !equal {
- return less
- }
- }
-}
-
-func getNextRune(str string, pos int) (rune, int, bool) {
- if pos < len(str) {
- r, w := utf8.DecodeRuneInString(str[pos:])
- // Fallback to ascii
- if r == utf8.RuneError {
- r = rune(str[pos])
- w = 1
- }
- return r, pos + w, false
- }
- return 0, pos, true
-}
-
-func isDecimal(r rune) bool {
- return '0' <= r && r <= '9'
-}
-
-func compareByNumbers(str1 string, pos1 int, str2 string, pos2 int) (i1, i2 int, less, equal bool) {
- d1, d2 := true, true
- var dec1, dec2 string
- for d1 || d2 {
- if d1 {
- r, j, end := getNextRune(str1, pos1)
- if !end && isDecimal(r) {
- dec1 += string(r)
- pos1 = j
- } else {
- d1 = false
- }
- }
- if d2 {
- r, j, end := getNextRune(str2, pos2)
- if !end && isDecimal(r) {
- dec2 += string(r)
- pos2 = j
- } else {
- d2 = false
- }
- }
- }
- less, equal = compareBigNumbers(dec1, dec2)
- return pos1, pos2, less, equal
-}
-
-func compareBigNumbers(dec1, dec2 string) (less, equal bool) {
- d1, _ := big.NewInt(0).SetString(dec1, 10)
- d2, _ := big.NewInt(0).SetString(dec2, 10)
- cmp := d1.Cmp(d2)
- return cmp < 0, cmp == 0
+ c := collate.New(language.English, collate.Numeric)
+ return c.CompareString(s1, s2) < 0
}
diff --git a/modules/base/natural_sort_test.go b/modules/base/natural_sort_test.go
index 91e864ad2a..f27a4eb53a 100644
--- a/modules/base/natural_sort_test.go
+++ b/modules/base/natural_sort_test.go
@@ -11,7 +11,7 @@ import (
func TestNaturalSortLess(t *testing.T) {
test := func(s1, s2 string, less bool) {
- assert.Equal(t, less, NaturalSortLess(s1, s2))
+ assert.Equal(t, less, NaturalSortLess(s1, s2), "s1=%q, s2=%q", s1, s2)
}
test("v1.20.0", "v1.2.0", false)
test("v1.20.0", "v1.29.0", true)
@@ -20,4 +20,11 @@ func TestNaturalSortLess(t *testing.T) {
test("a-1-a", "a-1-b", true)
test("2", "12", true)
test("a", "ab", true)
+
+ test("A", "b", true)
+ test("a", "B", true)
+
+ test("cafe", "café", true)
+ test("café", "cafe", false)
+ test("caff", "café", false)
}
diff --git a/modules/git/command.go b/modules/git/command.go
index 9305ef6f92..371109730a 100644
--- a/modules/git/command.go
+++ b/modules/git/command.go
@@ -12,6 +12,7 @@ import (
"io"
"os"
"os/exec"
+ "runtime"
"strings"
"time"
@@ -344,6 +345,17 @@ func (c *Command) Run(opts *RunOpts) error {
log.Debug("slow git.Command.Run: %s (%s)", c, elapsed)
}
+ // We need to check if the context is canceled by the program on Windows.
+ // This is because Windows does not have signal checking when terminating the process.
+ // It always returns exit code 1, unlike Linux, which has many exit codes for signals.
+ if runtime.GOOS == "windows" &&
+ err != nil &&
+ err.Error() == "" &&
+ cmd.ProcessState.ExitCode() == 1 &&
+ ctx.Err() == context.Canceled {
+ return ctx.Err()
+ }
+
if err != nil && ctx.Err() != context.DeadlineExceeded {
return err
}
diff --git a/modules/git/repo_base_gogit.go b/modules/git/repo_base_gogit.go
index 3ca5eb36c6..0cd07dcdc8 100644
--- a/modules/git/repo_base_gogit.go
+++ b/modules/git/repo_base_gogit.go
@@ -8,11 +8,11 @@ package git
import (
"context"
- "errors"
"path/filepath"
gitealog "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
"github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/osfs"
@@ -52,7 +52,7 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) {
if err != nil {
return nil, err
} else if !isDir(repoPath) {
- return nil, errors.New("no such file or directory")
+ return nil, util.NewNotExistErrorf("no such file or directory")
}
fs := osfs.New(repoPath)
diff --git a/modules/git/repo_base_nogogit.go b/modules/git/repo_base_nogogit.go
index 86b6a93567..7f6512200b 100644
--- a/modules/git/repo_base_nogogit.go
+++ b/modules/git/repo_base_nogogit.go
@@ -9,10 +9,10 @@ package git
import (
"bufio"
"context"
- "errors"
"path/filepath"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/util"
)
func init() {
@@ -54,7 +54,7 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) {
if err != nil {
return nil, err
} else if !isDir(repoPath) {
- return nil, errors.New("no such file or directory")
+ return nil, util.NewNotExistErrorf("no such file or directory")
}
// Now because of some insanity with git cat-file not immediately failing if not run in a valid git directory we need to run git rev-parse first!
diff --git a/modules/git/repo_branch.go b/modules/git/repo_branch.go
index 979c5dec91..552ae2bb8c 100644
--- a/modules/git/repo_branch.go
+++ b/modules/git/repo_branch.go
@@ -55,15 +55,8 @@ func (repo *Repository) GetHEADBranch() (*Branch, error) {
}, nil
}
-// SetDefaultBranch sets default branch of repository.
-func (repo *Repository) SetDefaultBranch(name string) error {
- _, _, err := NewCommand(repo.Ctx, "symbolic-ref", "HEAD").AddDynamicArguments(BranchPrefix + name).RunStdString(&RunOpts{Dir: repo.Path})
- return err
-}
-
-// GetDefaultBranch gets default branch of repository.
-func (repo *Repository) GetDefaultBranch() (string, error) {
- stdout, _, err := NewCommand(repo.Ctx, "symbolic-ref", "HEAD").RunStdString(&RunOpts{Dir: repo.Path})
+func GetDefaultBranch(ctx context.Context, repoPath string) (string, error) {
+ stdout, _, err := NewCommand(ctx, "symbolic-ref", "HEAD").RunStdString(&RunOpts{Dir: repoPath})
if err != nil {
return "", err
}
diff --git a/modules/gitrepo/branch.go b/modules/gitrepo/branch.go
index dcaf92668d..e13a4c82e1 100644
--- a/modules/gitrepo/branch.go
+++ b/modules/gitrepo/branch.go
@@ -30,3 +30,20 @@ func GetBranchCommitID(ctx context.Context, repo Repository, branch string) (str
return gitRepo.GetBranchCommitID(branch)
}
+
+// SetDefaultBranch sets default branch of repository.
+func SetDefaultBranch(ctx context.Context, repo Repository, name string) error {
+ _, _, err := git.NewCommand(ctx, "symbolic-ref", "HEAD").
+ AddDynamicArguments(git.BranchPrefix + name).
+ RunStdString(&git.RunOpts{Dir: repoPath(repo)})
+ return err
+}
+
+// GetDefaultBranch gets default branch of repository.
+func GetDefaultBranch(ctx context.Context, repo Repository) (string, error) {
+ return git.GetDefaultBranch(ctx, repoPath(repo))
+}
+
+func GetWikiDefaultBranch(ctx context.Context, repo Repository) (string, error) {
+ return git.GetDefaultBranch(ctx, wikiPath(repo))
+}
diff --git a/modules/graceful/manager_unix.go b/modules/graceful/manager_unix.go
index f4af4993d9..edf5fc248f 100644
--- a/modules/graceful/manager_unix.go
+++ b/modules/graceful/manager_unix.go
@@ -59,7 +59,15 @@ func (g *Manager) start() {
go func() {
defer close(startupDone)
// Wait till we're done getting all the listeners and then close the unused ones
- g.createServerWaitGroup.Wait()
+ func() {
+ // FIXME: there is a fundamental design problem of the "manager" and the "wait group".
+ // If nothing has started, the "Wait" just panics: sync: WaitGroup is reused before previous Wait has returned
+ // There is no clear solution besides a complete rewriting of the "manager"
+ defer func() {
+ _ = recover()
+ }()
+ g.createServerWaitGroup.Wait()
+ }()
// Ignore the error here there's not much we can do with it, they're logged in the CloseProvidedListeners function
_ = CloseProvidedListeners()
g.notify(readyMsg)
diff --git a/modules/graceful/manager_windows.go b/modules/graceful/manager_windows.go
index 0248dcb24d..ecf30af3f3 100644
--- a/modules/graceful/manager_windows.go
+++ b/modules/graceful/manager_windows.go
@@ -150,7 +150,15 @@ func (g *Manager) awaitServer(limit time.Duration) bool {
c := make(chan struct{})
go func() {
defer close(c)
- g.createServerWaitGroup.Wait()
+ func() {
+ // FIXME: there is a fundamental design problem of the "manager" and the "wait group".
+ // If nothing has started, the "Wait" just panics: sync: WaitGroup is reused before previous Wait has returned
+ // There is no clear solution besides a complete rewriting of the "manager"
+ defer func() {
+ _ = recover()
+ }()
+ g.createServerWaitGroup.Wait()
+ }()
}()
if limit > 0 {
select {
diff --git a/modules/indexer/code/bleve/bleve.go b/modules/indexer/code/bleve/bleve.go
index 8ba50ed77c..107dd23598 100644
--- a/modules/indexer/code/bleve/bleve.go
+++ b/modules/indexer/code/bleve/bleve.go
@@ -233,21 +233,21 @@ func (b *Indexer) Delete(_ context.Context, repoID int64) error {
// Search searches for files in the specified repo.
// Returns the matching file-paths
-func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) {
+func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isFuzzy bool) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) {
var (
indexerQuery query.Query
keywordQuery query.Query
)
- if isMatch {
- prefixQuery := bleve.NewPrefixQuery(keyword)
- prefixQuery.FieldVal = "Content"
- keywordQuery = prefixQuery
- } else {
+ if isFuzzy {
phraseQuery := bleve.NewMatchPhraseQuery(keyword)
phraseQuery.FieldVal = "Content"
phraseQuery.Analyzer = repoIndexerAnalyzer
keywordQuery = phraseQuery
+ } else {
+ prefixQuery := bleve.NewPrefixQuery(keyword)
+ prefixQuery.FieldVal = "Content"
+ keywordQuery = prefixQuery
}
if len(repoIDs) > 0 {
diff --git a/modules/indexer/code/elasticsearch/elasticsearch.go b/modules/indexer/code/elasticsearch/elasticsearch.go
index 0f70f13485..065b0b2061 100644
--- a/modules/indexer/code/elasticsearch/elasticsearch.go
+++ b/modules/indexer/code/elasticsearch/elasticsearch.go
@@ -281,10 +281,10 @@ func extractAggs(searchResult *elastic.SearchResult) []*internal.SearchResultLan
}
// Search searches for codes and language stats by given conditions.
-func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) {
- searchType := esMultiMatchTypeBestFields
- if isMatch {
- searchType = esMultiMatchTypePhrasePrefix
+func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isFuzzy bool) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) {
+ searchType := esMultiMatchTypePhrasePrefix
+ if isFuzzy {
+ searchType = esMultiMatchTypeBestFields
}
kwQuery := elastic.NewMultiMatchQuery(keyword, "content").Type(searchType)
diff --git a/modules/indexer/code/indexer_test.go b/modules/indexer/code/indexer_test.go
index 5eb8e61e3d..23dbd63410 100644
--- a/modules/indexer/code/indexer_test.go
+++ b/modules/indexer/code/indexer_test.go
@@ -70,7 +70,7 @@ func testIndexer(name string, t *testing.T, indexer internal.Indexer) {
for _, kw := range keywords {
t.Run(kw.Keyword, func(t *testing.T) {
- total, res, langs, err := indexer.Search(context.TODO(), kw.RepoIDs, "", kw.Keyword, 1, 10, false)
+ total, res, langs, err := indexer.Search(context.TODO(), kw.RepoIDs, "", kw.Keyword, 1, 10, true)
assert.NoError(t, err)
assert.Len(t, kw.IDs, int(total))
assert.Len(t, langs, kw.Langs)
diff --git a/modules/indexer/code/internal/indexer.go b/modules/indexer/code/internal/indexer.go
index da3ac3623c..c92419deb2 100644
--- a/modules/indexer/code/internal/indexer.go
+++ b/modules/indexer/code/internal/indexer.go
@@ -16,7 +16,7 @@ type Indexer interface {
internal.Indexer
Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *RepoChanges) error
Delete(ctx context.Context, repoID int64) error
- Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int64, []*SearchResult, []*SearchResultLanguages, error)
+ Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isFuzzy bool) (int64, []*SearchResult, []*SearchResultLanguages, error)
}
// NewDummyIndexer returns a dummy indexer
@@ -38,6 +38,6 @@ func (d *dummyIndexer) Delete(ctx context.Context, repoID int64) error {
return fmt.Errorf("indexer is not ready")
}
-func (d *dummyIndexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int64, []*SearchResult, []*SearchResultLanguages, error) {
+func (d *dummyIndexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isFuzzy bool) (int64, []*SearchResult, []*SearchResultLanguages, error) {
return 0, nil, nil, fmt.Errorf("indexer is not ready")
}
diff --git a/modules/indexer/code/search.go b/modules/indexer/code/search.go
index e19e22eea0..89a62a8d3e 100644
--- a/modules/indexer/code/search.go
+++ b/modules/indexer/code/search.go
@@ -16,14 +16,18 @@ import (
// Result a search result to display
type Result struct {
- RepoID int64
- Filename string
- CommitID string
- UpdatedUnix timeutil.TimeStamp
- Language string
- Color string
- LineNumbers []int
- FormattedLines template.HTML
+ RepoID int64
+ Filename string
+ CommitID string
+ UpdatedUnix timeutil.TimeStamp
+ Language string
+ Color string
+ Lines []ResultLine
+}
+
+type ResultLine struct {
+ Num int
+ FormattedContent template.HTML
}
type SearchResultLanguages = internal.SearchResultLanguages
@@ -70,7 +74,7 @@ func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Res
var formattedLinesBuffer bytes.Buffer
contentLines := strings.SplitAfter(result.Content[startIndex:endIndex], "\n")
- lineNumbers := make([]int, len(contentLines))
+ lines := make([]ResultLine, 0, len(contentLines))
index := startIndex
for i, line := range contentLines {
var err error
@@ -93,31 +97,40 @@ func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Res
return nil, err
}
- lineNumbers[i] = startLineNum + i
+ lines = append(lines, ResultLine{Num: startLineNum + i})
index += len(line)
}
- highlighted, _ := highlight.Code(result.Filename, "", formattedLinesBuffer.String())
+ // we should highlight the whole code block first, otherwise it doesn't work well with multiple line highlighting
+ hl, _ := highlight.Code(result.Filename, "", formattedLinesBuffer.String())
+ highlightedLines := strings.Split(string(hl), "\n")
+
+ // The lines outputted by highlight.Code might not match the original lines, because "highlight" removes the last `\n`
+ lines = lines[:min(len(highlightedLines), len(lines))]
+ highlightedLines = highlightedLines[:len(lines)]
+ for i := 0; i < len(lines); i++ {
+ lines[i].FormattedContent = template.HTML(highlightedLines[i])
+ }
return &Result{
- RepoID: result.RepoID,
- Filename: result.Filename,
- CommitID: result.CommitID,
- UpdatedUnix: result.UpdatedUnix,
- Language: result.Language,
- Color: result.Color,
- LineNumbers: lineNumbers,
- FormattedLines: highlighted,
+ RepoID: result.RepoID,
+ Filename: result.Filename,
+ CommitID: result.CommitID,
+ UpdatedUnix: result.UpdatedUnix,
+ Language: result.Language,
+ Color: result.Color,
+ Lines: lines,
}, nil
}
// PerformSearch perform a search on a repository
-func PerformSearch(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int, []*Result, []*internal.SearchResultLanguages, error) {
+// if isFuzzy is true set the Damerau-Levenshtein distance from 0 to 2
+func PerformSearch(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isFuzzy bool) (int, []*Result, []*internal.SearchResultLanguages, error) {
if len(keyword) == 0 {
return 0, nil, nil, nil
}
- total, results, resultLanguages, err := (*globalIndexer.Load()).Search(ctx, repoIDs, language, keyword, page, pageSize, isMatch)
+ total, results, resultLanguages, err := (*globalIndexer.Load()).Search(ctx, repoIDs, language, keyword, page, pageSize, isFuzzy)
if err != nil {
return 0, nil, nil, err
}
diff --git a/modules/indexer/internal/bleve/query.go b/modules/indexer/internal/bleve/query.go
index c7d66538c1..2a427c4020 100644
--- a/modules/indexer/internal/bleve/query.go
+++ b/modules/indexer/internal/bleve/query.go
@@ -25,6 +25,13 @@ func MatchPhraseQuery(matchPhrase, field, analyzer string) *query.MatchPhraseQue
return q
}
+// PrefixQuery generates a match prefix query for the given prefix and field
+func PrefixQuery(matchPrefix, field string) *query.PrefixQuery {
+ q := bleve.NewPrefixQuery(matchPrefix)
+ q.FieldVal = field
+ return q
+}
+
// BoolFieldQuery generates a bool field query for the given value and field
func BoolFieldQuery(value bool, field string) *query.BoolFieldQuery {
q := bleve.NewBoolFieldQuery(value)
diff --git a/modules/indexer/issues/bleve/bleve.go b/modules/indexer/issues/bleve/bleve.go
index 7c82cfbb79..aaea854efa 100644
--- a/modules/indexer/issues/bleve/bleve.go
+++ b/modules/indexer/issues/bleve/bleve.go
@@ -156,12 +156,19 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
var queries []query.Query
if options.Keyword != "" {
- keywordQueries := []query.Query{
- inner_bleve.MatchPhraseQuery(options.Keyword, "title", issueIndexerAnalyzer),
- inner_bleve.MatchPhraseQuery(options.Keyword, "content", issueIndexerAnalyzer),
- inner_bleve.MatchPhraseQuery(options.Keyword, "comments", issueIndexerAnalyzer),
+ if options.IsFuzzyKeyword {
+ queries = append(queries, bleve.NewDisjunctionQuery([]query.Query{
+ inner_bleve.MatchPhraseQuery(options.Keyword, "title", issueIndexerAnalyzer),
+ inner_bleve.MatchPhraseQuery(options.Keyword, "content", issueIndexerAnalyzer),
+ inner_bleve.MatchPhraseQuery(options.Keyword, "comments", issueIndexerAnalyzer),
+ }...))
+ } else {
+ queries = append(queries, bleve.NewDisjunctionQuery([]query.Query{
+ inner_bleve.PrefixQuery(options.Keyword, "title"),
+ inner_bleve.PrefixQuery(options.Keyword, "content"),
+ inner_bleve.PrefixQuery(options.Keyword, "comments"),
+ }...))
}
- queries = append(queries, bleve.NewDisjunctionQuery(keywordQueries...))
}
if len(options.RepoIDs) > 0 || options.AllPublic {
@@ -175,11 +182,11 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
queries = append(queries, bleve.NewDisjunctionQuery(repoQueries...))
}
- if !options.IsPull.IsNone() {
- queries = append(queries, inner_bleve.BoolFieldQuery(options.IsPull.IsTrue(), "is_pull"))
+ if options.IsPull.Has() {
+ queries = append(queries, inner_bleve.BoolFieldQuery(options.IsPull.Value(), "is_pull"))
}
- if !options.IsClosed.IsNone() {
- queries = append(queries, inner_bleve.BoolFieldQuery(options.IsClosed.IsTrue(), "is_closed"))
+ if options.IsClosed.Has() {
+ queries = append(queries, inner_bleve.BoolFieldQuery(options.IsClosed.Value(), "is_closed"))
}
if options.NoLabelOnly {
diff --git a/modules/indexer/issues/db/options.go b/modules/indexer/issues/db/options.go
index 5406715bbc..69146573a8 100644
--- a/modules/indexer/issues/db/options.go
+++ b/modules/indexer/issues/db/options.go
@@ -11,6 +11,7 @@ import (
issue_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/indexer/issues/internal"
+ "code.gitea.io/gitea/modules/optional"
)
func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_model.IssuesOptions, error) {
@@ -75,7 +76,7 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m
UpdatedAfterUnix: convertInt64(options.UpdatedAfterUnix),
UpdatedBeforeUnix: convertInt64(options.UpdatedBeforeUnix),
PriorityRepoID: 0,
- IsArchived: 0,
+ IsArchived: optional.None[bool](),
Org: nil,
Team: nil,
User: nil,
diff --git a/modules/indexer/issues/elasticsearch/elasticsearch.go b/modules/indexer/issues/elasticsearch/elasticsearch.go
index d059f76b32..0077da263a 100644
--- a/modules/indexer/issues/elasticsearch/elasticsearch.go
+++ b/modules/indexer/issues/elasticsearch/elasticsearch.go
@@ -19,6 +19,10 @@ import (
const (
issueIndexerLatestVersion = 1
+ // multi-match-types, currently only 2 types are used
+ // Reference: https://www.elastic.co/guide/en/elasticsearch/reference/7.0/query-dsl-multi-match-query.html#multi-match-types
+ esMultiMatchTypeBestFields = "best_fields"
+ esMultiMatchTypePhrasePrefix = "phrase_prefix"
)
var _ internal.Indexer = &Indexer{}
@@ -141,7 +145,13 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
query := elastic.NewBoolQuery()
if options.Keyword != "" {
- query.Must(elastic.NewMultiMatchQuery(options.Keyword, "title", "content", "comments"))
+
+ searchType := esMultiMatchTypePhrasePrefix
+ if options.IsFuzzyKeyword {
+ searchType = esMultiMatchTypeBestFields
+ }
+
+ query.Must(elastic.NewMultiMatchQuery(options.Keyword, "title", "content", "comments").Type(searchType))
}
if len(options.RepoIDs) > 0 {
@@ -153,11 +163,11 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
query.Must(q)
}
- if !options.IsPull.IsNone() {
- query.Must(elastic.NewTermQuery("is_pull", options.IsPull.IsTrue()))
+ if options.IsPull.Has() {
+ query.Must(elastic.NewTermQuery("is_pull", options.IsPull.Value()))
}
- if !options.IsClosed.IsNone() {
- query.Must(elastic.NewTermQuery("is_closed", options.IsClosed.IsTrue()))
+ if options.IsClosed.Has() {
+ query.Must(elastic.NewTermQuery("is_closed", options.IsClosed.Value()))
}
if options.NoLabelOnly {
diff --git a/modules/indexer/issues/indexer_test.go b/modules/indexer/issues/indexer_test.go
index 3b96686d98..10ffa7cbe6 100644
--- a/modules/indexer/issues/indexer_test.go
+++ b/modules/indexer/issues/indexer_test.go
@@ -10,8 +10,8 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/indexer/issues/internal"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
_ "code.gitea.io/gitea/models"
_ "code.gitea.io/gitea/models/actions"
@@ -210,13 +210,13 @@ func searchIssueIsPull(t *testing.T) {
}{
{
SearchOptions{
- IsPull: util.OptionalBoolFalse,
+ IsPull: optional.Some(false),
},
[]int64{17, 16, 15, 14, 13, 6, 5, 18, 10, 7, 4, 1},
},
{
SearchOptions{
- IsPull: util.OptionalBoolTrue,
+ IsPull: optional.Some(true),
},
[]int64{22, 21, 12, 11, 20, 19, 9, 8, 3, 2},
},
@@ -237,13 +237,13 @@ func searchIssueIsClosed(t *testing.T) {
}{
{
SearchOptions{
- IsClosed: util.OptionalBoolFalse,
+ IsClosed: optional.Some(false),
},
[]int64{22, 21, 17, 16, 15, 14, 13, 12, 11, 20, 6, 19, 18, 10, 7, 9, 8, 3, 2, 1},
},
{
SearchOptions{
- IsClosed: util.OptionalBoolTrue,
+ IsClosed: optional.Some(true),
},
[]int64{5, 4},
},
diff --git a/modules/indexer/issues/internal/model.go b/modules/indexer/issues/internal/model.go
index 031745dd2f..d41fec4aba 100644
--- a/modules/indexer/issues/internal/model.go
+++ b/modules/indexer/issues/internal/model.go
@@ -5,8 +5,8 @@ package internal
import (
"code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/timeutil"
- "code.gitea.io/gitea/modules/util"
)
// IndexerData data stored in the issue indexer
@@ -74,11 +74,13 @@ type SearchResult struct {
type SearchOptions struct {
Keyword string // keyword to search
+ IsFuzzyKeyword bool // if false the levenshtein distance is 0
+
RepoIDs []int64 // repository IDs which the issues belong to
AllPublic bool // if include all public repositories
- IsPull util.OptionalBool // if the issues is a pull request
- IsClosed util.OptionalBool // if the issues is closed
+ IsPull optional.Option[bool] // if the issues is a pull request
+ IsClosed optional.Option[bool] // if the issues is closed
IncludedLabelIDs []int64 // labels the issues have
ExcludedLabelIDs []int64 // labels the issues don't have
diff --git a/modules/indexer/issues/internal/tests/tests.go b/modules/indexer/issues/internal/tests/tests.go
index 06fddeb65b..6724471539 100644
--- a/modules/indexer/issues/internal/tests/tests.go
+++ b/modules/indexer/issues/internal/tests/tests.go
@@ -16,8 +16,8 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/indexer/issues/internal"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/timeutil"
- "code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -166,7 +166,7 @@ var cases = []*testIndexerCase{
Paginator: &db.ListOptions{
PageSize: 5,
},
- IsPull: util.OptionalBoolFalse,
+ IsPull: optional.Some(false),
},
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
assert.Equal(t, 5, len(result.Hits))
@@ -182,7 +182,7 @@ var cases = []*testIndexerCase{
Paginator: &db.ListOptions{
PageSize: 5,
},
- IsPull: util.OptionalBoolTrue,
+ IsPull: optional.Some(true),
},
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
assert.Equal(t, 5, len(result.Hits))
@@ -198,7 +198,7 @@ var cases = []*testIndexerCase{
Paginator: &db.ListOptions{
PageSize: 5,
},
- IsClosed: util.OptionalBoolFalse,
+ IsClosed: optional.Some(false),
},
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
assert.Equal(t, 5, len(result.Hits))
@@ -214,7 +214,7 @@ var cases = []*testIndexerCase{
Paginator: &db.ListOptions{
PageSize: 5,
},
- IsClosed: util.OptionalBoolTrue,
+ IsClosed: optional.Some(true),
},
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
assert.Equal(t, 5, len(result.Hits))
diff --git a/modules/indexer/issues/meilisearch/meilisearch.go b/modules/indexer/issues/meilisearch/meilisearch.go
index ab8dcd0af4..c429920065 100644
--- a/modules/indexer/issues/meilisearch/meilisearch.go
+++ b/modules/indexer/issues/meilisearch/meilisearch.go
@@ -5,6 +5,7 @@ package meilisearch
import (
"context"
+ "errors"
"strconv"
"strings"
@@ -16,12 +17,15 @@ import (
)
const (
- issueIndexerLatestVersion = 2
+ issueIndexerLatestVersion = 3
// TODO: make this configurable if necessary
maxTotalHits = 10000
)
+// ErrMalformedResponse is never expected as we initialize the indexer ourself and so define the types.
+var ErrMalformedResponse = errors.New("meilisearch returned unexpected malformed content")
+
var _ internal.Indexer = &Indexer{}
// Indexer implements Indexer interface
@@ -47,6 +51,9 @@ func NewIndexer(url, apiKey, indexerName string) *Indexer {
},
DisplayedAttributes: []string{
"id",
+ "title",
+ "content",
+ "comments",
},
FilterableAttributes: []string{
"repo_id",
@@ -131,11 +138,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 {
@@ -221,11 +228,9 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
return nil, err
}
- hits := make([]internal.Match, 0, len(searchRes.Hits))
- for _, hit := range searchRes.Hits {
- hits = append(hits, internal.Match{
- ID: int64(hit.(map[string]any)["id"].(float64)),
- })
+ hits, err := nonFuzzyWorkaround(searchRes, options.Keyword, options.IsFuzzyKeyword)
+ if err != nil {
+ return nil, err
}
return &internal.SearchResult{
@@ -241,3 +246,77 @@ func parseSortBy(sortBy internal.SortBy) string {
}
return field + ":asc"
}
+
+// nonFuzzyWorkaround is needed as meilisearch does not have an exact search
+// and you can only change "typo tolerance" per index. So we have to post-filter the results
+// https://www.meilisearch.com/docs/learn/configuration/typo_tolerance#configuring-typo-tolerance
+// TODO: remove once https://github.com/orgs/meilisearch/discussions/377 is addressed
+func nonFuzzyWorkaround(searchRes *meilisearch.SearchResponse, keyword string, isFuzzy bool) ([]internal.Match, error) {
+ hits := make([]internal.Match, 0, len(searchRes.Hits))
+ for _, hit := range searchRes.Hits {
+ hit, ok := hit.(map[string]any)
+ if !ok {
+ return nil, ErrMalformedResponse
+ }
+
+ if !isFuzzy {
+ keyword = strings.ToLower(keyword)
+
+ // declare a anon func to check if the title, content or at least one comment contains the keyword
+ found, err := func() (bool, error) {
+ // check if title match first
+ title, ok := hit["title"].(string)
+ if !ok {
+ return false, ErrMalformedResponse
+ } else if strings.Contains(strings.ToLower(title), keyword) {
+ return true, nil
+ }
+
+ // check if content has a match
+ content, ok := hit["content"].(string)
+ if !ok {
+ return false, ErrMalformedResponse
+ } else if strings.Contains(strings.ToLower(content), keyword) {
+ return true, nil
+ }
+
+ // now check for each comment if one has a match
+ // so we first try to cast and skip if there are no comments
+ comments, ok := hit["comments"].([]any)
+ if !ok {
+ return false, ErrMalformedResponse
+ } else if len(comments) == 0 {
+ return false, nil
+ }
+
+ // now we iterate over all and report as soon as we detect one match
+ for i := range comments {
+ comment, ok := comments[i].(string)
+ if !ok {
+ return false, ErrMalformedResponse
+ }
+ if strings.Contains(strings.ToLower(comment), keyword) {
+ return true, nil
+ }
+ }
+
+ // we got no match
+ return false, nil
+ }()
+
+ if err != nil {
+ return nil, err
+ } else if !found {
+ continue
+ }
+ }
+ issueID, ok := hit["id"].(float64)
+ if !ok {
+ return nil, ErrMalformedResponse
+ }
+ hits = append(hits, internal.Match{
+ ID: int64(issueID),
+ })
+ }
+ return hits, nil
+}
diff --git a/modules/indexer/issues/meilisearch/meilisearch_test.go b/modules/indexer/issues/meilisearch/meilisearch_test.go
index 3d7237268e..ecce704236 100644
--- a/modules/indexer/issues/meilisearch/meilisearch_test.go
+++ b/modules/indexer/issues/meilisearch/meilisearch_test.go
@@ -10,7 +10,11 @@ import (
"testing"
"time"
+ "code.gitea.io/gitea/modules/indexer/issues/internal"
"code.gitea.io/gitea/modules/indexer/issues/internal/tests"
+
+ "github.com/meilisearch/meilisearch-go"
+ "github.com/stretchr/testify/assert"
)
func TestMeilisearchIndexer(t *testing.T) {
@@ -48,3 +52,44 @@ func TestMeilisearchIndexer(t *testing.T) {
tests.TestIndexer(t, indexer)
}
+
+func TestNonFuzzyWorkaround(t *testing.T) {
+ // get unexpected return
+ _, err := nonFuzzyWorkaround(&meilisearch.SearchResponse{
+ Hits: []any{"aa", "bb", "cc", "dd"},
+ }, "bowling", false)
+ assert.ErrorIs(t, err, ErrMalformedResponse)
+
+ validResponse := &meilisearch.SearchResponse{
+ Hits: []any{
+ map[string]any{
+ "id": float64(11),
+ "title": "a title",
+ "content": "issue body with no match",
+ "comments": []any{"hey whats up?", "I'm currently bowling", "nice"},
+ },
+ map[string]any{
+ "id": float64(22),
+ "title": "Bowling as title",
+ "content": "",
+ "comments": []any{},
+ },
+ map[string]any{
+ "id": float64(33),
+ "title": "Bowl-ing as fuzzy match",
+ "content": "",
+ "comments": []any{},
+ },
+ },
+ }
+
+ // nonFuzzy
+ hits, err := nonFuzzyWorkaround(validResponse, "bowling", false)
+ assert.NoError(t, err)
+ assert.EqualValues(t, []internal.Match{{ID: 11}, {ID: 22}}, hits)
+
+ // fuzzy
+ hits, err = nonFuzzyWorkaround(validResponse, "bowling", true)
+ assert.NoError(t, err)
+ assert.EqualValues(t, []internal.Match{{ID: 11}, {ID: 22}, {ID: 33}}, hits)
+}
diff --git a/modules/issue/template/template.go b/modules/issue/template/template.go
index 4e813fc91f..3be48b9edc 100644
--- a/modules/issue/template/template.go
+++ b/modules/issue/template/template.go
@@ -122,7 +122,13 @@ func validateRequired(field *api.IssueFormField, idx int) error {
// The label is not required for a markdown or checkboxes field
return nil
}
- return validateBoolItem(newErrorPosition(idx, field.Type), field.Validations, "required")
+ if err := validateBoolItem(newErrorPosition(idx, field.Type), field.Validations, "required"); err != nil {
+ return err
+ }
+ if required, _ := field.Validations["required"].(bool); required && !field.VisibleOnForm() {
+ return newErrorPosition(idx, field.Type).Errorf("can not require a hidden field")
+ }
+ return nil
}
func validateID(field *api.IssueFormField, idx int, ids container.Set[string]) error {
@@ -172,10 +178,38 @@ func validateOptions(field *api.IssueFormField, idx int) error {
return position.Errorf("'label' is required and should be a string")
}
+ if visibility, ok := opt["visible"]; ok {
+ visibilityList, ok := visibility.([]any)
+ if !ok {
+ return position.Errorf("'visible' should be list")
+ }
+ for _, visibleType := range visibilityList {
+ visibleType, ok := visibleType.(string)
+ if !ok || !(visibleType == "form" || visibleType == "content") {
+ return position.Errorf("'visible' list can only contain strings of 'form' and 'content'")
+ }
+ }
+ }
+
if required, ok := opt["required"]; ok {
if _, ok := required.(bool); !ok {
return position.Errorf("'required' should be a bool")
}
+
+ // validate if hidden field is required
+ if visibility, ok := opt["visible"]; ok {
+ visibilityList, _ := visibility.([]any)
+ isVisible := false
+ for _, v := range visibilityList {
+ if vv, _ := v.(string); vv == "form" {
+ isVisible = true
+ break
+ }
+ }
+ if !isVisible {
+ return position.Errorf("can not require a hidden checkbox")
+ }
+ }
}
}
}
@@ -238,7 +272,7 @@ func RenderToMarkdown(template *api.IssueTemplate, values url.Values) string {
IssueFormField: field,
Values: values,
}
- if f.ID == "" {
+ if f.ID == "" || !f.VisibleInContent() {
continue
}
f.WriteTo(builder)
@@ -253,11 +287,6 @@ type valuedField struct {
}
func (f *valuedField) WriteTo(builder *strings.Builder) {
- if f.Type == api.IssueFormFieldTypeMarkdown {
- // markdown blocks do not appear in output
- return
- }
-
// write label
if !f.HideLabel() {
_, _ = fmt.Fprintf(builder, "### %s\n\n", f.Label())
@@ -269,6 +298,9 @@ func (f *valuedField) WriteTo(builder *strings.Builder) {
switch f.Type {
case api.IssueFormFieldTypeCheckboxes:
for _, option := range f.Options() {
+ if !option.VisibleInContent() {
+ continue
+ }
checked := " "
if option.IsChecked() {
checked = "x"
@@ -302,6 +334,10 @@ func (f *valuedField) WriteTo(builder *strings.Builder) {
} else {
_, _ = fmt.Fprintf(builder, "%s\n", value)
}
+ case api.IssueFormFieldTypeMarkdown:
+ if value, ok := f.Attributes["value"].(string); ok {
+ _, _ = fmt.Fprintf(builder, "%s\n", value)
+ }
}
_, _ = fmt.Fprintln(builder)
}
@@ -314,6 +350,9 @@ func (f *valuedField) Label() string {
}
func (f *valuedField) HideLabel() bool {
+ if f.Type == api.IssueFormFieldTypeMarkdown {
+ return true
+ }
if label, ok := f.Attributes["hide_label"].(bool); ok {
return label
}
@@ -385,6 +424,22 @@ func (o *valuedOption) IsChecked() bool {
return false
}
+func (o *valuedOption) VisibleInContent() bool {
+ if o.field.Type == api.IssueFormFieldTypeCheckboxes {
+ if vs, ok := o.data.(map[string]any); ok {
+ if vl, ok := vs["visible"].([]any); ok {
+ for _, v := range vl {
+ if vv, _ := v.(string); vv == "content" {
+ return true
+ }
+ }
+ return false
+ }
+ }
+ }
+ return true
+}
+
var minQuotesRegex = regexp.MustCompilePOSIX("^`{3,}")
// minQuotes return 3 or more back-quotes.
diff --git a/modules/issue/template/template_test.go b/modules/issue/template/template_test.go
index 06e6b70d35..e24b962d61 100644
--- a/modules/issue/template/template_test.go
+++ b/modules/issue/template/template_test.go
@@ -10,6 +10,7 @@ import (
"code.gitea.io/gitea/modules/json"
api "code.gitea.io/gitea/modules/structs"
+ "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -318,6 +319,42 @@ body:
`,
wantErr: "body[0](checkboxes), option[0]: 'required' should be a bool",
},
+ {
+ name: "field is required but hidden",
+ content: `
+name: "test"
+about: "this is about"
+body:
+ - type: "input"
+ id: "1"
+ attributes:
+ label: "a"
+ validations:
+ required: true
+ visible: [content]
+`,
+ wantErr: "body[0](input): can not require a hidden field",
+ },
+ {
+ name: "checkboxes is required but hidden",
+ content: `
+name: "test"
+about: "this is about"
+body:
+ - type: checkboxes
+ id: "1"
+ attributes:
+ label: Label of checkboxes
+ description: Description of checkboxes
+ options:
+ - label: Option 1
+ required: false
+ - label: Required and hidden
+ required: true
+ visible: [content]
+`,
+ wantErr: "body[0](checkboxes), option[1]: can not require a hidden checkbox",
+ },
{
name: "valid",
content: `
@@ -374,8 +411,11 @@ body:
required: true
- label: Option 2 of checkboxes
required: false
- - label: Option 3 of checkboxes
+ - label: Hidden Option 3 of checkboxes
+ visible: [content]
+ - label: Required but not submitted
required: true
+ visible: [form]
`,
want: &api.IssueTemplate{
Name: "Name",
@@ -390,6 +430,7 @@ body:
Attributes: map[string]any{
"value": "Value of the markdown",
},
+ Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm},
},
{
Type: "textarea",
@@ -404,6 +445,7 @@ body:
Validations: map[string]any{
"required": true,
},
+ Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent},
},
{
Type: "input",
@@ -419,6 +461,7 @@ body:
"is_number": true,
"regex": "[a-zA-Z0-9]+",
},
+ Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent},
},
{
Type: "dropdown",
@@ -436,6 +479,7 @@ body:
Validations: map[string]any{
"required": true,
},
+ Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent},
},
{
Type: "checkboxes",
@@ -446,9 +490,11 @@ body:
"options": []any{
map[string]any{"label": "Option 1 of checkboxes", "required": true},
map[string]any{"label": "Option 2 of checkboxes", "required": false},
- map[string]any{"label": "Option 3 of checkboxes", "required": true},
+ map[string]any{"label": "Hidden Option 3 of checkboxes", "visible": []string{"content"}},
+ map[string]any{"label": "Required but not submitted", "required": true, "visible": []string{"form"}},
},
},
+ Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent},
},
},
FileName: "test.yaml",
@@ -467,7 +513,12 @@ body:
- type: markdown
id: id1
attributes:
- value: Value of the markdown
+ value: Value of the markdown shown in form
+ - type: markdown
+ id: id2
+ attributes:
+ value: Value of the markdown shown in created issue
+ visible: [content]
`,
want: &api.IssueTemplate{
Name: "Name",
@@ -480,8 +531,17 @@ body:
Type: "markdown",
ID: "id1",
Attributes: map[string]any{
- "value": "Value of the markdown",
+ "value": "Value of the markdown shown in form",
},
+ Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm},
+ },
+ {
+ Type: "markdown",
+ ID: "id2",
+ Attributes: map[string]any{
+ "value": "Value of the markdown shown in created issue",
+ },
+ Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleContent},
},
},
FileName: "test.yaml",
@@ -515,6 +575,7 @@ body:
Attributes: map[string]any{
"value": "Value of the markdown",
},
+ Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm},
},
},
FileName: "test.yaml",
@@ -548,6 +609,7 @@ body:
Attributes: map[string]any{
"value": "Value of the markdown",
},
+ Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm},
},
},
FileName: "test.yaml",
@@ -622,9 +684,14 @@ body:
- type: markdown
id: id1
attributes:
- value: Value of the markdown
- - type: textarea
+ value: Value of the markdown shown in form
+ - type: markdown
id: id2
+ attributes:
+ value: Value of the markdown shown in created issue
+ visible: [content]
+ - type: textarea
+ id: id3
attributes:
label: Label of textarea
description: Description of textarea
@@ -634,7 +701,7 @@ body:
validations:
required: true
- type: input
- id: id3
+ id: id4
attributes:
label: Label of input
description: Description of input
@@ -646,7 +713,7 @@ body:
is_number: true
regex: "[a-zA-Z0-9]+"
- type: dropdown
- id: id4
+ id: id5
attributes:
label: Label of dropdown
description: Description of dropdown
@@ -658,7 +725,7 @@ body:
validations:
required: true
- type: checkboxes
- id: id5
+ id: id6
attributes:
label: Label of checkboxes
description: Description of checkboxes
@@ -669,20 +736,26 @@ body:
required: false
- label: Option 3 of checkboxes
required: true
+ visible: [form]
+ - label: Hidden Option of checkboxes
+ visible: [content]
`,
values: map[string][]string{
- "form-field-id2": {"Value of id2"},
"form-field-id3": {"Value of id3"},
- "form-field-id4": {"0,1"},
- "form-field-id5-0": {"on"},
- "form-field-id5-2": {"on"},
+ "form-field-id4": {"Value of id4"},
+ "form-field-id5": {"0,1"},
+ "form-field-id6-0": {"on"},
+ "form-field-id6-2": {"on"},
},
},
- want: `### Label of textarea
-` + "```bash\nValue of id2\n```" + `
+ want: `Value of the markdown shown in created issue
-Value of id3
+### Label of textarea
+
+` + "```bash\nValue of id3\n```" + `
+
+Value of id4
### Label of dropdown
@@ -692,7 +765,7 @@ Option 1 of dropdown, Option 2 of dropdown
- [x] Option 1 of checkboxes
- [ ] Option 2 of checkboxes
-- [x] Option 3 of checkboxes
+- [ ] Hidden Option of checkboxes
`,
},
@@ -704,7 +777,7 @@ Option 1 of dropdown, Option 2 of dropdown
t.Fatal(err)
}
if got := RenderToMarkdown(template, tt.args.values); got != tt.want {
- t.Errorf("RenderToMarkdown() = %v, want %v", got, tt.want)
+ assert.EqualValues(t, tt.want, got)
}
})
}
diff --git a/modules/issue/template/unmarshal.go b/modules/issue/template/unmarshal.go
index 8cae8d4c42..0fc13d7ddf 100644
--- a/modules/issue/template/unmarshal.go
+++ b/modules/issue/template/unmarshal.go
@@ -128,9 +128,18 @@ func unmarshal(filename string, content []byte) (*api.IssueTemplate, error) {
}
}
for i, v := range it.Fields {
+ // set default id value
if v.ID == "" {
v.ID = strconv.Itoa(i)
}
+ // set default visibility
+ if v.Visible == nil {
+ v.Visible = []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm}
+ // markdown is not submitted by default
+ if v.Type != api.IssueFormFieldTypeMarkdown {
+ v.Visible = append(v.Visible, api.IssueFormFieldVisibleContent)
+ }
+ }
}
}
diff --git a/modules/markup/csv/csv.go b/modules/markup/csv/csv.go
index 7af34a6cbc..12458e954a 100644
--- a/modules/markup/csv/csv.go
+++ b/modules/markup/csv/csv.go
@@ -93,8 +93,10 @@ func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Wri
if _, err := tmpBlock.WriteString(html.EscapeString(string(rawBytes))); err != nil {
return err
}
- _, err = tmpBlock.WriteString("")
- return err
+ if _, err := tmpBlock.WriteString(""); err != nil {
+ return err
+ }
+ return tmpBlock.Flush()
}
rd, err := csv.CreateReaderAndDetermineDelimiter(ctx, bytes.NewReader(rawBytes))
diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go
index c4b23e66fc..67817ce27b 100644
--- a/modules/markup/markdown/goldmark.go
+++ b/modules/markup/markdown/goldmark.go
@@ -219,21 +219,18 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
v.SetAttributeString("class", []byte("gt-py-3 attention attention-"+attentionType))
// create an emphasis to make it bold
+ attentionParagraph := ast.NewParagraph()
emphasis := ast.NewEmphasis(2)
emphasis.SetAttributeString("class", []byte("attention-"+attentionType))
- firstParagraph.InsertBefore(firstParagraph, firstTextNode, emphasis)
// capitalize first letter
attentionText := ast.NewString([]byte(strings.ToUpper(string(attentionType[0])) + attentionType[1:]))
- // replace the ![TYPE] with icon+Type
+ // replace the ![TYPE] with a dedicated paragraph of icon+Type
emphasis.AppendChild(emphasis, attentionText)
- for i := 0; i < 2; i++ {
- lineBreak := ast.NewText()
- lineBreak.SetSoftLineBreak(true)
- firstParagraph.InsertAfter(firstParagraph, emphasis, lineBreak)
- }
- firstParagraph.InsertBefore(firstParagraph, emphasis, NewAttention(attentionType))
+ attentionParagraph.AppendChild(attentionParagraph, NewAttention(attentionType))
+ attentionParagraph.AppendChild(attentionParagraph, emphasis)
+ firstParagraph.Parent().InsertBefore(firstParagraph.Parent(), firstParagraph, attentionParagraph)
firstParagraph.RemoveChild(firstParagraph, firstTextNode)
firstParagraph.RemoveChild(firstParagraph, secondTextNode)
firstParagraph.RemoveChild(firstParagraph, thirdTextNode)
diff --git a/modules/queue/workergroup.go b/modules/queue/workergroup.go
index 147a4f335e..e3801ef2b2 100644
--- a/modules/queue/workergroup.go
+++ b/modules/queue/workergroup.go
@@ -60,6 +60,9 @@ func (q *WorkerPoolQueue[T]) doDispatchBatchToWorker(wg *workerGroup[T], flushCh
full = true
}
+ // TODO: the logic could be improved in the future, to avoid a data-race between "doStartNewWorker" and "workerNum"
+ // The root problem is that if we skip "doStartNewWorker" here, the "workerNum" might be decreased by other workers later
+ // So ideally, it should check whether there are enough workers by some approaches, and start new workers if necessary.
q.workerNumMu.Lock()
noWorker := q.workerNum == 0
if full || noWorker {
@@ -143,7 +146,11 @@ func (q *WorkerPoolQueue[T]) doStartNewWorker(wp *workerGroup[T]) {
log.Debug("Queue %q starts new worker", q.GetName())
defer log.Debug("Queue %q stops idle worker", q.GetName())
+ atomic.AddInt32(&q.workerStartedCounter, 1) // Only increase counter, used for debugging
+
t := time.NewTicker(workerIdleDuration)
+ defer t.Stop()
+
keepWorking := true
stopWorking := func() {
q.workerNumMu.Lock()
@@ -158,13 +165,18 @@ func (q *WorkerPoolQueue[T]) doStartNewWorker(wp *workerGroup[T]) {
case batch, ok := <-q.batchChan:
if !ok {
stopWorking()
- } else {
- q.doWorkerHandle(batch)
- t.Reset(workerIdleDuration)
+ continue
+ }
+ q.doWorkerHandle(batch)
+ // reset the idle ticker, and drain the tick after reset in case a tick is already triggered
+ t.Reset(workerIdleDuration)
+ select {
+ case <-t.C:
+ default:
}
case <-t.C:
q.workerNumMu.Lock()
- keepWorking = q.workerNum <= 1
+ keepWorking = q.workerNum <= 1 // keep the last worker running
if !keepWorking {
q.workerNum--
}
diff --git a/modules/queue/workerqueue.go b/modules/queue/workerqueue.go
index b28fd88027..4160622d81 100644
--- a/modules/queue/workerqueue.go
+++ b/modules/queue/workerqueue.go
@@ -40,6 +40,8 @@ type WorkerPoolQueue[T any] struct {
workerMaxNum int
workerActiveNum int
workerNumMu sync.Mutex
+
+ workerStartedCounter int32
}
type flushType chan struct{}
diff --git a/modules/queue/workerqueue_test.go b/modules/queue/workerqueue_test.go
index e60120162a..e09669c542 100644
--- a/modules/queue/workerqueue_test.go
+++ b/modules/queue/workerqueue_test.go
@@ -11,6 +11,7 @@ import (
"time"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert"
)
@@ -175,11 +176,7 @@ func testWorkerPoolQueuePersistence(t *testing.T, queueSetting setting.QueueSett
}
func TestWorkerPoolQueueActiveWorkers(t *testing.T) {
- oldWorkerIdleDuration := workerIdleDuration
- workerIdleDuration = 300 * time.Millisecond
- defer func() {
- workerIdleDuration = oldWorkerIdleDuration
- }()
+ defer test.MockVariableValue(&workerIdleDuration, 300*time.Millisecond)()
handler := func(items ...int) (unhandled []int) {
time.Sleep(100 * time.Millisecond)
@@ -250,3 +247,25 @@ func TestWorkerPoolQueueShutdown(t *testing.T) {
q, _ = newWorkerPoolQueueForTest("test-workpoolqueue", qs, handler, false)
assert.EqualValues(t, 20, q.GetQueueItemNumber())
}
+
+func TestWorkerPoolQueueWorkerIdleReset(t *testing.T) {
+ defer test.MockVariableValue(&workerIdleDuration, 10*time.Millisecond)()
+
+ handler := func(items ...int) (unhandled []int) {
+ time.Sleep(50 * time.Millisecond)
+ return nil
+ }
+
+ q, _ := newWorkerPoolQueueForTest("test-workpoolqueue", setting.QueueSettings{Type: "channel", BatchLength: 1, MaxWorkers: 2, Length: 100}, handler, false)
+ stop := runWorkerPoolQueue(q)
+ for i := 0; i < 20; i++ {
+ assert.NoError(t, q.Push(i))
+ }
+
+ time.Sleep(500 * time.Millisecond)
+ assert.EqualValues(t, 2, q.GetWorkerNumber())
+ assert.EqualValues(t, 2, q.GetWorkerActiveNumber())
+ // when the queue never becomes empty, the existing workers should keep working
+ assert.EqualValues(t, 2, q.workerStartedCounter)
+ stop()
+}
diff --git a/modules/repository/collaborator.go b/modules/repository/collaborator.go
index ebe14e3a4c..f71c58fbdf 100644
--- a/modules/repository/collaborator.go
+++ b/modules/repository/collaborator.go
@@ -16,6 +16,14 @@ import (
)
func AddCollaborator(ctx context.Context, repo *repo_model.Repository, u *user_model.User) error {
+ if err := repo.LoadOwner(ctx); err != nil {
+ return err
+ }
+
+ if user_model.IsUserBlockedBy(ctx, u, repo.OwnerID) || user_model.IsUserBlockedBy(ctx, repo.Owner, u.ID) {
+ return user_model.ErrBlockedUser
+ }
+
return db.WithTx(ctx, func(ctx context.Context) error {
has, err := db.Exist[repo_model.Collaboration](ctx, builder.Eq{
"repo_id": repo.ID,
diff --git a/modules/repository/create.go b/modules/repository/create.go
index ca2150b972..4f18b9b3fa 100644
--- a/modules/repository/create.go
+++ b/modules/repository/create.go
@@ -93,6 +93,12 @@ func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, re
AllowRebaseUpdate: true,
},
})
+ } else if tp == unit.TypeProjects {
+ units = append(units, repo_model.RepoUnit{
+ RepoID: repo.ID,
+ Type: tp,
+ Config: &repo_model.ProjectsConfig{ProjectsMode: repo_model.ProjectsModeAll},
+ })
} else {
units = append(units, repo_model.RepoUnit{
RepoID: repo.ID,
@@ -147,7 +153,7 @@ func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, re
}
if setting.Service.AutoWatchNewRepos {
- if err = repo_model.WatchRepo(ctx, doer.ID, repo.ID, true); err != nil {
+ if err = repo_model.WatchRepo(ctx, doer, repo, true); err != nil {
return fmt.Errorf("WatchRepo: %w", err)
}
}
diff --git a/modules/setting/admin.go b/modules/setting/admin.go
index 29bb947bc4..be214a58ce 100644
--- a/modules/setting/admin.go
+++ b/modules/setting/admin.go
@@ -21,5 +21,6 @@ func loadAdminFrom(rootCfg ConfigProvider) {
const (
UserFeatureDeletion = "deletion"
+ UserFeatureManageSSHKeys = "manage_ssh_keys"
UserFeatureManageGPGKeys = "manage_gpg_keys"
)
diff --git a/modules/setting/session.go b/modules/setting/session.go
index 8b9b754b38..70497e5eaa 100644
--- a/modules/setting/session.go
+++ b/modules/setting/session.go
@@ -20,7 +20,7 @@ var SessionConfig = struct {
ProviderConfig string
// Cookie name to save session ID. Default is "MacaronSession".
CookieName string
- // Cookie path to store. Default is "/". HINT: there was a bug, the old value doesn't have trailing slash, and could be empty "".
+ // Cookie path to store. Default is "/".
CookiePath string
// GC interval time in seconds. Default is 3600.
Gclifetime int64
@@ -49,7 +49,10 @@ func loadSessionFrom(rootCfg ConfigProvider) {
fatalDuplicatedPath("session", SessionConfig.ProviderConfig)
}
SessionConfig.CookieName = sec.Key("COOKIE_NAME").MustString("i_like_gitea")
- SessionConfig.CookiePath = AppSubURL + "/" // there was a bug, old code only set CookePath=AppSubURL, no trailing slash
+ SessionConfig.CookiePath = AppSubURL
+ if SessionConfig.CookiePath == "" {
+ SessionConfig.CookiePath = "/"
+ }
SessionConfig.Secure = sec.Key("COOKIE_SECURE").MustBool(strings.HasPrefix(strings.ToLower(AppURL), "https://"))
SessionConfig.Gclifetime = sec.Key("GC_INTERVAL_TIME").MustInt64(86400)
SessionConfig.Maxlifetime = sec.Key("SESSION_LIFE_TIME").MustInt64(86400)
diff --git a/modules/structs/issue.go b/modules/structs/issue.go
index 34eae69329..16242d18ad 100644
--- a/modules/structs/issue.go
+++ b/modules/structs/issue.go
@@ -6,6 +6,7 @@ package structs
import (
"fmt"
"path"
+ "slices"
"strings"
"time"
@@ -141,12 +142,37 @@ const (
// IssueFormField represents a form field
// swagger:model
type IssueFormField struct {
- Type IssueFormFieldType `json:"type" yaml:"type"`
- ID string `json:"id" yaml:"id"`
- Attributes map[string]any `json:"attributes" yaml:"attributes"`
- Validations map[string]any `json:"validations" yaml:"validations"`
+ Type IssueFormFieldType `json:"type" yaml:"type"`
+ ID string `json:"id" yaml:"id"`
+ Attributes map[string]any `json:"attributes" yaml:"attributes"`
+ Validations map[string]any `json:"validations" yaml:"validations"`
+ Visible []IssueFormFieldVisible `json:"visible,omitempty"`
}
+func (iff IssueFormField) VisibleOnForm() bool {
+ if len(iff.Visible) == 0 {
+ return true
+ }
+ return slices.Contains(iff.Visible, IssueFormFieldVisibleForm)
+}
+
+func (iff IssueFormField) VisibleInContent() bool {
+ if len(iff.Visible) == 0 {
+ // we have our markdown exception
+ return iff.Type != IssueFormFieldTypeMarkdown
+ }
+ return slices.Contains(iff.Visible, IssueFormFieldVisibleContent)
+}
+
+// IssueFormFieldVisible defines issue form field visible
+// swagger:model
+type IssueFormFieldVisible string
+
+const (
+ IssueFormFieldVisibleForm IssueFormFieldVisible = "form"
+ IssueFormFieldVisibleContent IssueFormFieldVisible = "content"
+)
+
// IssueTemplate represents an issue template for a repository
// swagger:model
type IssueTemplate struct {
diff --git a/modules/structs/repo.go b/modules/structs/repo.go
index 56d6158bd8..bc8eb0b756 100644
--- a/modules/structs/repo.go
+++ b/modules/structs/repo.go
@@ -90,6 +90,7 @@ type Repository struct {
ExternalWiki *ExternalWiki `json:"external_wiki,omitempty"`
HasPullRequests bool `json:"has_pull_requests"`
HasProjects bool `json:"has_projects"`
+ ProjectsMode string `json:"projects_mode"`
HasReleases bool `json:"has_releases"`
HasPackages bool `json:"has_packages"`
HasActions bool `json:"has_actions"`
@@ -180,6 +181,8 @@ type EditRepoOption struct {
HasPullRequests *bool `json:"has_pull_requests,omitempty"`
// either `true` to enable project unit, or `false` to disable them.
HasProjects *bool `json:"has_projects,omitempty"`
+ // `repo` to only allow repo-level projects, `owner` to only allow owner projects, `all` to allow both.
+ ProjectsMode *string `json:"projects_mode,omitempty" binding:"In(repo,owner,all)"`
// either `true` to enable releases unit, or `false` to disable them.
HasReleases *bool `json:"has_releases,omitempty"`
// either `true` to enable packages unit, or `false` to disable them.
diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index 1487fce69d..0997239a55 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -208,14 +208,8 @@ func SafeHTML(s any) template.HTML {
}
// SanitizeHTML sanitizes the input by pre-defined markdown rules
-func SanitizeHTML(s any) template.HTML {
- switch v := s.(type) {
- case string:
- return template.HTML(markup.Sanitize(v))
- case template.HTML:
- return template.HTML(markup.Sanitize(string(v)))
- }
- panic(fmt.Sprintf("unexpected type %T", s))
+func SanitizeHTML(s string) template.HTML {
+ return template.HTML(markup.Sanitize(s))
}
func HTMLEscape(s any) template.HTML {
diff --git a/modules/templates/helper_test.go b/modules/templates/helper_test.go
index 3365278ac2..64f29d033e 100644
--- a/modules/templates/helper_test.go
+++ b/modules/templates/helper_test.go
@@ -64,5 +64,4 @@ func TestHTMLFormat(t *testing.T) {
func TestSanitizeHTML(t *testing.T) {
assert.Equal(t, template.HTML(`link xss