1
1
Fork 1
mirror of https://github.com/go-gitea/gitea.git synced 2024-05-19 22:26:15 +02:00

Merge branch 'main' into issue-updates

This commit is contained in:
Anbraten 2024-03-04 22:07:18 +01:00 committed by GitHub
commit 77f89b7314
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
434 changed files with 10501 additions and 3818 deletions

View File

@ -19,4 +19,5 @@ jobs:
steps:
- uses: dessant/lock-threads@v5
with:
issue-inactive-days: 45
issue-inactive-days: 10
pr-inactive-days: 7

View File

@ -32,9 +32,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
python-version: "3.12"
- run: pip install poetry
- run: make deps-py
- run: make lint-templates
@ -45,9 +45,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
python-version: "3.12"
- run: pip install poetry
- run: make deps-py
- run: make lint-yaml

2
.gitignore vendored
View File

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

View File

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

View File

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

View File

@ -908,6 +908,7 @@ fomantic:
cd $(FOMANTIC_WORK_DIR) && npm install --no-save
cp -f $(FOMANTIC_WORK_DIR)/theme.config.less $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/src/theme.config
cp -rf $(FOMANTIC_WORK_DIR)/_site $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/src/
$(SED_INPLACE) -e 's/ overrideBrowserslist\r/ overrideBrowserslist: ["defaults"]\r/g' $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/tasks/config/tasks.js
cd $(FOMANTIC_WORK_DIR) && npx gulp -f node_modules/fomantic-ui/gulpfile.js build
# fomantic uses "touchstart" as click event for some browsers, it's not ideal, so we force fomantic to always use "click" as click event
$(SED_INPLACE) -e 's/clickEvent[ \t]*=/clickEvent = "click", unstableClickEvent =/g' $(FOMANTIC_WORK_DIR)/build/semantic.js

View File

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

View File

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

View File

@ -956,6 +956,12 @@ LEVEL = Info
;GO_GET_CLONE_URL_PROTOCOL = https
;;
;; Close issues as long as a commit on any branch marks it as fixed
;DEFAULT_CLOSE_ISSUES_VIA_COMMITS_IN_ANY_BRANCH = false
;;
;; Allow users to push local repositories to Gitea and have them automatically created for a user or an org
;ENABLE_PUSH_CREATE_USER = false
;ENABLE_PUSH_CREATE_ORG = false
;;
;; Comma separated list of globally disabled repo units. Allowed values: repo.issues, repo.ext_issues, repo.pulls, repo.wiki, repo.ext_wiki, repo.projects, repo.packages, repo.actions.
;DISABLED_REPO_UNITS =
;;
@ -1474,8 +1480,10 @@ LEVEL = Info
;;
;; Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled
;DEFAULT_EMAIL_NOTIFICATIONS = enabled
;; Disabled features for users, could be "deletion", more features can be disabled in future
;; Disabled features for users, could be "deletion", "manage_ssh_keys","manage_gpg_keys" more features can be disabled in future
;; - deletion: a user cannot delete their own account
;; - manage_ssh_keys: a user cannot configure ssh keys
;; - manage_gpg_keys: a user cannot configure gpg keys
;USER_DISABLED_FEATURES =
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@ -518,8 +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` and more features can be added in future.
- `USER_DISABLED_FEATURES`: **_empty_** Disabled features for users, could be `deletion`, `manage_ssh_keys`, `manage_gpg_keys` and more features can be added in future.
- `deletion`: User cannot delete their own account.
- `manage_ssh_keys`: User cannot configure ssh keys.
- `manage_gpg_keys`: User cannot configure gpg keys.
## Security (`security`)

View File

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

View File

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

View File

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

View File

@ -221,9 +221,11 @@ Our translations are currently crowd-sourced on our [Crowdin project](https://cr
Whether you want to change a translation or add a new one, it will need to be there as all translations are overwritten in our CI via the Crowdin integration.
## Push Hook / Webhook aren't running
## Push Hook / Webhook / Actions aren't running
If you can push but can't see push activities on the home dashboard, or the push doesn't trigger webhook, there are a few possibilities:
If you can push but can't see push activities on the home dashboard, or the push doesn't trigger webhook and Actions workflows, it's likely that the git hooks are not working.
There are a few possibilities:
1. The git hooks are out of sync: run "Resynchronize pre-receive, update and post-receive hooks of all repositories" on the site admin panel
2. The git repositories (and hooks) are stored on some filesystems (ex: mounted by NAS) which don't support script execution, make sure the filesystem supports `chmod a+x any-script`

View File

@ -225,9 +225,11 @@ Gitea还提供了自己的SSH服务器用于在SSHD不可用时使用。
无论您想要更改翻译还是添加新的翻译都需要在Crowdin集成中进行因为所有翻译都会被CI覆盖。
## 推送钩子/ Webhook未运行
## 推送钩子/ Webhook / Actions 未运行
如果您可以推送但无法在主页仪表板上看到推送活动或者推送不触发Webhook有几种可能性
如果您可以推送但无法在主页仪表板上看到推送活动,或者推送不触发 Webhook 和 Actions可能是 git 钩子不工作而导致的。
这可能是由于以下原因:
1. Git钩子不同步在站点管理面板上运行“重新同步所有仓库的pre-receive、update和post-receive钩子”
2. Git仓库和钩子存储在一些不支持脚本执行的文件系统上例如由NAS挂载请确保文件系统支持`chmod a+x any-script`

View File

@ -0,0 +1,37 @@
---
date: "2023-02-25T00:00:00+00:00"
title: "Badge"
slug: "badge"
sidebar_position: 11
toc: false
draft: false
aliases:
- /en-us/badge
menu:
sidebar:
parent: "usage"
name: "Badge"
sidebar_position: 11
identifier: "Badge"
---
# Badge
Gitea has its builtin Badge system which allows you to display the status of your repository in other places. You can use the following badges:
## Workflow Badge
The Gitea Actions workflow badge is a badge that shows the status of the latest workflow run.
It is designed to be compatible with [GitHub Actions workflow badge](https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/adding-a-workflow-status-badge).
You can use the following URL to get the badge:
```
https://your-gitea-instance.com/{owner}/{repo}/actions/workflows/{workflow_file}?branch={branch}&event={event}
```
- `{owner}`: The owner of the repository.
- `{repo}`: The name of the repository.
- `{workflow_file}`: The name of the workflow file.
- `{branch}`: Optional. The branch of the workflow. Default to your repository's default branch.
- `{event}`: Optional. The event of the workflow. Default to none.

View File

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

View File

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

View File

@ -339,6 +339,23 @@ func GetRunByIndex(ctx context.Context, repoID, index int64) (*ActionRun, error)
return run, nil
}
func GetWorkflowLatestRun(ctx context.Context, repoID int64, workflowFile, branch, event string) (*ActionRun, error) {
var run ActionRun
q := db.GetEngine(ctx).Where("repo_id=?", repoID).
And("ref = ?", branch).
And("workflow_id = ?", workflowFile)
if event != "" {
q.And("event = ?", event)
}
has, err := q.Desc("id").Get(&run)
if err != nil {
return nil, err
} else if !has {
return nil, util.NewNotExistErrorf("run with repo_id %d, ref %s, workflow_id %s", repoID, branch, workflowFile)
}
return &run, nil
}
// UpdateRun updates a run.
// It requires the inputted run has Version set.
// It will return error if the version is not matched (it means the run has been changed after loaded).

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,3 +17,22 @@
updated: 1683636626
need_approval: 0
approved_by: 0
-
id: 792
title: "update actions"
repo_id: 4
owner_id: 1
workflow_id: "artifact.yaml"
index: 188
trigger_user_id: 1
ref: "refs/heads/master"
commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0"
event: "push"
is_fork_pull_request: 0
status: 1
started: 1683636528
stopped: 1683636626
created: 1683636108
updated: 1683636626
need_approval: 0
approved_by: 0

View File

@ -12,3 +12,17 @@
status: 1
started: 1683636528
stopped: 1683636626
-
id: 193
run_id: 792
repo_id: 4
owner_id: 1
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
is_fork_pull_request: 0
name: job_2
attempt: 1
job_id: job_2
task_id: 48
status: 1
started: 1683636528
stopped: 1683636626

View File

@ -18,3 +18,23 @@
log_length: 707
log_size: 90179
log_expired: 0
-
id: 48
job_id: 193
attempt: 1
runner_id: 1
status: 6 # 6 is the status code for "running", running task can upload artifacts
started: 1683636528
stopped: 1683636626
repo_id: 4
owner_id: 1
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
is_fork_pull_request: 0
token_hash: ffffcfffffffbffffffffffffffffefffffffafffffffffffffffffffffffffffffdffffffffffffffffffffffffffffffff
token_salt: ffffffffff
token_last_eight: ffffffff
log_filename: artifact-test2/2f/47.log
log_in_storage: 1
log_length: 707
log_size: 90179
log_expired: 0

View File

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

View File

@ -14,3 +14,7 @@
id: 4
assignee_id: 2
issue_id: 17
-
id: 5
assignee_id: 10
issue_id: 6

View File

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

View File

@ -650,12 +650,6 @@
type: 2
created_unix: 946684810
-
id: 98
repo_id: 1
type: 8
created_unix: 946684810
-
id: 99
repo_id: 1

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -172,13 +172,9 @@ func FetchIssueContentHistoryList(dbCtx context.Context, issueID, commentID int6
// HasIssueContentHistory check if a ContentHistory entry exists
func HasIssueContentHistory(dbCtx context.Context, issueID, commentID int64) (bool, error) {
exists, err := db.GetEngine(dbCtx).Cols("id").Exist(&ContentHistory{
IssueID: issueID,
CommentID: commentID,
})
exists, err := db.GetEngine(dbCtx).Where(builder.Eq{"issue_id": issueID, "comment_id": commentID}).Exist(&ContentHistory{})
if err != nil {
log.Error("can not fetch issue content history. err=%v", err)
return false, err
return false, fmt.Errorf("can not check issue content history. err: %w", err)
}
return exists, err
}

View File

@ -78,3 +78,22 @@ func TestContentHistory(t *testing.T) {
assert.EqualValues(t, 7, list2[1].HistoryID)
assert.EqualValues(t, 4, list2[2].HistoryID)
}
func TestHasIssueContentHistoryForCommentOnly(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
_ = db.TruncateBeans(db.DefaultContext, &issues_model.ContentHistory{})
hasHistory1, _ := issues_model.HasIssueContentHistory(db.DefaultContext, 10, 0)
assert.False(t, hasHistory1)
hasHistory2, _ := issues_model.HasIssueContentHistory(db.DefaultContext, 10, 100)
assert.False(t, hasHistory2)
_ = issues_model.SaveIssueContentHistory(db.DefaultContext, 1, 10, 100, timeutil.TimeStampNow(), "c-a", true)
_ = issues_model.SaveIssueContentHistory(db.DefaultContext, 1, 10, 100, timeutil.TimeStampNow().Add(5), "c-b", false)
hasHistory1, _ = issues_model.HasIssueContentHistory(db.DefaultContext, 10, 0)
assert.False(t, hasHistory1)
hasHistory2, _ = issues_model.HasIssueContentHistory(db.DefaultContext, 10, 100)
assert.True(t, hasHistory2)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -558,6 +558,10 @@ var migrations = []Migration{
NewMigration("Add PreviousDuration to ActionRun", v1_22.AddPreviousDurationToActionRun),
// v286 -> v287
NewMigration("Add support for SHA256 git repositories", v1_22.AdjustDBForSha256),
// v287 -> v288
NewMigration("Use Slug instead of ID for Badges", v1_22.UseSlugInsteadOfIDForBadges),
// v288 -> v289
NewMigration("Add user_blocking table", v1_22.AddUserBlockingTable),
}
// GetCurrentDBVersion returns the current db version

View File

@ -0,0 +1,46 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_22 //nolint
import (
"xorm.io/xorm"
)
type BadgeUnique struct {
ID int64 `xorm:"pk autoincr"`
Slug string `xorm:"UNIQUE"`
}
func (BadgeUnique) TableName() string {
return "badge"
}
func UseSlugInsteadOfIDForBadges(x *xorm.Engine) error {
type Badge struct {
Slug string
}
err := x.Sync(new(Badge))
if err != nil {
return err
}
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
_, err = sess.Exec("UPDATE `badge` SET `slug` = `id` Where `slug` IS NULL")
if err != nil {
return err
}
err = sess.Sync(new(BadgeUnique))
if err != nil {
return err
}
return sess.Commit()
}

View File

@ -0,0 +1,57 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_22 //nolint
import (
"fmt"
"testing"
"code.gitea.io/gitea/models/migrations/base"
"github.com/stretchr/testify/assert"
)
func Test_UpdateBadgeColName(t *testing.T) {
type Badge struct {
ID int64 `xorm:"pk autoincr"`
Description string
ImageURL string
}
// Prepare and load the testing database
x, deferable := base.PrepareTestEnv(t, 0, new(BadgeUnique), new(Badge))
defer deferable()
if x == nil || t.Failed() {
return
}
oldBadges := []Badge{
{ID: 1, Description: "Test Badge 1", ImageURL: "https://example.com/badge1.png"},
{ID: 2, Description: "Test Badge 2", ImageURL: "https://example.com/badge2.png"},
{ID: 3, Description: "Test Badge 3", ImageURL: "https://example.com/badge3.png"},
}
for _, badge := range oldBadges {
_, err := x.Insert(&badge)
assert.NoError(t, err)
}
if err := UseSlugInsteadOfIDForBadges(x); err != nil {
assert.NoError(t, err)
return
}
got := []BadgeUnique{}
if err := x.Table("badge").Asc("id").Find(&got); !assert.NoError(t, err) {
return
}
for i, e := range oldBadges {
got := got[i]
assert.Equal(t, e.ID, got.ID)
assert.Equal(t, fmt.Sprintf("%d", e.ID), got.Slug)
}
// TODO: check if badges have been updated
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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() {

View File

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

View File

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

View File

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

View File

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

View File

@ -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"
@ -410,6 +411,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{
@ -840,7 +848,7 @@ func (repo *Repository) TemplateRepo(ctx context.Context) *Repository {
type CountRepositoryOptions struct {
OwnerID int64
Private util.OptionalBool
Private optional.Option[bool]
}
// CountRepositories returns number of repositories.
@ -852,8 +860,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))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,13 +5,15 @@ package user
import (
"context"
"fmt"
"code.gitea.io/gitea/models/db"
)
// Badge represents a user badge
type Badge struct {
ID int64 `xorm:"pk autoincr"`
ID int64 `xorm:"pk autoincr"`
Slug string `xorm:"UNIQUE"`
Description string
ImageURL string
}
@ -39,3 +41,84 @@ func GetUserBadges(ctx context.Context, u *User) ([]*Badge, int64, error) {
count, err := sess.FindAndCount(&badges)
return badges, count, err
}
// CreateBadge creates a new badge.
func CreateBadge(ctx context.Context, badge *Badge) error {
_, err := db.GetEngine(ctx).Insert(badge)
return err
}
// GetBadge returns a badge
func GetBadge(ctx context.Context, slug string) (*Badge, error) {
badge := new(Badge)
has, err := db.GetEngine(ctx).Where("slug=?", slug).Get(badge)
if !has {
return nil, err
}
return badge, err
}
// UpdateBadge updates a badge based on its slug.
func UpdateBadge(ctx context.Context, badge *Badge) error {
_, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Update(badge)
return err
}
// DeleteBadge deletes a badge.
func DeleteBadge(ctx context.Context, badge *Badge) error {
_, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Delete(badge)
return err
}
// AddUserBadge adds a badge to a user.
func AddUserBadge(ctx context.Context, u *User, badge *Badge) error {
return AddUserBadges(ctx, u, []*Badge{badge})
}
// AddUserBadges adds badges to a user.
func AddUserBadges(ctx context.Context, u *User, badges []*Badge) error {
return db.WithTx(ctx, func(ctx context.Context) error {
for _, badge := range badges {
// hydrate badge and check if it exists
has, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Get(badge)
if err != nil {
return err
} else if !has {
return fmt.Errorf("badge with slug %s doesn't exist", badge.Slug)
}
if err := db.Insert(ctx, &UserBadge{
BadgeID: badge.ID,
UserID: u.ID,
}); err != nil {
return err
}
}
return nil
})
}
// RemoveUserBadge removes a badge from a user.
func RemoveUserBadge(ctx context.Context, u *User, badge *Badge) error {
return RemoveUserBadges(ctx, u, []*Badge{badge})
}
// RemoveUserBadges removes badges from a user.
func RemoveUserBadges(ctx context.Context, u *User, badges []*Badge) error {
return db.WithTx(ctx, func(ctx context.Context) error {
for _, badge := range badges {
if _, err := db.GetEngine(ctx).
Join("INNER", "badge", "badge.id = `user_badge`.badge_id").
Where("`user_badge`.user_id=? AND `badge`.slug=?", u.ID, badge.Slug).
Delete(&UserBadge{}); err != nil {
return err
}
}
return nil
})
}
// RemoveAllUserBadges removes all badges from a user.
func RemoveAllUserBadges(ctx context.Context, u *User) error {
_, err := db.GetEngine(ctx).Where("user_id=?", u.ID).Delete(&UserBadge{})
return err
}

123
models/user/block.go Normal file
View File

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

View File

@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/validation"
@ -21,9 +22,6 @@ import (
"xorm.io/builder"
)
// ErrEmailNotActivated e-mail address has not been activated error
var ErrEmailNotActivated = util.NewInvalidArgumentErrorf("e-mail address has not been activated")
// ErrEmailCharIsNotSupported e-mail address contains unsupported character
type ErrEmailCharIsNotSupported struct {
Email string
@ -313,27 +311,27 @@ func updateActivation(ctx context.Context, email *EmailAddress, activate bool) e
return UpdateUserCols(ctx, user, "rands")
}
// MakeEmailPrimary sets primary email address of given user.
func MakeEmailPrimary(ctx context.Context, email *EmailAddress) error {
has, err := db.GetEngine(ctx).Get(email)
if err != nil {
func MakeActiveEmailPrimary(ctx context.Context, emailID int64) error {
return makeEmailPrimaryInternal(ctx, emailID, true)
}
func MakeInactiveEmailPrimary(ctx context.Context, emailID int64) error {
return makeEmailPrimaryInternal(ctx, emailID, false)
}
func makeEmailPrimaryInternal(ctx context.Context, emailID int64, isActive bool) error {
email := &EmailAddress{}
if has, err := db.GetEngine(ctx).ID(emailID).Where(builder.Eq{"is_activated": isActive}).Get(email); err != nil {
return err
} else if !has {
return ErrEmailAddressNotExist{Email: email.Email}
}
if !email.IsActivated {
return ErrEmailNotActivated
return ErrEmailAddressNotExist{}
}
user := &User{}
has, err = db.GetEngine(ctx).ID(email.UID).Get(user)
if err != nil {
if has, err := db.GetEngine(ctx).ID(email.UID).Get(user); err != nil {
return err
} else if !has {
return ErrUserNotExist{
UID: email.UID,
}
return ErrUserNotExist{UID: email.UID}
}
ctx, committer, err := db.TxContext(ctx)
@ -365,6 +363,21 @@ func MakeEmailPrimary(ctx context.Context, email *EmailAddress) error {
return committer.Commit()
}
// ChangeInactivePrimaryEmail replaces the inactive primary email of a given user
func ChangeInactivePrimaryEmail(ctx context.Context, uid int64, oldEmailAddr, newEmailAddr string) error {
return db.WithTx(ctx, func(ctx context.Context) error {
_, err := db.GetEngine(ctx).Where(builder.Eq{"uid": uid, "lower_email": strings.ToLower(oldEmailAddr)}).Delete(&EmailAddress{})
if err != nil {
return err
}
newEmail, err := InsertEmailAddress(ctx, &EmailAddress{UID: uid, Email: newEmailAddr})
if err != nil {
return err
}
return MakeInactiveEmailPrimary(ctx, newEmail.ID)
})
}
// VerifyActiveEmailCode verifies active email code when active account
func VerifyActiveEmailCode(ctx context.Context, code, email string) *EmailAddress {
minutes := setting.Service.ActiveCodeLives
@ -404,8 +417,8 @@ type SearchEmailOptions struct {
db.ListOptions
Keyword string
SortType SearchEmailOrderBy
IsPrimary util.OptionalBool
IsActivated util.OptionalBool
IsPrimary optional.Option[bool]
IsActivated optional.Option[bool]
}
// SearchEmailResult is an e-mail address found in the user or email_address table
@ -432,18 +445,12 @@ func SearchEmails(ctx context.Context, opts *SearchEmailOptions) ([]*SearchEmail
))
}
switch {
case opts.IsPrimary.IsTrue():
cond = cond.And(builder.Eq{"email_address.is_primary": true})
case opts.IsPrimary.IsFalse():
cond = cond.And(builder.Eq{"email_address.is_primary": false})
if opts.IsPrimary.Has() {
cond = cond.And(builder.Eq{"email_address.is_primary": opts.IsPrimary.Value()})
}
switch {
case opts.IsActivated.IsTrue():
cond = cond.And(builder.Eq{"email_address.is_activated": true})
case opts.IsActivated.IsFalse():
cond = cond.And(builder.Eq{"email_address.is_activated": false})
if opts.IsActivated.Has() {
cond = cond.And(builder.Eq{"email_address.is_activated": opts.IsActivated.Value()})
}
count, err := db.GetEngine(ctx).Join("INNER", "`user`", "`user`.ID = email_address.uid").

View File

@ -9,7 +9,7 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/optional"
"github.com/stretchr/testify/assert"
)
@ -45,31 +45,22 @@ func TestIsEmailUsed(t *testing.T) {
func TestMakeEmailPrimary(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
email := &user_model.EmailAddress{
Email: "user567890@example.com",
}
err := user_model.MakeEmailPrimary(db.DefaultContext, email)
err := user_model.MakeActiveEmailPrimary(db.DefaultContext, 9999999)
assert.Error(t, err)
assert.EqualError(t, err, user_model.ErrEmailAddressNotExist{Email: email.Email}.Error())
assert.ErrorIs(t, err, user_model.ErrEmailAddressNotExist{})
email = &user_model.EmailAddress{
Email: "user11@example.com",
}
err = user_model.MakeEmailPrimary(db.DefaultContext, email)
email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "user11@example.com"})
err = user_model.MakeActiveEmailPrimary(db.DefaultContext, email.ID)
assert.Error(t, err)
assert.EqualError(t, err, user_model.ErrEmailNotActivated.Error())
assert.ErrorIs(t, err, user_model.ErrEmailAddressNotExist{}) // inactive email is considered as not exist for "MakeActiveEmailPrimary"
email = &user_model.EmailAddress{
Email: "user9999999@example.com",
}
err = user_model.MakeEmailPrimary(db.DefaultContext, email)
email = unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "user9999999@example.com"})
err = user_model.MakeActiveEmailPrimary(db.DefaultContext, email.ID)
assert.Error(t, err)
assert.True(t, user_model.IsErrUserNotExist(err))
email = &user_model.EmailAddress{
Email: "user101@example.com",
}
err = user_model.MakeEmailPrimary(db.DefaultContext, email)
email = unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "user101@example.com"})
err = user_model.MakeActiveEmailPrimary(db.DefaultContext, email.ID)
assert.NoError(t, err)
user, _ := user_model.GetUserByID(db.DefaultContext, int64(10))
@ -137,14 +128,14 @@ func TestListEmails(t *testing.T) {
assert.True(t, contains(func(s *user_model.SearchEmailResult) bool { return s.UID == 27 }))
// Must find only primary addresses (i.e. from the `user` table)
opts = &user_model.SearchEmailOptions{IsPrimary: util.OptionalBoolTrue}
opts = &user_model.SearchEmailOptions{IsPrimary: optional.Some(true)}
emails, _, err = user_model.SearchEmails(db.DefaultContext, opts)
assert.NoError(t, err)
assert.True(t, contains(func(s *user_model.SearchEmailResult) bool { return s.IsPrimary }))
assert.False(t, contains(func(s *user_model.SearchEmailResult) bool { return !s.IsPrimary }))
// Must find only inactive addresses (i.e. not validated)
opts = &user_model.SearchEmailOptions{IsActivated: util.OptionalBoolFalse}
opts = &user_model.SearchEmailOptions{IsActivated: optional.Some(false)}
emails, _, err = user_model.SearchEmails(db.DefaultContext, opts)
assert.NoError(t, err)
assert.True(t, contains(func(s *user_model.SearchEmailResult) bool { return !s.IsActivated }))

View File

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

View File

@ -10,8 +10,8 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"xorm.io/builder"
"xorm.io/xorm"
@ -33,11 +33,11 @@ type SearchUserOptions struct {
SupportedSortOrders container.Set[string] // if not nil, only allow to use the sort orders in this set
IsActive util.OptionalBool
IsAdmin util.OptionalBool
IsRestricted util.OptionalBool
IsTwoFactorEnabled util.OptionalBool
IsProhibitLogin util.OptionalBool
IsActive optional.Option[bool]
IsAdmin optional.Option[bool]
IsRestricted optional.Option[bool]
IsTwoFactorEnabled optional.Option[bool]
IsProhibitLogin optional.Option[bool]
IncludeReserved bool
ExtraParamStrings map[string]string
@ -89,24 +89,24 @@ func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Sess
cond = cond.And(builder.Eq{"login_name": opts.LoginName})
}
if !opts.IsActive.IsNone() {
cond = cond.And(builder.Eq{"is_active": opts.IsActive.IsTrue()})
if opts.IsActive.Has() {
cond = cond.And(builder.Eq{"is_active": opts.IsActive.Value()})
}
if !opts.IsAdmin.IsNone() {
cond = cond.And(builder.Eq{"is_admin": opts.IsAdmin.IsTrue()})
if opts.IsAdmin.Has() {
cond = cond.And(builder.Eq{"is_admin": opts.IsAdmin.Value()})
}
if !opts.IsRestricted.IsNone() {
cond = cond.And(builder.Eq{"is_restricted": opts.IsRestricted.IsTrue()})
if opts.IsRestricted.Has() {
cond = cond.And(builder.Eq{"is_restricted": opts.IsRestricted.Value()})
}
if !opts.IsProhibitLogin.IsNone() {
cond = cond.And(builder.Eq{"prohibit_login": opts.IsProhibitLogin.IsTrue()})
if opts.IsProhibitLogin.Has() {
cond = cond.And(builder.Eq{"prohibit_login": opts.IsProhibitLogin.Value()})
}
e := db.GetEngine(ctx)
if opts.IsTwoFactorEnabled.IsNone() {
if !opts.IsTwoFactorEnabled.Has() {
return e.Where(cond)
}
@ -114,7 +114,7 @@ func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Sess
// While using LEFT JOIN, sometimes the performance might not be good, but it won't be a problem now, such SQL is seldom executed.
// There are some possible methods to refactor this SQL in future when we really need to optimize the performance (but not now):
// (1) add a column in user table (2) add a setting value in user_setting table (3) use search engines (bleve/elasticsearch)
if opts.IsTwoFactorEnabled.IsTrue() {
if opts.IsTwoFactorEnabled.Value() {
cond = cond.And(builder.Expr("two_factor.uid IS NOT NULL"))
} else {
cond = cond.And(builder.Expr("two_factor.uid IS NULL"))
@ -131,7 +131,7 @@ func SearchUsers(ctx context.Context, opts *SearchUserOptions) (users []*User, _
defer sessCount.Close()
count, err := sessCount.Count(new(User))
if err != nil {
return nil, 0, fmt.Errorf("Count: %w", err)
return nil, 0, fmt.Errorf("count: %w", err)
}
if len(opts.OrderBy) == 0 {

View File

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

View File

@ -16,10 +16,10 @@ import (
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/auth/password/hash"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert"
)
@ -103,29 +103,29 @@ func TestSearchUsers(t *testing.T) {
testUserSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}},
[]int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37, 38, 39, 40})
testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolFalse},
testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(false)},
[]int64{9})
testUserSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolTrue},
testUserSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)},
[]int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37, 38, 39, 40})
testUserSuccess(&user_model.SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolTrue},
testUserSuccess(&user_model.SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)},
[]int64{1, 10, 11, 12, 13, 14, 15, 16, 18})
// order by name asc default
testUserSuccess(&user_model.SearchUserOptions{Keyword: "user1", ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolTrue},
testUserSuccess(&user_model.SearchUserOptions{Keyword: "user1", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)},
[]int64{1, 10, 11, 12, 13, 14, 15, 16, 18})
testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsAdmin: util.OptionalBoolTrue},
testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsAdmin: optional.Some(true)},
[]int64{1})
testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsRestricted: util.OptionalBoolTrue},
testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsRestricted: optional.Some(true)},
[]int64{29})
testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsProhibitLogin: util.OptionalBoolTrue},
testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsProhibitLogin: optional.Some(true)},
[]int64{37})
testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsTwoFactorEnabled: util.OptionalBoolTrue},
testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsTwoFactorEnabled: optional.Some(true)},
[]int64{24})
}
@ -399,14 +399,19 @@ func TestGetUserByOpenID(t *testing.T) {
func TestFollowUser(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
testSuccess := func(followerID, followedID int64) {
assert.NoError(t, user_model.FollowUser(db.DefaultContext, followerID, followedID))
unittest.AssertExistsAndLoadBean(t, &user_model.Follow{UserID: followerID, FollowID: followedID})
testSuccess := func(follower, followed *user_model.User) {
assert.NoError(t, user_model.FollowUser(db.DefaultContext, follower, followed))
unittest.AssertExistsAndLoadBean(t, &user_model.Follow{UserID: follower.ID, FollowID: followed.ID})
}
testSuccess(4, 2)
testSuccess(5, 2)
assert.NoError(t, user_model.FollowUser(db.DefaultContext, 2, 2))
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
testSuccess(user4, user2)
testSuccess(user5, user2)
assert.NoError(t, user_model.FollowUser(db.DefaultContext, user2, user2))
unittest.CheckConsistencyFor(t, &user_model.User{})
}

View File

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

View File

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

View File

@ -11,9 +11,9 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/optional"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
webhook_module "code.gitea.io/gitea/modules/webhook"
"github.com/stretchr/testify/assert"
@ -123,7 +123,7 @@ func TestGetWebhookByOwnerID(t *testing.T) {
func TestGetActiveWebhooksByRepoID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
hooks, err := db.Find[Webhook](db.DefaultContext, ListWebhookOptions{RepoID: 1, IsActive: util.OptionalBoolTrue})
hooks, err := db.Find[Webhook](db.DefaultContext, ListWebhookOptions{RepoID: 1, IsActive: optional.Some(true)})
assert.NoError(t, err)
if assert.Len(t, hooks, 1) {
assert.Equal(t, int64(1), hooks[0].ID)
@ -143,7 +143,7 @@ func TestGetWebhooksByRepoID(t *testing.T) {
func TestGetActiveWebhooksByOwnerID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
hooks, err := db.Find[Webhook](db.DefaultContext, ListWebhookOptions{OwnerID: 3, IsActive: util.OptionalBoolTrue})
hooks, err := db.Find[Webhook](db.DefaultContext, ListWebhookOptions{OwnerID: 3, IsActive: optional.Some(true)})
assert.NoError(t, err)
if assert.Len(t, hooks, 1) {
assert.Equal(t, int64(3), hooks[0].ID)

View File

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

View File

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

104
modules/badge/badge.go Normal file
View File

@ -0,0 +1,104 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package badge
import (
actions_model "code.gitea.io/gitea/models/actions"
)
// The Badge layout: |offset|label|message|
// We use 10x scale to calculate more precisely
// Then scale down to normal size in tmpl file
type Label struct {
text string
width int
}
func (l Label) Text() string {
return l.text
}
func (l Label) Width() int {
return l.width
}
func (l Label) TextLength() int {
return int(float64(l.width-defaultOffset) * 9.5)
}
func (l Label) X() int {
return l.width*5 + 10
}
type Message struct {
text string
width int
x int
}
func (m Message) Text() string {
return m.text
}
func (m Message) Width() int {
return m.width
}
func (m Message) X() int {
return m.x
}
func (m Message) TextLength() int {
return int(float64(m.width-defaultOffset) * 9.5)
}
type Badge struct {
Color string
FontSize int
Label Label
Message Message
}
func (b Badge) Width() int {
return b.Label.width + b.Message.width
}
const (
defaultOffset = 9
defaultFontSize = 11
DefaultColor = "#9f9f9f" // Grey
defaultFontWidth = 7 // approximate speculation
)
var StatusColorMap = map[actions_model.Status]string{
actions_model.StatusSuccess: "#4c1", // Green
actions_model.StatusSkipped: "#dfb317", // Yellow
actions_model.StatusUnknown: "#97ca00", // Light Green
actions_model.StatusFailure: "#e05d44", // Red
actions_model.StatusCancelled: "#fe7d37", // Orange
actions_model.StatusWaiting: "#dfb317", // Yellow
actions_model.StatusRunning: "#dfb317", // Yellow
actions_model.StatusBlocked: "#dfb317", // Yellow
}
// GenerateBadge generates badge with given template
func GenerateBadge(label, message, color string) Badge {
lw := defaultFontWidth*len(label) + defaultOffset
mw := defaultFontWidth*len(message) + defaultOffset
x := lw*10 + mw*5 - 10
return Badge{
Label: Label{
text: label,
width: lw,
},
Message: Message{
text: message,
width: mw,
x: x,
},
FontSize: defaultFontSize * 10,
Color: color,
}
}

View File

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

View File

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

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