Merge branch 'go-gitea:main' into main

This commit is contained in:
József Fényes 2024-05-28 12:14:13 +02:00 committed by GitHub
commit 56410b177c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
155 changed files with 1770 additions and 1335 deletions

View File

@ -3,7 +3,7 @@
<!--
1. Please speak English, this is the language all maintainers can speak and write.
2. Please ask questions or configuration/deploy problems on our Discord
server (https://discord.gg/gitea) or forum (https://discourse.gitea.io).
server (https://discord.gg/gitea) or forum (https://forum.gitea.com).
3. Please take a moment to check that your issue doesn't already exist.
4. Make sure it's not mentioned in the FAQ (https://docs.gitea.com/help/faq)
5. Please give all relevant information below for bug reports, because
@ -21,7 +21,7 @@
- [ ] MySQL
- [ ] MSSQL
- [ ] SQLite
- Can you reproduce the bug at https://try.gitea.io:
- Can you reproduce the bug at https://demo.gitea.com:
- [ ] Yes (provide example URL)
- [ ] No
- Log gist:

View File

@ -37,7 +37,7 @@ body:
label: Can you reproduce the bug on the Gitea demo site?
description: |
If so, please provide a URL in the Description field
URL of Gitea demo: https://try.gitea.io
URL of Gitea demo: https://demo.gitea.com
options:
- "Yes"
- "No"
@ -74,7 +74,7 @@ body:
attributes:
label: How are you running Gitea?
description: |
Please include information on whether you built Gitea yourself, used one of our downloads, are using https://try.gitea.io or are using some other package
Please include information on whether you built Gitea yourself, used one of our downloads, are using https://demo.gitea.com or are using some other package
Please also tell us how you are running Gitea, e.g. if it is being run from docker, a command-line, systemd etc.
If you are using a package or systemd tell us what distribution you are using
validations:

View File

@ -46,7 +46,7 @@ body:
label: Can you reproduce the bug on the Gitea demo site?
description: |
If so, please provide a URL in the Description field
URL of Gitea demo: https://try.gitea.io
URL of Gitea demo: https://demo.gitea.com
options:
- "Yes"
- "No"

View File

@ -77,7 +77,7 @@ If your issue has not been reported yet, [open an issue](https://github.com/go-g
and answer the questions so we can understand and reproduce the problematic behavior. \
Please write clear and concise instructions so that we can reproduce the behavior — even if it seems obvious. \
The more detailed and specific you are, the faster we can fix the issue. \
It is really helpful if you can reproduce your problem on a site running on the latest commits, i.e. <https://try.gitea.io>, as perhaps your problem has already been fixed on a current version. \
It is really helpful if you can reproduce your problem on a site running on the latest commits, i.e. <https://demo.gitea.com>, as perhaps your problem has already been fixed on a current version. \
Please follow the guidelines described in [How to Report Bugs Effectively](http://www.chiark.greenend.org.uk/~sgtatham/bugs.html) for your report.
Please be kind, remember that Gitea comes at no cost to you, and you're getting free help.
@ -362,7 +362,7 @@ If you add a new feature or change an existing aspect of Gitea, the documentatio
## API v1
The API is documented by [swagger](http://try.gitea.io/api/swagger) and is based on [the GitHub API](https://docs.github.com/en/rest).
The API is documented by [swagger](https://gitea.com/api/swagger) and is based on [the GitHub API](https://docs.github.com/en/rest).
### GitHub API compatibility

View File

@ -26,7 +26,7 @@ This project has been
[forked](https://blog.gitea.com/welcome-to-gitea/) from
[Gogs](https://gogs.io) since November of 2016, but a lot has changed.
For online demonstrations, you can visit [try.gitea.io](https://try.gitea.io).
For online demonstrations, you can visit [demo.gitea.com](https://demo.gitea.com).
For accessing free Gitea service (with a limited number of repositories), you can visit [gitea.com](https://gitea.com/user/login).
@ -56,7 +56,7 @@ More info: https://docs.gitea.com/installation/install-from-source
./gitea web
> [!NOTE]
> If you're interested in using our APIs, we have experimental support with [documentation](https://try.gitea.io/api/swagger).
> If you're interested in using our APIs, we have experimental support with [documentation](https://docs.gitea.com/api).
## Contributing
@ -80,7 +80,7 @@ https://docs.gitea.com/contributing/localization
## Further information
For more information and instructions about how to install Gitea, please look at our [documentation](https://docs.gitea.com/).
If you have questions that are not covered by the documentation, you can get in contact with us on our [Discord server](https://discord.gg/Gitea) or create a post in the [discourse forum](https://discourse.gitea.io/).
If you have questions that are not covered by the documentation, you can get in contact with us on our [Discord server](https://discord.gg/Gitea) or create a post in the [discourse forum](https://forum.gitea.com/).
We maintain a list of Gitea-related projects at [gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea).

View File

@ -18,7 +18,7 @@
Gitea 的首要目标是创建一个极易安装,运行非常快速,安装和使用体验良好的自建 Git 服务。我们采用 Go 作为后端语言,这使我们只要生成一个可执行程序即可。并且他还支持跨平台,支持 Linux, macOS 和 Windows 以及各种架构,除了 x86amd64还包括 ARM 和 PowerPC。
如果你想试用在线演示,请访问 [try.gitea.io](https://try.gitea.io/)。
如果你想试用在线演示和报告问题,请访问 [demo.gitea.com](https://demo.gitea.com/)。
如果你想使用免费的 Gitea 服务(有仓库数量限制),请访问 [gitea.com](https://gitea.com/user/login)。

View File

@ -1885,7 +1885,10 @@ LEVEL = Info
;; Minio endpoint to connect only available when STORAGE_TYPE is `minio`
;MINIO_ENDPOINT = localhost:9000
;;
;; Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`
;; Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`.
;; If not provided and STORAGE_TYPE is `minio`, will search for credentials in known
;; environment variables (MINIO_ACCESS_KEY_ID, AWS_ACCESS_KEY_ID), credentials files
;; (~/.mc/config.json, ~/.aws/credentials), and EC2 instance metadata.
;MINIO_ACCESS_KEY_ID =
;;
;; Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio`
@ -2586,7 +2589,10 @@ LEVEL = Info
;; Minio endpoint to connect only available when STORAGE_TYPE is `minio`
;MINIO_ENDPOINT = localhost:9000
;;
;; Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`
;; Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`.
;; If not provided and STORAGE_TYPE is `minio`, will search for credentials in known
;; environment variables (MINIO_ACCESS_KEY_ID, AWS_ACCESS_KEY_ID), credentials files
;; (~/.mc/config.json, ~/.aws/credentials), and EC2 instance metadata.
;MINIO_ACCESS_KEY_ID =
;;
;; Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio`

View File

@ -1,8 +1,5 @@
# Gitea: Docs
[![Join the chat at https://img.shields.io/discord/322538954119184384.svg](https://img.shields.io/discord/322538954119184384.svg)](https://discord.gg/Gitea)
[![](https://images.microbadger.com/badges/image/gitea/docs.svg)](http://microbadger.com/images/gitea/docs "Get your own image badge on microbadger.com")
These docs are ingested by our [docs repo](https://gitea.com/gitea/gitea-docusaurus).
## Authors
@ -18,5 +15,5 @@ for the full license text.
## Copyright
```
Copyright (c) 2016 The Gitea Authors <https://gitea.io>
Copyright (c) 2016 The Gitea Authors
```

View File

@ -1,9 +1,5 @@
# Gitea: 文档
[![Build Status](http://drone.gitea.io/api/badges/go-gitea/docs/status.svg)](http://drone.gitea.io/go-gitea/docs)
[![Join the chat at https://img.shields.io/discord/322538954119184384.svg](https://img.shields.io/discord/322538954119184384.svg)](https://discord.gg/Gitea)
[![](https://images.microbadger.com/badges/image/gitea/docs.svg)](http://microbadger.com/images/gitea/docs "Get your own image badge on microbadger.com")
https://gitea.com/gitea/gitea-docusaurus
## 关于我们
@ -18,5 +14,5 @@ https://gitea.com/gitea/gitea-docusaurus
## 版权声明
```
Copyright (c) 2016 The Gitea Authors <https://gitea.io>
Copyright (c) 2016 The Gitea Authors
```

View File

@ -828,7 +828,7 @@ and
## Project (`project`)
Default templates for project boards:
Default templates for project board view:
- `PROJECT_BOARD_BASIC_KANBAN_TYPE`: **To Do, In Progress, Done**
- `PROJECT_BOARD_BUG_TRIAGE_TYPE`: **Needs Triage, High Priority, Low Priority, Closed**
@ -843,7 +843,7 @@ Default templates for project boards:
- `SERVE_DIRECT`: **false**: Allows the storage driver to redirect to authenticated URLs to serve files directly. Currently, only Minio/S3 is supported via signed URLs, local does nothing.
- `PATH`: **attachments**: Path to store attachments only available when STORAGE_TYPE is `local`, relative paths will be resolved to `${AppDataPath}/${attachment.PATH}`.
- `MINIO_ENDPOINT`: **localhost:9000**: Minio endpoint to connect only available when STORAGE_TYPE is `minio`
- `MINIO_ACCESS_KEY_ID`: Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`
- `MINIO_ACCESS_KEY_ID`: Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`. If not provided and STORAGE_TYPE is `minio`, will search for credentials in known environment variables (MINIO_ACCESS_KEY_ID, AWS_ACCESS_KEY_ID), credentials files (~/.mc/config.json, ~/.aws/credentials), and EC2 instance metadata.
- `MINIO_SECRET_ACCESS_KEY`: Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio`
- `MINIO_BUCKET`: **gitea**: Minio bucket to store the attachments only available when STORAGE_TYPE is `minio`
- `MINIO_LOCATION`: **us-east-1**: Minio location to create bucket only available when STORAGE_TYPE is `minio`
@ -1280,7 +1280,7 @@ is `data/lfs` and the default of `MINIO_BASE_PATH` is `lfs/`.
- `SERVE_DIRECT`: **false**: Allows the storage driver to redirect to authenticated URLs to serve files directly. Currently, only Minio/S3 is supported via signed URLs, local does nothing.
- `PATH`: **./data/lfs**: Where to store LFS files, only available when `STORAGE_TYPE` is `local`. If not set it fall back to deprecated LFS_CONTENT_PATH value in [server] section.
- `MINIO_ENDPOINT`: **localhost:9000**: Minio endpoint to connect only available when `STORAGE_TYPE` is `minio`
- `MINIO_ACCESS_KEY_ID`: Minio accessKeyID to connect only available when `STORAGE_TYPE` is `minio`
- `MINIO_ACCESS_KEY_ID`: Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`. If not provided and STORAGE_TYPE is `minio`, will search for credentials in known environment variables (MINIO_ACCESS_KEY_ID, AWS_ACCESS_KEY_ID), credentials files (~/.mc/config.json, ~/.aws/credentials), and EC2 instance metadata.
- `MINIO_SECRET_ACCESS_KEY`: Minio secretAccessKey to connect only available when `STORAGE_TYPE is` `minio`
- `MINIO_BUCKET`: **gitea**: Minio bucket to store the lfs only available when `STORAGE_TYPE` is `minio`
- `MINIO_LOCATION`: **us-east-1**: Minio location to create bucket only available when `STORAGE_TYPE` is `minio`
@ -1296,7 +1296,7 @@ Default storage configuration for attachments, lfs, avatars, repo-avatars, repo-
- `STORAGE_TYPE`: **local**: Storage type, `local` for local disk or `minio` for s3 compatible object storage service.
- `SERVE_DIRECT`: **false**: Allows the storage driver to redirect to authenticated URLs to serve files directly. Currently, only Minio/S3 is supported via signed URLs, local does nothing.
- `MINIO_ENDPOINT`: **localhost:9000**: Minio endpoint to connect only available when `STORAGE_TYPE` is `minio`
- `MINIO_ACCESS_KEY_ID`: Minio accessKeyID to connect only available when `STORAGE_TYPE` is `minio`
- `MINIO_ACCESS_KEY_ID`: Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`. If not provided and STORAGE_TYPE is `minio`, will search for credentials in known environment variables (MINIO_ACCESS_KEY_ID, AWS_ACCESS_KEY_ID), credentials files (~/.mc/config.json, ~/.aws/credentials), and EC2 instance metadata.
- `MINIO_SECRET_ACCESS_KEY`: Minio secretAccessKey to connect only available when `STORAGE_TYPE is` `minio`
- `MINIO_BUCKET`: **gitea**: Minio bucket to store the data only available when `STORAGE_TYPE` is `minio`
- `MINIO_LOCATION`: **us-east-1**: Minio location to create bucket only available when `STORAGE_TYPE` is `minio`
@ -1311,7 +1311,10 @@ The recommended storage configuration for minio like below:
STORAGE_TYPE = minio
; Minio endpoint to connect only available when STORAGE_TYPE is `minio`
MINIO_ENDPOINT = localhost:9000
; Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`
; Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`.
; If not provided and STORAGE_TYPE is `minio`, will search for credentials in known
; environment variables (MINIO_ACCESS_KEY_ID, AWS_ACCESS_KEY_ID), credentials files
; (~/.mc/config.json, ~/.aws/credentials), and EC2 instance metadata.
MINIO_ACCESS_KEY_ID =
; Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio`
MINIO_SECRET_ACCESS_KEY =
@ -1360,7 +1363,10 @@ STORAGE_TYPE = my_minio
STORAGE_TYPE = minio
; Minio endpoint to connect only available when STORAGE_TYPE is `minio`
MINIO_ENDPOINT = localhost:9000
; Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`
; Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`.
; If not provided and STORAGE_TYPE is `minio`, will search for credentials in known
; environment variables (MINIO_ACCESS_KEY_ID, AWS_ACCESS_KEY_ID), credentials files
; (~/.mc/config.json, ~/.aws/credentials), and EC2 instance metadata.
MINIO_ACCESS_KEY_ID =
; Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio`
MINIO_SECRET_ACCESS_KEY =
@ -1386,7 +1392,7 @@ is `data/repo-archive` and the default of `MINIO_BASE_PATH` is `repo-archive/`.
- `SERVE_DIRECT`: **false**: Allows the storage driver to redirect to authenticated URLs to serve files directly. Currently, only Minio/S3 is supported via signed URLs, local does nothing.
- `PATH`: **./data/repo-archive**: Where to store archive files, only available when `STORAGE_TYPE` is `local`.
- `MINIO_ENDPOINT`: **localhost:9000**: Minio endpoint to connect only available when `STORAGE_TYPE` is `minio`
- `MINIO_ACCESS_KEY_ID`: Minio accessKeyID to connect only available when `STORAGE_TYPE` is `minio`
- `MINIO_ACCESS_KEY_ID`: Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`. If not provided and STORAGE_TYPE is `minio`, will search for credentials in known environment variables (MINIO_ACCESS_KEY_ID, AWS_ACCESS_KEY_ID), credentials files (~/.mc/config.json, ~/.aws/credentials), and EC2 instance metadata.
- `MINIO_SECRET_ACCESS_KEY`: Minio secretAccessKey to connect only available when `STORAGE_TYPE is` `minio`
- `MINIO_BUCKET`: **gitea**: Minio bucket to store the lfs only available when `STORAGE_TYPE` is `minio`
- `MINIO_LOCATION`: **us-east-1**: Minio location to create bucket only available when `STORAGE_TYPE` is `minio`

View File

@ -38,12 +38,10 @@ FROM gitea/gitea:@version@
COPY custom/app.ini /data/gitea/conf/app.ini
[...]
RUN apk --no-cache add asciidoctor freetype freetype-dev gcc g++ libpng libffi-dev py-pip python3-dev py3-pip py3-pyzmq
RUN apk --no-cache add asciidoctor freetype freetype-dev gcc g++ libpng libffi-dev pandoc python3-dev py3-pyzmq pipx
# install any other package you need for your external renderers
RUN pip3 install --upgrade pip
RUN pip3 install -U setuptools
RUN pip3 install jupyter docutils
RUN pipx install jupyter docutils --include-deps
# add above any other python package you may need to install
```

View File

@ -37,12 +37,10 @@ FROM gitea/gitea:@version@
COPY custom/app.ini /data/gitea/conf/app.ini
[...]
RUN apk --no-cache add asciidoctor freetype freetype-dev gcc g++ libpng libffi-dev py-pip python3-dev py3-pip py3-pyzmq
RUN apk --no-cache add asciidoctor freetype freetype-dev gcc g++ libpng libffi-dev pandoc python3-dev py3-pyzmq pipx
# 安装其他您需要的外部渲染器的软件包
RUN pip3 install --upgrade pip
RUN pip3 install -U setuptools
RUN pip3 install jupyter docutils
RUN pipx install jupyter docutils --include-deps
# 在上面添加您需要安装的任何其他 Python 软件包
```

View File

@ -117,7 +117,7 @@ curl -v "http://localhost/api/v1/repos/search?limit=1"
API Reference guide is auto-generated by swagger and available on:
`https://gitea.your.host/api/swagger`
or on the
[Gitea demo instance](https://try.gitea.io/api/swagger)
[Gitea instance](https://gitea.com/api/swagger)
The OpenAPI document is at:
`https://gitea.your.host/swagger.v1.json`

View File

@ -45,7 +45,7 @@ To migrate from GitHub to Gitea, you can use Gitea's built-in migration form.
In order to migrate items such as issues, pull requests, etc. you will need to input at least your username.
[Example (requires login)](https://try.gitea.io/repo/migrate)
[Example (requires login)](https://demo.gitea.com/repo/migrate)
To migrate from GitLab to Gitea, you can use this non-affiliated tool:
@ -137,9 +137,9 @@ All Gitea instances have the built-in API and there is no way to disable it comp
You can, however, disable showing its documentation by setting `ENABLE_SWAGGER` to `false` in the `api` section of your `app.ini`.
For more information, refer to Gitea's [API docs](development/api-usage.md).
You can see the latest API (for example) on https://try.gitea.io/api/swagger
You can see the latest API (for example) on https://gitea.com/api/swagger
You can also see an example of the `swagger.json` file at https://try.gitea.io/swagger.v1.json
You can also see an example of the `swagger.json` file at https://gitea.com/swagger.v1.json
## Adjusting your server for public/private use

View File

@ -47,7 +47,7 @@ menu:
为了迁移诸如问题、拉取请求等项目,您需要至少输入您的用户名。
[Example (requires login)](https://try.gitea.io/repo/migrate)
[Example (requires login)](https://demo.gitea.com/repo/migrate)
要从GitLab迁移到Gitea您可以使用这个非关联的工具
@ -141,9 +141,9 @@ Gitea不提供内置的Pages服务器。您需要一个专用的域名来提供
但是您可以在app.ini的api部分将ENABLE_SWAGGER设置为false以禁用其文档显示。
有关更多信息请参阅Gitea的[API文档](development/api-usage.md)。
您可以在上查看最新的API例如https://try.gitea.io/api/swagger
您可以在上查看最新的API例如https://gitea.com/api/swagger
您还可以在上查看`swagger.json`文件的示例 https://try.gitea.io/swagger.v1.json
您还可以在上查看`swagger.json`文件的示例 https://gitea.com/swagger.v1.json
## 调整服务器用于公共/私有使用

View File

@ -19,11 +19,11 @@ menu:
- [Paid Commercial Support](https://about.gitea.com/)
- [Discord](https://discord.gg/Gitea)
- [Discourse Forum](https://discourse.gitea.io/)
- [Forum](https://forum.gitea.com/)
- [Matrix](https://matrix.to/#/#gitea-space:matrix.org)
- NOTE: Most of the Matrix channels are bridged with their counterpart in Discord and may experience some degree of flakiness with the bridge process.
- Chinese Support
- [Discourse Chinese Category](https://discourse.gitea.io/c/5-category/5)
- [Discourse Chinese Category](https://forum.gitea.com/c/5-category/5)
- QQ Group 328432459
# Bug Report
@ -39,7 +39,7 @@ If you found a bug, please [create an issue on GitHub](https://github.com/go-git
- When using systemd, use `journalctl --lines 1000 --unit gitea` to collect logs.
- When using docker, use `docker logs --tail 1000 <gitea-container>` to collect logs.
4. Reproducible steps so that others could reproduce and understand the problem more quickly and easily.
- [try.gitea.io](https://try.gitea.io) could be used to reproduce the problem.
- [demo.gitea.com](https://demo.gitea.com) could be used to reproduce the problem.
5. If you encounter slow/hanging/deadlock problems, please report the stacktrace when the problem occurs.
Go to the "Site Admin" -> "Monitoring" -> "Stacktrace" -> "Download diagnosis report".

View File

@ -19,11 +19,11 @@ menu:
- [付费商业支持](https://about.gitea.com/)
- [Discord](https://discord.gg/Gitea)
- [Discourse 论坛](https://discourse.gitea.io/)
- [论坛](https://forum.gitea.com/)
- [Matrix](https://matrix.to/#/#gitea-space:matrix.org)
- 注意:大多数 Matrix 频道都与 Discord 中的对应频道桥接,可能在桥接过程中会出现一定程度的不稳定性。
- 中文支持
- [Discourse 中文分类](https://discourse.gitea.io/c/5-category/5)
- [Discourse 中文分类](https://forum.gitea.com/c/5-category/5)
- QQ 群 328432459
# Bug 报告
@ -39,7 +39,7 @@ menu:
- 在使用 systemd 时,使用 `journalctl --lines 1000 --unit gitea` 收集日志。
- 在使用 Docker 时,使用 `docker logs --tail 1000 <gitea-container>` 收集日志。
4. 可重现的步骤,以便他人能够更快速、更容易地重现和理解问题。
- [try.gitea.io](https://try.gitea.io) 可用于重现问题。
- [demo.gitea.com](https://demo.gitea.com) 可用于重现问题。
5. 如果遇到慢速/挂起/死锁等问题,请在出现问题时报告堆栈跟踪。
转到 "Site Admin" -> "Monitoring" -> "Stacktrace" -> "Download diagnosis report"。

View File

@ -21,7 +21,7 @@ up a self-hosted Git service.
With Go, this can be done platform-independently across
**all platforms** which Go supports, including Linux, macOS, and Windows,
on x86, amd64, ARM and PowerPC architectures.
You can try it out using [the online demo](https://try.gitea.io/).
You can try it out using [the online demo](https://demo.gitea.com).
## Features
@ -37,7 +37,7 @@ You can try it out using [the online demo](https://try.gitea.io/).
- CI/CD: Gitea Actions supports CI/CD functionality, compatible with GitHub Actions. Users can write workflows in familiar YAML format and reuse a variety of existing Actions plugins. Actions plugins support downloading from any Git website.
- Project Management: Gitea tracks project requirements, features, and bugs through boards and issues. Issues support features like branches, tags, milestones, assignments, time tracking, due dates, dependencies, and more.
- Project Management: Gitea tracks project requirements, features, and bugs through columns and issues. Issues support features like branches, tags, milestones, assignments, time tracking, due dates, dependencies, and more.
- Artifact Repository: Gitea supports over 20 different types of public or private software package management, including Cargo, Chef, Composer, Conan, Conda, Container, Helm, Maven, npm, NuGet, Pub, PyPI, RubyGems, Vagrant, and more.

View File

@ -104,7 +104,7 @@ _Symbols used in table:_
| Comment reactions | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | ✘ |
| Lock Discussion | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | ✘ |
| Batch issue handling | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | ✘ |
| Issue Boards (Kanban) | [/](https://github.com/go-gitea/gitea/issues/14710) | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | ✘ |
| Projects | [/](https://github.com/go-gitea/gitea/issues/14710) | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | ✘ |
| Create branch from issue | [](https://github.com/go-gitea/gitea/issues/20226) | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | ✘ |
| Convert comment to new issue | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | ✘ |
| Issue search | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | ✘ |

View File

@ -5,11 +5,9 @@ slug: "badge"
sidebar_position: 11
toc: false
draft: false
aliases:
- /en-us/badge
menu:
sidebar:
parent: "usage"
parent: "actions"
name: "Badge"
sidebar_position: 11
identifier: "Badge"

View File

@ -5,11 +5,9 @@ slug: "secrets"
sidebar_position: 50
draft: false
toc: false
aliases:
- /en-us/secrets
menu:
sidebar:
parent: "usage"
parent: "actions"
name: "Secrets"
sidebar_position: 50
identifier: "usage-secrets"

View File

@ -5,11 +5,9 @@ slug: "secrets"
sidebar_position: 50
draft: false
toc: false
aliases:
- /zh-cn/secrets
menu:
sidebar:
parent: "usage"
parent: "actions"
name: "密钥管理"
sidebar_position: 50
identifier: "usage-secrets"

View File

@ -236,7 +236,7 @@ configure this, set the fields below:
- Restrict what domains can log in if using a public SMTP host or SMTP host
with multiple domains.
- Example: `gitea.io,mydomain.com,mydomain2.com`
- Example: `gitea.com,mydomain.com,mydomain2.com`
- Force SMTPS

View File

@ -194,7 +194,7 @@ PAM提供了一种机制通过对用户进行PAM认证来自动将其添加
- 如果使用公共 SMTP 主机或有多个域的 SMTP 主机,限制哪些域可以登录
限制哪些域可以登录。
- 示例: `gitea.io,mydomain.com,mydomain2.com`
- 示例: `gitea.com,mydomain.com,mydomain2.com`
- 强制使用 SMTPS
- 默认情况下将使用SMTPS连接到端口465.如果您希望将smtp用于其他端口自行设置

View File

@ -308,7 +308,7 @@ This is a example for a issue config file
blank_issues_enabled: true
contact_links:
- name: Gitea
url: https://gitea.io
url: https://gitea.com
about: Visit the Gitea Website
```

View File

@ -48,7 +48,7 @@ With different permissions, people could do different things with these units.
| Wiki | View wiki pages. Clone the wiki repository. | Create/Edit wiki pages, push | - |
| ExternalWiki | Link to an external wiki | - | - |
| ExternalTracker | Link to an external issue tracker | - | - |
| Projects | View the boards | Change issues across boards | - |
| Projects | View the columns of projects | Change issues across columns | - |
| Packages | View the packages | Upload/Delete packages | - |
| Actions | View the Actions logs | Approve / Cancel / Restart | - |
| Settings | - | - | Manage the repository |

View File

@ -30,7 +30,7 @@ type Statistic struct {
Mirror, Release, AuthSource, Webhook,
Milestone, Label, HookTask,
Team, UpdateTask, Project,
ProjectBoard, Attachment,
ProjectColumn, Attachment,
Branches, Tags, CommitStatus int64
IssueByLabel []IssueByLabelCount
IssueByRepository []IssueByRepositoryCount
@ -115,6 +115,6 @@ func GetStatistic(ctx context.Context) (stats Statistic) {
stats.Counter.Team, _ = e.Count(new(organization.Team))
stats.Counter.Attachment, _ = e.Count(new(repo_model.Attachment))
stats.Counter.Project, _ = e.Count(new(project_model.Project))
stats.Counter.ProjectBoard, _ = e.Count(new(project_model.Board))
stats.Counter.ProjectColumn, _ = e.Count(new(project_model.Column))
return stats
}

View File

@ -52,6 +52,8 @@ func (err ErrCommentNotExist) Unwrap() error {
return util.ErrNotExist
}
var ErrCommentAlreadyChanged = util.NewInvalidArgumentErrorf("the comment is already changed")
// CommentType defines whether a comment is just a simple comment, an action (like close) or a reference.
type CommentType int
@ -100,8 +102,8 @@ const (
CommentTypeMergePull // 28 merge pull request
CommentTypePullRequestPush // 29 push to PR head branch
CommentTypeProject // 30 Project changed
CommentTypeProjectBoard // 31 Project board changed
CommentTypeProject // 30 Project changed
CommentTypeProjectColumn // 31 Project column changed
CommentTypeDismissReview // 32 Dismiss Review
@ -146,7 +148,7 @@ var commentStrings = []string{
"merge_pull",
"pull_push",
"project",
"project_board",
"project_board", // FIXME: the name should be project_column
"dismiss_review",
"change_issue_ref",
"pull_scheduled_merge",
@ -262,6 +264,7 @@ type Comment struct {
Line int64 // - previous line / + proposed line
TreePath string
Content string `xorm:"LONGTEXT"`
ContentVersion int `xorm:"NOT NULL DEFAULT 0"`
RenderedContent template.HTML `xorm:"-"`
// Path represents the 4 lines of code cemented by this comment
@ -1111,7 +1114,7 @@ func UpdateCommentInvalidate(ctx context.Context, c *Comment) error {
}
// UpdateComment updates information of comment.
func UpdateComment(ctx context.Context, c *Comment, doer *user_model.User) error {
func UpdateComment(ctx context.Context, c *Comment, contentVersion int, doer *user_model.User) error {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return err
@ -1119,9 +1122,15 @@ func UpdateComment(ctx context.Context, c *Comment, doer *user_model.User) error
defer committer.Close()
sess := db.GetEngine(ctx)
if _, err := sess.ID(c.ID).AllCols().Update(c); err != nil {
c.ContentVersion = contentVersion + 1
affected, err := sess.ID(c.ID).AllCols().Where("content_version = ?", contentVersion).Update(c)
if err != nil {
return err
}
if affected == 0 {
return ErrCommentAlreadyChanged
}
if err := c.LoadIssue(ctx); err != nil {
return err
}

View File

@ -94,6 +94,8 @@ func (err ErrIssueWasClosed) Error() string {
return fmt.Sprintf("Issue [%d] %d was already closed", err.ID, err.Index)
}
var ErrIssueAlreadyChanged = util.NewInvalidArgumentErrorf("the issue is already changed")
// Issue represents an issue or pull request of repository.
type Issue struct {
ID int64 `xorm:"pk autoincr"`
@ -107,6 +109,7 @@ type Issue struct {
Title string `xorm:"name"`
Content string `xorm:"LONGTEXT"`
RenderedContent template.HTML `xorm:"-"`
ContentVersion int `xorm:"NOT NULL DEFAULT 0"`
Labels []*Label `xorm:"-"`
MilestoneID int64 `xorm:"INDEX"`
Milestone *Milestone `xorm:"-"`

View File

@ -37,22 +37,22 @@ func (issue *Issue) projectID(ctx context.Context) int64 {
return ip.ProjectID
}
// ProjectBoardID return project board id if issue was assigned to one
func (issue *Issue) ProjectBoardID(ctx context.Context) int64 {
// ProjectColumnID return project column id if issue was assigned to one
func (issue *Issue) ProjectColumnID(ctx context.Context) int64 {
var ip project_model.ProjectIssue
has, err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).Get(&ip)
if err != nil || !has {
return 0
}
return ip.ProjectBoardID
return ip.ProjectColumnID
}
// LoadIssuesFromBoard load issues assigned to this board
func LoadIssuesFromBoard(ctx context.Context, b *project_model.Board) (IssueList, error) {
// LoadIssuesFromColumn load issues assigned to this column
func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column) (IssueList, error) {
issueList, err := Issues(ctx, &IssuesOptions{
ProjectBoardID: b.ID,
ProjectID: b.ProjectID,
SortType: "project-column-sorting",
ProjectColumnID: b.ID,
ProjectID: b.ProjectID,
SortType: "project-column-sorting",
})
if err != nil {
return nil, err
@ -60,9 +60,9 @@ func LoadIssuesFromBoard(ctx context.Context, b *project_model.Board) (IssueList
if b.Default {
issues, err := Issues(ctx, &IssuesOptions{
ProjectBoardID: db.NoConditionID,
ProjectID: b.ProjectID,
SortType: "project-column-sorting",
ProjectColumnID: db.NoConditionID,
ProjectID: b.ProjectID,
SortType: "project-column-sorting",
})
if err != nil {
return nil, err
@ -77,11 +77,11 @@ func LoadIssuesFromBoard(ctx context.Context, b *project_model.Board) (IssueList
return issueList, nil
}
// LoadIssuesFromBoardList load issues assigned to the boards
func LoadIssuesFromBoardList(ctx context.Context, bs project_model.BoardList) (map[int64]IssueList, error) {
// LoadIssuesFromColumnList load issues assigned to the columns
func LoadIssuesFromColumnList(ctx context.Context, bs project_model.ColumnList) (map[int64]IssueList, error) {
issuesMap := make(map[int64]IssueList, len(bs))
for i := range bs {
il, err := LoadIssuesFromBoard(ctx, bs[i])
il, err := LoadIssuesFromColumn(ctx, bs[i])
if err != nil {
return nil, err
}
@ -110,7 +110,7 @@ func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_mo
return util.NewPermissionDeniedErrorf("issue %d can't be accessed by project %d", issue.ID, newProject.ID)
}
if newColumnID == 0 {
newDefaultColumn, err := newProject.GetDefaultBoard(ctx)
newDefaultColumn, err := newProject.GetDefaultColumn(ctx)
if err != nil {
return err
}
@ -153,10 +153,10 @@ func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_mo
}
newSorting := util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0)
return db.Insert(ctx, &project_model.ProjectIssue{
IssueID: issue.ID,
ProjectID: newProjectID,
ProjectBoardID: newColumnID,
Sorting: newSorting,
IssueID: issue.ID,
ProjectID: newProjectID,
ProjectColumnID: newColumnID,
Sorting: newSorting,
})
})
}

View File

@ -33,7 +33,7 @@ type IssuesOptions struct { //nolint
SubscriberID int64
MilestoneIDs []int64
ProjectID int64
ProjectBoardID int64
ProjectColumnID int64
IsClosed optional.Option[bool]
IsPull optional.Option[bool]
LabelIDs []int64
@ -169,12 +169,12 @@ func applyProjectCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Sessio
return sess
}
func applyProjectBoardCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
// opts.ProjectBoardID == 0 means all project boards,
func applyProjectColumnCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
// opts.ProjectColumnID == 0 means all project columns,
// do not need to apply any condition
if opts.ProjectBoardID > 0 {
sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": opts.ProjectBoardID}))
} else if opts.ProjectBoardID == db.NoConditionID {
if opts.ProjectColumnID > 0 {
sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": opts.ProjectColumnID}))
} else if opts.ProjectColumnID == db.NoConditionID {
sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": 0}))
}
return sess
@ -246,7 +246,7 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
applyProjectCondition(sess, opts)
applyProjectBoardCondition(sess, opts)
applyProjectColumnCondition(sess, opts)
if opts.IsPull.Has() {
sess.And("issue.is_pull=?", opts.IsPull.Value())

View File

@ -235,7 +235,7 @@ func UpdateIssueAttachments(ctx context.Context, issueID int64, uuids []string)
}
// ChangeIssueContent changes issue content, as the given user.
func ChangeIssueContent(ctx context.Context, issue *Issue, doer *user_model.User, content string) (err error) {
func ChangeIssueContent(ctx context.Context, issue *Issue, doer *user_model.User, content string, contentVersion int) (err error) {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return err
@ -254,9 +254,14 @@ func ChangeIssueContent(ctx context.Context, issue *Issue, doer *user_model.User
}
issue.Content = content
issue.ContentVersion = contentVersion + 1
if err = UpdateIssueCols(ctx, issue, "content"); err != nil {
return fmt.Errorf("UpdateIssueCols: %w", err)
affected, err := db.GetEngine(ctx).ID(issue.ID).Cols("content", "content_version").Where("content_version = ?", contentVersion).Update(issue)
if err != nil {
return err
}
if affected == 0 {
return ErrIssueAlreadyChanged
}
if err = SaveIssueContentHistory(ctx, doer.ID, issue.ID, 0,

View File

@ -430,6 +430,21 @@ func (pr *PullRequest) GetGitHeadBranchRefName() string {
return fmt.Sprintf("%s%s", git.BranchPrefix, pr.HeadBranch)
}
// GetReviewCommentsCount returns the number of review comments made on the diff of a PR review (not including comments on commits or issues in a PR)
func (pr *PullRequest) GetReviewCommentsCount(ctx context.Context) int {
opts := FindCommentsOptions{
Type: CommentTypeReview,
IssueID: pr.IssueID,
}
conds := opts.ToConds()
count, err := db.GetEngine(ctx).Where(conds).Count(new(Comment))
if err != nil {
return 0
}
return int(count)
}
// IsChecking returns true if this pull request is still checking conflict.
func (pr *PullRequest) IsChecking() bool {
return pr.Status == PullRequestStatusChecking

View File

@ -21,6 +21,7 @@ import (
"code.gitea.io/gitea/models/migrations/v1_20"
"code.gitea.io/gitea/models/migrations/v1_21"
"code.gitea.io/gitea/models/migrations/v1_22"
"code.gitea.io/gitea/models/migrations/v1_23"
"code.gitea.io/gitea/models/migrations/v1_6"
"code.gitea.io/gitea/models/migrations/v1_7"
"code.gitea.io/gitea/models/migrations/v1_8"
@ -587,6 +588,9 @@ var migrations = []Migration{
NewMigration("Drop wrongly created table o_auth2_application", v1_22.DropWronglyCreatedTable),
// Gitea 1.22.0-rc1 ends at 299
// v299 -> v300
NewMigration("Add content version to issue and comment table", v1_23.AddContentVersionToIssueAndComment),
}
// GetCurrentDBVersion returns the current db version

View File

@ -15,7 +15,7 @@ import (
func Test_CheckProjectColumnsConsistency(t *testing.T) {
// Prepare and load the testing database
x, deferable := base.PrepareTestEnv(t, 0, new(project.Project), new(project.Board))
x, deferable := base.PrepareTestEnv(t, 0, new(project.Project), new(project.Column))
defer deferable()
if x == nil || t.Failed() {
return
@ -23,22 +23,22 @@ func Test_CheckProjectColumnsConsistency(t *testing.T) {
assert.NoError(t, CheckProjectColumnsConsistency(x))
// check if default board was added
var defaultBoard project.Board
has, err := x.Where("project_id=? AND `default` = ?", 1, true).Get(&defaultBoard)
// check if default column was added
var defaultColumn project.Column
has, err := x.Where("project_id=? AND `default` = ?", 1, true).Get(&defaultColumn)
assert.NoError(t, err)
assert.True(t, has)
assert.Equal(t, int64(1), defaultBoard.ProjectID)
assert.True(t, defaultBoard.Default)
assert.Equal(t, int64(1), defaultColumn.ProjectID)
assert.True(t, defaultColumn.Default)
// check if multiple defaults, previous were removed and last will be kept
expectDefaultBoard, err := project.GetBoard(db.DefaultContext, 2)
expectDefaultColumn, err := project.GetColumn(db.DefaultContext, 2)
assert.NoError(t, err)
assert.Equal(t, int64(2), expectDefaultBoard.ProjectID)
assert.False(t, expectDefaultBoard.Default)
assert.Equal(t, int64(2), expectDefaultColumn.ProjectID)
assert.False(t, expectDefaultColumn.Default)
expectNonDefaultBoard, err := project.GetBoard(db.DefaultContext, 3)
expectNonDefaultColumn, err := project.GetColumn(db.DefaultContext, 3)
assert.NoError(t, err)
assert.Equal(t, int64(2), expectNonDefaultBoard.ProjectID)
assert.True(t, expectNonDefaultBoard.Default)
assert.Equal(t, int64(2), expectNonDefaultColumn.ProjectID)
assert.True(t, expectNonDefaultColumn.Default)
}

View File

@ -0,0 +1,18 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_23 //nolint
import "xorm.io/xorm"
func AddContentVersionToIssueAndComment(x *xorm.Engine) error {
type Issue struct {
ContentVersion int `xorm:"NOT NULL DEFAULT 0"`
}
type Comment struct {
ContentVersion int `xorm:"NOT NULL DEFAULT 0"`
}
return x.Sync(new(Comment), new(Issue))
}

View File

@ -1,389 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package project
import (
"context"
"errors"
"fmt"
"regexp"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"xorm.io/builder"
)
type (
// BoardType is used to represent a project board type
BoardType uint8
// CardType is used to represent a project board card type
CardType uint8
// BoardList is a list of all project boards in a repository
BoardList []*Board
)
const (
// BoardTypeNone is a project board type that has no predefined columns
BoardTypeNone BoardType = iota
// BoardTypeBasicKanban is a project board type that has basic predefined columns
BoardTypeBasicKanban
// BoardTypeBugTriage is a project board type that has predefined columns suited to hunting down bugs
BoardTypeBugTriage
)
const (
// CardTypeTextOnly is a project board card type that is text only
CardTypeTextOnly CardType = iota
// CardTypeImagesAndText is a project board card type that has images and text
CardTypeImagesAndText
)
// BoardColorPattern is a regexp witch can validate BoardColor
var BoardColorPattern = regexp.MustCompile("^#[0-9a-fA-F]{6}$")
// Board is used to represent boards on a project
type Board struct {
ID int64 `xorm:"pk autoincr"`
Title string
Default bool `xorm:"NOT NULL DEFAULT false"` // issues not assigned to a specific board will be assigned to this board
Sorting int8 `xorm:"NOT NULL DEFAULT 0"`
Color string `xorm:"VARCHAR(7)"`
ProjectID int64 `xorm:"INDEX NOT NULL"`
CreatorID int64 `xorm:"NOT NULL"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
}
// TableName return the real table name
func (Board) TableName() string {
return "project_board"
}
// NumIssues return counter of all issues assigned to the board
func (b *Board) NumIssues(ctx context.Context) int {
c, err := db.GetEngine(ctx).Table("project_issue").
Where("project_id=?", b.ProjectID).
And("project_board_id=?", b.ID).
GroupBy("issue_id").
Cols("issue_id").
Count()
if err != nil {
return 0
}
return int(c)
}
func (b *Board) GetIssues(ctx context.Context) ([]*ProjectIssue, error) {
issues := make([]*ProjectIssue, 0, 5)
if err := db.GetEngine(ctx).Where("project_id=?", b.ProjectID).
And("project_board_id=?", b.ID).
OrderBy("sorting, id").
Find(&issues); err != nil {
return nil, err
}
return issues, nil
}
func init() {
db.RegisterModel(new(Board))
}
// IsBoardTypeValid checks if the project board type is valid
func IsBoardTypeValid(p BoardType) bool {
switch p {
case BoardTypeNone, BoardTypeBasicKanban, BoardTypeBugTriage:
return true
default:
return false
}
}
// IsCardTypeValid checks if the project board card type is valid
func IsCardTypeValid(p CardType) bool {
switch p {
case CardTypeTextOnly, CardTypeImagesAndText:
return true
default:
return false
}
}
func createBoardsForProjectsType(ctx context.Context, project *Project) error {
var items []string
switch project.BoardType {
case BoardTypeBugTriage:
items = setting.Project.ProjectBoardBugTriageType
case BoardTypeBasicKanban:
items = setting.Project.ProjectBoardBasicKanbanType
case BoardTypeNone:
fallthrough
default:
return nil
}
board := Board{
CreatedUnix: timeutil.TimeStampNow(),
CreatorID: project.CreatorID,
Title: "Backlog",
ProjectID: project.ID,
Default: true,
}
if err := db.Insert(ctx, board); err != nil {
return err
}
if len(items) == 0 {
return nil
}
boards := make([]Board, 0, len(items))
for _, v := range items {
boards = append(boards, Board{
CreatedUnix: timeutil.TimeStampNow(),
CreatorID: project.CreatorID,
Title: v,
ProjectID: project.ID,
})
}
return db.Insert(ctx, boards)
}
// maxProjectColumns max columns allowed in a project, this should not bigger than 127
// because sorting is int8 in database
const maxProjectColumns = 20
// NewBoard adds a new project board to a given project
func NewBoard(ctx context.Context, board *Board) error {
if len(board.Color) != 0 && !BoardColorPattern.MatchString(board.Color) {
return fmt.Errorf("bad color code: %s", board.Color)
}
res := struct {
MaxSorting int64
ColumnCount int64
}{}
if _, err := db.GetEngine(ctx).Select("max(sorting) as max_sorting, count(*) as column_count").Table("project_board").
Where("project_id=?", board.ProjectID).Get(&res); err != nil {
return err
}
if res.ColumnCount >= maxProjectColumns {
return fmt.Errorf("NewBoard: maximum number of columns reached")
}
board.Sorting = int8(util.Iif(res.ColumnCount > 0, res.MaxSorting+1, 0))
_, err := db.GetEngine(ctx).Insert(board)
return err
}
// DeleteBoardByID removes all issues references to the project board.
func DeleteBoardByID(ctx context.Context, boardID int64) error {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return err
}
defer committer.Close()
if err := deleteBoardByID(ctx, boardID); err != nil {
return err
}
return committer.Commit()
}
func deleteBoardByID(ctx context.Context, boardID int64) error {
board, err := GetBoard(ctx, boardID)
if err != nil {
if IsErrProjectBoardNotExist(err) {
return nil
}
return err
}
if board.Default {
return fmt.Errorf("deleteBoardByID: cannot delete default board")
}
// move all issues to the default column
project, err := GetProjectByID(ctx, board.ProjectID)
if err != nil {
return err
}
defaultColumn, err := project.GetDefaultBoard(ctx)
if err != nil {
return err
}
if err = board.moveIssuesToAnotherColumn(ctx, defaultColumn); err != nil {
return err
}
if _, err := db.GetEngine(ctx).ID(board.ID).NoAutoCondition().Delete(board); err != nil {
return err
}
return nil
}
func deleteBoardByProjectID(ctx context.Context, projectID int64) error {
_, err := db.GetEngine(ctx).Where("project_id=?", projectID).Delete(&Board{})
return err
}
// GetBoard fetches the current board of a project
func GetBoard(ctx context.Context, boardID int64) (*Board, error) {
board := new(Board)
has, err := db.GetEngine(ctx).ID(boardID).Get(board)
if err != nil {
return nil, err
} else if !has {
return nil, ErrProjectBoardNotExist{BoardID: boardID}
}
return board, nil
}
// UpdateBoard updates a project board
func UpdateBoard(ctx context.Context, board *Board) error {
var fieldToUpdate []string
if board.Sorting != 0 {
fieldToUpdate = append(fieldToUpdate, "sorting")
}
if board.Title != "" {
fieldToUpdate = append(fieldToUpdate, "title")
}
if len(board.Color) != 0 && !BoardColorPattern.MatchString(board.Color) {
return fmt.Errorf("bad color code: %s", board.Color)
}
fieldToUpdate = append(fieldToUpdate, "color")
_, err := db.GetEngine(ctx).ID(board.ID).Cols(fieldToUpdate...).Update(board)
return err
}
// GetBoards fetches all boards related to a project
func (p *Project) GetBoards(ctx context.Context) (BoardList, error) {
boards := make([]*Board, 0, 5)
if err := db.GetEngine(ctx).Where("project_id=?", p.ID).OrderBy("sorting, id").Find(&boards); err != nil {
return nil, err
}
return boards, nil
}
// GetDefaultBoard return default board and ensure only one exists
func (p *Project) GetDefaultBoard(ctx context.Context) (*Board, error) {
var board Board
has, err := db.GetEngine(ctx).
Where("project_id=? AND `default` = ?", p.ID, true).
Desc("id").Get(&board)
if err != nil {
return nil, err
}
if has {
return &board, nil
}
// create a default board if none is found
board = Board{
ProjectID: p.ID,
Default: true,
Title: "Uncategorized",
CreatorID: p.CreatorID,
}
if _, err := db.GetEngine(ctx).Insert(&board); err != nil {
return nil, err
}
return &board, nil
}
// SetDefaultBoard represents a board for issues not assigned to one
func SetDefaultBoard(ctx context.Context, projectID, boardID int64) error {
return db.WithTx(ctx, func(ctx context.Context) error {
if _, err := GetBoard(ctx, boardID); err != nil {
return err
}
if _, err := db.GetEngine(ctx).Where(builder.Eq{
"project_id": projectID,
"`default`": true,
}).Cols("`default`").Update(&Board{Default: false}); err != nil {
return err
}
_, err := db.GetEngine(ctx).ID(boardID).
Where(builder.Eq{"project_id": projectID}).
Cols("`default`").Update(&Board{Default: true})
return err
})
}
// UpdateBoardSorting update project board sorting
func UpdateBoardSorting(ctx context.Context, bs BoardList) error {
return db.WithTx(ctx, func(ctx context.Context) error {
for i := range bs {
if _, err := db.GetEngine(ctx).ID(bs[i].ID).Cols(
"sorting",
).Update(bs[i]); err != nil {
return err
}
}
return nil
})
}
func GetColumnsByIDs(ctx context.Context, projectID int64, columnsIDs []int64) (BoardList, error) {
columns := make([]*Board, 0, 5)
if err := db.GetEngine(ctx).
Where("project_id =?", projectID).
In("id", columnsIDs).
OrderBy("sorting").Find(&columns); err != nil {
return nil, err
}
return columns, nil
}
// MoveColumnsOnProject sorts columns in a project
func MoveColumnsOnProject(ctx context.Context, project *Project, sortedColumnIDs map[int64]int64) error {
return db.WithTx(ctx, func(ctx context.Context) error {
sess := db.GetEngine(ctx)
columnIDs := util.ValuesOfMap(sortedColumnIDs)
movedColumns, err := GetColumnsByIDs(ctx, project.ID, columnIDs)
if err != nil {
return err
}
if len(movedColumns) != len(sortedColumnIDs) {
return errors.New("some columns do not exist")
}
for _, column := range movedColumns {
if column.ProjectID != project.ID {
return fmt.Errorf("column[%d]'s projectID is not equal to project's ID [%d]", column.ProjectID, project.ID)
}
}
for sorting, columnID := range sortedColumnIDs {
if _, err := sess.Exec("UPDATE `project_board` SET sorting=? WHERE id=?", sorting, columnID); err != nil {
return err
}
}
return nil
})
}

359
models/project/column.go Normal file
View File

@ -0,0 +1,359 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package project
import (
"context"
"errors"
"fmt"
"regexp"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"xorm.io/builder"
)
type (
// CardType is used to represent a project column card type
CardType uint8
// ColumnList is a list of all project columns in a repository
ColumnList []*Column
)
const (
// CardTypeTextOnly is a project column card type that is text only
CardTypeTextOnly CardType = iota
// CardTypeImagesAndText is a project column card type that has images and text
CardTypeImagesAndText
)
// ColumnColorPattern is a regexp witch can validate ColumnColor
var ColumnColorPattern = regexp.MustCompile("^#[0-9a-fA-F]{6}$")
// Column is used to represent column on a project
type Column struct {
ID int64 `xorm:"pk autoincr"`
Title string
Default bool `xorm:"NOT NULL DEFAULT false"` // issues not assigned to a specific column will be assigned to this column
Sorting int8 `xorm:"NOT NULL DEFAULT 0"`
Color string `xorm:"VARCHAR(7)"`
ProjectID int64 `xorm:"INDEX NOT NULL"`
CreatorID int64 `xorm:"NOT NULL"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
}
// TableName return the real table name
func (Column) TableName() string {
return "project_board" // TODO: the legacy table name should be project_column
}
// NumIssues return counter of all issues assigned to the column
func (c *Column) NumIssues(ctx context.Context) int {
total, err := db.GetEngine(ctx).Table("project_issue").
Where("project_id=?", c.ProjectID).
And("project_board_id=?", c.ID).
GroupBy("issue_id").
Cols("issue_id").
Count()
if err != nil {
return 0
}
return int(total)
}
func (c *Column) GetIssues(ctx context.Context) ([]*ProjectIssue, error) {
issues := make([]*ProjectIssue, 0, 5)
if err := db.GetEngine(ctx).Where("project_id=?", c.ProjectID).
And("project_board_id=?", c.ID).
OrderBy("sorting, id").
Find(&issues); err != nil {
return nil, err
}
return issues, nil
}
func init() {
db.RegisterModel(new(Column))
}
// IsCardTypeValid checks if the project column card type is valid
func IsCardTypeValid(p CardType) bool {
switch p {
case CardTypeTextOnly, CardTypeImagesAndText:
return true
default:
return false
}
}
func createDefaultColumnsForProject(ctx context.Context, project *Project) error {
var items []string
switch project.TemplateType {
case TemplateTypeBugTriage:
items = setting.Project.ProjectBoardBugTriageType
case TemplateTypeBasicKanban:
items = setting.Project.ProjectBoardBasicKanbanType
case TemplateTypeNone:
fallthrough
default:
return nil
}
return db.WithTx(ctx, func(ctx context.Context) error {
column := Column{
CreatedUnix: timeutil.TimeStampNow(),
CreatorID: project.CreatorID,
Title: "Backlog",
ProjectID: project.ID,
Default: true,
}
if err := db.Insert(ctx, column); err != nil {
return err
}
if len(items) == 0 {
return nil
}
columns := make([]Column, 0, len(items))
for _, v := range items {
columns = append(columns, Column{
CreatedUnix: timeutil.TimeStampNow(),
CreatorID: project.CreatorID,
Title: v,
ProjectID: project.ID,
})
}
return db.Insert(ctx, columns)
})
}
// maxProjectColumns max columns allowed in a project, this should not bigger than 127
// because sorting is int8 in database
const maxProjectColumns = 20
// NewColumn adds a new project column to a given project
func NewColumn(ctx context.Context, column *Column) error {
if len(column.Color) != 0 && !ColumnColorPattern.MatchString(column.Color) {
return fmt.Errorf("bad color code: %s", column.Color)
}
res := struct {
MaxSorting int64
ColumnCount int64
}{}
if _, err := db.GetEngine(ctx).Select("max(sorting) as max_sorting, count(*) as column_count").Table("project_board").
Where("project_id=?", column.ProjectID).Get(&res); err != nil {
return err
}
if res.ColumnCount >= maxProjectColumns {
return fmt.Errorf("NewBoard: maximum number of columns reached")
}
column.Sorting = int8(util.Iif(res.ColumnCount > 0, res.MaxSorting+1, 0))
_, err := db.GetEngine(ctx).Insert(column)
return err
}
// DeleteColumnByID removes all issues references to the project column.
func DeleteColumnByID(ctx context.Context, columnID int64) error {
return db.WithTx(ctx, func(ctx context.Context) error {
return deleteColumnByID(ctx, columnID)
})
}
func deleteColumnByID(ctx context.Context, columnID int64) error {
column, err := GetColumn(ctx, columnID)
if err != nil {
if IsErrProjectColumnNotExist(err) {
return nil
}
return err
}
if column.Default {
return fmt.Errorf("deleteColumnByID: cannot delete default column")
}
// move all issues to the default column
project, err := GetProjectByID(ctx, column.ProjectID)
if err != nil {
return err
}
defaultColumn, err := project.GetDefaultColumn(ctx)
if err != nil {
return err
}
if err = column.moveIssuesToAnotherColumn(ctx, defaultColumn); err != nil {
return err
}
if _, err := db.GetEngine(ctx).ID(column.ID).NoAutoCondition().Delete(column); err != nil {
return err
}
return nil
}
func deleteColumnByProjectID(ctx context.Context, projectID int64) error {
_, err := db.GetEngine(ctx).Where("project_id=?", projectID).Delete(&Column{})
return err
}
// GetColumn fetches the current column of a project
func GetColumn(ctx context.Context, columnID int64) (*Column, error) {
column := new(Column)
has, err := db.GetEngine(ctx).ID(columnID).Get(column)
if err != nil {
return nil, err
} else if !has {
return nil, ErrProjectColumnNotExist{ColumnID: columnID}
}
return column, nil
}
// UpdateColumn updates a project column
func UpdateColumn(ctx context.Context, column *Column) error {
var fieldToUpdate []string
if column.Sorting != 0 {
fieldToUpdate = append(fieldToUpdate, "sorting")
}
if column.Title != "" {
fieldToUpdate = append(fieldToUpdate, "title")
}
if len(column.Color) != 0 && !ColumnColorPattern.MatchString(column.Color) {
return fmt.Errorf("bad color code: %s", column.Color)
}
fieldToUpdate = append(fieldToUpdate, "color")
_, err := db.GetEngine(ctx).ID(column.ID).Cols(fieldToUpdate...).Update(column)
return err
}
// GetColumns fetches all columns related to a project
func (p *Project) GetColumns(ctx context.Context) (ColumnList, error) {
columns := make([]*Column, 0, 5)
if err := db.GetEngine(ctx).Where("project_id=?", p.ID).OrderBy("sorting, id").Find(&columns); err != nil {
return nil, err
}
return columns, nil
}
// GetDefaultColumn return default column and ensure only one exists
func (p *Project) GetDefaultColumn(ctx context.Context) (*Column, error) {
var column Column
has, err := db.GetEngine(ctx).
Where("project_id=? AND `default` = ?", p.ID, true).
Desc("id").Get(&column)
if err != nil {
return nil, err
}
if has {
return &column, nil
}
// create a default column if none is found
column = Column{
ProjectID: p.ID,
Default: true,
Title: "Uncategorized",
CreatorID: p.CreatorID,
}
if _, err := db.GetEngine(ctx).Insert(&column); err != nil {
return nil, err
}
return &column, nil
}
// SetDefaultColumn represents a column for issues not assigned to one
func SetDefaultColumn(ctx context.Context, projectID, columnID int64) error {
return db.WithTx(ctx, func(ctx context.Context) error {
if _, err := GetColumn(ctx, columnID); err != nil {
return err
}
if _, err := db.GetEngine(ctx).Where(builder.Eq{
"project_id": projectID,
"`default`": true,
}).Cols("`default`").Update(&Column{Default: false}); err != nil {
return err
}
_, err := db.GetEngine(ctx).ID(columnID).
Where(builder.Eq{"project_id": projectID}).
Cols("`default`").Update(&Column{Default: true})
return err
})
}
// UpdateColumnSorting update project column sorting
func UpdateColumnSorting(ctx context.Context, cl ColumnList) error {
return db.WithTx(ctx, func(ctx context.Context) error {
for i := range cl {
if _, err := db.GetEngine(ctx).ID(cl[i].ID).Cols(
"sorting",
).Update(cl[i]); err != nil {
return err
}
}
return nil
})
}
func GetColumnsByIDs(ctx context.Context, projectID int64, columnsIDs []int64) (ColumnList, error) {
columns := make([]*Column, 0, 5)
if err := db.GetEngine(ctx).
Where("project_id =?", projectID).
In("id", columnsIDs).
OrderBy("sorting").Find(&columns); err != nil {
return nil, err
}
return columns, nil
}
// MoveColumnsOnProject sorts columns in a project
func MoveColumnsOnProject(ctx context.Context, project *Project, sortedColumnIDs map[int64]int64) error {
return db.WithTx(ctx, func(ctx context.Context) error {
sess := db.GetEngine(ctx)
columnIDs := util.ValuesOfMap(sortedColumnIDs)
movedColumns, err := GetColumnsByIDs(ctx, project.ID, columnIDs)
if err != nil {
return err
}
if len(movedColumns) != len(sortedColumnIDs) {
return errors.New("some columns do not exist")
}
for _, column := range movedColumns {
if column.ProjectID != project.ID {
return fmt.Errorf("column[%d]'s projectID is not equal to project's ID [%d]", column.ProjectID, project.ID)
}
}
for sorting, columnID := range sortedColumnIDs {
if _, err := sess.Exec("UPDATE `project_board` SET sorting=? WHERE id=?", sorting, columnID); err != nil {
return err
}
}
return nil
})
}

View File

@ -14,48 +14,48 @@ import (
"github.com/stretchr/testify/assert"
)
func TestGetDefaultBoard(t *testing.T) {
func TestGetDefaultColumn(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
projectWithoutDefault, err := GetProjectByID(db.DefaultContext, 5)
assert.NoError(t, err)
// check if default board was added
board, err := projectWithoutDefault.GetDefaultBoard(db.DefaultContext)
// check if default column was added
column, err := projectWithoutDefault.GetDefaultColumn(db.DefaultContext)
assert.NoError(t, err)
assert.Equal(t, int64(5), board.ProjectID)
assert.Equal(t, "Uncategorized", board.Title)
assert.Equal(t, int64(5), column.ProjectID)
assert.Equal(t, "Uncategorized", column.Title)
projectWithMultipleDefaults, err := GetProjectByID(db.DefaultContext, 6)
assert.NoError(t, err)
// check if multiple defaults were removed
board, err = projectWithMultipleDefaults.GetDefaultBoard(db.DefaultContext)
column, err = projectWithMultipleDefaults.GetDefaultColumn(db.DefaultContext)
assert.NoError(t, err)
assert.Equal(t, int64(6), board.ProjectID)
assert.Equal(t, int64(9), board.ID)
assert.Equal(t, int64(6), column.ProjectID)
assert.Equal(t, int64(9), column.ID)
// set 8 as default board
assert.NoError(t, SetDefaultBoard(db.DefaultContext, board.ProjectID, 8))
// set 8 as default column
assert.NoError(t, SetDefaultColumn(db.DefaultContext, column.ProjectID, 8))
// then 9 will become a non-default board
board, err = GetBoard(db.DefaultContext, 9)
// then 9 will become a non-default column
column, err = GetColumn(db.DefaultContext, 9)
assert.NoError(t, err)
assert.Equal(t, int64(6), board.ProjectID)
assert.False(t, board.Default)
assert.Equal(t, int64(6), column.ProjectID)
assert.False(t, column.Default)
}
func Test_moveIssuesToAnotherColumn(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
column1 := unittest.AssertExistsAndLoadBean(t, &Board{ID: 1, ProjectID: 1})
column1 := unittest.AssertExistsAndLoadBean(t, &Column{ID: 1, ProjectID: 1})
issues, err := column1.GetIssues(db.DefaultContext)
assert.NoError(t, err)
assert.Len(t, issues, 1)
assert.EqualValues(t, 1, issues[0].ID)
column2 := unittest.AssertExistsAndLoadBean(t, &Board{ID: 2, ProjectID: 1})
column2 := unittest.AssertExistsAndLoadBean(t, &Column{ID: 2, ProjectID: 1})
issues, err = column2.GetIssues(db.DefaultContext)
assert.NoError(t, err)
assert.Len(t, issues, 1)
@ -81,7 +81,7 @@ func Test_MoveColumnsOnProject(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
project1 := unittest.AssertExistsAndLoadBean(t, &Project{ID: 1})
columns, err := project1.GetBoards(db.DefaultContext)
columns, err := project1.GetColumns(db.DefaultContext)
assert.NoError(t, err)
assert.Len(t, columns, 3)
assert.EqualValues(t, 0, columns[0].Sorting) // even if there is no default sorting, the code should also work
@ -95,7 +95,7 @@ func Test_MoveColumnsOnProject(t *testing.T) {
})
assert.NoError(t, err)
columnsAfter, err := project1.GetBoards(db.DefaultContext)
columnsAfter, err := project1.GetColumns(db.DefaultContext)
assert.NoError(t, err)
assert.Len(t, columnsAfter, 3)
assert.EqualValues(t, columns[1].ID, columnsAfter[0].ID)
@ -103,23 +103,23 @@ func Test_MoveColumnsOnProject(t *testing.T) {
assert.EqualValues(t, columns[0].ID, columnsAfter[2].ID)
}
func Test_NewBoard(t *testing.T) {
func Test_NewColumn(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
project1 := unittest.AssertExistsAndLoadBean(t, &Project{ID: 1})
columns, err := project1.GetBoards(db.DefaultContext)
columns, err := project1.GetColumns(db.DefaultContext)
assert.NoError(t, err)
assert.Len(t, columns, 3)
for i := 0; i < maxProjectColumns-3; i++ {
err := NewBoard(db.DefaultContext, &Board{
Title: fmt.Sprintf("board-%d", i+4),
err := NewColumn(db.DefaultContext, &Column{
Title: fmt.Sprintf("column-%d", i+4),
ProjectID: project1.ID,
})
assert.NoError(t, err)
}
err = NewBoard(db.DefaultContext, &Board{
Title: "board-21",
err = NewColumn(db.DefaultContext, &Column{
Title: "column-21",
ProjectID: project1.ID,
})
assert.Error(t, err)

View File

@ -18,10 +18,10 @@ type ProjectIssue struct { //revive:disable-line:exported
IssueID int64 `xorm:"INDEX"`
ProjectID int64 `xorm:"INDEX"`
// ProjectBoardID should not be zero since 1.22. If it's zero, the issue will not be displayed on UI and it might result in errors.
ProjectBoardID int64 `xorm:"INDEX"`
// ProjectColumnID should not be zero since 1.22. If it's zero, the issue will not be displayed on UI and it might result in errors.
ProjectColumnID int64 `xorm:"'project_board_id' INDEX"`
// the sorting order on the board
// the sorting order on the column
Sorting int64 `xorm:"NOT NULL DEFAULT 0"`
}
@ -76,13 +76,13 @@ func (p *Project) NumOpenIssues(ctx context.Context) int {
return int(c)
}
// MoveIssuesOnProjectBoard moves or keeps issues in a column and sorts them inside that column
func MoveIssuesOnProjectBoard(ctx context.Context, board *Board, sortedIssueIDs map[int64]int64) error {
// MoveIssuesOnProjectColumn moves or keeps issues in a column and sorts them inside that column
func MoveIssuesOnProjectColumn(ctx context.Context, column *Column, sortedIssueIDs map[int64]int64) error {
return db.WithTx(ctx, func(ctx context.Context) error {
sess := db.GetEngine(ctx)
issueIDs := util.ValuesOfMap(sortedIssueIDs)
count, err := sess.Table(new(ProjectIssue)).Where("project_id=?", board.ProjectID).In("issue_id", issueIDs).Count()
count, err := sess.Table(new(ProjectIssue)).Where("project_id=?", column.ProjectID).In("issue_id", issueIDs).Count()
if err != nil {
return err
}
@ -91,7 +91,7 @@ func MoveIssuesOnProjectBoard(ctx context.Context, board *Board, sortedIssueIDs
}
for sorting, issueID := range sortedIssueIDs {
_, err = sess.Exec("UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=?", board.ID, sorting, issueID)
_, err = sess.Exec("UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=?", column.ID, sorting, issueID)
if err != nil {
return err
}
@ -100,12 +100,12 @@ func MoveIssuesOnProjectBoard(ctx context.Context, board *Board, sortedIssueIDs
})
}
func (b *Board) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Board) error {
if b.ProjectID != newColumn.ProjectID {
func (c *Column) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Column) error {
if c.ProjectID != newColumn.ProjectID {
return fmt.Errorf("columns have to be in the same project")
}
if b.ID == newColumn.ID {
if c.ID == newColumn.ID {
return nil
}
@ -121,7 +121,7 @@ func (b *Board) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Board)
return err
}
issues, err := b.GetIssues(ctx)
issues, err := c.GetIssues(ctx)
if err != nil {
return err
}
@ -132,7 +132,7 @@ func (b *Board) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Board)
nextSorting := util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0)
return db.WithTx(ctx, func(ctx context.Context) error {
for i, issue := range issues {
issue.ProjectBoardID = newColumn.ID
issue.ProjectColumnID = newColumn.ID
issue.Sorting = nextSorting + int64(i)
if _, err := db.GetEngine(ctx).ID(issue.ID).Cols("project_board_id", "sorting").Update(issue); err != nil {
return err

View File

@ -21,13 +21,7 @@ import (
)
type (
// BoardConfig is used to identify the type of board that is being created
BoardConfig struct {
BoardType BoardType
Translation string
}
// CardConfig is used to identify the type of board card that is being used
// CardConfig is used to identify the type of column card that is being used
CardConfig struct {
CardType CardType
Translation string
@ -38,7 +32,7 @@ type (
)
const (
// TypeIndividual is a type of project board that is owned by an individual
// TypeIndividual is a type of project column that is owned by an individual
TypeIndividual Type = iota + 1
// TypeRepository is a project that is tied to a repository
@ -68,39 +62,39 @@ func (err ErrProjectNotExist) Unwrap() error {
return util.ErrNotExist
}
// ErrProjectBoardNotExist represents a "ProjectBoardNotExist" kind of error.
type ErrProjectBoardNotExist struct {
BoardID int64
// ErrProjectColumnNotExist represents a "ErrProjectColumnNotExist" kind of error.
type ErrProjectColumnNotExist struct {
ColumnID int64
}
// IsErrProjectBoardNotExist checks if an error is a ErrProjectBoardNotExist
func IsErrProjectBoardNotExist(err error) bool {
_, ok := err.(ErrProjectBoardNotExist)
// IsErrProjectColumnNotExist checks if an error is a ErrProjectColumnNotExist
func IsErrProjectColumnNotExist(err error) bool {
_, ok := err.(ErrProjectColumnNotExist)
return ok
}
func (err ErrProjectBoardNotExist) Error() string {
return fmt.Sprintf("project board does not exist [id: %d]", err.BoardID)
func (err ErrProjectColumnNotExist) Error() string {
return fmt.Sprintf("project column does not exist [id: %d]", err.ColumnID)
}
func (err ErrProjectBoardNotExist) Unwrap() error {
func (err ErrProjectColumnNotExist) Unwrap() error {
return util.ErrNotExist
}
// Project represents a project board
// Project represents a project
type Project struct {
ID int64 `xorm:"pk autoincr"`
Title string `xorm:"INDEX NOT NULL"`
Description string `xorm:"TEXT"`
OwnerID int64 `xorm:"INDEX"`
Owner *user_model.User `xorm:"-"`
RepoID int64 `xorm:"INDEX"`
Repo *repo_model.Repository `xorm:"-"`
CreatorID int64 `xorm:"NOT NULL"`
IsClosed bool `xorm:"INDEX"`
BoardType BoardType
CardType CardType
Type Type
ID int64 `xorm:"pk autoincr"`
Title string `xorm:"INDEX NOT NULL"`
Description string `xorm:"TEXT"`
OwnerID int64 `xorm:"INDEX"`
Owner *user_model.User `xorm:"-"`
RepoID int64 `xorm:"INDEX"`
Repo *repo_model.Repository `xorm:"-"`
CreatorID int64 `xorm:"NOT NULL"`
IsClosed bool `xorm:"INDEX"`
TemplateType TemplateType `xorm:"'board_type'"` // TODO: rename the column to template_type
CardType CardType
Type Type
RenderedContent template.HTML `xorm:"-"`
@ -172,16 +166,7 @@ func init() {
db.RegisterModel(new(Project))
}
// GetBoardConfig retrieves the types of configurations project boards could have
func GetBoardConfig() []BoardConfig {
return []BoardConfig{
{BoardTypeNone, "repo.projects.type.none"},
{BoardTypeBasicKanban, "repo.projects.type.basic_kanban"},
{BoardTypeBugTriage, "repo.projects.type.bug_triage"},
}
}
// GetCardConfig retrieves the types of configurations project board cards could have
// GetCardConfig retrieves the types of configurations project column cards could have
func GetCardConfig() []CardConfig {
return []CardConfig{
{CardTypeTextOnly, "repo.projects.card_type.text_only"},
@ -251,8 +236,8 @@ func GetSearchOrderByBySortType(sortType string) db.SearchOrderBy {
// NewProject creates a new Project
func NewProject(ctx context.Context, p *Project) error {
if !IsBoardTypeValid(p.BoardType) {
p.BoardType = BoardTypeNone
if !IsTemplateTypeValid(p.TemplateType) {
p.TemplateType = TemplateTypeNone
}
if !IsCardTypeValid(p.CardType) {
@ -263,27 +248,19 @@ func NewProject(ctx context.Context, p *Project) error {
return util.NewInvalidArgumentErrorf("project type is not valid")
}
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return err
}
defer committer.Close()
if err := db.Insert(ctx, p); err != nil {
return err
}
if p.RepoID > 0 {
if _, err := db.Exec(ctx, "UPDATE `repository` SET num_projects = num_projects + 1 WHERE id = ?", p.RepoID); err != nil {
return db.WithTx(ctx, func(ctx context.Context) error {
if err := db.Insert(ctx, p); err != nil {
return err
}
}
if err := createBoardsForProjectsType(ctx, p); err != nil {
return err
}
if p.RepoID > 0 {
if _, err := db.Exec(ctx, "UPDATE `repository` SET num_projects = num_projects + 1 WHERE id = ?", p.RepoID); err != nil {
return err
}
}
return committer.Commit()
return createDefaultColumnsForProject(ctx, p)
})
}
// GetProjectByID returns the projects in a repository
@ -417,7 +394,7 @@ func DeleteProjectByID(ctx context.Context, id int64) error {
return err
}
if err := deleteBoardByProjectID(ctx, id); err != nil {
if err := deleteColumnByProjectID(ctx, id); err != nil {
return err
}

View File

@ -51,13 +51,13 @@ func TestProject(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
project := &Project{
Type: TypeRepository,
BoardType: BoardTypeBasicKanban,
CardType: CardTypeTextOnly,
Title: "New Project",
RepoID: 1,
CreatedUnix: timeutil.TimeStampNow(),
CreatorID: 2,
Type: TypeRepository,
TemplateType: TemplateTypeBasicKanban,
CardType: CardTypeTextOnly,
Title: "New Project",
RepoID: 1,
CreatedUnix: timeutil.TimeStampNow(),
CreatorID: 2,
}
assert.NoError(t, NewProject(db.DefaultContext, project))

View File

@ -0,0 +1,45 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package project
type (
// TemplateType is used to represent a project template type
TemplateType uint8
// TemplateConfig is used to identify the template type of project that is being created
TemplateConfig struct {
TemplateType TemplateType
Translation string
}
)
const (
// TemplateTypeNone is a project template type that has no predefined columns
TemplateTypeNone TemplateType = iota
// TemplateTypeBasicKanban is a project template type that has basic predefined columns
TemplateTypeBasicKanban
// TemplateTypeBugTriage is a project template type that has predefined columns suited to hunting down bugs
TemplateTypeBugTriage
)
// GetTemplateConfigs retrieves the template configs of configurations project columns could have
func GetTemplateConfigs() []TemplateConfig {
return []TemplateConfig{
{TemplateTypeNone, "repo.projects.type.none"},
{TemplateTypeBasicKanban, "repo.projects.type.basic_kanban"},
{TemplateTypeBugTriage, "repo.projects.type.bug_triage"},
}
}
// IsTemplateTypeValid checks if the project template type is valid
func IsTemplateTypeValid(p TemplateType) bool {
switch p {
case TemplateTypeNone, TemplateTypeBasicKanban, TemplateTypeBugTriage:
return true
default:
return false
}
}

View File

@ -28,7 +28,7 @@ const (
TypeWiki // 5 Wiki
TypeExternalWiki // 6 ExternalWiki
TypeExternalTracker // 7 ExternalTracker
TypeProjects // 8 Kanban board
TypeProjects // 8 Projects
TypePackages // 9 Packages
TypeActions // 10 Actions
)

View File

@ -8,6 +8,8 @@ import (
"time"
"code.gitea.io/gitea/modules/setting"
_ "gitea.com/go-chi/cache/memcache" //nolint:depguard // memcache plugin for cache, it is required for config "ADAPTER=memcache"
)
var defaultCache StringCache

View File

@ -224,8 +224,8 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
if options.ProjectID.Has() {
queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectID.Value(), "project_id"))
}
if options.ProjectBoardID.Has() {
queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectBoardID.Value(), "project_board_id"))
if options.ProjectColumnID.Has() {
queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectColumnID.Value(), "project_board_id"))
}
if options.PosterID.Has() {

View File

@ -61,7 +61,7 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m
ReviewedID: convertID(options.ReviewedID),
SubscriberID: convertID(options.SubscriberID),
ProjectID: convertID(options.ProjectID),
ProjectBoardID: convertID(options.ProjectBoardID),
ProjectColumnID: convertID(options.ProjectColumnID),
IsClosed: options.IsClosed,
IsPull: options.IsPull,
IncludedLabelNames: nil,

View File

@ -50,7 +50,7 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp
}
searchOpt.ProjectID = convertID(opts.ProjectID)
searchOpt.ProjectBoardID = convertID(opts.ProjectBoardID)
searchOpt.ProjectColumnID = convertID(opts.ProjectColumnID)
searchOpt.PosterID = convertID(opts.PosterID)
searchOpt.AssigneeID = convertID(opts.AssigneeID)
searchOpt.MentionID = convertID(opts.MentionedID)

View File

@ -197,8 +197,8 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
if options.ProjectID.Has() {
query.Must(elastic.NewTermQuery("project_id", options.ProjectID.Value()))
}
if options.ProjectBoardID.Has() {
query.Must(elastic.NewTermQuery("project_board_id", options.ProjectBoardID.Value()))
if options.ProjectColumnID.Has() {
query.Must(elastic.NewTermQuery("project_board_id", options.ProjectColumnID.Value()))
}
if options.PosterID.Has() {

View File

@ -369,13 +369,13 @@ func searchIssueInProject(t *testing.T) {
},
{
SearchOptions{
ProjectBoardID: optional.Some(int64(1)),
ProjectColumnID: optional.Some(int64(1)),
},
[]int64{1},
},
{
SearchOptions{
ProjectBoardID: optional.Some(int64(0)), // issue with in default board
ProjectColumnID: optional.Some(int64(0)), // issue with in default column
},
[]int64{2},
},

View File

@ -27,7 +27,7 @@ type IndexerData struct {
NoLabel bool `json:"no_label"` // True if LabelIDs is empty
MilestoneID int64 `json:"milestone_id"`
ProjectID int64 `json:"project_id"`
ProjectBoardID int64 `json:"project_board_id"`
ProjectColumnID int64 `json:"project_board_id"` // the key should be kept as project_board_id to keep compatible
PosterID int64 `json:"poster_id"`
AssigneeID int64 `json:"assignee_id"`
MentionIDs []int64 `json:"mention_ids"`
@ -89,8 +89,8 @@ type SearchOptions struct {
MilestoneIDs []int64 // milestones the issues have
ProjectID optional.Option[int64] // project the issues belong to
ProjectBoardID optional.Option[int64] // project board the issues belong to
ProjectID optional.Option[int64] // project the issues belong to
ProjectColumnID optional.Option[int64] // project column the issues belong to
PosterID optional.Option[int64] // poster of the issues

View File

@ -338,38 +338,38 @@ var cases = []*testIndexerCase{
},
},
{
Name: "ProjectBoardID",
Name: "ProjectColumnID",
SearchOptions: &internal.SearchOptions{
Paginator: &db.ListOptions{
PageSize: 5,
},
ProjectBoardID: optional.Some(int64(1)),
ProjectColumnID: optional.Some(int64(1)),
},
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
assert.Equal(t, 5, len(result.Hits))
for _, v := range result.Hits {
assert.Equal(t, int64(1), data[v.ID].ProjectBoardID)
assert.Equal(t, int64(1), data[v.ID].ProjectColumnID)
}
assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool {
return v.ProjectBoardID == 1
return v.ProjectColumnID == 1
}), result.Total)
},
},
{
Name: "no ProjectBoardID",
Name: "no ProjectColumnID",
SearchOptions: &internal.SearchOptions{
Paginator: &db.ListOptions{
PageSize: 5,
},
ProjectBoardID: optional.Some(int64(0)),
ProjectColumnID: optional.Some(int64(0)),
},
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
assert.Equal(t, 5, len(result.Hits))
for _, v := range result.Hits {
assert.Equal(t, int64(0), data[v.ID].ProjectBoardID)
assert.Equal(t, int64(0), data[v.ID].ProjectColumnID)
}
assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool {
return v.ProjectBoardID == 0
return v.ProjectColumnID == 0
}), result.Total)
},
},
@ -706,7 +706,7 @@ func generateDefaultIndexerData() []*internal.IndexerData {
NoLabel: len(labelIDs) == 0,
MilestoneID: issueIndex % 4,
ProjectID: issueIndex % 5,
ProjectBoardID: issueIndex % 6,
ProjectColumnID: issueIndex % 6,
PosterID: id%10 + 1, // PosterID should not be 0
AssigneeID: issueIndex % 10,
MentionIDs: mentionIDs,

View File

@ -174,8 +174,8 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
if options.ProjectID.Has() {
query.And(inner_meilisearch.NewFilterEq("project_id", options.ProjectID.Value()))
}
if options.ProjectBoardID.Has() {
query.And(inner_meilisearch.NewFilterEq("project_board_id", options.ProjectBoardID.Value()))
if options.ProjectColumnID.Has() {
query.And(inner_meilisearch.NewFilterEq("project_board_id", options.ProjectColumnID.Value()))
}
if options.PosterID.Has() {

View File

@ -105,7 +105,7 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD
NoLabel: len(labels) == 0,
MilestoneID: issue.MilestoneID,
ProjectID: projectID,
ProjectBoardID: issue.ProjectBoardID(ctx),
ProjectColumnID: issue.ProjectColumnID(ctx),
PosterID: issue.PosterID,
AssigneeID: issue.AssigneeID,
MentionIDs: mentionIDs,

View File

@ -36,7 +36,7 @@ type Collector struct {
Oauths *prometheus.Desc
Organizations *prometheus.Desc
Projects *prometheus.Desc
ProjectBoards *prometheus.Desc
ProjectColumns *prometheus.Desc
PublicKeys *prometheus.Desc
Releases *prometheus.Desc
Repositories *prometheus.Desc
@ -146,9 +146,9 @@ func NewCollector() Collector {
"Number of projects",
nil, nil,
),
ProjectBoards: prometheus.NewDesc(
namespace+"projects_boards",
"Number of project boards",
ProjectColumns: prometheus.NewDesc(
namespace+"projects_boards", // TODO: change the key name will affect the consume's result history
"Number of project columns",
nil, nil,
),
PublicKeys: prometheus.NewDesc(
@ -219,7 +219,7 @@ func (c Collector) Describe(ch chan<- *prometheus.Desc) {
ch <- c.Oauths
ch <- c.Organizations
ch <- c.Projects
ch <- c.ProjectBoards
ch <- c.ProjectColumns
ch <- c.PublicKeys
ch <- c.Releases
ch <- c.Repositories
@ -336,9 +336,9 @@ func (c Collector) Collect(ch chan<- prometheus.Metric) {
float64(stats.Counter.Project),
)
ch <- prometheus.MustNewConstMetric(
c.ProjectBoards,
c.ProjectColumns,
prometheus.GaugeValue,
float64(stats.Counter.ProjectBoard),
float64(stats.Counter.ProjectColumn),
)
ch <- prometheus.MustNewConstMetric(
c.PublicKeys,

View File

@ -97,7 +97,7 @@ func NewMinioStorage(ctx context.Context, cfg *setting.Storage) (ObjectStorage,
}
minioClient, err := minio.New(config.Endpoint, &minio.Options{
Creds: credentials.NewStaticV4(config.AccessKeyID, config.SecretAccessKey, ""),
Creds: buildMinioCredentials(config, credentials.DefaultIAMRoleEndpoint),
Secure: config.UseSSL,
Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify}},
Region: config.Location,
@ -164,6 +164,35 @@ func (m *MinioStorage) buildMinioDirPrefix(p string) string {
return p
}
func buildMinioCredentials(config setting.MinioStorageConfig, iamEndpoint string) *credentials.Credentials {
// If static credentials are provided, use those
if config.AccessKeyID != "" {
return credentials.NewStaticV4(config.AccessKeyID, config.SecretAccessKey, "")
}
// Otherwise, fallback to a credentials chain for S3 access
chain := []credentials.Provider{
// configure based upon MINIO_ prefixed environment variables
&credentials.EnvMinio{},
// configure based upon AWS_ prefixed environment variables
&credentials.EnvAWS{},
// read credentials from MINIO_SHARED_CREDENTIALS_FILE
// environment variable, or default json config files
&credentials.FileMinioClient{},
// read credentials from AWS_SHARED_CREDENTIALS_FILE
// environment variable, or default credentials file
&credentials.FileAWSCredentials{},
// read IAM role from EC2 metadata endpoint if available
&credentials.IAM{
Endpoint: iamEndpoint,
Client: &http.Client{
Transport: http.DefaultTransport,
},
},
}
return credentials.NewChainCredentials(chain)
}
// Open opens a file
func (m *MinioStorage) Open(path string) (Object, error) {
opts := minio.GetObjectOptions{}

View File

@ -6,6 +6,7 @@ package storage
import (
"context"
"net/http"
"net/http/httptest"
"os"
"testing"
@ -92,3 +93,106 @@ func TestS3StorageBadRequest(t *testing.T) {
_, err := NewStorage(setting.MinioStorageType, cfg)
assert.ErrorContains(t, err, message)
}
func TestMinioCredentials(t *testing.T) {
const (
ExpectedAccessKey = "ExampleAccessKeyID"
ExpectedSecretAccessKey = "ExampleSecretAccessKeyID"
// Use a FakeEndpoint for IAM credentials to avoid logging any
// potential real IAM credentials when running in EC2.
FakeEndpoint = "http://localhost"
)
t.Run("Static Credentials", func(t *testing.T) {
cfg := setting.MinioStorageConfig{
AccessKeyID: ExpectedAccessKey,
SecretAccessKey: ExpectedSecretAccessKey,
}
creds := buildMinioCredentials(cfg, FakeEndpoint)
v, err := creds.Get()
assert.NoError(t, err)
assert.Equal(t, ExpectedAccessKey, v.AccessKeyID)
assert.Equal(t, ExpectedSecretAccessKey, v.SecretAccessKey)
})
t.Run("Chain", func(t *testing.T) {
cfg := setting.MinioStorageConfig{}
t.Run("EnvMinio", func(t *testing.T) {
t.Setenv("MINIO_ACCESS_KEY", ExpectedAccessKey+"Minio")
t.Setenv("MINIO_SECRET_KEY", ExpectedSecretAccessKey+"Minio")
creds := buildMinioCredentials(cfg, FakeEndpoint)
v, err := creds.Get()
assert.NoError(t, err)
assert.Equal(t, ExpectedAccessKey+"Minio", v.AccessKeyID)
assert.Equal(t, ExpectedSecretAccessKey+"Minio", v.SecretAccessKey)
})
t.Run("EnvAWS", func(t *testing.T) {
t.Setenv("AWS_ACCESS_KEY", ExpectedAccessKey+"AWS")
t.Setenv("AWS_SECRET_KEY", ExpectedSecretAccessKey+"AWS")
creds := buildMinioCredentials(cfg, FakeEndpoint)
v, err := creds.Get()
assert.NoError(t, err)
assert.Equal(t, ExpectedAccessKey+"AWS", v.AccessKeyID)
assert.Equal(t, ExpectedSecretAccessKey+"AWS", v.SecretAccessKey)
})
t.Run("FileMinio", func(t *testing.T) {
t.Setenv("MINIO_SHARED_CREDENTIALS_FILE", "testdata/minio.json")
// prevent loading any actual credentials files from the user
t.Setenv("AWS_SHARED_CREDENTIALS_FILE", "testdata/fake")
creds := buildMinioCredentials(cfg, FakeEndpoint)
v, err := creds.Get()
assert.NoError(t, err)
assert.Equal(t, ExpectedAccessKey+"MinioFile", v.AccessKeyID)
assert.Equal(t, ExpectedSecretAccessKey+"MinioFile", v.SecretAccessKey)
})
t.Run("FileAWS", func(t *testing.T) {
// prevent loading any actual credentials files from the user
t.Setenv("MINIO_SHARED_CREDENTIALS_FILE", "testdata/fake.json")
t.Setenv("AWS_SHARED_CREDENTIALS_FILE", "testdata/aws_credentials")
creds := buildMinioCredentials(cfg, FakeEndpoint)
v, err := creds.Get()
assert.NoError(t, err)
assert.Equal(t, ExpectedAccessKey+"AWSFile", v.AccessKeyID)
assert.Equal(t, ExpectedSecretAccessKey+"AWSFile", v.SecretAccessKey)
})
t.Run("IAM", func(t *testing.T) {
// prevent loading any actual credentials files from the user
t.Setenv("MINIO_SHARED_CREDENTIALS_FILE", "testdata/fake.json")
t.Setenv("AWS_SHARED_CREDENTIALS_FILE", "testdata/fake")
// Spawn a server to emulate the EC2 Instance Metadata
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// The client will actually make 3 requests here,
// first will be to get the IMDSv2 token, second to
// get the role, and third for the actual
// credentials. However, we can return credentials
// every request since we're not emulating a full
// IMDSv2 flow.
w.Write([]byte(`{"Code":"Success","AccessKeyId":"ExampleAccessKeyIDIAM","SecretAccessKey":"ExampleSecretAccessKeyIDIAM"}`))
}))
defer server.Close()
// Use the provided EC2 Instance Metadata server
creds := buildMinioCredentials(cfg, server.URL)
v, err := creds.Get()
assert.NoError(t, err)
assert.Equal(t, ExpectedAccessKey+"IAM", v.AccessKeyID)
assert.Equal(t, ExpectedSecretAccessKey+"IAM", v.SecretAccessKey)
})
})
}

View File

@ -0,0 +1,3 @@
[default]
aws_access_key_id=ExampleAccessKeyIDAWSFile
aws_secret_access_key=ExampleSecretAccessKeyIDAWSFile

12
modules/storage/testdata/minio.json vendored Normal file
View File

@ -0,0 +1,12 @@
{
"version": "10",
"aliases": {
"s3": {
"url": "https://s3.amazonaws.com",
"accessKey": "ExampleAccessKeyIDMinioFile",
"secretKey": "ExampleSecretAccessKeyIDMinioFile",
"api": "S3v4",
"path": "dns"
}
}
}

View File

@ -30,6 +30,7 @@ type PullRequestMeta struct {
HasMerged bool `json:"merged"`
Merged *time.Time `json:"merged_at"`
IsWorkInProgress bool `json:"draft"`
HTMLURL string `json:"html_url"`
}
// RepositoryMeta basic repository information

View File

@ -21,8 +21,14 @@ type PullRequest struct {
Assignees []*User `json:"assignees"`
RequestedReviewers []*User `json:"requested_reviewers"`
State StateType `json:"state"`
Draft bool `json:"draft"`
IsLocked bool `json:"is_locked"`
Comments int `json:"comments"`
// number of review comments made on the diff of a PR review (not including comments on commits or issues in a PR)
ReviewComments int `json:"review_comments"`
Additions int `json:"additions"`
Deletions int `json:"deletions"`
ChangedFiles int `json:"changed_files"`
HTMLURL string `json:"html_url"`
DiffURL string `json:"diff_url"`

View File

@ -113,6 +113,7 @@ type Repository struct {
// swagger:strfmt date-time
MirrorUpdated time.Time `json:"mirror_updated,omitempty"`
RepoTransfer *RepoTransfer `json:"repo_transfer"`
Topics []string `json:"topics"`
}
// CreateRepoOption options when creating repository

View File

@ -28,6 +28,8 @@ type User struct {
Email string `json:"email"`
// URL to the user's avatar
AvatarURL string `json:"avatar_url"`
// URL to the user's gitea page
HTMLURL string `json:"html_url"`
// User locale
Language string `json:"language"`
// Is the user an administrator

2
options/license/Gutmann Normal file
View File

@ -0,0 +1,2 @@
You can use this code in whatever way you want, as long as you don't try
to claim you wrote it.

View File

@ -0,0 +1,21 @@
Copyright 2004-2008 Apple Inc. All Rights Reserved.
Export of this software from the United States of America may
require a specific license from the United States Government.
It is the responsibility of any person or organization
contemplating export to obtain such a license before exporting.
WITHIN THAT CONSTRAINT, permission to use, copy, modify, and
distribute this software and its documentation for any purpose and
without fee is hereby granted, provided that the above copyright
notice appear in all copies and that both that copyright notice and
this permission notice appear in supporting documentation, and that
the name of Apple Inc. not be used in advertising or publicity
pertaining to distribution of the software without specific,
written prior permission. Apple Inc. makes no representations
about the suitability of this software for any purpose. It is
provided "as is" without express or implied warranty.
THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.

View File

@ -0,0 +1,9 @@
Copyright (C) 2004 Christian Groessler <chris@groessler.org>
Permission to use, copy, modify, and distribute this file
for any purpose is hereby granted without fee, provided that
the above copyright notice and this notice appears in all
copies.
This file is distributed WITHOUT ANY WARRANTY; without even the implied
warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

View File

@ -0,0 +1,66 @@
FLOSS License Exception
=======================
(Adapted from http://www.mysql.com/company/legal/licensing/foss-exception.html)
I want specified Free/Libre and Open Source Software ("FLOSS")
applications to be able to use specified GPL-licensed RRDtool
libraries (the "Program") despite the fact that not all FLOSS licenses are
compatible with version 2 of the GNU General Public License (the "GPL").
As a special exception to the terms and conditions of version 2.0 of the GPL:
You are free to distribute a Derivative Work that is formed entirely from
the Program and one or more works (each, a "FLOSS Work") licensed under one
or more of the licenses listed below, as long as:
1. You obey the GPL in all respects for the Program and the Derivative
Work, except for identifiable sections of the Derivative Work which are
not derived from the Program, and which can reasonably be considered
independent and separate works in themselves,
2. all identifiable sections of the Derivative Work which are not derived
from the Program, and which can reasonably be considered independent and
separate works in themselves,
1. are distributed subject to one of the FLOSS licenses listed
below, and
2. the object code or executable form of those sections are
accompanied by the complete corresponding machine-readable source
code for those sections on the same medium and under the same FLOSS
license as the corresponding object code or executable forms of
those sections, and
3. any works which are aggregated with the Program or with a Derivative
Work on a volume of a storage or distribution medium in accordance with
the GPL, can reasonably be considered independent and separate works in
themselves which are not derivatives of either the Program, a Derivative
Work or a FLOSS Work.
If the above conditions are not met, then the Program may only be copied,
modified, distributed or used under the terms and conditions of the GPL.
FLOSS License List
==================
License name Version(s)/Copyright Date
Academic Free License 2.0
Apache Software License 1.0/1.1/2.0
Apple Public Source License 2.0
Artistic license From Perl 5.8.0
BSD license "July 22 1999"
Common Public License 1.0
GNU Library or "Lesser" General Public License (LGPL) 2.0/2.1
IBM Public License, Version 1.0
Jabber Open Source License 1.0
MIT License (As listed in file MIT-License.txt) -
Mozilla Public License (MPL) 1.0/1.1
Open Software License 2.0
OpenSSL license (with original SSLeay license) "2003" ("1998")
PHP License 3.01
Python license (CNRI Python License) -
Python Software Foundation License 2.1.1
Sleepycat License "1999"
W3C License "2001"
X11 License "2001"
Zlib/libpng License -
Zope Public License 2.0/2.1

View File

@ -1205,7 +1205,7 @@ branches=Větve
tags=Značky
issues=Úkoly
pulls=Pull requesty
project_board=Projekty
projects=Projekty
packages=Balíčky
actions=Akce
labels=Štítky
@ -1364,8 +1364,6 @@ commitstatus.success=Úspěch
ext_issues=Přístup k externím úkolům
ext_issues.desc=Odkaz na externí systém úkolů.
projects=Projekty
projects.desc=Spravovat úkoly a požadavky na natažení na projektových nástěnkách.
projects.description=Popis (volitelné)
projects.description_placeholder=Popis
projects.create=Vytvořit projekt
@ -1887,6 +1885,7 @@ pulls.recently_pushed_new_branches=Nahráli jste větev <strong>%[1]s</strong> %
pull.deleted_branch=(odstraněno):%s
milestones.new=Nový milník
milestones.closed=Zavřen dne %s
milestones.update_ago=Aktualizováno %s

View File

@ -1206,7 +1206,7 @@ branches=Branches
tags=Tags
issues=Issues
pulls=Pull-Requests
project_board=Projekte
projects=Projekte
packages=Pakete
actions=Actions
labels=Label
@ -1366,8 +1366,6 @@ commitstatus.success=Erfolg
ext_issues=Zugriff auf Externe Issues
ext_issues.desc=Link zu externem Issuetracker.
projects=Projekte
projects.desc=Verwalte Issues und Pull-Requests in Projektboards.
projects.description=Beschreibung (optional)
projects.description_placeholder=Beschreibung
projects.create=Projekt erstellen
@ -1891,6 +1889,7 @@ pulls.recently_pushed_new_branches=Du hast auf den Branch <strong>%[1]s</strong>
pull.deleted_branch=(gelöscht):%s
milestones.new=Neuer Meilenstein
milestones.closed=Geschlossen %s
milestones.update_ago=%s aktualisiert

View File

@ -1137,7 +1137,7 @@ branches=Κλάδοι
tags=Ετικέτες
issues=Ζητήματα
pulls=Pull Requests
project_board=Έργα
projects=Έργα
packages=Πακέτα
actions=Δράσεις
labels=Σήματα
@ -1292,8 +1292,6 @@ commitstatus.success=Επιτυχές
ext_issues=Πρόσβαση στα Εξωτερικά Ζητήματα
ext_issues.desc=Σύνδεση σε εξωτερικό εφαρμογή ζητημάτων.
projects=Έργα
projects.desc=Διαχείριση ζητημάτων και pulls στους πίνακες των έργων.
projects.description=Περιγραφή (προαιρετικό)
projects.description_placeholder=Περιγραφή
projects.create=Δημιουργία Έργου
@ -1810,6 +1808,7 @@ pulls.recently_pushed_new_branches=Ωθήσατε στο κλάδο <strong>%[1]
pull.deleted_branch=(διαγράφηκε):%s
milestones.new=Νέο Ορόσημο
milestones.closed=Έκλεισε %s
milestones.update_ago=Ενημερώθηκε %s

View File

@ -1215,7 +1215,7 @@ branches = Branches
tags = Tags
issues = Issues
pulls = Pull Requests
project_board = Projects
projects = Projects
packages = Packages
actions = Actions
labels = Labels
@ -1379,7 +1379,7 @@ ext_issues = Access to External Issues
ext_issues.desc = Link to an external issue tracker.
projects = Projects
projects.desc = Manage issues and pulls in project boards.
projects.desc = Manage issues and pulls in projects.
projects.description = Description (optional)
projects.description_placeholder = Description
projects.create = Create Project
@ -1443,6 +1443,7 @@ 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.already_changed = Unable to save changes to the issue. It appears the content has already been changed by another user. Please refresh the page and try editing again to avoid overwriting their changes
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
@ -1758,6 +1759,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.edit.already_changed = Unable to save changes to the pull request. It appears the content has already been changed by another user. Please refresh the page and try editing again to avoid overwriting their changes
pulls.view = View Pull Request
pulls.compare_changes = New Pull Request
pulls.allow_edits_from_maintainers = Allow edits from maintainers
@ -1903,6 +1905,8 @@ pulls.recently_pushed_new_branches = You pushed on branch <strong>%[1]s</strong>
pull.deleted_branch = (deleted):%s
comments.edit.already_changed = Unable to save changes to the comment. It appears the content has already been changed by another user. Please refresh the page and try editing again to avoid overwriting their changes
milestones.new = New Milestone
milestones.closed = Closed %s
milestones.update_ago = Updated %s
@ -3641,6 +3645,7 @@ runs.pushed_by = pushed by
runs.invalid_workflow_helper = Workflow config file is invalid. Please check your config file: %s
runs.no_matching_online_runner_helper = No matching online runner with label: %s
runs.no_job_without_needs = The workflow must contain at least one job without dependencies.
runs.no_job = The workflow must contain at least one job
runs.actor = Actor
runs.status = Status
runs.actors_no_select = All actors

View File

@ -1130,7 +1130,7 @@ branches=Ramas
tags=Etiquetas
issues=Incidencias
pulls=Pull Requests
project_board=Proyectos
projects=Proyectos
packages=Paquetes
actions=Acciones
labels=Etiquetas
@ -1285,8 +1285,6 @@ commitstatus.success=Éxito
ext_issues=Acceso a incidencias externas
ext_issues.desc=Enlace a un gestor de incidencias externo.
projects=Proyectos
projects.desc=Gestionar problemas y pulls en los tablones del proyecto.
projects.description=Descripción (opcional)
projects.description_placeholder=Descripción
projects.create=Crear Proyecto
@ -1796,6 +1794,7 @@ pulls.recently_pushed_new_branches=Has realizado push en la rama <strong>%[1]s</
pull.deleted_branch=(eliminado):%s
milestones.new=Nuevo hito
milestones.closed=Cerrada %s
milestones.update_ago=Actualizado %s

View File

@ -891,7 +891,7 @@ branches=شاخه‎ها
tags=برچسب‎ها
issues=مسائل
pulls=تقاضاهای واکشی
project_board=پروژه‌ها
projects=پروژه‌ها
labels=برچسب‌ها
org_labels_desc=برچسب های سطح سازمان که می توانند برای <strong>تمامی مخازن</strong> ذیل این سازمان استفاده شوند
org_labels_desc_manage=مدیریت
@ -986,8 +986,6 @@ commitstatus.pending=در انتظار
ext_issues.desc=پیوند به ردیاب خارجی برای موضوع.
projects=پروژه‌ها
projects.desc=مدیریت مشکلات و درخواست‌های درج در بورد پروژه.
projects.description=توضیحات (دلخواه)
projects.description_placeholder=توضیحات
projects.create=ایجاد پروژه جدید
@ -1377,6 +1375,7 @@ pulls.reopened_at=`این درخواست pull را بازگشایی کرد <a id
milestones.new=نقطه عطف جدید
milestones.closed=%s بسته شد
milestones.no_due_date=بدون موعد مقرر

View File

@ -730,7 +730,7 @@ branches=Branchit
tags=Tagit
issues=Ongelmat
pulls=Pull-pyynnöt
project_board=Projektit
projects=Projektit
packages=Paketit
labels=Tunnisteet
@ -792,7 +792,6 @@ commitstatus.error=Virhe
commitstatus.pending=Odottaa
projects=Projektit
projects.description_placeholder=Kuvaus
projects.create=Luo projekti
projects.title=Otsikko
@ -1002,6 +1001,7 @@ pulls.can_auto_merge_desc=Tämä pull-pyyntö voidaan yhdistää automaattisesti
milestones.new=Uusi merkkipaalu
milestones.closed=Suljettu %s
milestones.no_due_date=Ei määräpäivää

View File

@ -1149,7 +1149,7 @@ branches=Branches
tags=Étiquettes
issues=Tickets
pulls=Demandes d'ajout
project_board=Projets
projects=Projets
packages=Paquets
actions=Actions
labels=Labels
@ -1306,8 +1306,6 @@ commitstatus.success=Succès
ext_issues=Accès aux tickets externes
ext_issues.desc=Lien vers un gestionnaire de tickets externe.
projects=Projets
projects.desc=Gérer les tickets et les demandes dajouts dans les tableaux de projet.
projects.description=Description (facultative)
projects.description_placeholder=Description
projects.create=Créer un projet
@ -1826,6 +1824,7 @@ pulls.recently_pushed_new_branches=Vous avez soumis sur la branche <strong>%[1]s
pull.deleted_branch=(supprimé) : %s
milestones.new=Nouveau jalon
milestones.closed=%s fermé
milestones.update_ago=Actualisé %s

View File

@ -670,7 +670,7 @@ branches=Ágak
tags=Címkék
issues=Hibajegyek
pulls=Egyesítési kérések
project_board=Projektek
projects=Projektek
labels=Címkék
org_labels_desc_manage=kezelés
@ -736,7 +736,6 @@ commitstatus.pending=Függőben
ext_issues.desc=Külső hibakövető csatlakoztatás.
projects=Projektek
projects.description_placeholder=Leírás
projects.title=Cím
projects.new=Új projekt
@ -949,6 +948,7 @@ pulls.status_checks_success=Minden ellenőrzés sikeres volt
milestones.new=Új mérföldkő
milestones.closed=Lezárva: %s
milestones.no_due_date=Nincs határidő

View File

@ -763,6 +763,7 @@ pulls.can_auto_merge_desc=Permintaan tarik ini dapat digabung secara otomatis.
milestones.new=Milestone Baru
milestones.closed=Tertutup %s
milestones.no_due_date=Tidak ada jatuh tempo

View File

@ -660,7 +660,7 @@ branches=Greinar
tags=Merki
issues=Vandamál
pulls=Sameiningarbeiðnir
project_board=Verkefni
projects=Verkefni
packages=Pakkar
labels=Skýringar
@ -714,7 +714,6 @@ commitstatus.error=Villa
commitstatus.pending=Í bið
projects=Verkefni
projects.description=Lýsing (valfrjálst)
projects.description_placeholder=Lýsing
projects.create=Stofna Verkefni
@ -912,6 +911,7 @@ pulls.status_checks_details=Nánar
milestones.new=Nýtt tímamót
milestones.closed=Lokaði %s
milestones.no_due_date=Enginn eindagi

View File

@ -954,7 +954,7 @@ branches=Rami (Branch)
tags=Tag
issues=Problemi
pulls=Pull Requests
project_board=Progetti
projects=Progetti
packages=Pacchetti
labels=Etichette
org_labels_desc=Etichette a livello di organizzazione che possono essere utilizzate con <strong>tutti i repository</strong> sotto questa organizzazione
@ -1072,8 +1072,6 @@ commitstatus.pending=In sospeso
ext_issues=Accesso ai Problemi Esterni
ext_issues.desc=Collegamento al puntatore di una issue esterna.
projects=Progetti
projects.desc=Gestisci problemi e pull nelle schede di progetto.
projects.description=Descrizione (opzionale)
projects.description_placeholder=Descrizione
projects.create=Crea un progetto
@ -1500,6 +1498,7 @@ pulls.delete.text=Vuoi davvero eliminare questo problema? (Questo rimuoverà per
milestones.new=Nuova Milestone
milestones.closed=Chiuso %s
milestones.no_due_date=Nessuna data di scadenza

View File

@ -1215,7 +1215,7 @@ branches=ブランチ
tags=タグ
issues=イシュー
pulls=プルリクエスト
project_board=プロジェクト
projects=プロジェクト
packages=パッケージ
actions=Actions
labels=ラベル
@ -1378,8 +1378,6 @@ commitstatus.success=成功
ext_issues=外部イシューへのアクセス
ext_issues.desc=外部のイシュートラッカーへのリンク。
projects=プロジェクト
projects.desc=プロジェクトボードでイシューとプルを管理します。
projects.description=説明 (オプション)
projects.description_placeholder=説明
projects.create=プロジェクトを作成
@ -1903,6 +1901,7 @@ pulls.recently_pushed_new_branches=%[2]s 、あなたはブランチ <strong>%[1
pull.deleted_branch=(削除済み):%s
milestones.new=新しいマイルストーン
milestones.closed=%s にクローズ
milestones.update_ago=%sに更新

View File

@ -862,6 +862,7 @@ pulls.invalid_merge_option=이 풀 리퀘스트에서 설정한 머지 옵션을
milestones.new=새로운 마일스톤
milestones.closed=닫힘 %s
milestones.no_due_date=기한 없음

View File

@ -1139,7 +1139,7 @@ branches=Atzari
tags=Tagi
issues=Problēmas
pulls=Izmaiņu pieprasījumi
project_board=Projekti
projects=Projekti
packages=Pakotnes
actions=Darbības
labels=Iezīmes
@ -1294,8 +1294,6 @@ commitstatus.success=Pabeigts
ext_issues=Piekļuve ārējām problēmām
ext_issues.desc=Saite uz ārējo problēmu sekotāju.
projects=Projekti
projects.desc=Pārvaldīt problēmu un izmaiņu pieprasījumu projektu dēļus.
projects.description=Apraksts (neobligāts)
projects.description_placeholder=Apraksts
projects.create=Izveidot projektu
@ -1812,6 +1810,7 @@ pulls.recently_pushed_new_branches=Tu iesūtīji izmaiņas atzarā <strong>%[1]s
pull.deleted_branch=(izdzēsts):%s
milestones.new=Jauns atskaites punkts
milestones.closed=Aizvērts %s
milestones.update_ago=Atjaunots %s

View File

@ -952,7 +952,7 @@ branches=Branches
tags=Labels
issues=Kwesties
pulls=Pull-aanvragen
project_board=Projecten
projects=Projecten
packages=Paketten
labels=Labels
org_labels_desc=Organisatielabel dat gebruikt kan worden met <strong>alle repositories</strong> onder deze organisatie
@ -1070,8 +1070,6 @@ commitstatus.pending=In behandeling
ext_issues=Toegang tot Externe Issues
ext_issues.desc=Koppelen aan een externe kwestie-tracker.
projects=Projecten
projects.desc=Beheer issues en pulls in projectborden.
projects.description=Omschrijving (optioneel)
projects.description_placeholder=Omschrijving
projects.create=Project aanmaken
@ -1495,6 +1493,7 @@ pulls.delete.text=Weet je zeker dat je deze pull-verzoek wilt verwijderen? (Dit
milestones.new=Nieuwe mijlpaal
milestones.closed=%s werd gesloten
milestones.no_due_date=Geen vervaldatum

View File

@ -894,7 +894,7 @@ branches=Gałęzie
tags=Tagi
issues=Zgłoszenia
pulls=Oczekujące zmiany
project_board=Projekty
projects=Projekty
labels=Etykiety
org_labels_desc=Etykiety organizacji, które mogą być używane z <strong>wszystkimi repozytoriami</strong> w tej organizacji
org_labels_desc_manage=zarządzaj
@ -987,7 +987,6 @@ commitstatus.pending=Oczekująca
ext_issues.desc=Link do zewnętrznego systemu śledzenia zgłoszeń.
projects=Projekty
projects.description=Opis (opcjonalnie)
projects.description_placeholder=Opis
projects.create=Utwórz projekt
@ -1347,6 +1346,7 @@ pulls.reopened_at=`otworzył(-a) ponownie ten Pull Request <a id="%[1]s" href="#
milestones.new=Nowy kamień milowy
milestones.closed=Zamknięto %s
milestones.no_due_date=Nie ustalono terminu

View File

@ -1134,7 +1134,7 @@ branches=Branches
tags=Tags
issues=Issues
pulls=Pull requests
project_board=Projetos
projects=Projetos
packages=Pacotes
actions=Ações
labels=Etiquetas
@ -1290,8 +1290,6 @@ commitstatus.success=Sucesso
ext_issues=Acesso a Issues Externos
ext_issues.desc=Link para o issue tracker externo.
projects=Projetos
projects.desc=Gerencie issues e PRs nos quadros do projeto.
projects.description=Descrição (opcional)
projects.description_placeholder=Descrição
projects.create=Criar Projeto
@ -1804,6 +1802,7 @@ pulls.recently_pushed_new_branches=Você fez push no branch <strong>%[1]s</stron
pull.deleted_branch=(excluído):%s
milestones.new=Novo marco
milestones.closed=Fechado %s
milestones.update_ago=Atualizado há %s

View File

@ -1215,7 +1215,7 @@ branches=Ramos
tags=Etiquetas
issues=Questões
pulls=Pedidos de integração
project_board=Planeamentos
projects=Planeamentos
packages=Pacotes
actions=Operações
labels=Rótulos
@ -1378,8 +1378,6 @@ commitstatus.success=Sucesso
ext_issues=Acesso a questões externas
ext_issues.desc=Ligação para um rastreador de questões externo.
projects=Planeamentos
projects.desc=Gerir questões e integrações nos quadros do planeamento.
projects.description=Descrição (opcional)
projects.description_placeholder=Descrição
projects.create=Criar planeamento
@ -1903,6 +1901,7 @@ pulls.recently_pushed_new_branches=Enviou para o ramo <strong>%[1]s</strong> %[2
pull.deleted_branch=(eliminado):%s
milestones.new=Nova etapa
milestones.closed=Encerrada %s
milestones.update_ago=Modificou %s

View File

@ -1117,7 +1117,7 @@ branches=Ветки
tags=Теги
issues=Задачи
pulls=Запросы на слияние
project_board=Проекты
projects=Проекты
packages=Пакеты
actions=Действия
labels=Метки
@ -1269,8 +1269,6 @@ commitstatus.success=Успешно
ext_issues=Доступ к внешним задачам
ext_issues.desc=Ссылка на внешнюю систему отслеживания ошибок.
projects=Проекты
projects.desc=Управление задачами и pull'ами в досках проекта.
projects.description=Описание (необязательно)
projects.description_placeholder=Описание
projects.create=Создать проект
@ -1774,6 +1772,7 @@ pulls.delete.text=Вы действительно хотите удалить э
pull.deleted_branch=(удалена):%s
milestones.new=Новый этап
milestones.closed=Закрыт %s
milestones.update_ago=Обновлено %s

View File

@ -863,7 +863,7 @@ branches=ශාඛා
tags=ටැග්
issues=ගැටළු
pulls=ඉල්ලීම් අදින්න
project_board=ව්‍යාපෘති
projects=ව්‍යාපෘති
labels=ලේබල
org_labels_desc=මෙම සංවිධානය යටතේ <strong>සියලුම ගබඩාවලදී</strong> සමඟ භාවිතා කළ හැකි සංවිධාන මට්ටමේ ලේබල්
org_labels_desc_manage=කළමනාකරණය
@ -958,8 +958,6 @@ commitstatus.pending=වංගු
ext_issues.desc=බාහිර නිකුතුවකට සම්බන්ධ වන්න ට්රැකර්.
projects=ව්‍යාපෘති
projects.desc=ව්යාපෘති මණ්ඩලවල ගැටළු සහ අදින කළමනාකරණය කිරීම.
projects.description=විස්තරය (විකල්ප)
projects.description_placeholder=සවිස්තරය
projects.create=ව්‍යාපෘතිය සාදන්න
@ -1340,6 +1338,7 @@ pulls.reopened_at=`මෙම අදින්න ඉල්ලීම නැවත
milestones.new=නව සන්ධිස්ථානයක්
milestones.closed=%s වසා ඇත
milestones.no_due_date=නියමිත දිනයක් නැත

View File

@ -981,7 +981,7 @@ find_tag=Hľadať tag
branches=Vetvy
tags=Tagy
pulls=Pull requesty
project_board=Projekty
projects=Projekty
packages=Balíčky
actions=Akcie
labels=Štítky
@ -1050,7 +1050,6 @@ commit.cherry-pick-content=Vyberte vetvu pre cherry-pick na:
commitstatus.error=Chyba
projects=Projekty
projects.title=Názov
projects.new=Nový projekt
projects.deletion=Vymazať projekt
@ -1121,6 +1120,7 @@ pulls.merge_commit_id=ID zlučovacieho commitu
milestones.open=Otvoriť
milestones.close=Zavrieť
milestones.cancel=Zrušiť

View File

@ -734,7 +734,7 @@ branches=Grenar
tags=Taggar
issues=Ärenden
pulls=Pull-förfrågningar
project_board=Projekt
projects=Projekt
labels=Etiketter
org_labels_desc=Etiketter på organisationsnivå som kan användas i <strong>alla utvecklingskataloger</strong> tillhörande denna organisation
org_labels_desc_manage=hantera
@ -814,7 +814,6 @@ commitstatus.pending=Väntande
ext_issues.desc=Länk till externt ärendehanteringssystem.
projects=Projekt
projects.description_placeholder=Beskrivning
projects.create=Skapa projekt
projects.title=Titel
@ -1119,6 +1118,7 @@ pulls.outdated_with_base_branch=Denna branch är föråldrad gentemot bas-branch
milestones.new=Ny milstolpe
milestones.closed=Stängt %s
milestones.no_due_date=Inget förfallodatum

View File

@ -763,6 +763,7 @@ manage_themes=Varsayılan temayı seç
manage_openid=OpenID Adreslerini Yönet
email_desc=Ana e-posta adresiniz bildirimler, parola kurtarma ve gizlenmemişse eğer web tabanlı Git işlemleri için kullanılacaktır.
theme_desc=Bu, sitedeki varsayılan temanız olacak.
theme_colorblindness_help=Renk Körlüğü için Tema Desteği
primary=Birincil
activated=Aktifleştirildi
requires_activation=Etkinleştirme gerekiyor
@ -1212,7 +1213,7 @@ branches=Dal
tags=Etiket
issues=Konular
pulls=Değişiklik İstekleri
project_board=Projeler
projects=Projeler
packages=Paketler
actions=İşlemler
labels=Etiketler
@ -1375,8 +1376,6 @@ commitstatus.success=Başarılı
ext_issues=Harici Konulara Erişim
ext_issues.desc=Dışsal konu takip sistemine bağla.
projects=Projeler
projects.desc=Proje panolarındaki konuları ve değişiklikleri yönetin.
projects.description=ıklama (isteğe bağlı)
projects.description_placeholder=ıklama
projects.create=Proje Oluştur
@ -1900,6 +1899,7 @@ pulls.recently_pushed_new_branches=<strong>%[1]s</strong> dalına ittiniz %[2]s
pull.deleted_branch=(silindi): %s
milestones.new=Yeni Kilometre Taşı
milestones.closed=Kapalı %s
milestones.update_ago=%s tarihinde güncellendi

View File

@ -899,7 +899,7 @@ branches=Гілки
tags=Теги
issues=Задачі
pulls=Запити на злиття
project_board=Проєкти
projects=Проєкти
labels=Мітки
org_labels_desc=Мітки рівня організації можуть використовуватися <strong>в усіх репозиторіях</strong> цієї організації
org_labels_desc_manage=керувати
@ -995,8 +995,6 @@ commitstatus.pending=Очікування
ext_issues=Доступ до зовнішніх задач
ext_issues.desc=Посилання на зовнішню систему відстеження задач.
projects=Проєкти
projects.desc=Керуйте задачами та запитами злиття на дошках проєкту.
projects.description=Опис (необов'язково)
projects.description_placeholder=Опис
projects.create=Створити проєкт
@ -1387,6 +1385,7 @@ pulls.reopened_at=`повторно відкрив цей запит на зли
milestones.new=Новий етап
milestones.closed=Закрито %s
milestones.no_due_date=Немає дати завершення

View File

@ -1215,7 +1215,7 @@ branches=分支列表
tags=标签列表
issues=工单
pulls=合并请求
project_board=项目
projects=项目
packages=软件包
actions=Actions
labels=标签
@ -1378,8 +1378,6 @@ commitstatus.success=成功
ext_issues=访问外部工单
ext_issues.desc=链接到外部工单跟踪系统。
projects=项目
projects.desc=在项目看板中管理工单和合并请求。
projects.description=描述(可选)
projects.description_placeholder=描述
projects.create=创建项目
@ -1903,6 +1901,7 @@ pulls.recently_pushed_new_branches=您已经于%[2]s推送了分支 <strong>%[1]
pull.deleted_branch=(已删除): %s
milestones.new=新的里程碑
milestones.closed=于 %s关闭
milestones.update_ago=已更新 %s

View File

@ -500,6 +500,7 @@ pulls.can_auto_merge_desc=這個拉請求可以自動合併。
milestones.new=新的里程碑
milestones.closed=於 %s關閉
milestones.no_due_date=暫無截止日期

View File

@ -1035,7 +1035,7 @@ branches=分支
tags=標籤
issues=問題
pulls=合併請求
project_board=專案
projects=專案
packages=套件
actions=Actions
labels=標籤
@ -1176,8 +1176,6 @@ commitstatus.success=成功
ext_issues=存取外部問題
ext_issues.desc=連結到外部問題追蹤器。
projects=專案
projects.desc=在專案看板中管理問題與合併請求。
projects.description=描述 (選用)
projects.description_placeholder=描述
projects.create=建立專案
@ -1641,6 +1639,7 @@ pulls.delete.text=您真的要刪除此合併請求嗎?(這將會永久移除
milestones.new=新增里程碑
milestones.closed=於 %s關閉
milestones.update_ago=已更新 %s

632
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,11 +17,11 @@
"add-asset-webpack-plugin": "3.0.0",
"ansi_up": "6.0.2",
"asciinema-player": "3.7.1",
"chart.js": "4.4.2",
"chart.js": "4.4.3",
"chartjs-adapter-dayjs-4": "1.0.4",
"chartjs-plugin-zoom": "2.0.1",
"clippie": "4.1.1",
"css-loader": "7.1.1",
"css-loader": "7.1.2",
"dayjs": "1.11.11",
"dropzone": "6.0.0-beta.2",
"easymde": "2.18.0",
@ -33,17 +33,17 @@
"jquery": "3.7.1",
"katex": "0.16.10",
"license-checker-webpack-plugin": "0.2.1",
"mermaid": "10.9.0",
"mermaid": "10.9.1",
"mini-css-extract-plugin": "2.9.0",
"minimatch": "9.0.4",
"monaco-editor": "0.48.0",
"monaco-editor": "0.49.0",
"monaco-editor-webpack-plugin": "7.1.0",
"pdfobject": "2.3.0",
"postcss": "8.4.38",
"postcss-loader": "8.1.1",
"postcss-nesting": "12.1.2",
"postcss-nesting": "12.1.5",
"sortablejs": "1.15.2",
"swagger-ui-dist": "5.17.7",
"swagger-ui-dist": "5.17.13",
"tailwindcss": "3.4.3",
"temporal-polyfill": "0.2.4",
"throttle-debounce": "5.0.0",
@ -63,19 +63,19 @@
},
"devDependencies": {
"@eslint-community/eslint-plugin-eslint-comments": "4.3.0",
"@playwright/test": "1.44.0",
"@playwright/test": "1.44.1",
"@stoplight/spectral-cli": "6.11.1",
"@stylistic/eslint-plugin-js": "2.1.0",
"@stylistic/stylelint-plugin": "2.1.2",
"@vitejs/plugin-vue": "5.0.4",
"eslint": "8.57.0",
"eslint-plugin-array-func": "4.0.0",
"eslint-plugin-github": "4.10.2",
"eslint-plugin-github": "5.0.0-2",
"eslint-plugin-i": "2.29.1",
"eslint-plugin-jquery": "1.5.1",
"eslint-plugin-no-jquery": "2.7.0",
"eslint-plugin-no-use-extend-native": "0.5.0",
"eslint-plugin-regexp": "2.5.0",
"eslint-plugin-regexp": "2.6.0",
"eslint-plugin-sonarjs": "1.0.3",
"eslint-plugin-unicorn": "53.0.0",
"eslint-plugin-vitest": "0.4.1",
@ -83,15 +83,15 @@
"eslint-plugin-vue": "9.26.0",
"eslint-plugin-vue-scoped-css": "2.8.0",
"eslint-plugin-wc": "2.1.0",
"happy-dom": "14.10.1",
"markdownlint-cli": "0.40.0",
"happy-dom": "14.11.1",
"markdownlint-cli": "0.41.0",
"postcss-html": "1.7.0",
"stylelint": "16.5.0",
"stylelint": "16.6.0",
"stylelint-declaration-block-no-ignored-properties": "2.8.0",
"stylelint-declaration-strict-value": "1.10.4",
"stylelint-value-no-unknown-custom-properties": "6.0.1",
"svgo": "3.3.2",
"updates": "16.0.1",
"updates": "16.1.1",
"vite-string-plugin": "1.3.1",
"vitest": "1.6.0"
},

View File

@ -810,8 +810,13 @@ func EditIssue(ctx *context.APIContext) {
}
}
if form.Body != nil {
err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body)
err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body, issue.ContentVersion)
if err != nil {
if errors.Is(err, issues_model.ErrIssueAlreadyChanged) {
ctx.Error(http.StatusBadRequest, "ChangeContent", err)
return
}
ctx.Error(http.StatusInternalServerError, "ChangeContent", err)
return
}

View File

@ -198,7 +198,7 @@ func CreateIssueAttachment(ctx *context.APIContext) {
issue.Attachments = append(issue.Attachments, attachment)
if err := issue_service.ChangeContent(ctx, issue, ctx.Doer, issue.Content); err != nil {
if err := issue_service.ChangeContent(ctx, issue, ctx.Doer, issue.Content, issue.ContentVersion); err != nil {
ctx.Error(http.StatusInternalServerError, "ChangeContent", err)
return
}

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