Merge branch 'main' into actions_support_workflow_dispatch_event
|
@ -48,6 +48,7 @@ jobs:
|
|||
- "Makefile"
|
||||
- ".golangci.yml"
|
||||
- ".editorconfig"
|
||||
- "options/locale/locale_en-US.ini"
|
||||
|
||||
frontend:
|
||||
- "**/*.js"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
4
Makefile
|
@ -147,6 +147,7 @@ GO_DIRS := build cmd models modules routers services tests
|
|||
WEB_DIRS := web_src/js web_src/css
|
||||
|
||||
SPELLCHECK_FILES := $(GO_DIRS) $(WEB_DIRS) docs/content templates options/locale/locale_en-US.ini .github
|
||||
EDITORCONFIG_FILES := templates .github/workflows options/locale/locale_en-US.ini
|
||||
|
||||
GO_SOURCES := $(wildcard *.go)
|
||||
GO_SOURCES += $(shell find $(GO_DIRS) -type f -name "*.go" ! -path modules/options/bindata.go ! -path modules/public/bindata.go ! -path modules/templates/bindata.go)
|
||||
|
@ -426,7 +427,7 @@ lint-go-vet:
|
|||
|
||||
.PHONY: lint-editorconfig
|
||||
lint-editorconfig:
|
||||
$(GO) run $(EDITORCONFIG_CHECKER_PACKAGE) templates .github/workflows
|
||||
@$(GO) run $(EDITORCONFIG_CHECKER_PACKAGE) $(EDITORCONFIG_FILES)
|
||||
|
||||
.PHONY: lint-actions
|
||||
lint-actions:
|
||||
|
@ -908,6 +909,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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -1480,8 +1480,9 @@ LEVEL = Info
|
|||
;;
|
||||
;; Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled
|
||||
;DEFAULT_EMAIL_NOTIFICATIONS = enabled
|
||||
;; Disabled features for users, could be "deletion","manage_gpg_keys" more features can be disabled in future
|
||||
;; Disabled features for users, could be "deletion", "manage_ssh_keys","manage_gpg_keys" more features can be disabled in future
|
||||
;; - deletion: a user cannot delete their own account
|
||||
;; - manage_ssh_keys: a user cannot configure ssh keys
|
||||
;; - manage_gpg_keys: a user cannot configure gpg keys
|
||||
;USER_DISABLED_FEATURES =
|
||||
|
||||
|
|
|
@ -518,9 +518,10 @@ And the following unique queues:
|
|||
|
||||
- `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**: Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled
|
||||
- `DISABLE_REGULAR_ORG_CREATION`: **false**: Disallow regular (non-admin) users from creating organizations.
|
||||
- `USER_DISABLED_FEATURES`: **_empty_** Disabled features for users, could be `deletion`, `manage_gpg_keys` and more features can be added in future.
|
||||
- `USER_DISABLED_FEATURES`: **_empty_** Disabled features for users, could be `deletion`, `manage_ssh_keys`, `manage_gpg_keys` and more features can be added in future.
|
||||
- `deletion`: User cannot delete their own account.
|
||||
- `manage_gpg_keys`: User cannot configure gpg keys
|
||||
- `manage_ssh_keys`: User cannot configure ssh keys.
|
||||
- `manage_gpg_keys`: User cannot configure gpg keys.
|
||||
|
||||
## Security (`security`)
|
||||
|
||||
|
|
|
@ -497,9 +497,10 @@ Gitea 创建以下非唯一队列:
|
|||
|
||||
- `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**:用户电子邮件通知的默认配置(用户可配置)。选项:enabled、onmention、disabled
|
||||
- `DISABLE_REGULAR_ORG_CREATION`: **false**:禁止普通(非管理员)用户创建组织。
|
||||
- `USER_DISABLED_FEATURES`:**_empty_** 禁用的用户特性,当前允许为空或者 `deletion`,`manage_gpg_keys` 未来可以增加更多设置。
|
||||
- `USER_DISABLED_FEATURES`:**_empty_** 禁用的用户特性,当前允许为空或者 `deletion`,`manage_ssh_keys`, `manage_gpg_keys` 未来可以增加更多设置。
|
||||
- `deletion`: 用户不能通过界面或者API删除他自己。
|
||||
- `manage_gpg_keys`: 用户不能配置 GPG 密钥
|
||||
- `manage_ssh_keys`: 用户不能通过界面或者API配置SSH Keys。
|
||||
- `manage_gpg_keys`: 用户不能配置 GPG 密钥。
|
||||
|
||||
## 安全性 (`security`)
|
||||
|
||||
|
|
|
@ -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 | SanitizeHTML}}
|
||||
{{.Body}}
|
||||
{{end}}
|
||||
</p>
|
||||
<hr>
|
||||
|
@ -259,14 +259,14 @@ This template produces something along these lines:
|
|||
The template system contains several functions that can be used to further process and format
|
||||
the messages. Here's a list of some of them:
|
||||
|
||||
| Name | Parameters | Available | Usage |
|
||||
| ---------------- | ----------- | --------- |-----------------------------------------------------------------------------|
|
||||
| `AppUrl` | - | Any | Gitea's URL |
|
||||
| `AppName` | - | Any | Set from `app.ini`, usually "Gitea" |
|
||||
| `AppDomain` | - | Any | Gitea's host name |
|
||||
| `EllipsisString` | string, int | Any | Truncates a string to the specified length; adds ellipsis as needed |
|
||||
| `SanitizeHTML` | string | Body only | Sanitizes text by removing any dangerous HTML tags from it. |
|
||||
| `SafeHTML` | string | Body only | Takes the input as HTML; can be used for `.ReviewComments.RenderedContent`. |
|
||||
| Name | Parameters | Available | Usage |
|
||||
| ---------------- | ----------- | --------- | ------------------------------------------------------------------- |
|
||||
| `AppUrl` | - | Any | Gitea's URL |
|
||||
| `AppName` | - | Any | Set from `app.ini`, usually "Gitea" |
|
||||
| `AppDomain` | - | Any | Gitea's host name |
|
||||
| `EllipsisString` | string, int | Any | Truncates a string to the specified length; adds ellipsis as needed |
|
||||
| `SanitizeHTML` | string | Body only | Sanitizes text by removing any dangerous HTML tags from it |
|
||||
| `SafeHTML` | string | Body only | Takes the input as HTML, can be used for outputing raw HTML content |
|
||||
|
||||
These are _functions_, not metadata, so they have to be used:
|
||||
|
||||
|
|
|
@ -207,7 +207,7 @@ _主题_ 和 _邮件正文_ 由 [Golang的模板引擎](https://go.dev/pkg/text/
|
|||
{{if not (eq .Body "")}}
|
||||
<h3>消息内容:</h3>
|
||||
<hr>
|
||||
{{.Body | SanitizeHTML}}
|
||||
{{.Body}}
|
||||
{{end}}
|
||||
</p>
|
||||
<hr>
|
||||
|
@ -242,14 +242,14 @@ _主题_ 和 _邮件正文_ 由 [Golang的模板引擎](https://go.dev/pkg/text/
|
|||
|
||||
模板系统包含一些函数,可用于进一步处理和格式化消息。以下是其中一些函数的列表:
|
||||
|
||||
| 函数名 | 参数 | 可用于 | 用法 |
|
||||
|------------------| ----------- | ------------ |---------------------------------------------------------|
|
||||
| `AppUrl` | - | 任何地方 | Gitea 的 URL |
|
||||
| `AppName` | - | 任何地方 | 从 `app.ini` 中设置,通常为 "Gitea" |
|
||||
| `AppDomain` | - | 任何地方 | Gitea 的主机名 |
|
||||
| `EllipsisString` | string, int | 任何地方 | 将字符串截断为指定长度;根据需要添加省略号 |
|
||||
| `SanitizeHTML` | string | 仅正文部分 | 通过删除其中的危险 HTML 标签对文本进行清理 |
|
||||
| `SafeHTML` | string | 仅正文部分 | 将输入作为 HTML 处理;可用于 `.ReviewComments.RenderedContent` 等字段 |
|
||||
| 函数名 | 参数 | 可用于 | 用法 |
|
||||
|------------------| ----------- | ------------ | ------------------------------ |
|
||||
| `AppUrl` | - | 任何地方 | Gitea 的 URL |
|
||||
| `AppName` | - | 任何地方 | 从 `app.ini` 中设置,通常为 "Gitea" |
|
||||
| `AppDomain` | - | 任何地方 | Gitea 的主机名 |
|
||||
| `EllipsisString` | string, int | 任何地方 | 将字符串截断为指定长度;根据需要添加省略号 |
|
||||
| `SanitizeHTML` | string | 仅正文部分 | 通过删除其中的危险 HTML 标签对文本进行清理 |
|
||||
| `SafeHTML` | string | 仅正文部分 | 将输入作为 HTML 处理;可用于输出原始的 HTML 内容 |
|
||||
|
||||
这些都是 _函数_,而不是元数据,因此必须按以下方式使用:
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ We recommend [Google HTML/CSS Style Guide](https://google.github.io/styleguide/h
|
|||
9. Avoid unnecessary `!important` in CSS, add comments to explain why it's necessary if it can't be avoided.
|
||||
10. Avoid mixing different events in one event listener, prefer to use individual event listeners for every event.
|
||||
11. Custom event names are recommended to use `ce-` prefix.
|
||||
12. Gitea's tailwind-style CSS classes use `gt-` prefix (`gt-relative`), while Gitea's own private framework-level CSS classes use `g-` prefix (`g-modal-confirm`).
|
||||
12. Prefer using Tailwind CSS which is available via `tw-` prefix, e.g. `tw-relative`. Gitea's helper CSS classes use `gt-` prefix (`gt-df`), while Gitea's own private framework-level CSS classes use `g-` prefix (`g-modal-confirm`).
|
||||
13. Avoid inline scripts & styles as much as possible, it's recommended to put JS code into JS files and use CSS classes. If inline scripts & styles are unavoidable, explain the reason why it can't be avoided.
|
||||
|
||||
### Accessibility / ARIA
|
||||
|
|
|
@ -47,7 +47,7 @@ HTML 页面由[Go HTML Template](https://pkg.go.dev/html/template)渲染。
|
|||
9. 避免在 CSS 中使用不必要的`!important`,如果无法避免,添加注释解释为什么需要它。
|
||||
10. 避免在一个事件监听器中混合不同的事件,优先为每个事件使用独立的事件监听器。
|
||||
11. 推荐使用自定义事件名称前缀`ce-`。
|
||||
12. Gitea 的 tailwind-style CSS 类使用`gt-`前缀(`gt-relative`),而 Gitea 自身的私有框架级 CSS 类使用`g-`前缀(`g-modal-confirm`)。
|
||||
12. 建议使用 Tailwind CSS,它可以通过 `tw-` 前缀获得,例如 `tw-relative`. Gitea 自身的助手类 CSS 使用 `gt-` 前缀(`gt-df`),Gitea 自身的私有框架级 CSS 类使用 `g-` 前缀(`g-modal-confirm`)。
|
||||
13. 尽量避免内联脚本和样式,建议将JS代码放入JS文件中并使用CSS类。如果内联脚本和样式不可避免,请解释无法避免的原因。
|
||||
|
||||
### 可访问性 / ARIA
|
||||
|
|
|
@ -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)
|
|
@ -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 |
|
||||
|
|
2
go.mod
|
@ -113,7 +113,7 @@ require (
|
|||
golang.org/x/text v0.14.0
|
||||
golang.org/x/tools v0.17.0
|
||||
google.golang.org/grpc v1.60.1
|
||||
google.golang.org/protobuf v1.32.0
|
||||
google.golang.org/protobuf v1.33.0
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||
gopkg.in/ini.v1 v1.67.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
|
|
4
go.sum
|
@ -1308,8 +1308,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
|
|||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
||||
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
|
|
@ -393,10 +393,14 @@ func (a *Action) GetCreate() time.Time {
|
|||
return a.CreatedUnix.AsTime()
|
||||
}
|
||||
|
||||
// GetIssueInfos returns a list of issues associated with
|
||||
// the action.
|
||||
// GetIssueInfos returns a list of associated information with the action.
|
||||
func (a *Action) GetIssueInfos() []string {
|
||||
return strings.SplitN(a.Content, "|", 3)
|
||||
// make sure it always returns 3 elements, because there are some access to the a[1] and a[2] without checking the length
|
||||
ret := strings.SplitN(a.Content, "|", 3)
|
||||
for len(ret) < 3 {
|
||||
ret = append(ret, "")
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// GetIssueTitle returns the title of first issue associated with the action.
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -14,3 +14,7 @@
|
|||
id: 4
|
||||
assignee_id: 2
|
||||
issue_id: 17
|
||||
-
|
||||
id: 5
|
||||
assignee_id: 10
|
||||
issue_id: 6
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -650,12 +650,6 @@
|
|||
type: 2
|
||||
created_unix: 946684810
|
||||
|
||||
-
|
||||
id: 98
|
||||
repo_id: 1
|
||||
type: 8
|
||||
created_unix: 946684810
|
||||
|
||||
-
|
||||
id: 99
|
||||
repo_id: 1
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -158,6 +158,11 @@ func GetBranch(ctx context.Context, repoID int64, branchName string) (*Branch, e
|
|||
return &branch, nil
|
||||
}
|
||||
|
||||
func GetBranches(ctx context.Context, repoID int64, branchNames []string) ([]*Branch, error) {
|
||||
branches := make([]*Branch, 0, len(branchNames))
|
||||
return branches, db.GetEngine(ctx).Where("repo_id=?", repoID).In("name", branchNames).Find(&branches)
|
||||
}
|
||||
|
||||
func AddBranches(ctx context.Context, branches []*Branch) error {
|
||||
for _, branch := range branches {
|
||||
if _, err := db.GetEngine(ctx).Insert(branch); err != nil {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -560,6 +560,10 @@ var migrations = []Migration{
|
|||
NewMigration("Add support for SHA256 git repositories", v1_22.AdjustDBForSha256),
|
||||
// v287 -> v288
|
||||
NewMigration("Use Slug instead of ID for Badges", v1_22.UseSlugInsteadOfIDForBadges),
|
||||
// v288 -> v289
|
||||
NewMigration("Add user_blocking table", v1_22.AddUserBlockingTable),
|
||||
// v289 -> v290
|
||||
NewMigration("Add default_wiki_branch to repository table", v1_22.AddDefaultWikiBranch),
|
||||
}
|
||||
|
||||
// GetCurrentDBVersion returns the current db version
|
||||
|
|
|
@ -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{})
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_22 //nolint
|
||||
|
||||
import "xorm.io/xorm"
|
||||
|
||||
func AddDefaultWikiBranch(x *xorm.Engine) error {
|
||||
type Repository struct {
|
||||
ID int64
|
||||
DefaultWikiBranch string
|
||||
}
|
||||
if err := x.Sync(&Repository{}); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := x.Exec("UPDATE `repository` SET default_wiki_branch = 'master' WHERE (default_wiki_branch IS NULL) OR (default_wiki_branch = '')")
|
||||
return err
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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{})
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
|
||||
|
|
|
@ -136,6 +136,7 @@ type Repository struct {
|
|||
OriginalServiceType api.GitServiceType `xorm:"index"`
|
||||
OriginalURL string `xorm:"VARCHAR(2048)"`
|
||||
DefaultBranch string
|
||||
DefaultWikiBranch string
|
||||
|
||||
NumWatches int
|
||||
NumStars int
|
||||
|
@ -285,6 +286,9 @@ func (repo *Repository) AfterLoad() {
|
|||
repo.NumOpenMilestones = repo.NumMilestones - repo.NumClosedMilestones
|
||||
repo.NumOpenProjects = repo.NumProjects - repo.NumClosedProjects
|
||||
repo.NumOpenActionRuns = repo.NumActionRuns - repo.NumClosedActionRuns
|
||||
if repo.DefaultWikiBranch == "" {
|
||||
repo.DefaultWikiBranch = setting.Repository.DefaultBranch
|
||||
}
|
||||
}
|
||||
|
||||
// LoadAttributes loads attributes of the repository.
|
||||
|
@ -411,6 +415,13 @@ func (repo *Repository) MustGetUnit(ctx context.Context, tp unit.Type) *RepoUnit
|
|||
Type: tp,
|
||||
Config: new(ActionsConfig),
|
||||
}
|
||||
} else if tp == unit.TypeProjects {
|
||||
cfg := new(ProjectsConfig)
|
||||
cfg.ProjectsMode = ProjectsModeNone
|
||||
return &RepoUnit{
|
||||
Type: tp,
|
||||
Config: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
return &RepoUnit{
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -154,37 +154,18 @@ func UpdateEmailAddress(ctx context.Context, email *EmailAddress) error {
|
|||
|
||||
var emailRegexp = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
|
||||
|
||||
// ValidateEmail check if email is a allowed address
|
||||
// ValidateEmail check if email is a valid & allowed address
|
||||
func ValidateEmail(email string) error {
|
||||
if len(email) == 0 {
|
||||
return ErrEmailInvalid{email}
|
||||
if err := validateEmailBasic(email); err != nil {
|
||||
return err
|
||||
}
|
||||
return validateEmailDomain(email)
|
||||
}
|
||||
|
||||
if !emailRegexp.MatchString(email) {
|
||||
return ErrEmailCharIsNotSupported{email}
|
||||
}
|
||||
|
||||
if email[0] == '-' {
|
||||
return ErrEmailInvalid{email}
|
||||
}
|
||||
|
||||
if _, err := mail.ParseAddress(email); err != nil {
|
||||
return ErrEmailInvalid{email}
|
||||
}
|
||||
|
||||
// if there is no allow list, then check email against block list
|
||||
if len(setting.Service.EmailDomainAllowList) == 0 &&
|
||||
validation.IsEmailDomainListed(setting.Service.EmailDomainBlockList, email) {
|
||||
return ErrEmailInvalid{email}
|
||||
}
|
||||
|
||||
// if there is an allow list, then check email against allow list
|
||||
if len(setting.Service.EmailDomainAllowList) > 0 &&
|
||||
!validation.IsEmailDomainListed(setting.Service.EmailDomainAllowList, email) {
|
||||
return ErrEmailInvalid{email}
|
||||
}
|
||||
|
||||
return nil
|
||||
// ValidateEmailForAdmin check if email is a valid address when admins manually add or edit users
|
||||
func ValidateEmailForAdmin(email string) error {
|
||||
return validateEmailBasic(email)
|
||||
// In this case we do not need to check the email domain
|
||||
}
|
||||
|
||||
func GetEmailAddressByEmail(ctx context.Context, email string) (*EmailAddress, error) {
|
||||
|
@ -534,3 +515,41 @@ func ActivateUserEmail(ctx context.Context, userID int64, email string, activate
|
|||
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
// validateEmailBasic checks whether the email complies with the rules
|
||||
func validateEmailBasic(email string) error {
|
||||
if len(email) == 0 {
|
||||
return ErrEmailInvalid{email}
|
||||
}
|
||||
|
||||
if !emailRegexp.MatchString(email) {
|
||||
return ErrEmailCharIsNotSupported{email}
|
||||
}
|
||||
|
||||
if email[0] == '-' {
|
||||
return ErrEmailInvalid{email}
|
||||
}
|
||||
|
||||
if _, err := mail.ParseAddress(email); err != nil {
|
||||
return ErrEmailInvalid{email}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateEmailDomain checks whether the email domain is allowed or blocked
|
||||
func validateEmailDomain(email string) error {
|
||||
// if there is no allow list, then check email against block list
|
||||
if len(setting.Service.EmailDomainAllowList) == 0 &&
|
||||
validation.IsEmailDomainListed(setting.Service.EmailDomainBlockList, email) {
|
||||
return ErrEmailInvalid{email}
|
||||
}
|
||||
|
||||
// if there is an allow list, then check email against allow list
|
||||
if len(setting.Service.EmailDomainAllowList) > 0 &&
|
||||
!validation.IsEmailDomainListed(setting.Service.EmailDomainAllowList, email) {
|
||||
return ErrEmailInvalid{email}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -586,6 +586,16 @@ type CreateUserOverwriteOptions struct {
|
|||
|
||||
// CreateUser creates record of a new user.
|
||||
func CreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err error) {
|
||||
return createUser(ctx, u, false, overwriteDefault...)
|
||||
}
|
||||
|
||||
// AdminCreateUser is used by admins to manually create users
|
||||
func AdminCreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err error) {
|
||||
return createUser(ctx, u, true, overwriteDefault...)
|
||||
}
|
||||
|
||||
// createUser creates record of a new user.
|
||||
func createUser(ctx context.Context, u *User, createdByAdmin bool, overwriteDefault ...*CreateUserOverwriteOptions) (err error) {
|
||||
if err = IsUsableUsername(u.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -639,8 +649,14 @@ func CreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOve
|
|||
return err
|
||||
}
|
||||
|
||||
if err := ValidateEmail(u.Email); err != nil {
|
||||
return err
|
||||
if createdByAdmin {
|
||||
if err := ValidateEmailForAdmin(u.Email); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := ValidateEmail(u.Email); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
|
@ -1167,7 +1183,7 @@ func IsUserVisibleToViewer(ctx context.Context, u, viewer *User) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// If they follow - they see each over
|
||||
// If they follow - they see each other
|
||||
follower := IsFollowing(ctx, u.ID, viewer.ID)
|
||||
if follower {
|
||||
return true
|
||||
|
|
|
@ -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{})
|
||||
}
|
||||
|
|
|
@ -4,85 +4,12 @@
|
|||
package base
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"unicode/utf8"
|
||||
"golang.org/x/text/collate"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
// NaturalSortLess compares two strings so that they could be sorted in natural order
|
||||
func NaturalSortLess(s1, s2 string) bool {
|
||||
var i1, i2 int
|
||||
for {
|
||||
rune1, j1, end1 := getNextRune(s1, i1)
|
||||
rune2, j2, end2 := getNextRune(s2, i2)
|
||||
if end1 || end2 {
|
||||
return end1 != end2 && end1
|
||||
}
|
||||
dec1 := isDecimal(rune1)
|
||||
dec2 := isDecimal(rune2)
|
||||
var less, equal bool
|
||||
if dec1 && dec2 {
|
||||
i1, i2, less, equal = compareByNumbers(s1, i1, s2, i2)
|
||||
} else if !dec1 && !dec2 {
|
||||
equal = rune1 == rune2
|
||||
less = rune1 < rune2
|
||||
i1 = j1
|
||||
i2 = j2
|
||||
} else {
|
||||
return rune1 < rune2
|
||||
}
|
||||
if !equal {
|
||||
return less
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getNextRune(str string, pos int) (rune, int, bool) {
|
||||
if pos < len(str) {
|
||||
r, w := utf8.DecodeRuneInString(str[pos:])
|
||||
// Fallback to ascii
|
||||
if r == utf8.RuneError {
|
||||
r = rune(str[pos])
|
||||
w = 1
|
||||
}
|
||||
return r, pos + w, false
|
||||
}
|
||||
return 0, pos, true
|
||||
}
|
||||
|
||||
func isDecimal(r rune) bool {
|
||||
return '0' <= r && r <= '9'
|
||||
}
|
||||
|
||||
func compareByNumbers(str1 string, pos1 int, str2 string, pos2 int) (i1, i2 int, less, equal bool) {
|
||||
d1, d2 := true, true
|
||||
var dec1, dec2 string
|
||||
for d1 || d2 {
|
||||
if d1 {
|
||||
r, j, end := getNextRune(str1, pos1)
|
||||
if !end && isDecimal(r) {
|
||||
dec1 += string(r)
|
||||
pos1 = j
|
||||
} else {
|
||||
d1 = false
|
||||
}
|
||||
}
|
||||
if d2 {
|
||||
r, j, end := getNextRune(str2, pos2)
|
||||
if !end && isDecimal(r) {
|
||||
dec2 += string(r)
|
||||
pos2 = j
|
||||
} else {
|
||||
d2 = false
|
||||
}
|
||||
}
|
||||
}
|
||||
less, equal = compareBigNumbers(dec1, dec2)
|
||||
return pos1, pos2, less, equal
|
||||
}
|
||||
|
||||
func compareBigNumbers(dec1, dec2 string) (less, equal bool) {
|
||||
d1, _ := big.NewInt(0).SetString(dec1, 10)
|
||||
d2, _ := big.NewInt(0).SetString(dec2, 10)
|
||||
cmp := d1.Cmp(d2)
|
||||
return cmp < 0, cmp == 0
|
||||
c := collate.New(language.English, collate.Numeric)
|
||||
return c.CompareString(s1, s2) < 0
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import (
|
|||
|
||||
func TestNaturalSortLess(t *testing.T) {
|
||||
test := func(s1, s2 string, less bool) {
|
||||
assert.Equal(t, less, NaturalSortLess(s1, s2))
|
||||
assert.Equal(t, less, NaturalSortLess(s1, s2), "s1=%q, s2=%q", s1, s2)
|
||||
}
|
||||
test("v1.20.0", "v1.2.0", false)
|
||||
test("v1.20.0", "v1.29.0", true)
|
||||
|
@ -20,4 +20,11 @@ func TestNaturalSortLess(t *testing.T) {
|
|||
test("a-1-a", "a-1-b", true)
|
||||
test("2", "12", true)
|
||||
test("a", "ab", true)
|
||||
|
||||
test("A", "b", true)
|
||||
test("a", "B", true)
|
||||
|
||||
test("cafe", "café", true)
|
||||
test("café", "cafe", false)
|
||||
test("caff", "café", false)
|
||||
}
|
||||
|
|
|
@ -8,11 +8,11 @@ package git
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
|
||||
gitealog "code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/go-git/go-billy/v5"
|
||||
"github.com/go-git/go-billy/v5/osfs"
|
||||
|
@ -52,7 +52,7 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
} else if !isDir(repoPath) {
|
||||
return nil, errors.New("no such file or directory")
|
||||
return nil, util.NewNotExistErrorf("no such file or directory")
|
||||
}
|
||||
|
||||
fs := osfs.New(repoPath)
|
||||
|
|
|
@ -9,10 +9,10 @@ package git
|
|||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -54,7 +54,7 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
} else if !isDir(repoPath) {
|
||||
return nil, errors.New("no such file or directory")
|
||||
return nil, util.NewNotExistErrorf("no such file or directory")
|
||||
}
|
||||
|
||||
// Now because of some insanity with git cat-file not immediately failing if not run in a valid git directory we need to run git rev-parse first!
|
||||
|
|
|
@ -59,7 +59,15 @@ func (g *Manager) start() {
|
|||
go func() {
|
||||
defer close(startupDone)
|
||||
// Wait till we're done getting all the listeners and then close the unused ones
|
||||
g.createServerWaitGroup.Wait()
|
||||
func() {
|
||||
// FIXME: there is a fundamental design problem of the "manager" and the "wait group".
|
||||
// If nothing has started, the "Wait" just panics: sync: WaitGroup is reused before previous Wait has returned
|
||||
// There is no clear solution besides a complete rewriting of the "manager"
|
||||
defer func() {
|
||||
_ = recover()
|
||||
}()
|
||||
g.createServerWaitGroup.Wait()
|
||||
}()
|
||||
// Ignore the error here there's not much we can do with it, they're logged in the CloseProvidedListeners function
|
||||
_ = CloseProvidedListeners()
|
||||
g.notify(readyMsg)
|
||||
|
|
|
@ -150,7 +150,15 @@ func (g *Manager) awaitServer(limit time.Duration) bool {
|
|||
c := make(chan struct{})
|
||||
go func() {
|
||||
defer close(c)
|
||||
g.createServerWaitGroup.Wait()
|
||||
func() {
|
||||
// FIXME: there is a fundamental design problem of the "manager" and the "wait group".
|
||||
// If nothing has started, the "Wait" just panics: sync: WaitGroup is reused before previous Wait has returned
|
||||
// There is no clear solution besides a complete rewriting of the "manager"
|
||||
defer func() {
|
||||
_ = recover()
|
||||
}()
|
||||
g.createServerWaitGroup.Wait()
|
||||
}()
|
||||
}()
|
||||
if limit > 0 {
|
||||
select {
|
||||
|
|
|
@ -16,14 +16,18 @@ import (
|
|||
|
||||
// Result a search result to display
|
||||
type Result struct {
|
||||
RepoID int64
|
||||
Filename string
|
||||
CommitID string
|
||||
UpdatedUnix timeutil.TimeStamp
|
||||
Language string
|
||||
Color string
|
||||
LineNumbers []int
|
||||
FormattedLines template.HTML
|
||||
RepoID int64
|
||||
Filename string
|
||||
CommitID string
|
||||
UpdatedUnix timeutil.TimeStamp
|
||||
Language string
|
||||
Color string
|
||||
Lines []ResultLine
|
||||
}
|
||||
|
||||
type ResultLine struct {
|
||||
Num int
|
||||
FormattedContent template.HTML
|
||||
}
|
||||
|
||||
type SearchResultLanguages = internal.SearchResultLanguages
|
||||
|
@ -70,7 +74,7 @@ func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Res
|
|||
var formattedLinesBuffer bytes.Buffer
|
||||
|
||||
contentLines := strings.SplitAfter(result.Content[startIndex:endIndex], "\n")
|
||||
lineNumbers := make([]int, len(contentLines))
|
||||
lines := make([]ResultLine, 0, len(contentLines))
|
||||
index := startIndex
|
||||
for i, line := range contentLines {
|
||||
var err error
|
||||
|
@ -93,21 +97,29 @@ func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Res
|
|||
return nil, err
|
||||
}
|
||||
|
||||
lineNumbers[i] = startLineNum + i
|
||||
lines = append(lines, ResultLine{Num: startLineNum + i})
|
||||
index += len(line)
|
||||
}
|
||||
|
||||
highlighted, _ := highlight.Code(result.Filename, "", formattedLinesBuffer.String())
|
||||
// we should highlight the whole code block first, otherwise it doesn't work well with multiple line highlighting
|
||||
hl, _ := highlight.Code(result.Filename, "", formattedLinesBuffer.String())
|
||||
highlightedLines := strings.Split(string(hl), "\n")
|
||||
|
||||
// The lines outputted by highlight.Code might not match the original lines, because "highlight" removes the last `\n`
|
||||
lines = lines[:min(len(highlightedLines), len(lines))]
|
||||
highlightedLines = highlightedLines[:len(lines)]
|
||||
for i := 0; i < len(lines); i++ {
|
||||
lines[i].FormattedContent = template.HTML(highlightedLines[i])
|
||||
}
|
||||
|
||||
return &Result{
|
||||
RepoID: result.RepoID,
|
||||
Filename: result.Filename,
|
||||
CommitID: result.CommitID,
|
||||
UpdatedUnix: result.UpdatedUnix,
|
||||
Language: result.Language,
|
||||
Color: result.Color,
|
||||
LineNumbers: lineNumbers,
|
||||
FormattedLines: highlighted,
|
||||
RepoID: result.RepoID,
|
||||
Filename: result.Filename,
|
||||
CommitID: result.CommitID,
|
||||
UpdatedUnix: result.UpdatedUnix,
|
||||
Language: result.Language,
|
||||
Color: result.Color,
|
||||
Lines: lines,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -122,7 +122,13 @@ func validateRequired(field *api.IssueFormField, idx int) error {
|
|||
// The label is not required for a markdown or checkboxes field
|
||||
return nil
|
||||
}
|
||||
return validateBoolItem(newErrorPosition(idx, field.Type), field.Validations, "required")
|
||||
if err := validateBoolItem(newErrorPosition(idx, field.Type), field.Validations, "required"); err != nil {
|
||||
return err
|
||||
}
|
||||
if required, _ := field.Validations["required"].(bool); required && !field.VisibleOnForm() {
|
||||
return newErrorPosition(idx, field.Type).Errorf("can not require a hidden field")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateID(field *api.IssueFormField, idx int, ids container.Set[string]) error {
|
||||
|
@ -172,10 +178,38 @@ func validateOptions(field *api.IssueFormField, idx int) error {
|
|||
return position.Errorf("'label' is required and should be a string")
|
||||
}
|
||||
|
||||
if visibility, ok := opt["visible"]; ok {
|
||||
visibilityList, ok := visibility.([]any)
|
||||
if !ok {
|
||||
return position.Errorf("'visible' should be list")
|
||||
}
|
||||
for _, visibleType := range visibilityList {
|
||||
visibleType, ok := visibleType.(string)
|
||||
if !ok || !(visibleType == "form" || visibleType == "content") {
|
||||
return position.Errorf("'visible' list can only contain strings of 'form' and 'content'")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if required, ok := opt["required"]; ok {
|
||||
if _, ok := required.(bool); !ok {
|
||||
return position.Errorf("'required' should be a bool")
|
||||
}
|
||||
|
||||
// validate if hidden field is required
|
||||
if visibility, ok := opt["visible"]; ok {
|
||||
visibilityList, _ := visibility.([]any)
|
||||
isVisible := false
|
||||
for _, v := range visibilityList {
|
||||
if vv, _ := v.(string); vv == "form" {
|
||||
isVisible = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isVisible {
|
||||
return position.Errorf("can not require a hidden checkbox")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -238,7 +272,7 @@ func RenderToMarkdown(template *api.IssueTemplate, values url.Values) string {
|
|||
IssueFormField: field,
|
||||
Values: values,
|
||||
}
|
||||
if f.ID == "" {
|
||||
if f.ID == "" || !f.VisibleInContent() {
|
||||
continue
|
||||
}
|
||||
f.WriteTo(builder)
|
||||
|
@ -253,11 +287,6 @@ type valuedField struct {
|
|||
}
|
||||
|
||||
func (f *valuedField) WriteTo(builder *strings.Builder) {
|
||||
if f.Type == api.IssueFormFieldTypeMarkdown {
|
||||
// markdown blocks do not appear in output
|
||||
return
|
||||
}
|
||||
|
||||
// write label
|
||||
if !f.HideLabel() {
|
||||
_, _ = fmt.Fprintf(builder, "### %s\n\n", f.Label())
|
||||
|
@ -269,6 +298,9 @@ func (f *valuedField) WriteTo(builder *strings.Builder) {
|
|||
switch f.Type {
|
||||
case api.IssueFormFieldTypeCheckboxes:
|
||||
for _, option := range f.Options() {
|
||||
if !option.VisibleInContent() {
|
||||
continue
|
||||
}
|
||||
checked := " "
|
||||
if option.IsChecked() {
|
||||
checked = "x"
|
||||
|
@ -302,6 +334,10 @@ func (f *valuedField) WriteTo(builder *strings.Builder) {
|
|||
} else {
|
||||
_, _ = fmt.Fprintf(builder, "%s\n", value)
|
||||
}
|
||||
case api.IssueFormFieldTypeMarkdown:
|
||||
if value, ok := f.Attributes["value"].(string); ok {
|
||||
_, _ = fmt.Fprintf(builder, "%s\n", value)
|
||||
}
|
||||
}
|
||||
_, _ = fmt.Fprintln(builder)
|
||||
}
|
||||
|
@ -314,6 +350,9 @@ func (f *valuedField) Label() string {
|
|||
}
|
||||
|
||||
func (f *valuedField) HideLabel() bool {
|
||||
if f.Type == api.IssueFormFieldTypeMarkdown {
|
||||
return true
|
||||
}
|
||||
if label, ok := f.Attributes["hide_label"].(bool); ok {
|
||||
return label
|
||||
}
|
||||
|
@ -385,6 +424,22 @@ func (o *valuedOption) IsChecked() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (o *valuedOption) VisibleInContent() bool {
|
||||
if o.field.Type == api.IssueFormFieldTypeCheckboxes {
|
||||
if vs, ok := o.data.(map[string]any); ok {
|
||||
if vl, ok := vs["visible"].([]any); ok {
|
||||
for _, v := range vl {
|
||||
if vv, _ := v.(string); vv == "content" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
var minQuotesRegex = regexp.MustCompilePOSIX("^`{3,}")
|
||||
|
||||
// minQuotes return 3 or more back-quotes.
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/json"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
@ -318,6 +319,42 @@ body:
|
|||
`,
|
||||
wantErr: "body[0](checkboxes), option[0]: 'required' should be a bool",
|
||||
},
|
||||
{
|
||||
name: "field is required but hidden",
|
||||
content: `
|
||||
name: "test"
|
||||
about: "this is about"
|
||||
body:
|
||||
- type: "input"
|
||||
id: "1"
|
||||
attributes:
|
||||
label: "a"
|
||||
validations:
|
||||
required: true
|
||||
visible: [content]
|
||||
`,
|
||||
wantErr: "body[0](input): can not require a hidden field",
|
||||
},
|
||||
{
|
||||
name: "checkboxes is required but hidden",
|
||||
content: `
|
||||
name: "test"
|
||||
about: "this is about"
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: "1"
|
||||
attributes:
|
||||
label: Label of checkboxes
|
||||
description: Description of checkboxes
|
||||
options:
|
||||
- label: Option 1
|
||||
required: false
|
||||
- label: Required and hidden
|
||||
required: true
|
||||
visible: [content]
|
||||
`,
|
||||
wantErr: "body[0](checkboxes), option[1]: can not require a hidden checkbox",
|
||||
},
|
||||
{
|
||||
name: "valid",
|
||||
content: `
|
||||
|
@ -374,8 +411,11 @@ body:
|
|||
required: true
|
||||
- label: Option 2 of checkboxes
|
||||
required: false
|
||||
- label: Option 3 of checkboxes
|
||||
- label: Hidden Option 3 of checkboxes
|
||||
visible: [content]
|
||||
- label: Required but not submitted
|
||||
required: true
|
||||
visible: [form]
|
||||
`,
|
||||
want: &api.IssueTemplate{
|
||||
Name: "Name",
|
||||
|
@ -390,6 +430,7 @@ body:
|
|||
Attributes: map[string]any{
|
||||
"value": "Value of the markdown",
|
||||
},
|
||||
Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm},
|
||||
},
|
||||
{
|
||||
Type: "textarea",
|
||||
|
@ -404,6 +445,7 @@ body:
|
|||
Validations: map[string]any{
|
||||
"required": true,
|
||||
},
|
||||
Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent},
|
||||
},
|
||||
{
|
||||
Type: "input",
|
||||
|
@ -419,6 +461,7 @@ body:
|
|||
"is_number": true,
|
||||
"regex": "[a-zA-Z0-9]+",
|
||||
},
|
||||
Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent},
|
||||
},
|
||||
{
|
||||
Type: "dropdown",
|
||||
|
@ -436,6 +479,7 @@ body:
|
|||
Validations: map[string]any{
|
||||
"required": true,
|
||||
},
|
||||
Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent},
|
||||
},
|
||||
{
|
||||
Type: "checkboxes",
|
||||
|
@ -446,9 +490,11 @@ body:
|
|||
"options": []any{
|
||||
map[string]any{"label": "Option 1 of checkboxes", "required": true},
|
||||
map[string]any{"label": "Option 2 of checkboxes", "required": false},
|
||||
map[string]any{"label": "Option 3 of checkboxes", "required": true},
|
||||
map[string]any{"label": "Hidden Option 3 of checkboxes", "visible": []string{"content"}},
|
||||
map[string]any{"label": "Required but not submitted", "required": true, "visible": []string{"form"}},
|
||||
},
|
||||
},
|
||||
Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent},
|
||||
},
|
||||
},
|
||||
FileName: "test.yaml",
|
||||
|
@ -467,7 +513,12 @@ body:
|
|||
- type: markdown
|
||||
id: id1
|
||||
attributes:
|
||||
value: Value of the markdown
|
||||
value: Value of the markdown shown in form
|
||||
- type: markdown
|
||||
id: id2
|
||||
attributes:
|
||||
value: Value of the markdown shown in created issue
|
||||
visible: [content]
|
||||
`,
|
||||
want: &api.IssueTemplate{
|
||||
Name: "Name",
|
||||
|
@ -480,8 +531,17 @@ body:
|
|||
Type: "markdown",
|
||||
ID: "id1",
|
||||
Attributes: map[string]any{
|
||||
"value": "Value of the markdown",
|
||||
"value": "Value of the markdown shown in form",
|
||||
},
|
||||
Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm},
|
||||
},
|
||||
{
|
||||
Type: "markdown",
|
||||
ID: "id2",
|
||||
Attributes: map[string]any{
|
||||
"value": "Value of the markdown shown in created issue",
|
||||
},
|
||||
Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleContent},
|
||||
},
|
||||
},
|
||||
FileName: "test.yaml",
|
||||
|
@ -515,6 +575,7 @@ body:
|
|||
Attributes: map[string]any{
|
||||
"value": "Value of the markdown",
|
||||
},
|
||||
Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm},
|
||||
},
|
||||
},
|
||||
FileName: "test.yaml",
|
||||
|
@ -548,6 +609,7 @@ body:
|
|||
Attributes: map[string]any{
|
||||
"value": "Value of the markdown",
|
||||
},
|
||||
Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm},
|
||||
},
|
||||
},
|
||||
FileName: "test.yaml",
|
||||
|
@ -622,9 +684,14 @@ body:
|
|||
- type: markdown
|
||||
id: id1
|
||||
attributes:
|
||||
value: Value of the markdown
|
||||
- type: textarea
|
||||
value: Value of the markdown shown in form
|
||||
- type: markdown
|
||||
id: id2
|
||||
attributes:
|
||||
value: Value of the markdown shown in created issue
|
||||
visible: [content]
|
||||
- type: textarea
|
||||
id: id3
|
||||
attributes:
|
||||
label: Label of textarea
|
||||
description: Description of textarea
|
||||
|
@ -634,7 +701,7 @@ body:
|
|||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: id3
|
||||
id: id4
|
||||
attributes:
|
||||
label: Label of input
|
||||
description: Description of input
|
||||
|
@ -646,7 +713,7 @@ body:
|
|||
is_number: true
|
||||
regex: "[a-zA-Z0-9]+"
|
||||
- type: dropdown
|
||||
id: id4
|
||||
id: id5
|
||||
attributes:
|
||||
label: Label of dropdown
|
||||
description: Description of dropdown
|
||||
|
@ -658,7 +725,7 @@ body:
|
|||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: id5
|
||||
id: id6
|
||||
attributes:
|
||||
label: Label of checkboxes
|
||||
description: Description of checkboxes
|
||||
|
@ -669,20 +736,26 @@ body:
|
|||
required: false
|
||||
- label: Option 3 of checkboxes
|
||||
required: true
|
||||
visible: [form]
|
||||
- label: Hidden Option of checkboxes
|
||||
visible: [content]
|
||||
`,
|
||||
values: map[string][]string{
|
||||
"form-field-id2": {"Value of id2"},
|
||||
"form-field-id3": {"Value of id3"},
|
||||
"form-field-id4": {"0,1"},
|
||||
"form-field-id5-0": {"on"},
|
||||
"form-field-id5-2": {"on"},
|
||||
"form-field-id4": {"Value of id4"},
|
||||
"form-field-id5": {"0,1"},
|
||||
"form-field-id6-0": {"on"},
|
||||
"form-field-id6-2": {"on"},
|
||||
},
|
||||
},
|
||||
want: `### Label of textarea
|
||||
|
||||
` + "```bash\nValue of id2\n```" + `
|
||||
want: `Value of the markdown shown in created issue
|
||||
|
||||
Value of id3
|
||||
### Label of textarea
|
||||
|
||||
` + "```bash\nValue of id3\n```" + `
|
||||
|
||||
Value of id4
|
||||
|
||||
### Label of dropdown
|
||||
|
||||
|
@ -692,7 +765,7 @@ Option 1 of dropdown, Option 2 of dropdown
|
|||
|
||||
- [x] Option 1 of checkboxes
|
||||
- [ ] Option 2 of checkboxes
|
||||
- [x] Option 3 of checkboxes
|
||||
- [ ] Hidden Option of checkboxes
|
||||
|
||||
`,
|
||||
},
|
||||
|
@ -704,7 +777,7 @@ Option 1 of dropdown, Option 2 of dropdown
|
|||
t.Fatal(err)
|
||||
}
|
||||
if got := RenderToMarkdown(template, tt.args.values); got != tt.want {
|
||||
t.Errorf("RenderToMarkdown() = %v, want %v", got, tt.want)
|
||||
assert.EqualValues(t, tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -128,9 +128,18 @@ func unmarshal(filename string, content []byte) (*api.IssueTemplate, error) {
|
|||
}
|
||||
}
|
||||
for i, v := range it.Fields {
|
||||
// set default id value
|
||||
if v.ID == "" {
|
||||
v.ID = strconv.Itoa(i)
|
||||
}
|
||||
// set default visibility
|
||||
if v.Visible == nil {
|
||||
v.Visible = []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm}
|
||||
// markdown is not submitted by default
|
||||
if v.Type != api.IssueFormFieldTypeMarkdown {
|
||||
v.Visible = append(v.Visible, api.IssueFormFieldVisibleContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,14 @@ import (
|
|||
)
|
||||
|
||||
func AddCollaborator(ctx context.Context, repo *repo_model.Repository, u *user_model.User) error {
|
||||
if err := repo.LoadOwner(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if user_model.IsUserBlockedBy(ctx, u, repo.OwnerID) || user_model.IsUserBlockedBy(ctx, repo.Owner, u.ID) {
|
||||
return user_model.ErrBlockedUser
|
||||
}
|
||||
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
has, err := db.Exist[repo_model.Collaboration](ctx, builder.Eq{
|
||||
"repo_id": repo.ID,
|
||||
|
|
|
@ -93,6 +93,12 @@ func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, re
|
|||
AllowRebaseUpdate: true,
|
||||
},
|
||||
})
|
||||
} else if tp == unit.TypeProjects {
|
||||
units = append(units, repo_model.RepoUnit{
|
||||
RepoID: repo.ID,
|
||||
Type: tp,
|
||||
Config: &repo_model.ProjectsConfig{ProjectsMode: repo_model.ProjectsModeAll},
|
||||
})
|
||||
} else {
|
||||
units = append(units, repo_model.RepoUnit{
|
||||
RepoID: repo.ID,
|
||||
|
@ -147,7 +153,7 @@ func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, re
|
|||
}
|
||||
|
||||
if setting.Service.AutoWatchNewRepos {
|
||||
if err = repo_model.WatchRepo(ctx, doer.ID, repo.ID, true); err != nil {
|
||||
if err = repo_model.WatchRepo(ctx, doer, repo, true); err != nil {
|
||||
return fmt.Errorf("WatchRepo: %w", err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,5 +21,6 @@ func loadAdminFrom(rootCfg ConfigProvider) {
|
|||
|
||||
const (
|
||||
UserFeatureDeletion = "deletion"
|
||||
UserFeatureManageSSHKeys = "manage_ssh_keys"
|
||||
UserFeatureManageGPGKeys = "manage_gpg_keys"
|
||||
)
|
||||
|
|
|
@ -6,6 +6,7 @@ package structs
|
|||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -141,12 +142,37 @@ const (
|
|||
// IssueFormField represents a form field
|
||||
// swagger:model
|
||||
type IssueFormField struct {
|
||||
Type IssueFormFieldType `json:"type" yaml:"type"`
|
||||
ID string `json:"id" yaml:"id"`
|
||||
Attributes map[string]any `json:"attributes" yaml:"attributes"`
|
||||
Validations map[string]any `json:"validations" yaml:"validations"`
|
||||
Type IssueFormFieldType `json:"type" yaml:"type"`
|
||||
ID string `json:"id" yaml:"id"`
|
||||
Attributes map[string]any `json:"attributes" yaml:"attributes"`
|
||||
Validations map[string]any `json:"validations" yaml:"validations"`
|
||||
Visible []IssueFormFieldVisible `json:"visible,omitempty"`
|
||||
}
|
||||
|
||||
func (iff IssueFormField) VisibleOnForm() bool {
|
||||
if len(iff.Visible) == 0 {
|
||||
return true
|
||||
}
|
||||
return slices.Contains(iff.Visible, IssueFormFieldVisibleForm)
|
||||
}
|
||||
|
||||
func (iff IssueFormField) VisibleInContent() bool {
|
||||
if len(iff.Visible) == 0 {
|
||||
// we have our markdown exception
|
||||
return iff.Type != IssueFormFieldTypeMarkdown
|
||||
}
|
||||
return slices.Contains(iff.Visible, IssueFormFieldVisibleContent)
|
||||
}
|
||||
|
||||
// IssueFormFieldVisible defines issue form field visible
|
||||
// swagger:model
|
||||
type IssueFormFieldVisible string
|
||||
|
||||
const (
|
||||
IssueFormFieldVisibleForm IssueFormFieldVisible = "form"
|
||||
IssueFormFieldVisibleContent IssueFormFieldVisible = "content"
|
||||
)
|
||||
|
||||
// IssueTemplate represents an issue template for a repository
|
||||
// swagger:model
|
||||
type IssueTemplate struct {
|
||||
|
|
|
@ -90,6 +90,7 @@ type Repository struct {
|
|||
ExternalWiki *ExternalWiki `json:"external_wiki,omitempty"`
|
||||
HasPullRequests bool `json:"has_pull_requests"`
|
||||
HasProjects bool `json:"has_projects"`
|
||||
ProjectsMode string `json:"projects_mode"`
|
||||
HasReleases bool `json:"has_releases"`
|
||||
HasPackages bool `json:"has_packages"`
|
||||
HasActions bool `json:"has_actions"`
|
||||
|
@ -180,6 +181,8 @@ type EditRepoOption struct {
|
|||
HasPullRequests *bool `json:"has_pull_requests,omitempty"`
|
||||
// either `true` to enable project unit, or `false` to disable them.
|
||||
HasProjects *bool `json:"has_projects,omitempty"`
|
||||
// `repo` to only allow repo-level projects, `owner` to only allow owner projects, `all` to allow both.
|
||||
ProjectsMode *string `json:"projects_mode,omitempty" binding:"In(repo,owner,all)"`
|
||||
// either `true` to enable releases unit, or `false` to disable them.
|
||||
HasReleases *bool `json:"has_releases,omitempty"`
|
||||
// either `true` to enable packages unit, or `false` to disable them.
|
||||
|
|
|
@ -208,14 +208,8 @@ func SafeHTML(s any) template.HTML {
|
|||
}
|
||||
|
||||
// SanitizeHTML sanitizes the input by pre-defined markdown rules
|
||||
func SanitizeHTML(s any) template.HTML {
|
||||
switch v := s.(type) {
|
||||
case string:
|
||||
return template.HTML(markup.Sanitize(v))
|
||||
case template.HTML:
|
||||
return template.HTML(markup.Sanitize(string(v)))
|
||||
}
|
||||
panic(fmt.Sprintf("unexpected type %T", s))
|
||||
func SanitizeHTML(s string) template.HTML {
|
||||
return template.HTML(markup.Sanitize(s))
|
||||
}
|
||||
|
||||
func HTMLEscape(s any) template.HTML {
|
||||
|
|
|
@ -64,5 +64,4 @@ func TestHTMLFormat(t *testing.T) {
|
|||
|
||||
func TestSanitizeHTML(t *testing.T) {
|
||||
assert.Equal(t, template.HTML(`<a href="/" rel="nofollow">link</a> xss <div>inline</div>`), SanitizeHTML(`<a href="/">link</a> <a href="javascript:">xss</a> <div style="dangerous">inline</div>`))
|
||||
assert.Equal(t, template.HTML(`<a href="/" rel="nofollow">link</a> xss <div>inline</div>`), SanitizeHTML(template.HTML(`<a href="/">link</a> <a href="javascript:">xss</a> <div style="dangerous">inline</div>`)))
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ package templates
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
@ -33,7 +34,7 @@ func mailSubjectTextFuncMap() texttmpl.FuncMap {
|
|||
}
|
||||
}
|
||||
|
||||
func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template, name string, content []byte) {
|
||||
func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template, name string, content []byte) error {
|
||||
// Split template into subject and body
|
||||
var subjectContent []byte
|
||||
bodyContent := content
|
||||
|
@ -42,20 +43,13 @@ func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template,
|
|||
subjectContent = content[0:loc[0]]
|
||||
bodyContent = content[loc[1]:]
|
||||
}
|
||||
if _, err := stpl.New(name).
|
||||
Parse(string(subjectContent)); err != nil {
|
||||
log.Error("Failed to parse template [%s/subject]: %v", name, err)
|
||||
if !setting.IsProd {
|
||||
log.Fatal("Please fix the mail template error")
|
||||
}
|
||||
if _, err := stpl.New(name).Parse(string(subjectContent)); err != nil {
|
||||
return fmt.Errorf("failed to parse template [%s/subject]: %w", name, err)
|
||||
}
|
||||
if _, err := btpl.New(name).
|
||||
Parse(string(bodyContent)); err != nil {
|
||||
log.Error("Failed to parse template [%s/body]: %v", name, err)
|
||||
if !setting.IsProd {
|
||||
log.Fatal("Please fix the mail template error")
|
||||
}
|
||||
if _, err := btpl.New(name).Parse(string(bodyContent)); err != nil {
|
||||
return fmt.Errorf("failed to parse template [%s/body]: %w", name, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Mailer provides the templates required for sending notification mails.
|
||||
|
@ -87,7 +81,13 @@ func Mailer(ctx context.Context) (*texttmpl.Template, *template.Template) {
|
|||
if firstRun {
|
||||
log.Trace("Adding mail template %s: %s by %s", tmplName, assetPath, layerName)
|
||||
}
|
||||
buildSubjectBodyTemplate(subjectTemplates, bodyTemplates, tmplName, content)
|
||||
if err = buildSubjectBodyTemplate(subjectTemplates, bodyTemplates, tmplName, content); err != nil {
|
||||
if firstRun {
|
||||
log.Fatal("Failed to parse mail template, err: %v", err)
|
||||
} else {
|
||||
log.Error("Failed to parse mail template, err: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,9 @@ import (
|
|||
)
|
||||
|
||||
// MockLocale provides a mocked locale without any translations
|
||||
type MockLocale struct{}
|
||||
type MockLocale struct {
|
||||
Lang, LangName string // these fields are used directly in templates: ctx.Locale.Lang
|
||||
}
|
||||
|
||||
var _ Locale = (*MockLocale)(nil)
|
||||
|
||||
|
|
|
@ -144,7 +144,7 @@ func Match(tags ...language.Tag) language.Tag {
|
|||
// locale represents the information of localization.
|
||||
type locale struct {
|
||||
i18n.Locale
|
||||
Lang, LangName string // these fields are used directly in templates: .i18n.Lang
|
||||
Lang, LangName string // these fields are used directly in templates: ctx.Locale.Lang
|
||||
msgPrinter *message.Printer
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
Copyright (c) 2014-2020 The Khronos Group Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and/or associated documentation files (the "Materials"),
|
||||
to deal in the Materials without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Materials, and to permit persons to whom the
|
||||
Materials are furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Materials.
|
||||
|
||||
MODIFICATIONS TO THIS FILE MAY MEAN IT NO LONGER ACCURATELY REFLECTS KHRONOS
|
||||
STANDARDS. THE UNMODIFIED, NORMATIVE VERSIONS OF KHRONOS SPECIFICATIONS AND
|
||||
HEADER INFORMATION ARE LOCATED AT https://www.khronos.org/registry/
|
||||
|
||||
THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM,OUT OF OR IN CONNECTION WITH THE MATERIALS OR THE USE OR OTHER DEALINGS
|
||||
IN THE MATERIALS.
|
|
@ -142,6 +142,19 @@ confirm_delete_selected = Confirm to delete all selected items?
|
|||
name = Name
|
||||
value = Value
|
||||
|
||||
filter = Filter
|
||||
filter.clear = Clear Filter
|
||||
filter.is_archived = Archived
|
||||
filter.not_archived = Not Archived
|
||||
filter.is_fork = Forked
|
||||
filter.not_fork = Not Forked
|
||||
filter.is_mirror = Mirrored
|
||||
filter.not_mirror = Not Mirrored
|
||||
filter.is_template = Template
|
||||
filter.not_template = Not Template
|
||||
filter.public = Public
|
||||
filter.private = Private
|
||||
|
||||
[aria]
|
||||
navbar = Navigation Bar
|
||||
footer = Footer
|
||||
|
@ -307,6 +320,7 @@ env_config_keys = Environment Configuration
|
|||
env_config_keys_prompt = The following environment variables will also be applied to your configuration file:
|
||||
|
||||
[home]
|
||||
nav_menu = Navigation Menu
|
||||
uname_holder = Username or Email Address
|
||||
password_holder = Password
|
||||
switch_dashboard_context = Switch Dashboard Context
|
||||
|
@ -620,6 +634,30 @@ form.name_reserved = The username "%s" is reserved.
|
|||
form.name_pattern_not_allowed = The pattern "%s" is not allowed in a username.
|
||||
form.name_chars_not_allowed = User name "%s" contains invalid characters.
|
||||
|
||||
block.block = Block
|
||||
block.block.user = Block user
|
||||
block.block.org = Block user for organization
|
||||
block.block.failure = Failed to block user: %s
|
||||
block.unblock = Unblock
|
||||
block.unblock.failure = Failed to unblock user: %s
|
||||
block.blocked = You have blocked this user.
|
||||
block.title = Block a user
|
||||
block.info = Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
|
||||
block.info_1 = Blocking a user prevents the following actions on your account and your repositories:
|
||||
block.info_2 = following your account
|
||||
block.info_3 = send you notifications by @mentioning your username
|
||||
block.info_4 = inviting you as a collaborator to their repositories
|
||||
block.info_5 = starring, forking or watching on repositories
|
||||
block.info_6 = opening and commenting on issues or pull requests
|
||||
block.info_7 = reacting on your comments in issues or pull requests
|
||||
block.user_to_block = User to block
|
||||
block.note = Note
|
||||
block.note.title = Optional note:
|
||||
block.note.info = The note is not visible to the blocked user.
|
||||
block.note.edit = Edit note
|
||||
block.list = Blocked users
|
||||
block.list.none = You have not blocked any users.
|
||||
|
||||
[settings]
|
||||
profile = Profile
|
||||
account = Account
|
||||
|
@ -957,6 +995,7 @@ fork_visibility_helper = The visibility of a forked repository cannot be changed
|
|||
fork_branch = Branch to be cloned to the fork
|
||||
all_branches = All branches
|
||||
fork_no_valid_owners = This repository can not be forked because there are no valid owners.
|
||||
fork.blocked_user = Cannot fork the repository because you are blocked by the repository owner.
|
||||
use_template = Use this template
|
||||
open_with_editor = Open with %s
|
||||
download_zip = Download ZIP
|
||||
|
@ -1132,6 +1171,7 @@ watch = Watch
|
|||
unstar = Unstar
|
||||
star = Star
|
||||
fork = Fork
|
||||
action.blocked_user = Cannot perform action because you are blocked by the repository owner.
|
||||
download_archive = Download Repository
|
||||
more_operations = More Operations
|
||||
|
||||
|
@ -1382,6 +1422,8 @@ issues.new.assignees = Assignees
|
|||
issues.new.clear_assignees = Clear assignees
|
||||
issues.new.no_assignees = No Assignees
|
||||
issues.new.no_reviewers = No reviewers
|
||||
issues.new.blocked_user = Cannot create issue because you are blocked by the repository owner.
|
||||
issues.edit.blocked_user = Cannot edit content because you are blocked by the poster or repository owner.
|
||||
issues.choose.get_started = Get Started
|
||||
issues.choose.open_external_link = Open
|
||||
issues.choose.blank = Default
|
||||
|
@ -1497,6 +1539,7 @@ issues.close_comment_issue = Comment and Close
|
|||
issues.reopen_issue = Reopen
|
||||
issues.reopen_comment_issue = Comment and Reopen
|
||||
issues.create_comment = Comment
|
||||
issues.comment.blocked_user = Cannot create or edit comment because you are blocked by the poster or repository owner.
|
||||
issues.closed_at = `closed this issue <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
issues.reopened_at = `reopened this issue <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
issues.commit_ref_at = `referenced this issue from a commit <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
|
@ -1695,6 +1738,7 @@ compare.compare_head = compare
|
|||
|
||||
pulls.desc = Enable pull requests and code reviews.
|
||||
pulls.new = New Pull Request
|
||||
pulls.new.blocked_user = Cannot create pull request because you are blocked by the repository owner.
|
||||
pulls.view = View Pull Request
|
||||
pulls.compare_changes = New Pull Request
|
||||
pulls.allow_edits_from_maintainers = Allow edits from maintainers
|
||||
|
@ -2049,6 +2093,8 @@ settings.branches.add_new_rule = Add New Rule
|
|||
settings.advanced_settings = Advanced Settings
|
||||
settings.wiki_desc = Enable Repository Wiki
|
||||
settings.use_internal_wiki = Use Built-In Wiki
|
||||
settings.default_wiki_branch_name = Default Wiki Branch Name
|
||||
settings.failed_to_change_default_wiki_branch = Failed to change the default wiki branch.
|
||||
settings.use_external_wiki = Use External Wiki
|
||||
settings.external_wiki_url = External Wiki URL
|
||||
settings.external_wiki_url_error = The external wiki URL is not a valid URL.
|
||||
|
@ -2078,7 +2124,11 @@ settings.pulls.default_delete_branch_after_merge = Delete pull request branch af
|
|||
settings.pulls.default_allow_edits_from_maintainers = Allow edits from maintainers by default
|
||||
settings.releases_desc = Enable Repository Releases
|
||||
settings.packages_desc = Enable Repository Packages Registry
|
||||
settings.projects_desc = Enable Repository Projects
|
||||
settings.projects_desc = Enable Projects
|
||||
settings.projects_mode_desc = Projects Mode (which kinds of projects to show)
|
||||
settings.projects_mode_repo = Repo projects only
|
||||
settings.projects_mode_owner = Only user or org projects
|
||||
settings.projects_mode_all = All projects
|
||||
settings.actions_desc = Enable Repository Actions
|
||||
settings.admin_settings = Administrator Settings
|
||||
settings.admin_enable_health_check = Enable Repository Health Checks (git fsck)
|
||||
|
@ -2104,6 +2154,7 @@ settings.convert_fork_succeed = The fork has been converted into a regular repos
|
|||
settings.transfer = Transfer Ownership
|
||||
settings.transfer.rejected = Repository transfer was rejected.
|
||||
settings.transfer.success = Repository transfer was successful.
|
||||
settings.transfer.blocked_user = Cannot transfer repository because you are blocked by the new owner.
|
||||
settings.transfer_abort = Cancel transfer
|
||||
settings.transfer_abort_invalid = You cannot cancel a non existent repository transfer.
|
||||
settings.transfer_abort_success = The repository transfer to %s was successfully canceled.
|
||||
|
@ -2149,6 +2200,7 @@ settings.add_collaborator_success = The collaborator has been added.
|
|||
settings.add_collaborator_inactive_user = Cannot add an inactive user as a collaborator.
|
||||
settings.add_collaborator_owner = Cannot add an owner as a collaborator.
|
||||
settings.add_collaborator_duplicate = The collaborator is already added to this repository.
|
||||
settings.add_collaborator.blocked_user = The collaborator is blocked by the repository owner or vice versa.
|
||||
settings.delete_collaborator = Remove
|
||||
settings.collaborator_deletion = Remove Collaborator
|
||||
settings.collaborator_deletion_desc = Removing a collaborator will revoke their access to this repository. Continue?
|
||||
|
@ -2592,6 +2644,7 @@ find_file.no_matching = No matching file found
|
|||
error.csv.too_large = Can't render this file because it is too large.
|
||||
error.csv.unexpected = Can't render this file because it contains an unexpected character in line %d and column %d.
|
||||
error.csv.invalid_field_count = Can't render this file because it has a wrong number of fields in line %d.
|
||||
error.broken_git_hook = Git hooks of this repository seem to be broken. Please follow the <a target="_blank" rel="noreferrer" href="%s">documentation</a> to fix them, then push some commits to refresh the status.
|
||||
|
||||
[graphs]
|
||||
component_loading = Loading %s...
|
||||
|
@ -2715,6 +2768,7 @@ teams.add_nonexistent_repo = "The repository you're trying to add doesn't exist,
|
|||
teams.add_duplicate_users = User is already a team member.
|
||||
teams.repos.none = No repositories could be accessed by this team.
|
||||
teams.members.none = No members on this team.
|
||||
teams.members.blocked_user = Cannot add the user because it is blocked by the organization.
|
||||
teams.specific_repositories = Specific repositories
|
||||
teams.specific_repositories_helper = Members will only have access to repositories explicitly added to the team. Selecting this <strong>will not</strong> automatically remove repositories already added with <i>All repositories</i>.
|
||||
teams.all_repositories = All repositories
|
||||
|
|
|
@ -149,8 +149,8 @@ footer.software=ソフトウェアについて
|
|||
footer.links=リンク
|
||||
|
||||
[heatmap]
|
||||
number_of_contributions_in_the_last_12_months=過去 12 か月間で %s 個の貢献
|
||||
no_contributions=貢献なし
|
||||
number_of_contributions_in_the_last_12_months=過去 12 か月間で %s 件の実績
|
||||
no_contributions=実績なし
|
||||
less=少
|
||||
more=多
|
||||
|
||||
|
@ -1510,7 +1510,7 @@ issues.role.member_helper=このユーザーはこのリポジトリを所有し
|
|||
issues.role.collaborator=共同作業者
|
||||
issues.role.collaborator_helper=このユーザーはリポジトリ上で共同作業するように招待されています。
|
||||
issues.role.first_time_contributor=初めての貢献者
|
||||
issues.role.first_time_contributor_helper=これは、このユーザーのリポジトリへの最初の貢献です。
|
||||
issues.role.first_time_contributor_helper=これは、このユーザーによるリポジトリへの最初の貢献です。
|
||||
issues.role.contributor=貢献者
|
||||
issues.role.contributor_helper=このユーザーは以前にリポジトリにコミットしています。
|
||||
issues.re_request_review=レビューを再依頼
|
||||
|
@ -2011,7 +2011,8 @@ settings.mirror_settings.docs.more_information_if_disabled=プッシュミラー
|
|||
settings.mirror_settings.docs.doc_link_title=リポジトリをミラーリングするには?
|
||||
settings.mirror_settings.docs.doc_link_pull_section=ドキュメントの「リモートリポジトリからのプル」セクション。
|
||||
settings.mirror_settings.docs.pulling_remote_title=リモートリポジトリからのプル
|
||||
settings.mirror_settings.mirrored_repository=同期するリポジトリ
|
||||
settings.mirror_settings.mirrored_repository=ミラー元のリポジトリ
|
||||
settings.mirror_settings.pushed_repository=プッシュ先のリポジトリ
|
||||
settings.mirror_settings.direction=方向
|
||||
settings.mirror_settings.direction.pull=プル
|
||||
settings.mirror_settings.direction.push=プッシュ
|
||||
|
@ -3546,6 +3547,8 @@ runs.actors_no_select=すべてのアクター
|
|||
runs.status_no_select=すべてのステータス
|
||||
runs.no_results=一致する結果はありません。
|
||||
runs.no_workflows=ワークフローはまだありません。
|
||||
runs.no_workflows.quick_start=Gitea Actions の始め方がわからない? では<a target="_blank" rel="noopener noreferrer" href="%s">クイックスタートガイド</a>をご覧ください。
|
||||
runs.no_workflows.documentation=Gitea Actions の詳細については、<a target="_blank" rel="noopener noreferrer" href="%s">ドキュメント</a>を参照してください。
|
||||
runs.no_runs=ワークフローはまだ実行されていません。
|
||||
runs.empty_commit_message=(空のコミットメッセージ)
|
||||
|
||||
|
|
33
package.json
|
@ -9,7 +9,7 @@
|
|||
"@citation-js/plugin-csl": "0.7.6",
|
||||
"@citation-js/plugin-software-formats": "0.6.1",
|
||||
"@claviska/jquery-minicolors": "2.3.6",
|
||||
"@github/markdown-toolbar-element": "2.2.1",
|
||||
"@github/markdown-toolbar-element": "2.2.3",
|
||||
"@github/relative-time-element": "4.3.1",
|
||||
"@github/text-expander-element": "2.6.1",
|
||||
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
|
||||
|
@ -17,13 +17,12 @@
|
|||
"@webcomponents/custom-elements": "1.6.0",
|
||||
"add-asset-webpack-plugin": "2.0.1",
|
||||
"ansi_up": "6.0.2",
|
||||
"asciinema-player": "3.6.4",
|
||||
"chart.js": "4.4.1",
|
||||
"asciinema-player": "3.7.0",
|
||||
"chart.js": "4.4.2",
|
||||
"chartjs-adapter-dayjs-4": "1.0.4",
|
||||
"chartjs-plugin-zoom": "2.0.1",
|
||||
"clippie": "4.0.6",
|
||||
"clippie": "4.0.7",
|
||||
"css-loader": "6.10.0",
|
||||
"css-variables-parser": "1.0.1",
|
||||
"dayjs": "1.11.10",
|
||||
"dropzone": "6.0.0-beta.2",
|
||||
"easymde": "2.18.0",
|
||||
|
@ -36,16 +35,16 @@
|
|||
"katex": "0.16.9",
|
||||
"license-checker-webpack-plugin": "0.2.1",
|
||||
"mermaid": "10.8.0",
|
||||
"mini-css-extract-plugin": "2.8.0",
|
||||
"mini-css-extract-plugin": "2.8.1",
|
||||
"minimatch": "9.0.3",
|
||||
"monaco-editor": "0.46.0",
|
||||
"monaco-editor-webpack-plugin": "7.1.0",
|
||||
"pdfobject": "2.3.0",
|
||||
"postcss": "8.4.35",
|
||||
"postcss-loader": "8.1.0",
|
||||
"postcss-loader": "8.1.1",
|
||||
"pretty-ms": "9.0.0",
|
||||
"sortablejs": "1.15.2",
|
||||
"swagger-ui-dist": "5.11.6",
|
||||
"swagger-ui-dist": "5.11.8",
|
||||
"tailwindcss": "3.4.1",
|
||||
"throttle-debounce": "5.0.0",
|
||||
"tinycolor2": "1.6.0",
|
||||
|
@ -53,25 +52,25 @@
|
|||
"toastify-js": "1.12.0",
|
||||
"tributejs": "5.1.3",
|
||||
"uint8-to-base64": "0.2.0",
|
||||
"vue": "3.4.19",
|
||||
"vue": "3.4.21",
|
||||
"vue-bar-graph": "2.0.0",
|
||||
"vue-chartjs": "5.3.0",
|
||||
"vue-loader": "17.4.2",
|
||||
"vue3-calendar-heatmap": "2.0.5",
|
||||
"webpack": "5.90.2",
|
||||
"webpack": "5.90.3",
|
||||
"webpack-cli": "5.1.4",
|
||||
"wrap-ansi": "9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint-community/eslint-plugin-eslint-comments": "4.1.0",
|
||||
"@playwright/test": "1.41.2",
|
||||
"@playwright/test": "1.42.1",
|
||||
"@stoplight/spectral-cli": "6.11.0",
|
||||
"@stylistic/eslint-plugin-js": "1.6.2",
|
||||
"@stylistic/stylelint-plugin": "2.0.0",
|
||||
"@stylistic/eslint-plugin-js": "1.6.3",
|
||||
"@stylistic/stylelint-plugin": "2.1.0",
|
||||
"@vitejs/plugin-vue": "5.0.4",
|
||||
"eslint": "8.56.0",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-plugin-array-func": "4.0.0",
|
||||
"eslint-plugin-github": "4.10.1",
|
||||
"eslint-plugin-github": "4.10.2",
|
||||
"eslint-plugin-i": "2.29.1",
|
||||
"eslint-plugin-jquery": "1.5.1",
|
||||
"eslint-plugin-no-jquery": "2.7.0",
|
||||
|
@ -81,7 +80,7 @@
|
|||
"eslint-plugin-unicorn": "51.0.1",
|
||||
"eslint-plugin-vitest": "0.3.22",
|
||||
"eslint-plugin-vitest-globals": "1.4.0",
|
||||
"eslint-plugin-vue": "9.21.1",
|
||||
"eslint-plugin-vue": "9.22.0",
|
||||
"eslint-plugin-vue-scoped-css": "2.7.2",
|
||||
"eslint-plugin-wc": "2.0.4",
|
||||
"jsdom": "24.0.0",
|
||||
|
@ -93,7 +92,7 @@
|
|||
"svgo": "3.2.0",
|
||||
"updates": "15.1.2",
|
||||
"vite-string-plugin": "1.1.5",
|
||||
"vitest": "1.2.2"
|
||||
"vitest": "1.3.1"
|
||||
},
|
||||
"browserslist": [
|
||||
"defaults"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
|
@ -27,12 +27,12 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "cssbeautifier"
|
||||
version = "1.14.11"
|
||||
version = "1.15.1"
|
||||
description = "CSS unobfuscator and beautifier."
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "cssbeautifier-1.14.11.tar.gz", hash = "sha256:40544c2b62bbcb64caa5e7f37a02df95654e5ce1bcacadac4ca1f3dc89c31513"},
|
||||
{file = "cssbeautifier-1.15.1.tar.gz", hash = "sha256:9f7064362aedd559c55eeecf6b6bed65e05f33488dcbe39044f0403c26e1c006"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -67,13 +67,12 @@ tqdm = ">=4.62.2,<5.0.0"
|
|||
|
||||
[[package]]
|
||||
name = "editorconfig"
|
||||
version = "0.12.3"
|
||||
version = "0.12.4"
|
||||
description = "EditorConfig File Locator and Interpreter for Python"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "EditorConfig-0.12.3-py3-none-any.whl", hash = "sha256:6b0851425aa875b08b16789ee0eeadbd4ab59666e9ebe728e526314c4a2e52c1"},
|
||||
{file = "EditorConfig-0.12.3.tar.gz", hash = "sha256:57f8ce78afcba15c8b18d46b5170848c88d56fd38f05c2ec60dbbfcb8996e89e"},
|
||||
{file = "EditorConfig-0.12.4.tar.gz", hash = "sha256:24857fa1793917dd9ccf0c7810a07e05404ce9b823521c7dce22a4fb5d125f80"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -100,12 +99,12 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "jsbeautifier"
|
||||
version = "1.14.11"
|
||||
version = "1.15.1"
|
||||
description = "JavaScript unobfuscator and beautifier."
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "jsbeautifier-1.14.11.tar.gz", hash = "sha256:6b632581ea60dd1c133cd25a48ad187b4b91f526623c4b0fb5443ef805250505"},
|
||||
{file = "jsbeautifier-1.15.1.tar.gz", hash = "sha256:ebd733b560704c602d744eafc839db60a1ee9326e30a2a80c4adb8718adc1b24"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -114,13 +113,13 @@ six = ">=1.13.0"
|
|||
|
||||
[[package]]
|
||||
name = "json5"
|
||||
version = "0.9.14"
|
||||
version = "0.9.18"
|
||||
description = "A Python implementation of the JSON5 data format."
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "json5-0.9.14-py2.py3-none-any.whl", hash = "sha256:740c7f1b9e584a468dbb2939d8d458db3427f2c93ae2139d05f47e453eae964f"},
|
||||
{file = "json5-0.9.14.tar.gz", hash = "sha256:9ed66c3a6ca3510a976a9ef9b8c0787de24802724ab1860bc0153c7fdd589b02"},
|
||||
{file = "json5-0.9.18-py2.py3-none-any.whl", hash = "sha256:3f20193ff8dfdec6ab114b344e7ac5d76fac453c8bab9bdfe1460d1d528ec393"},
|
||||
{file = "json5-0.9.18.tar.gz", hash = "sha256:ecb8ac357004e3522fb989da1bf08b146011edbd14fdffae6caad3bd68493467"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
|
@ -322,13 +321,13 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "4.66.1"
|
||||
version = "4.66.2"
|
||||
description = "Fast, Extensible Progress Meter"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "tqdm-4.66.1-py3-none-any.whl", hash = "sha256:d302b3c5b53d47bce91fea46679d9c3c6508cf6332229aa1e7d8653723793386"},
|
||||
{file = "tqdm-4.66.1.tar.gz", hash = "sha256:d88e651f9db8d8551a62556d3cff9e3034274ca5d66e93197cf2490e2dcb69c7"},
|
||||
{file = "tqdm-4.66.2-py3-none-any.whl", hash = "sha256:1ee4f8a893eb9bef51c6e35730cebf234d5d0b6bd112b0271e10ed7c24a02bd9"},
|
||||
{file = "tqdm-4.66.2.tar.gz", hash = "sha256:6cd52cdf0fef0e0f543299cfc96fec90d7b8a7e88745f411ec33eb44d5ed3531"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -342,13 +341,13 @@ telegram = ["requests"]
|
|||
|
||||
[[package]]
|
||||
name = "yamllint"
|
||||
version = "1.35.0"
|
||||
version = "1.35.1"
|
||||
description = "A linter for YAML files."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "yamllint-1.35.0-py3-none-any.whl", hash = "sha256:601b0adaaac6d9bacb16a2e612e7ee8d23caf941ceebf9bfe2cff0f196266004"},
|
||||
{file = "yamllint-1.35.0.tar.gz", hash = "sha256:9bc99c3e9fe89b4c6ee26e17aa817cf2d14390de6577cb6e2e6ed5f72120c835"},
|
||||
{file = "yamllint-1.35.1-py3-none-any.whl", hash = "sha256:2e16e504bb129ff515b37823b472750b36b6de07963bd74b307341ef5ad8bdc3"},
|
||||
{file = "yamllint-1.35.1.tar.gz", hash = "sha256:7a003809f88324fd2c877734f2d575ee7881dd9043360657cc8049c809eba6cd"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -360,5 +359,5 @@ dev = ["doc8", "flake8", "flake8-import-order", "rstcheck[sphinx]", "sphinx"]
|
|||
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.8"
|
||||
content-hash = "ba1c2c4235872f67354b5f52aa5bf0cd616354961530d9dc907f9fba28cc1ece"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "cd2ff218e9f27a464dfbc8ec2387824a90f4360e04c3f2e58cc375796b7df33a"
|
||||
|
|
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="gitea-bitbucket__svg gitea-bitbucket__gitea-bitbucket svg gitea-bitbucket" preserveAspectRatio="xMinYMin meet" viewBox="0 0 256 295" width="16" height="16"><g fill="#205081"><path d="M128 0C57.732 0 .012 18.822.012 42.663c0 6.274 15.057 95.364 21.331 130.498 2.51 16.312 43.918 38.898 106.657 38.898 62.74 0 102.893-22.586 106.657-38.898 6.274-35.134 21.331-124.224 21.331-130.498C254.734 18.822 198.268 0 128 0m0 183.199c-22.586 0-40.153-17.567-40.153-40.153s17.567-40.153 40.153-40.153 40.153 17.567 40.153 40.153c0 21.331-17.567 40.153-40.153 40.153m0-127.988c-45.172 0-81.561-7.53-81.561-17.567 0-10.039 36.389-17.567 81.561-17.567s81.561 7.528 81.561 17.567c0 10.038-36.389 17.567-81.561 17.567"/><path d="M220.608 207.04c-2.51 0-3.764 1.255-3.764 1.255s-31.37 25.096-87.835 25.096c-56.466 0-87.835-25.096-87.835-25.096s-2.51-1.255-3.765-1.255c-2.51 0-5.019 1.255-5.019 5.02v1.254c5.02 26.35 8.784 45.172 8.784 47.682 3.764 18.822 41.408 33.88 86.58 33.88s82.816-15.058 86.58-33.88c0-2.51 3.765-21.332 8.784-47.682v-1.255c1.255-2.51 0-5.019-2.51-5.019"/><circle cx="128" cy="141.791" r="20.077"/></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 62.42 62.42" class="svg gitea-bitbucket" width="16" height="16" aria-hidden="true"><defs><linearGradient id="gitea-bitbucket__a" x1="64.01" x2="32.99" y1="30.27" y2="54.48" gradientUnits="userSpaceOnUse"><stop offset=".18" stop-color="#0052cc"/><stop offset="1" stop-color="#2684ff"/></linearGradient></defs><g data-name="Layer 2"><path fill="#2684ff" d="M2 3.13a2 2 0 0 0-2 2.32l8.49 51.54a2.72 2.72 0 0 0 2.66 2.27h40.73a2 2 0 0 0 2-1.68l8.49-52.12a2 2 0 0 0-2-2.32Zm35.75 37.25h-13l-3.52-18.39H40.9Z"/><path fill="url(#gitea-bitbucket__a)" d="M59.67 25.12H40.9l-3.15 18.39h-13L9.4 61.73a2.7 2.7 0 0 0 1.75.66h40.74a2 2 0 0 0 2-1.68Z" transform="translate(0 -3.13)"/></g></svg>
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 732 B |
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="gitea-facebook__svg gitea-facebook__gitea-facebook svg gitea-facebook" style="shape-rendering:geometricPrecision;text-rendering:geometricPrecision;image-rendering:optimizeQuality;fill-rule:evenodd;clip-rule:evenodd" viewBox="0 0 128 128" width="16" height="16"><path fill="#395b97" d="M93.5 8.5q-2.177 1.203-5 1.5L10 88.5q-.297 2.823-1.5 5a552 552 0 0 1-.5-56Q11.75 11.75 37.5 8a552 552 0 0 1 56 .5" style="opacity:.995"/><path fill="#366098" d="M93.5 8.5q23.832 6.337 26 31a677 677 0 0 0-1.5 37l-35 35a32.4 32.4 0 0 0-.5 8 442 442 0 0 1-1-42h14a380 380 0 0 0 3-17h-17q-3.75-20.745 17-18v-16q-38.632-4.865-33 34h-14v17h14v42q-14.01.25-28-.5-23.177-2.93-29-25.5 1.203-2.177 1.5-5L88.5 10q2.823-.297 5-1.5" style="opacity:.976"/><path fill="#346499" d="M119.5 39.5q.25 25.005-.5 50-5.432 30.368-36.5 30a32.4 32.4 0 0 1 .5-8l35-35q.254-18.76 1.5-37" style="opacity:.918"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" fill-rule="evenodd" clip-rule="evenodd" image-rendering="optimizeQuality" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" viewBox="0 0 14222 14222" class="svg gitea-facebook" width="16" height="16" aria-hidden="true"><g fill-rule="nonzero"><path fill="#1977f3" d="M14222 7111C14222 3184 11038 0 7111 0S0 3184 0 7111c0 3549 2600 6491 6000 7025V9167H4194V7111h1806V5544c0-1782 1062-2767 2686-2767 778 0 1592 139 1592 139v1750h-897c-883 0-1159 548-1159 1111v1334h1972l-315 2056H8222v4969c3400-533 6000-3475 6000-7025"/><path fill="#fefefe" d="m9879 9167 315-2056H8222V5777c0-562 275-1111 1159-1111h897V2916s-814-139-1592-139c-1624 0-2686 984-2686 2767v1567H4194v2056h1806v4969c362 57 733 86 1111 86s749-30 1111-86V9167z"/></g></svg>
|
Before Width: | Height: | Size: 941 B After Width: | Height: | Size: 815 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 70 70" class="svg gitea-jetbrains" width="16" height="16" aria-hidden="true"><linearGradient id="gitea-jetbrains__a" x1=".79" x2="33.317" y1="40.089" y2="40.089" gradientUnits="userSpaceOnUse"><stop offset=".258" style="stop-color:#f97a12"/><stop offset=".459" style="stop-color:#b07b58"/><stop offset=".724" style="stop-color:#577bae"/><stop offset=".91" style="stop-color:#1e7ce5"/><stop offset="1" style="stop-color:#087cfa"/></linearGradient><path d="M17.7 54.6.8 41.2l8.4-15.6L33.3 35z" style="fill:url(#gitea-jetbrains__a)"/><linearGradient id="gitea-jetbrains__b" x1="25.767" x2="79.424" y1="24.88" y2="54.57" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#f97a12"/><stop offset=".072" style="stop-color:#cb7a3e"/><stop offset=".154" style="stop-color:#9e7b6a"/><stop offset=".242" style="stop-color:#757b91"/><stop offset=".334" style="stop-color:#537bb1"/><stop offset=".432" style="stop-color:#387ccc"/><stop offset=".538" style="stop-color:#237ce0"/><stop offset=".655" style="stop-color:#147cef"/><stop offset=".792" style="stop-color:#0b7cf7"/><stop offset="1" style="stop-color:#087cfa"/></linearGradient><path d="m70 18.7-1.3 40.5L41.8 70 25.6 59.6 49.3 35 38.9 12.3l9.3-11.2z" style="fill:url(#gitea-jetbrains__b)"/><linearGradient id="gitea-jetbrains__c" x1="63.228" x2="48.29" y1="42.915" y2="-1.719" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#fe315d"/><stop offset=".078" style="stop-color:#cb417e"/><stop offset=".16" style="stop-color:#9e4e9b"/><stop offset=".247" style="stop-color:#755bb4"/><stop offset=".339" style="stop-color:#5365ca"/><stop offset=".436" style="stop-color:#386ddb"/><stop offset=".541" style="stop-color:#2374e9"/><stop offset=".658" style="stop-color:#1478f3"/><stop offset=".794" style="stop-color:#0b7bf8"/><stop offset="1" style="stop-color:#087cfa"/></linearGradient><path d="M70 18.7 48.7 43.9l-9.8-31.6 9.3-11.2z" style="fill:url(#gitea-jetbrains__c)"/><linearGradient id="gitea-jetbrains__d" x1="10.72" x2="55.524" y1="16.473" y2="90.58" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#fe315d"/><stop offset=".04" style="stop-color:#f63462"/><stop offset=".104" style="stop-color:#df3a71"/><stop offset=".167" style="stop-color:#c24383"/><stop offset=".291" style="stop-color:#ad4a91"/><stop offset=".55" style="stop-color:#755bb4"/><stop offset=".917" style="stop-color:#1d76ed"/><stop offset="1" style="stop-color:#087cfa"/></linearGradient><path d="M33.7 58.1 5.6 68.3l4.5-15.8L16 33.1 0 27.7 10.1 0l22 2.7 21.6 24.7z" style="fill:url(#gitea-jetbrains__d)"/><path d="M13.7 13.5h43.2v43.2H13.7z" style="fill:#000"/><path d="M17.7 48.6h16.2v2.7H17.7zM29.4 22.4v-3.3h-9v3.3H23v11.3h-2.6V37h9v-3.3h-2.5V22.4zM38 37.3c-1.4 0-2.6-.3-3.5-.8s-1.7-1.2-2.3-1.9l2.5-2.8c.5.6 1 1 1.5 1.3s1.1.5 1.7.5c.7 0 1.3-.2 1.8-.7.4-.5.6-1.2.6-2.3V19.1h4v11.7c0 1.1-.1 2-.4 2.8s-.7 1.4-1.3 2c-.5.5-1.2 1-2 1.2-.8.3-1.6.5-2.6.5" style="fill:#fff"/></svg>
|
After Width: | Height: | Size: 3.0 KiB |
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="gitea-microsoftonline__svg gitea-microsoftonline__gitea-microsoftonline svg gitea-microsoftonline" viewBox="0 0 2075 2499.8" width="16" height="16"><path fill="#eb3c00" d="M0 2016.6V496.8L1344.4 0 2075 233.7v2045.9l-730.6 220.3zl1344.4 161.8V409.2L467.6 613.8v1198.3z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 48 48" class="svg gitea-microsoftonline" width="16" height="16" aria-hidden="true"><path fill="url(#gitea-microsoftonline__a)" d="m20.084 3.026-.224.136a8 8 0 0 0-1.009.722l.648-.456H25L26 11l-5 5-5 3.475v4.008a8 8 0 0 0 3.857 6.844l5.264 3.186L14 40h-2.145l-3.998-2.42A8 8 0 0 1 4 30.737V17.26a8 8 0 0 1 3.86-6.846l12-7.258q.111-.068.224-.131Z"/><path fill="url(#gitea-microsoftonline__b)" d="m20.084 3.026-.224.136a8 8 0 0 0-1.009.722l.648-.456H25L26 11l-5 5-5 3.475v4.008a8 8 0 0 0 3.857 6.844l5.264 3.186L14 40h-2.145l-3.998-2.42A8 8 0 0 1 4 30.737V17.26a8 8 0 0 1 3.86-6.846l12-7.258q.111-.068.224-.131Z"/><path fill="url(#gitea-microsoftonline__c)" d="M32 19v4.48a8 8 0 0 1-3.857 6.844l-12 7.264a8 8 0 0 1-8.008.16l11.722 7.096a8 8 0 0 0 8.286 0l12-7.264A8 8 0 0 0 44 30.736V27.5L43 26z"/><path fill="url(#gitea-microsoftonline__d)" d="M32 19v4.48a8 8 0 0 1-3.857 6.844l-12 7.264a8 8 0 0 1-8.008.16l11.722 7.096a8 8 0 0 0 8.286 0l12-7.264A8 8 0 0 0 44 30.736V27.5L43 26z"/><path fill="url(#gitea-microsoftonline__e)" d="m40.14 10.415-12-7.258a8 8 0 0 0-8.042-.139l-.238.144A8 8 0 0 0 16 10.008v9.483l3.86-2.334a8 8 0 0 1 8.28 0l12 7.258A8 8 0 0 1 43.997 31q.004-.132.004-.263V17.26a8 8 0 0 0-3.86-6.845Z"/><path fill="url(#gitea-microsoftonline__f)" d="m40.14 10.415-12-7.258a8 8 0 0 0-8.042-.139l-.238.144A8 8 0 0 0 16 10.008v9.483l3.86-2.334a8 8 0 0 1 8.28 0l12 7.258A8 8 0 0 1 43.997 31q.004-.132.004-.263V17.26a8 8 0 0 0-3.86-6.845Z"/><path fill="url(#gitea-microsoftonline__g)" d="M4.004 30.998"/><path fill="url(#gitea-microsoftonline__h)" d="M4.004 30.998"/><defs><radialGradient id="gitea-microsoftonline__a" cx="0" cy="0" r="1" gradientTransform="rotate(110.528 5.021 11.358)scale(33.3657 58.1966)" gradientUnits="userSpaceOnUse"><stop offset=".064" stop-color="#AE7FE2"/><stop offset="1" stop-color="#0078D4"/></radialGradient><radialGradient id="gitea-microsoftonline__c" cx="0" cy="0" r="1" gradientTransform="matrix(30.7198 -4.51832 2.98465 20.29248 10.43 36.351)" gradientUnits="userSpaceOnUse"><stop offset=".134" stop-color="#D59DFF"/><stop offset="1" stop-color="#5E438F"/></radialGradient><radialGradient id="gitea-microsoftonline__e" cx="0" cy="0" r="1" gradientTransform="matrix(-24.1583 -6.12555 10.3118 -40.66824 41.055 26.504)" gradientUnits="userSpaceOnUse"><stop offset=".058" stop-color="#50E6FF"/><stop offset="1" stop-color="#436DCD"/></radialGradient><radialGradient id="gitea-microsoftonline__g" cx="0" cy="0" r="1" gradientTransform="matrix(-24.1583 -6.12555 10.3118 -40.66824 41.055 26.504)" gradientUnits="userSpaceOnUse"><stop offset=".058" stop-color="#50E6FF"/><stop offset="1" stop-color="#436DCD"/></radialGradient><linearGradient id="gitea-microsoftonline__b" x1="17.512" x2="12.751" y1="37.868" y2="29.635" gradientUnits="userSpaceOnUse"><stop stop-color="#114A8B"/><stop offset="1" stop-color="#0078D4" stop-opacity="0"/></linearGradient><linearGradient id="gitea-microsoftonline__d" x1="40.357" x2="35.255" y1="25.377" y2="32.692" gradientUnits="userSpaceOnUse"><stop stop-color="#493474"/><stop offset="1" stop-color="#8C66BA" stop-opacity="0"/></linearGradient><linearGradient id="gitea-microsoftonline__f" x1="16.976" x2="24.487" y1="3.057" y2="3.057" gradientUnits="userSpaceOnUse"><stop stop-color="#2D3F80"/><stop offset="1" stop-color="#436DCD" stop-opacity="0"/></linearGradient><linearGradient id="gitea-microsoftonline__h" x1="16.976" x2="24.487" y1="3.057" y2="3.057" gradientUnits="userSpaceOnUse"><stop stop-color="#2D3F80"/><stop offset="1" stop-color="#436DCD" stop-opacity="0"/></linearGradient></defs></svg>
|
Before Width: | Height: | Size: 342 B After Width: | Height: | Size: 3.6 KiB |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 70 70" class="svg gitea-open-with-jetbrains" width="16" height="16" aria-hidden="true"><linearGradient id="gitea-open-with-jetbrains__a" x1=".79" x2="33.317" y1="40.089" y2="40.089" gradientUnits="userSpaceOnUse"><stop offset=".258" style="stop-color:#f97a12"/><stop offset=".459" style="stop-color:#b07b58"/><stop offset=".724" style="stop-color:#577bae"/><stop offset=".91" style="stop-color:#1e7ce5"/><stop offset="1" style="stop-color:#087cfa"/></linearGradient><path d="M17.7 54.6.8 41.2l8.4-15.6L33.3 35z" style="fill:url(#gitea-open-with-jetbrains__a)"/><linearGradient id="gitea-open-with-jetbrains__b" x1="25.767" x2="79.424" y1="24.88" y2="54.57" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#f97a12"/><stop offset=".072" style="stop-color:#cb7a3e"/><stop offset=".154" style="stop-color:#9e7b6a"/><stop offset=".242" style="stop-color:#757b91"/><stop offset=".334" style="stop-color:#537bb1"/><stop offset=".432" style="stop-color:#387ccc"/><stop offset=".538" style="stop-color:#237ce0"/><stop offset=".655" style="stop-color:#147cef"/><stop offset=".792" style="stop-color:#0b7cf7"/><stop offset="1" style="stop-color:#087cfa"/></linearGradient><path d="m70 18.7-1.3 40.5L41.8 70 25.6 59.6 49.3 35 38.9 12.3l9.3-11.2z" style="fill:url(#gitea-open-with-jetbrains__b)"/><linearGradient id="gitea-open-with-jetbrains__c" x1="63.228" x2="48.29" y1="42.915" y2="-1.719" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#fe315d"/><stop offset=".078" style="stop-color:#cb417e"/><stop offset=".16" style="stop-color:#9e4e9b"/><stop offset=".247" style="stop-color:#755bb4"/><stop offset=".339" style="stop-color:#5365ca"/><stop offset=".436" style="stop-color:#386ddb"/><stop offset=".541" style="stop-color:#2374e9"/><stop offset=".658" style="stop-color:#1478f3"/><stop offset=".794" style="stop-color:#0b7bf8"/><stop offset="1" style="stop-color:#087cfa"/></linearGradient><path d="M70 18.7 48.7 43.9l-9.8-31.6 9.3-11.2z" style="fill:url(#gitea-open-with-jetbrains__c)"/><linearGradient id="gitea-open-with-jetbrains__d" x1="10.72" x2="55.524" y1="16.473" y2="90.58" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#fe315d"/><stop offset=".04" style="stop-color:#f63462"/><stop offset=".104" style="stop-color:#df3a71"/><stop offset=".167" style="stop-color:#c24383"/><stop offset=".291" style="stop-color:#ad4a91"/><stop offset=".55" style="stop-color:#755bb4"/><stop offset=".917" style="stop-color:#1d76ed"/><stop offset="1" style="stop-color:#087cfa"/></linearGradient><path d="M33.7 58.1 5.6 68.3l4.5-15.8L16 33.1 0 27.7 10.1 0l22 2.7 21.6 24.7z" style="fill:url(#gitea-open-with-jetbrains__d)"/><path d="M13.7 13.5h43.2v43.2H13.7z" style="fill:#000"/><path d="M17.7 48.6h16.2v2.7H17.7zM29.4 22.4v-3.3h-9v3.3H23v11.3h-2.6V37h9v-3.3h-2.5V22.4zM38 37.3c-1.4 0-2.6-.3-3.5-.8s-1.7-1.2-2.3-1.9l2.5-2.8c.5.6 1 1 1.5 1.3s1.1.5 1.7.5c.7 0 1.3-.2 1.8-.7.4-.5.6-1.2.6-2.3V19.1h4v11.7c0 1.1-.1 2-.4 2.8s-.7 1.4-1.3 2c-.5.5-1.2 1-2 1.2-.8.3-1.6.5-2.6.5" style="fill:#fff"/></svg>
|
Before Width: | Height: | Size: 3.0 KiB |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-1 -1 34 34" class="svg gitea-open-with-vscode" width="16" height="16" aria-hidden="true"><path d="M30.9 3.4 24.3.3a2 2 0 0 0-2.3.4L9.4 12.2 3.9 8c-.5-.4-1.2-.4-1.7 0L.4 9.8c-.5.5-.5 1.4 0 2L5.2 16 .4 20.3c-.5.6-.5 1.5 0 2L2.2 24c.5.5 1.2.5 1.7 0l5.5-4L22 31.2a2 2 0 0 0 2.3.4l6.6-3.2a2 2 0 0 0 1.1-1.8V5.2a2 2 0 0 0-1.1-1.8M24 23.3 14.4 16 24 8.7z"/></svg>
|
Before Width: | Height: | Size: 406 B |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" clip-rule="evenodd" viewBox="0 0 16 16" class="svg gitea-open-with-vscodium" width="16" height="16" aria-hidden="true"><path fill-rule="nonzero" d="m10.2.2.5-.3c.3 0 .5.2.7.4l.2.8-.2 1-.8 2.4c-.3 1-.4 2 0 2.9l.8-2c.2 0 .4.1.4.3l-.3 1L9.2 13l3.1-2.9c.3-.2.7-.5.8-1a2 2 0 0 0-.3-1c-.2-.5-.5-.9-.6-1.4l.1-.7c.1-.1.3-.2.5-.1.2 0 .3.2.4.4.3.5.4 1.2.5 1.8l.6-1.2c0-.2.2-.4.4-.6l.4-.2c.2 0 .4.3.4.4v.6l-.8 1.6-1.4 1.8 1-.4c.2 0 .6.2.7.5 0 .2 0 .4-.2.5-.3.2-.6.2-1 .2-1 0-2.2.6-2.9 1.4L9.6 15c-.4.4-.9 1-1.4.8-.8-.1-.8-1.3-1-1.8 0-.3-.2-.6-.4-.7-.3-.2-.5-.3-.8-.3-.6-.1-1.2 0-1.8-.2l-.8-.4-.4-.7c-.3-.6-.3-1.2-.5-1.8A4 4 0 0 0 1 8l-.4-.4v-.4c.2-.2.5-.2.7 0 .5.2.5.8 1 1.1V6.2s.3-.1.4 0l.2.5L3 9c.4-.4.6-1 .5-1.5L3.4 7l.3-.2c.2 0 .3.2.4.3v.7c0 .6-.3 1.1-.4 1.7-.2.4-.3 1-.1 1.4.1.5.5.9.9 1 .5.3 1.1.4 1.7.4-.4-.6-.7-1.2-.7-2 0-.7.4-1.3.6-2C6.3 7 5.7 5.8 4.8 5l-1.5-.7c-.4-.2-.7-.7-.7-1.2.3-.1.7 0 1 .1L5 4.5l.6.1c.2-.3 0-.6-.2-.8-.3-.5-1-.6-1.3-1a.9.9 0 0 1-.2-.8c0-.2.3-.4.5-.4.4 0 .7.3.9.5.8.8 1.2 1.8 1.4 3s0 2.5-.2 3.7c0 .3-.2.5-.1.8l.2.2c.2 0 .4 0 .5-.2.4-.3.8-.8.9-1.3l.1-1.2.1-.6.4-.2.3.3v.6c-.1.5-.2 1-.5 1.6a2 2 0 0 1-.6 1l-1 1c-.1.2-.2.6-.1.9 0 .2.2.4.4.5.4.2.8.2 1 0 .3-.1.5-.4.7-.6l.5-1.4.4-2.5C9.7 7 9.6 6 9 5.2c-.2-.4-.5-.7-1-1l-1-.8c-.2-.3-.4-.7-.3-1.2h.6c.4.1.7.4.9.8s.4.8.9 1l-1-2c-.1-.3-.3-.5-.2-.8 0-.2.2-.4.4-.4s.4.1.5.3l.2.5 1 3.1a4 4 0 0 0 .4-2.3L10 1V.2Z"/></svg>
|
Before Width: | Height: | Size: 1.5 KiB |
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" aria-hidden="true" class="gitea-twitter__svg gitea-twitter__gitea-twitter svg gitea-twitter" clip-rule="evenodd" viewBox="-89.009 -46.884 643.937 446.884" width="16" height="16"><path fill="#1da1f2" fill-rule="nonzero" d="M154.729 400c185.669 0 287.205-153.876 287.205-287.312 0-4.37-.089-8.72-.286-13.052A205.3 205.3 0 0 0 492 47.346c-18.087 8.044-37.55 13.458-57.968 15.899 20.841-12.501 36.84-32.278 44.389-55.852a202.4 202.4 0 0 1-64.098 24.511C395.903 12.276 369.679 0 340.641 0c-55.744 0-100.948 45.222-100.948 100.965 0 7.925.887 15.631 2.619 23.025-83.895-4.223-158.287-44.405-208.074-105.504A100.74 100.74 0 0 0 20.57 69.24c0 35.034 17.82 65.961 44.92 84.055a100.2 100.2 0 0 1-45.716-12.63c-.015.424-.015.837-.015 1.29 0 48.903 34.794 89.734 80.982 98.986a101 101 0 0 1-26.617 3.553c-6.493 0-12.821-.639-18.971-1.82 12.851 40.122 50.115 69.319 94.296 70.135-34.549 27.089-78.07 43.224-125.371 43.224A205 205 0 0 1 0 354.634c44.674 28.645 97.72 45.359 154.734 45.359"/></svg>
|
||||
<svg viewBox="0 0 24 24" class="svg gitea-twitter" xmlns="http://www.w3.org/2000/svg" width="16" height="16" aria-hidden="true"><path d="M14.095 10.316 22.286 1h-1.94L13.23 9.088 7.551 1H1l8.59 12.231L1 23h1.94l7.51-8.543 6 8.543H23zm-2.658 3.022-.872-1.218L3.64 2.432h2.98l5.59 7.821.869 1.219 7.265 10.166h-2.982z"/></svg>
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 324 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-1 -1 34 34" class="svg gitea-vscode" width="16" height="16" aria-hidden="true"><path d="M30.9 3.4 24.3.3a2 2 0 0 0-2.3.4L9.4 12.2 3.9 8c-.5-.4-1.2-.4-1.7 0L.4 9.8c-.5.5-.5 1.4 0 2L5.2 16 .4 20.3c-.5.6-.5 1.5 0 2L2.2 24c.5.5 1.2.5 1.7 0l5.5-4L22 31.2a2 2 0 0 0 2.3.4l6.6-3.2a2 2 0 0 0 1.1-1.8V5.2a2 2 0 0 0-1.1-1.8M24 23.3 14.4 16 24 8.7z"/></svg>
|
After Width: | Height: | Size: 396 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" clip-rule="evenodd" viewBox="0 0 16 16" class="svg gitea-vscodium" width="16" height="16" aria-hidden="true"><path fill-rule="nonzero" d="m10.2.2.5-.3c.3 0 .5.2.7.4l.2.8-.2 1-.8 2.4c-.3 1-.4 2 0 2.9l.8-2c.2 0 .4.1.4.3l-.3 1L9.2 13l3.1-2.9c.3-.2.7-.5.8-1a2 2 0 0 0-.3-1c-.2-.5-.5-.9-.6-1.4l.1-.7c.1-.1.3-.2.5-.1.2 0 .3.2.4.4.3.5.4 1.2.5 1.8l.6-1.2c0-.2.2-.4.4-.6l.4-.2c.2 0 .4.3.4.4v.6l-.8 1.6-1.4 1.8 1-.4c.2 0 .6.2.7.5 0 .2 0 .4-.2.5-.3.2-.6.2-1 .2-1 0-2.2.6-2.9 1.4L9.6 15c-.4.4-.9 1-1.4.8-.8-.1-.8-1.3-1-1.8 0-.3-.2-.6-.4-.7-.3-.2-.5-.3-.8-.3-.6-.1-1.2 0-1.8-.2l-.8-.4-.4-.7c-.3-.6-.3-1.2-.5-1.8A4 4 0 0 0 1 8l-.4-.4v-.4c.2-.2.5-.2.7 0 .5.2.5.8 1 1.1V6.2s.3-.1.4 0l.2.5L3 9c.4-.4.6-1 .5-1.5L3.4 7l.3-.2c.2 0 .3.2.4.3v.7c0 .6-.3 1.1-.4 1.7-.2.4-.3 1-.1 1.4.1.5.5.9.9 1 .5.3 1.1.4 1.7.4-.4-.6-.7-1.2-.7-2 0-.7.4-1.3.6-2C6.3 7 5.7 5.8 4.8 5l-1.5-.7c-.4-.2-.7-.7-.7-1.2.3-.1.7 0 1 .1L5 4.5l.6.1c.2-.3 0-.6-.2-.8-.3-.5-1-.6-1.3-1a.9.9 0 0 1-.2-.8c0-.2.3-.4.5-.4.4 0 .7.3.9.5.8.8 1.2 1.8 1.4 3s0 2.5-.2 3.7c0 .3-.2.5-.1.8l.2.2c.2 0 .4 0 .5-.2.4-.3.8-.8.9-1.3l.1-1.2.1-.6.4-.2.3.3v.6c-.1.5-.2 1-.5 1.6a2 2 0 0 1-.6 1l-1 1c-.1.2-.2.6-.1.9 0 .2.2.4.4.5.4.2.8.2 1 0 .3-.1.5-.4.7-.6l.5-1.4.4-2.5C9.7 7 9.6 6 9 5.2c-.2-.4-.5-.7-1-1l-1-.8c-.2-.3-.4-.7-.3-1.2h.6c.4.1.7.4.9.8s.4.8.9 1l-1-2c-.1-.3-.3-.5-.2-.8 0-.2.2-.4.4-.4s.4.1.5.3l.2.5 1 3.1a4 4 0 0 0 .4-2.3L10 1V.2Z"/></svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -5,11 +5,11 @@ description = ""
|
|||
authors = []
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.8"
|
||||
python = "^3.10"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
djlint = "1.34.1"
|
||||
yamllint = "1.35.0"
|
||||
yamllint = "1.35.1"
|
||||
|
||||
[tool.djlint]
|
||||
profile="golang"
|
||||
|
|
|
@ -133,7 +133,7 @@ func CreateUser(ctx *context.APIContext) {
|
|||
u.UpdatedUnix = u.CreatedUnix
|
||||
}
|
||||
|
||||
if err := user_model.CreateUser(ctx, u, overwriteDefault); err != nil {
|
||||
if err := user_model.AdminCreateUser(ctx, u, overwriteDefault); err != nil {
|
||||
if user_model.IsErrUserAlreadyExist(err) ||
|
||||
user_model.IsErrEmailAlreadyUsed(err) ||
|
||||
db.IsErrNameReserved(err) ||
|
||||
|
@ -209,7 +209,7 @@ func EditUser(ctx *context.APIContext) {
|
|||
}
|
||||
|
||||
if form.Email != nil {
|
||||
if err := user_service.AddOrSetPrimaryEmailAddress(ctx, ctx.ContextUser, *form.Email); err != nil {
|
||||
if err := user_service.AdminAddOrSetPrimaryEmailAddress(ctx, ctx.ContextUser, *form.Email); err != nil {
|
||||
switch {
|
||||
case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err):
|
||||
ctx.Error(http.StatusBadRequest, "EmailInvalid", err)
|
||||
|
|