Merge remote-tracking branch 'upstream/main' into issue-updates

This commit is contained in:
Anbraten 2024-02-16 15:24:32 +01:00
commit 21485f89a3
188 changed files with 3524 additions and 1332 deletions

View File

@ -1,6 +1,6 @@
{
"name": "Gitea DevContainer",
"image": "mcr.microsoft.com/devcontainers/go:1.21-bullseye",
"image": "mcr.microsoft.com/devcontainers/go:1.22-bullseye",
"features": {
// installs nodejs into container
"ghcr.io/devcontainers/features/node:1": {

View File

@ -10,10 +10,19 @@ tasks:
- name: Run backend
command: |
gp sync-await setup
if [ ! -f custom/conf/app.ini ]
then
# Get the URL and extract the domain
url=$(gp url 3000)
domain=$(echo $url | awk -F[/:] '{print $4}')
if [ -f custom/conf/app.ini ]; then
sed -i "s|^ROOT_URL =.*|ROOT_URL = ${url}/|" custom/conf/app.ini
sed -i "s|^DOMAIN =.*|DOMAIN = ${domain}|" custom/conf/app.ini
sed -i "s|^SSH_DOMAIN =.*|SSH_DOMAIN = ${domain}|" custom/conf/app.ini
sed -i "s|^NO_REPLY_ADDRESS =.*|SSH_DOMAIN = noreply.${domain}|" custom/conf/app.ini
else
mkdir -p custom/conf/
echo -e "[server]\nROOT_URL=$(gp url 3000)/" > custom/conf/app.ini
echo -e "[server]\nROOT_URL = ${url}/" > custom/conf/app.ini
echo -e "\n[database]\nDB_TYPE = sqlite3\nPATH = $GITPOD_REPO_ROOT/data/gitea.db" >> custom/conf/app.ini
fi
export TAGS="sqlite sqlite_unlock_notify"

View File

@ -47,6 +47,7 @@
- [Release Cycle](#release-cycle)
- [Maintainers](#maintainers)
- [Technical Oversight Committee (TOC)](#technical-oversight-committee-toc)
- [TOC election process](#toc-election-process)
- [Current TOC members](#current-toc-members)
- [Previous TOC/owners members](#previous-tocowners-members)
- [Governance Compensation](#governance-compensation)
@ -486,36 +487,53 @@ if possible provide GPG signed commits.
https://help.github.com/articles/securing-your-account-with-two-factor-authentication-2fa/
https://help.github.com/articles/signing-commits-with-gpg/
Furthermore, any account with write access (like bots and TOC members) **must** use 2FA.
https://help.github.com/articles/securing-your-account-with-two-factor-authentication-2fa/
## Technical Oversight Committee (TOC)
At the start of 2023, the `Owners` team was dissolved. Instead, the governance charter proposed a technical oversight committee (TOC) which expands the ownership team of the Gitea project from three elected positions to six positions. Three positions would be elected as it has been over the past years, and the other three would consist of appointed members from the Gitea company.
At the start of 2023, the `Owners` team was dissolved. Instead, the governance charter proposed a technical oversight committee (TOC) which expands the ownership team of the Gitea project from three elected positions to six positions. Three positions are elected as it has been over the past years, and the other three consist of appointed members from the Gitea company.
https://blog.gitea.com/quarterly-23q1/
When the new community members have been elected, the old members will give up ownership to the newly elected members. For security reasons, TOC members or any account with write access (like a bot) must use 2FA.
https://help.github.com/articles/securing-your-account-with-two-factor-authentication-2fa/
### TOC election process
Any maintainer is eligible to be part of the community TOC if they are not associated with the Gitea company.
A maintainer can either nominate themselves, or can be nominated by other maintainers to be a candidate for the TOC election.
If you are nominated by someone else, you must first accept your nomination before the vote starts to be a candidate.
The TOC is elected for one year, the TOC election happens yearly.
After the announcement of the results of the TOC election, elected members have two weeks time to confirm or refuse the seat.
If an elected member does not answer within this timeframe, they are automatically assumed to refuse the seat.
Refusals result in the person with the next highest vote getting the same choice.
As long as seats are empty in the TOC, members of the previous TOC can fill them until an elected member accepts the seat.
If an elected member that accepts the seat does not have 2FA configured yet, they will be temporarily counted as `answer pending` until they manage to configure 2FA, thus leaving their seat empty for this duration.
### Current TOC members
- 2023-01-01 ~ 2023-12-31 - https://blog.gitea.com/quarterly-23q1/
- 2024-01-01 ~ 2024-12-31
- Company
- [Jason Song](https://gitea.com/wolfogre) <i@wolfogre.com>
- [Lunny Xiao](https://gitea.com/lunny) <xiaolunwen@gmail.com>
- [Matti Ranta](https://gitea.com/techknowlogick) <techknowlogick@gitea.io>
- [Matti Ranta](https://gitea.com/techknowlogick) <techknowlogick@gitea.com>
- Community
- [6543](https://gitea.com/6543) <6543@obermui.de>
- [Andrew Thornton](https://gitea.com/zeripath) <art27@cantab.net>
- [delvh](https://gitea.com/delvh) <dev.lh@web.de>
- [John Olheiser](https://gitea.com/jolheiser) <john.olheiser@gmail.com>
### Previous TOC/owners members
Here's the history of the owners and the time they served:
- [Lunny Xiao](https://gitea.com/lunny) - 2016, 2017, [2018](https://github.com/go-gitea/gitea/issues/3255), [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872)
- [Lunny Xiao](https://gitea.com/lunny) - 2016, 2017, [2018](https://github.com/go-gitea/gitea/issues/3255), [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023
- [Kim Carlbäcker](https://github.com/bkcsoft) - 2016, 2017
- [Thomas Boerger](https://gitea.com/tboerger) - 2016, 2017
- [Lauris Bukšis-Haberkorns](https://gitea.com/lafriks) - [2018](https://github.com/go-gitea/gitea/issues/3255), [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801)
- [Matti Ranta](https://gitea.com/techknowlogick) - [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872)
- [Andrew Thornton](https://gitea.com/zeripath) - [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872)
- [Matti Ranta](https://gitea.com/techknowlogick) - [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023
- [Andrew Thornton](https://gitea.com/zeripath) - [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023
- [6543](https://gitea.com/6543) - 2023
- [John Olheiser](https://gitea.com/jolheiser) - 2023
- [Jason Song](https://gitea.com/wolfogre) - 2023
## Governance Compensation

View File

@ -1,5 +1,5 @@
# Build stage
FROM docker.io/library/golang:1.21-alpine3.19 AS build-env
FROM docker.io/library/golang:1.22-alpine3.19 AS build-env
ARG GOPROXY
ENV GOPROXY ${GOPROXY:-direct}

View File

@ -1,5 +1,5 @@
# Build stage
FROM docker.io/library/golang:1.21-alpine3.19 AS build-env
FROM docker.io/library/golang:1.22-alpine3.19 AS build-env
ARG GOPROXY
ENV GOPROXY ${GOPROXY:-direct}

View File

@ -23,12 +23,12 @@ SHASUM ?= shasum -a 256
HAS_GO := $(shell hash $(GO) > /dev/null 2>&1 && echo yes)
COMMA := ,
XGO_VERSION := go-1.21.x
XGO_VERSION := go-1.22.x
AIR_PACKAGE ?= github.com/cosmtrek/air@v1.49.0
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/cmd/editorconfig-checker@2.7.0
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.6.0
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.2
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.56.1
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.11
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.4.1
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.30.5
@ -988,3 +988,8 @@ docker:
# This endif closes the if at the top of the file
endif
# Disable parallel execution because it would break some targets that don't
# specify exact dependencies like 'backend' which does currently not depend
# on 'frontend' to enable Node.js-less builds from source tarballs.
.NOTPARALLEL:

View File

@ -89,25 +89,23 @@ The `build` target is split into two sub-targets:
Internet connectivity is required to download the go and npm modules. When building from the official source tarballs which include pre-built frontend files, the `frontend` target will not be triggered, making it possible to build without Node.js.
Parallelism (`make -j <num>`) is not supported.
More info: https://docs.gitea.com/installation/install-from-source
## Using
./gitea web
NOTE: If you're interested in using our APIs, we have experimental
support with [documentation](https://try.gitea.io/api/swagger).
> [!NOTE]
> If you're interested in using our APIs, we have experimental support with [documentation](https://try.gitea.io/api/swagger).
## Contributing
Expected workflow is: Fork -> Patch -> Push -> Pull Request
NOTES:
1. **YOU MUST READ THE [CONTRIBUTORS GUIDE](CONTRIBUTING.md) BEFORE STARTING TO WORK ON A PULL REQUEST.**
2. If you have found a vulnerability in the project, please write privately to **security@gitea.io**. Thanks!
> [!NOTE]
>
> 1. **YOU MUST READ THE [CONTRIBUTORS GUIDE](CONTRIBUTING.md) BEFORE STARTING TO WORK ON A PULL REQUEST.**
> 2. If you have found a vulnerability in the project, please write privately to **security@gitea.io**. Thanks!
## Translating

View File

@ -1044,7 +1044,7 @@ LEVEL = Info
;; List of keywords used in Pull Request comments to automatically reopen a related issue
;REOPEN_KEYWORDS = reopen,reopens,reopened
;;
;; Set default merge style for repository creating, valid options: merge, rebase, rebase-merge, squash
;; Set default merge style for repository creating, valid options: merge, rebase, rebase-merge, squash, fast-forward-only
;DEFAULT_MERGE_STYLE = merge
;;
;; In the default merge message for squash commits include at most this many commits

View File

@ -19,6 +19,12 @@ menu:
Gitea 已经实现了 `dump` 命令可以用来备份所有需要的文件到一个zip压缩文件。该压缩文件可以被用来进行数据恢复。
## 备份一致性
为了确保 Gitea 实例的一致性,在备份期间必须关闭它。
Gitea 包括数据库、文件和 Git 仓库,当它被使用时所有这些都会发生变化。例如,当迁移正在进行时,在数据库中创建一个事务,而 Git 仓库正在被复制。如果备份发生在迁移的中间Git 仓库可能是不完整的,尽管数据库声称它是完整的,因为它是在之后被转储的。避免这种竞争条件的唯一方法是在备份期间停止 Gitea 实例。
## 备份命令 (`dump`)
先转到git用户的权限: `su git`. 再Gitea目录运行 `./gitea dump`。一般会显示类似如下的输出:
@ -34,15 +40,43 @@ Gitea 已经实现了 `dump` 命令可以用来备份所有需要的文件到一
最后生成的 `gitea-dump-1482906742.zip` 文件将会包含如下内容:
* `custom` - 所有保存在 `custom/` 目录下的配置和自定义的文件。
* `data` - 数据目录下的所有内容不包含使用文件session的文件。该目录包含 `attachments`, `avatars`, `lfs`, `indexers`, 如果使用sqlite 还会包含 sqlite 数据库文件。
* `app.ini` - 如果原先存储在默认的 custom/ 目录之外,则是配置文件的可选副本
* `custom/` - 所有保存在 `custom/` 目录下的配置和自定义的文件。
* `data/` - 数据目录APP_DATA_PATH如果使用文件会话则不包括会话。该目录包括 `attachments``avatars``lfs``indexers`、如果使用 SQLite 则包括 SQLite 文件。
* `repos/` - 仓库目录的完整副本。
* `gitea-db.sql` - 数据库dump出来的 SQL。
* `gitea-repo.zip` - Git仓库压缩文件。
* `log/` - Logs文件如果用作迁移不是必须的。
中间备份文件将会在临时目录进行创建,如果您要重新指定临时目录,可以用 `--tempdir` 参数,或者用 `TMPDIR` 环境变量。
## Restore Command (`restore`)
## 备份数据库
`gitea dump` 创建的 SQL 转储使用 XORMGitea 管理员可能更喜欢使用本地的 MySQL 和 PostgreSQL 转储工具。使用 XORM 转储数据库时仍然存在一些问题,可能会导致在尝试恢复时出现问题。
```sh
# mysql
mysqldump -u$USER -p$PASS --database $DATABASE > gitea-db.sql
# postgres
pg_dump -U $USER $DATABASE > gitea-db.sql
```
### 使用Docker `dump`
在使用 Docker 时,使用 `dump` 命令有一些注意事项。
必须以 `gitea/conf/app.ini` 中指定的 `RUN_USER = <OS_USERNAME>` 执行该命令;并且,为了让备份文件夹的压缩过程能够顺利执行,`docker exec` 命令必须在 `--tempdir` 内部执行。
示例:
```none
docker exec -u <OS_USERNAME> -it -w <--tempdir> $(docker ps -qf 'name=^<NAME_OF_DOCKER_CONTAINER>$') bash -c '/usr/local/bin/gitea dump -c </path/to/app.ini>'
```
\*注意:`--tempdir` 指的是 Gitea 使用的 Docker 环境的临时目录;如果您没有指定自定义的 `--tempdir`,那么 Gitea 将使用 `/tmp` 或 Docker 容器的 `TMPDIR` 环境变量。对于 `--tempdir`,请相应调整您的 `docker exec` 命令选项。
结果应该是一个文件,存储在指定的 `--tempdir` 中,类似于:`gitea-dump-1482906742.zip`
## 恢复命令 (`restore`)
当前还没有恢复命令,恢复需要人工进行。主要是把文件和数据库进行恢复。
@ -51,10 +85,10 @@ Gitea 已经实现了 `dump` 命令可以用来备份所有需要的文件到一
```sh
unzip gitea-dump-1610949662.zip
cd gitea-dump-1610949662
mv data/conf/app.ini /etc/gitea/conf/app.ini
mv app.ini /etc/gitea/conf/app.ini
mv data/* /var/lib/gitea/data/
mv log/* /var/lib/gitea/log/
mv repos/* /var/lib/gitea/repositories/
mv repos/* /var/lib/gitea/gitea-repositories/
chown -R gitea:gitea /etc/gitea/conf/app.ini /var/lib/gitea
# mysql
@ -66,3 +100,55 @@ psql -U $USER -d $DATABASE < gitea-db.sql
service gitea restart
```
如果安装方式发生了变化(例如 二进制 -> Docker或者 Gitea 安装到了与之前安装不同的目录,则需要重新生成仓库 Git 钩子。
在 Gitea 运行时,并从 Gitea 二进制文件所在的目录执行:`./gitea admin regenerate hooks`
这样可以确保仓库 Git 钩子中的应用程序和配置文件路径与当前安装一致。如果这些路径没有更新,仓库的 `push` 操作将失败。
### 使用 Docker (`restore`)
在基于 Docker 的 Gitea 实例中,也没有恢复命令的支持。恢复过程与前面描述的步骤相同,但路径不同。
示例:
```sh
# 在容器中打开 bash 会话
docker exec --user git -it 2a83b293548e bash
# 在容器内解压您的备份文件
unzip gitea-dump-1610949662.zip
cd gitea-dump-1610949662
# 恢复 Gitea 数据
mv data/* /data/gitea
# 恢复仓库本身
mv repos/* /data/git/gitea-repositories/
# 调整文件权限
chown -R git:git /data
# 重新生成 Git 钩子
/usr/local/bin/gitea -c '/data/gitea/conf/app.ini' admin regenerate hooks
```
Gitea 容器中的默认用户是 `git`1000:1000。请用您的 Gitea 容器 ID 或名称替换 `2a83b293548e`
### 使用 Docker-rootless (`restore`)
在 Docker-rootless 容器中的恢复工作流程只是要使用的目录不同:
```sh
# 在容器中打开 bash 会话
docker exec --user git -it 2a83b293548e bash
# 在容器内解压您的备份文件
unzip gitea-dump-1610949662.zip
cd gitea-dump-1610949662
# 恢复 app.ini
mv data/conf/app.ini /etc/gitea/app.ini
# 恢复 Gitea 数据
mv data/* /var/lib/gitea
# 恢复仓库本身
mv repos/* /var/lib/gitea/git/gitea-repositories
# 调整文件权限
chown -R git:git /etc/gitea/app.ini /var/lib/gitea
# 重新生成 Git 钩子
/usr/local/bin/gitea -c '/etc/gitea/app.ini' admin regenerate hooks
```

View File

@ -126,7 +126,7 @@ In addition, there is _`StaticRootPath`_ which can be set as a built-in at build
keywords used in Pull Request comments to automatically close a related issue
- `REOPEN_KEYWORDS`: **reopen**, **reopens**, **reopened**: List of keywords used in Pull Request comments to automatically reopen
a related issue
- `DEFAULT_MERGE_STYLE`: **merge**: Set default merge style for repository creating, valid options: `merge`, `rebase`, `rebase-merge`, `squash`
- `DEFAULT_MERGE_STYLE`: **merge**: Set default merge style for repository creating, valid options: `merge`, `rebase`, `rebase-merge`, `squash`, `fast-forward-only`
- `DEFAULT_MERGE_MESSAGE_COMMITS_LIMIT`: **50**: In the default merge message for squash commits include at most this many commits. Set to `-1` to include all commits
- `DEFAULT_MERGE_MESSAGE_SIZE`: **5120**: In the default merge message for squash commits limit the size of the commit messages. Set to `-1` to have no limit. Only used if `POPULATE_SQUASH_COMMENT_WITH_COMMIT_MESSAGES` is `true`.
- `DEFAULT_MERGE_MESSAGE_ALL_AUTHORS`: **false**: In the default merge message for squash commits walk all commits to include all authors in the Co-authored-by otherwise just use those in the limited list

View File

@ -125,7 +125,7 @@ menu:
- `CLOSE_KEYWORDS`: **close**, **closes**, **closed**, **fix**, **fixes**, **fixed**, **resolve**, **resolves**, **resolved**: 在拉取请求评论中用于自动关闭相关问题的关键词列表。
- `REOPEN_KEYWORDS`: **reopen**, **reopens**, **reopened**: 在拉取请求评论中用于自动重新打开相关问题的
关键词列表。
- `DEFAULT_MERGE_STYLE`: **merge**: 设置创建仓库的默认合并方式,可选: `merge`, `rebase`, `rebase-merge`, `squash`
- `DEFAULT_MERGE_STYLE`: **merge**: 设置创建仓库的默认合并方式,可选: `merge`, `rebase`, `rebase-merge`, `squash`, `fast-forward-only`
- `DEFAULT_MERGE_MESSAGE_COMMITS_LIMIT`: **50**: 在默认合并消息中,对于`squash`提交,最多包括此数量的提交。设置为 -1 以包括所有提交。
- `DEFAULT_MERGE_MESSAGE_SIZE`: **5120**: 在默认的合并消息中,对于`squash`提交,限制提交消息的大小。设置为 `-1`以取消限制。仅在`POPULATE_SQUASH_COMMENT_WITH_COMMIT_MESSAGES``true`时使用。
- `DEFAULT_MERGE_MESSAGE_ALL_AUTHORS`: **false**: 在默认合并消息中,对于`squash`提交,遍历所有提交以包括所有作者的`Co-authored-by`,否则仅使用限定列表中的作者。

View File

@ -48,6 +48,7 @@ HTML 页面由[Go HTML Template](https://pkg.go.dev/html/template)渲染。
10. 避免在一个事件监听器中混合不同的事件,优先为每个事件使用独立的事件监听器。
11. 推荐使用自定义事件名称前缀`ce-`
12. Gitea 的 tailwind-style CSS 类使用`gt-`前缀(`gt-relative`),而 Gitea 自身的私有框架级 CSS 类使用`g-`前缀(`g-modal-confirm`)。
13. 尽量避免内联脚本和样式建议将JS代码放入JS文件中并使用CSS类。如果内联脚本和样式不可避免请解释无法避免的原因。
### 可访问性 / ARIA
@ -64,18 +65,21 @@ Gitea使用一些补丁使Fomantic UI更具可访问性参见`aria.js`和`ari
* Vue + Vanilla JS
* Fomantic-UIjQuery
* htmx (部分页面重新加载其他静态组件)
* Vanilla JS
不推荐的实现方式:
* Vue + Fomantic-UIjQuery
* jQuery + Vanilla JS
* htmx + 任何其他需要大量 JavaScript 代码或不必要的功能,如 htmx 脚本 (`hx-on`)
为了保持界面一致Vue 组件可以使用 Fomantic-UI 的 CSS 类。
尽管不建议混合使用不同的框架,
我们使用 htmx 进行简单的交互。您可以在此 [PR](https://github.com/go-gitea/gitea/pull/28908) 中查看一个简单交互的示例,其中应使用 htmx。如果您需要更高级的反应性请不要使用 htmx请使用其他框架Vue/Vanilla JS
但如果混合使用是必要的,并且代码设计良好且易于维护,也可以工作。
### async 函数
### `async` 函数
只有当函数内部存在`await`调用或返回`Promise`时,才将函数标记为`async`
@ -91,6 +95,12 @@ Gitea使用一些补丁使Fomantic UI更具可访问性参见`aria.js`和`ari
这是有意为之的我们想调用异步函数并忽略Promise。
一些 lint 规则和 IDE 也会在未处理返回的 Promise 时发出警告。
### 获取数据
要获取数据,请使用`modules/fetch.js`中的包装函数`GET``POST`等。他们
接受内容的`data`选项,将自动设置 CSRF 令牌并返回
[Response](https://developer.mozilla.org/en-US/docs/Web/API/Response)。
### HTML 属性和 dataset
禁止使用`dataset`,它的驼峰命名行为使得搜索属性变得困难。
@ -132,3 +142,7 @@ Gitea使用一些补丁使Fomantic UI更具可访问性参见`aria.js`和`ari
### Vue3 和 JSX
Gitea 现在正在使用 Vue3。我们决定不引入 JSX以保持 HTML 代码和 JavaScript 代码分离。
### UI示例
Gitea 使用一些自制的 UI 元素并自定义其他元素,以将它们更好地集成到通用 UI 方法中。当在开发模式(`RUN_MODE=dev`)下运行 Gitea 时,在 `http(s)://your-gitea-url:port/devtest` 下会提供一个包含一些标准化 UI 示例的页面。

View File

@ -15,11 +15,64 @@ menu:
identifier: "support"
---
## 需要帮助?
# 支持选项
如果您在使用或者开发过程中遇到问题,请到以下渠道咨询:
- [付费商业支持](https://about.gitea.com/)
- [Discord](https://discord.gg/Gitea)
- [Discourse 论坛](https://discourse.gitea.io/)
- [Matrix](https://matrix.to/#/#gitea-space:matrix.org)
- 注意:大多数 Matrix 频道都与 Discord 中的对应频道桥接,可能在桥接过程中会出现一定程度的不稳定性。
- 中文支持
- [Discourse 中文分类](https://discourse.gitea.io/c/5-category/5)
- QQ 群 328432459
- 到 [GitHub Issue](https://github.com/go-gitea/gitea/issues) 提问(因为项目维护人员来自世界各地,为保证沟通顺畅,请使用英文提问)
- 中文问题到 [Gitea 论坛](https://discourse.gitea.io/c/5-category/5) 提问
- 访问 [Discord Gitea 聊天室 - 英文](https://discord.gg/Gitea)
- 加入 QQ群 328432459 获得进一步的支持
# Bug 报告
如果您发现了 Bug请在 GitHub 上 [创建一个问题](https://github.com/go-gitea/gitea/issues)。
**注意:** 在请求支持时,可能需要准备以下信息,以便帮助者获得所需的所有信息:
1. 您的 `app.ini`(将任何敏感数据进行必要的清除)。
2. 您看到的任何错误消息。
3. Gitea 日志以及与情况相关的所有其他日志。
- 收集 `trace` / `debug` 级别的日志更有用(参见下一节)。
- 在使用 systemd 时,使用 `journalctl --lines 1000 --unit gitea` 收集日志。
- 在使用 Docker 时,使用 `docker logs --tail 1000 <gitea-container>` 收集日志。
4. 可重现的步骤,以便他人能够更快速、更容易地重现和理解问题。
- [try.gitea.io](https://try.gitea.io) 可用于重现问题。
5. 如果遇到慢速/挂起/死锁等问题,请在出现问题时报告堆栈跟踪。
转到 "Site Admin" -> "Monitoring" -> "Stacktrace" -> "Download diagnosis report"。
# 高级 Bug 报告提示
## 更多日志的配置选项
默认情况下,日志以 `info` 级别输出到控制台。
如果您需要设置日志级别和/或从文件中收集日志,
您只需将以下配置复制到您的 `app.ini` 中(删除所有其他 `[log]` 部分),
然后您将在 Gitea 的日志目录中找到 `*.log` 文件(默认为 `%(GITEA_WORK_DIR)/log`)。
```ini
; 要显示所有 SQL 日志,您还可以在 [database] 部分中设置 LOG_SQL=true
[log]
LEVEL=debug
MODE=console,file
```
## 使用命令行收集堆栈跟踪
Gitea 可以使用 Golang 的 pprof 处理程序和工具链来收集堆栈跟踪和其他运行时信息。
如果 Web UI 停止工作,您可以尝试通过命令行收集堆栈跟踪:
1. 设置 app.ini
```
[server]
ENABLE_PPROF = true
```
2. 重新启动 Gitea
3. 尝试触发bug当请求卡住一段时间使用或浏览器访问获取堆栈跟踪。
`curl http://127.0.0.1:6060/debug/pprof/goroutine?debug=1`

View File

@ -17,7 +17,9 @@ menu:
# 数据库准备
在使用 Gitea 前您需要准备一个数据库。Gitea 支持 PostgreSQL>= 12、MySQL>= 8.0、SQLite 和 MSSQL>= 2012 SP4这几种数据库。本页将指导您准备数据库。由于 PostgreSQL 和 MySQL 在生产环境中被广泛使用,因此本文档将仅涵盖这两种数据库。如果您计划使用 SQLite则可以忽略本章内容。
在使用 Gitea 前您需要准备一个数据库。Gitea 支持 PostgreSQL>= 12、MySQL>= 8.0、MariaDB>= 10.4、SQLite内置 和 MSSQL>= 2012 SP4这几种数据库。本页将指导您准备数据库。由于 PostgreSQL 和 MySQL 在生产环境中被广泛使用,因此本文档将仅涵盖这两种数据库。如果您计划使用 SQLite则可以忽略本章内容。
如果您使用不受支持的数据库版本,请通过 [联系我们](/help/support) 以获取有关我们的扩展支持的信息。我们可以为旧数据库提供测试和支持,并将这些修复集成到 Gitea 代码库中。
数据库实例可以与 Gitea 实例在相同机器上(本地数据库),也可以与 Gitea 实例在不同机器上(远程数据库)。
@ -61,7 +63,9 @@ menu:
4. 使用 UTF-8 字符集和大小写敏感的排序规则创建数据库。
Gitea 启动后会尝试把数据库修改为更合适的字符集,如果你想指定自己的字符集规则,可以在 app.ini 中设置 `[database].CHARSET_COLLATION`
`utf8mb4_bin` 是 MySQL/MariaDB 的通用排序规则。
Gitea 启动后会尝试把数据库修改为更合适的字符集 (`utf8mb4_0900_as_cs` 或者 `uca1400_as_cs`) 并在可能的情况下更改数据库。
如果你想指定自己的字符集规则,可以在 `app.ini` 中设置 `[database].CHARSET_COLLATION`
```sql
CREATE DATABASE giteadb CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_bin';
@ -85,7 +89,7 @@ menu:
FLUSH PRIVILEGES;
```
6. 通过 exit 退出数据库控制台。
6. 通过 `exit` 退出数据库控制台。
7. 在您的 Gitea 服务器上,测试与数据库的连接:
@ -93,13 +97,13 @@ menu:
mysql -u gitea -h 203.0.113.3 -p giteadb
```
其中 `gitea` 是数据库用户名,`giteadb` 是数据库名称,`203.0.113.3` 是数据库实例的 IP 地址。对于本地数据库,省略 -h 选项。
其中 `gitea` 是数据库用户名,`giteadb` 是数据库名称,`203.0.113.3` 是数据库实例的 IP 地址。对于本地数据库,省略 `-h` 选项。
到此您应该能够连接到数据库了。
## PostgreSQL
1. 对于远程数据库设置,通过编辑数据库实例上的 postgresql.conf 文件中的 listen_addresses 将 PostgreSQL 配置为监听您的 IP 地址:
1. 对于远程数据库设置,通过编辑数据库实例上的 postgresql.conf 文件中的 `listen_addresses``PostgreSQL` 配置为监听您的 IP 地址:
```ini
listen_addresses = 'localhost, 203.0.113.3'

View File

@ -15,7 +15,7 @@ menu:
identifier: "windows-service"
---
# 准备工作
## 准备工作
在 C:\gitea\custom\conf\app.ini 中进行了以下更改:
@ -27,7 +27,7 @@ RUN_USER = COMPUTERNAME$
COMPUTERNAME 是从命令行中运行 `echo %COMPUTERNAME%` 后得到的响应。如果响应是 `USER-PC`,那么 `RUN_USER = USER-PC$`
## 使用绝对路径
### 使用绝对路径
如果您使用 SQLite3请将 `PATH` 更改为包含完整路径:
@ -36,7 +36,7 @@ COMPUTERNAME 是从命令行中运行 `echo %COMPUTERNAME%` 后得到的响应
PATH = c:/gitea/data/gitea.db
```
# 注册为Windows服务
## 注册为Windows服务
要注册为Windows服务首先以Administrator身份运行 `cmd`,然后执行以下命令:
@ -48,7 +48,16 @@ sc.exe create gitea start= auto binPath= "\"C:\gitea\gitea.exe\" web --config \"
之后在控制面板打开 "Windows Services",搜索 "gitea",右键选择 "Run"。在浏览器打开 `http://localhost:3000` 就可以访问了。如果你修改了端口请访问对应的端口3000是默认端口
## 添加启动依赖项
### 服务启动类型
据观察在启动期间加载的系统上Gitea 服务可能无法启动,并在 Windows 事件日志中记录超时。
在这种情况下,将启动类型更改为`Automatic-Delayed`。这可以在服务创建期间完成,或者通过运行配置命令来完成。
```
sc.exe config gitea start= delayed-auto
```
### 添加启动依赖项
要将启动依赖项添加到 Gitea Windows 服务(例如 Mysql、Mariadb作为管理员然后运行以下命令

View File

@ -120,6 +120,8 @@ A registration token can also be obtained from the gitea [command-line interface
gitea --config /etc/gitea/app.ini actions generate-runner-token
```
Tokens are valid for registering multiple runners, until they are revoked and replaced by a new token using the token reset link in the web interface.
### Register the runner
The act runner can be registered by running the following command:

View File

@ -61,8 +61,8 @@ It is always a bad idea to use a loopback address such as `127.0.0.1` or `localh
If you are unsure which address to use, the LAN address is usually the right choice.
`token` is used for authentication and identification, such as `P2U1U0oB4XaRCi8azcngmPCLbRpUGapalhmddh23`.
It is one-time use only and cannot be used to register multiple runners.
You can obtain different levels of 'tokens' from the following places to create the corresponding level of' runners':
Each token can be used to create multiple runners, until it is replaced with a new token using the reset link.
You can obtain different levels of 'tokens' from the following places to create the corresponding level of 'runners':
- Instance level: The admin settings page, like `<your_gitea.com>/admin/actions/runners`.
- Organization level: The organization settings page, like `<your_gitea.com>/<org>/settings/actions/runners`.

View File

@ -39,6 +39,16 @@ Images must follow this naming convention:
`{registry}/{owner}/{image}`
When building your docker image, using the naming convention above, this looks like:
```shell
# build an image with tag
docker build -t {registry}/{owner}/{image}:{tag} .
# name an existing image with tag
docker tag {some-existing-image}:{tag} {registry}/{owner}/{image}:{tag}
```
where your registry is the domain of your gitea instance (e.g. gitea.example.com).
For example, these are all valid image names for the owner `testuser`:
`gitea.example.com/testuser/myimage`

View File

@ -15,6 +15,10 @@ menu:
# 个人资料 README
要在您的 Gitea 个人资料页面显示一个 Markdown 文件,只需创建一个名为 ".profile" 的仓库,并编辑其中的 README.md 文件。Gitea 将自动获取该文件并在您的仓库上方显示。
要在您的 Gitea 个人资料页面显示一个 Markdown 文件,只需创建一个名为 `.profile` 的仓库,并编辑其中的 `README.md` 文件。Gitea 将自动获取该文件并在您的仓库上方显示。
注意您可以将此仓库设为私有。这样可以隐藏您的源文件使其对公众不可见并允许您将某些文件设为私有。但是README.md 文件将是您个人资料上唯一存在的文件。如果您希望完全私有化 .profile 仓库,则需删除或重命名 README.md 文件。
用户示例 `.profile/README.md`:
![个人资料自述文件截图](/images/usage/profile-readme.png)

View File

@ -97,7 +97,7 @@ func (r *ActionRunner) StatusName() string {
}
func (r *ActionRunner) StatusLocaleName(lang translation.Locale) string {
return lang.Tr("actions.runners.status." + r.StatusName())
return lang.TrString("actions.runners.status." + r.StatusName())
}
func (r *ActionRunner) IsOnline() bool {

View File

@ -41,7 +41,7 @@ func (s Status) String() string {
// LocaleString returns the locale string name of the Status
func (s Status) LocaleString(lang translation.Locale) string {
return lang.Tr("actions.status." + s.String())
return lang.TrString("actions.status." + s.String())
}
// IsDone returns whether the Status is final

View File

@ -493,6 +493,23 @@ func (err ErrMergeUnrelatedHistories) Error() string {
return fmt.Sprintf("Merge UnrelatedHistories Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut)
}
// ErrMergeDivergingFastForwardOnly represents an error if a fast-forward-only merge fails because the branches diverge
type ErrMergeDivergingFastForwardOnly struct {
StdOut string
StdErr string
Err error
}
// IsErrMergeDivergingFastForwardOnly checks if an error is a ErrMergeDivergingFastForwardOnly.
func IsErrMergeDivergingFastForwardOnly(err error) bool {
_, ok := err.(ErrMergeDivergingFastForwardOnly)
return ok
}
func (err ErrMergeDivergingFastForwardOnly) Error() string {
return fmt.Sprintf("Merge DivergingFastForwardOnly Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut)
}
// ErrRebaseConflicts represents an error if rebase fails with a conflict
type ErrRebaseConflicts struct {
Style repo_model.MergeStyle

View File

@ -194,7 +194,7 @@ func (status *CommitStatus) APIURL(ctx context.Context) string {
// LocaleString returns the locale string name of the Status
func (status *CommitStatus) LocaleString(lang translation.Locale) string {
return lang.Tr("repo.commitstatus." + status.State.String())
return lang.TrString("repo.commitstatus." + status.State.String())
}
// CalcCommitStatus returns commit status state via some status, the commit statues should order by id desc

View File

@ -210,12 +210,12 @@ const (
// LocaleString returns the locale string name of the role
func (r RoleInRepo) LocaleString(lang translation.Locale) string {
return lang.Tr("repo.issues.role." + string(r))
return lang.TrString("repo.issues.role." + string(r))
}
// LocaleHelper returns the locale tooltip of the role
func (r RoleInRepo) LocaleHelper(lang translation.Locale) string {
return lang.Tr("repo.issues.role." + string(r) + "_helper")
return lang.TrString("repo.issues.role." + string(r) + "_helper")
}
// Comment represents a comment in commit and issue page.
@ -695,8 +695,15 @@ func (c *Comment) LoadReactions(ctx context.Context, repo *repo_model.Repository
}
func (c *Comment) loadReview(ctx context.Context) (err error) {
if c.ReviewID == 0 {
return nil
}
if c.Review == nil {
if c.Review, err = GetReviewByID(ctx, c.ReviewID); err != nil {
// review request which has been replaced by actual reviews doesn't exist in database anymore, so ignorem them.
if c.Type == CommentTypeReviewRequest {
return nil
}
return err
}
}

View File

@ -225,6 +225,10 @@ func (comments CommentList) loadAssignees(ctx context.Context) error {
for _, comment := range comments {
comment.Assignee = assignees[comment.AssigneeID]
if comment.Assignee == nil {
comment.AssigneeID = user_model.GhostUserID
comment.Assignee = user_model.NewGhostUser()
}
}
return nil
}
@ -430,7 +434,8 @@ func (comments CommentList) loadReviews(ctx context.Context) error {
for _, comment := range comments {
comment.Review = reviews[comment.ReviewID]
if comment.Review == nil {
if comment.ReviewID > 0 {
// review request which has been replaced by actual reviews doesn't exist in database anymore, so don't log errors for them.
if comment.ReviewID > 0 && comment.Type != CommentTypeReviewRequest {
log.Error("comment with review id [%d] but has no review record", comment.ReviewID)
}
continue

View File

@ -161,7 +161,11 @@ func FetchIssueContentHistoryList(dbCtx context.Context, issueID, commentID int6
}
for _, item := range res {
item.UserAvatarLink = avatars.GenerateUserAvatarFastLink(item.UserName, 0)
if item.UserID > 0 {
item.UserAvatarLink = avatars.GenerateUserAvatarFastLink(item.UserName, 0)
} else {
item.UserAvatarLink = avatars.DefaultAvatarLink()
}
}
return res, nil
}

View File

@ -46,10 +46,10 @@ func neuterCrossReferences(ctx context.Context, issueID, commentID int64) error
for i, c := range active {
ids[i] = c.ID
}
return neuterCrossReferencesIds(ctx, ids)
return neuterCrossReferencesIDs(ctx, ids)
}
func neuterCrossReferencesIds(ctx context.Context, ids []int64) error {
func neuterCrossReferencesIDs(ctx context.Context, ids []int64) error {
_, err := db.GetEngine(ctx).In("id", ids).Cols("`ref_action`").Update(&Comment{RefAction: references.XRefActionNeutered})
return err
}
@ -100,7 +100,7 @@ func (issue *Issue) createCrossReferences(stdCtx context.Context, ctx *crossRefe
}
}
if len(ids) > 0 {
if err = neuterCrossReferencesIds(stdCtx, ids); err != nil {
if err = neuterCrossReferencesIDs(stdCtx, ids); err != nil {
return err
}
}

View File

@ -159,6 +159,14 @@ func (r *Review) LoadReviewer(ctx context.Context) (err error) {
return err
}
r.Reviewer, err = user_model.GetPossibleUserByID(ctx, r.ReviewerID)
if err != nil {
if !user_model.IsErrUserNotExist(err) {
return fmt.Errorf("GetPossibleUserByID [%d]: %w", r.ReviewerID, err)
}
r.ReviewerID = user_model.GhostUserID
r.Reviewer = user_model.NewGhostUser()
return nil
}
return err
}
@ -621,6 +629,9 @@ func AddReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_mo
return nil, err
}
// func caller use the created comment to retrieve created review too.
comment.Review = review
return comment, committer.Commit()
}

View File

@ -18,11 +18,11 @@ type ReviewList []*Review
// LoadReviewers loads reviewers
func (reviews ReviewList) LoadReviewers(ctx context.Context) error {
reviewerIds := make([]int64, len(reviews))
reviewerIDs := make([]int64, len(reviews))
for i := 0; i < len(reviews); i++ {
reviewerIds[i] = reviews[i].ReviewerID
reviewerIDs[i] = reviews[i].ReviewerID
}
reviewers, err := user_model.GetPossibleUserByIDs(ctx, reviewerIds)
reviewers, err := user_model.GetPossibleUserByIDs(ctx, reviewerIDs)
if err != nil {
return err
}
@ -38,12 +38,12 @@ func (reviews ReviewList) LoadReviewers(ctx context.Context) error {
}
func (reviews ReviewList) LoadIssues(ctx context.Context) error {
issueIds := container.Set[int64]{}
issueIDs := container.Set[int64]{}
for i := 0; i < len(reviews); i++ {
issueIds.Add(reviews[i].IssueID)
issueIDs.Add(reviews[i].IssueID)
}
issues, err := GetIssuesByIDs(ctx, issueIds.Values())
issues, err := GetIssuesByIDs(ctx, issueIDs.Values())
if err != nil {
return err
}

View File

@ -594,9 +594,7 @@ func GetOrgByID(ctx context.Context, id int64) (*Organization, error) {
return nil, err
} else if !has {
return nil, user_model.ErrUserNotExist{
UID: id,
Name: "",
KeyID: 0,
UID: id,
}
}
return u, nil

View File

@ -21,6 +21,8 @@ const (
MergeStyleRebaseMerge MergeStyle = "rebase-merge"
// MergeStyleSquash squash commits into single commit before merging
MergeStyleSquash MergeStyle = "squash"
// MergeStyleFastForwardOnly fast-forward merge if possible, otherwise fail
MergeStyleFastForwardOnly MergeStyle = "fast-forward-only"
// MergeStyleManuallyMerged pr has been merged manually, just mark it as merged directly
MergeStyleManuallyMerged MergeStyle = "manually-merged"
// MergeStyleRebaseUpdate not a merge style, used to update pull head by rebase

View File

@ -122,6 +122,7 @@ type PullRequestsConfig struct {
AllowRebase bool
AllowRebaseMerge bool
AllowSquash bool
AllowFastForwardOnly bool
AllowManualMerge bool
AutodetectManualMerge bool
AllowRebaseUpdate bool
@ -148,6 +149,7 @@ func (cfg *PullRequestsConfig) IsMergeStyleAllowed(mergeStyle MergeStyle) bool {
mergeStyle == MergeStyleRebase && cfg.AllowRebase ||
mergeStyle == MergeStyleRebaseMerge && cfg.AllowRebaseMerge ||
mergeStyle == MergeStyleSquash && cfg.AllowSquash ||
mergeStyle == MergeStyleFastForwardOnly && cfg.AllowFastForwardOnly ||
mergeStyle == MergeStyleManuallyMerged && cfg.AllowManualMerge
}

View File

@ -17,13 +17,13 @@ const (
func (o OwnerType) LocaleString(locale translation.Locale) string {
switch o {
case OwnerTypeSystemGlobal:
return locale.Tr("concept_system_global")
return locale.TrString("concept_system_global")
case OwnerTypeIndividual:
return locale.Tr("concept_user_individual")
return locale.TrString("concept_user_individual")
case OwnerTypeRepository:
return locale.Tr("concept_code_repository")
return locale.TrString("concept_code_repository")
case OwnerTypeOrganization:
return locale.Tr("concept_user_organization")
return locale.TrString("concept_user_organization")
}
return locale.Tr("unknown")
return locale.TrString("unknown")
}

View File

@ -332,9 +332,7 @@ func MakeEmailPrimary(ctx context.Context, email *EmailAddress) error {
return err
} else if !has {
return ErrUserNotExist{
UID: email.UID,
Name: "",
KeyID: 0,
UID: email.UID,
}
}

View File

@ -31,9 +31,8 @@ func (err ErrUserAlreadyExist) Unwrap() error {
// ErrUserNotExist represents a "UserNotExist" kind of error.
type ErrUserNotExist struct {
UID int64
Name string
KeyID int64
UID int64
Name string
}
// IsErrUserNotExist checks if an error is a ErrUserNotExist.
@ -43,7 +42,7 @@ func IsErrUserNotExist(err error) bool {
}
func (err ErrUserNotExist) Error() string {
return fmt.Sprintf("user does not exist [uid: %d, name: %s, keyid: %d]", err.UID, err.Name, err.KeyID)
return fmt.Sprintf("user does not exist [uid: %d, name: %s]", err.UID, err.Name)
}
// Unwrap unwraps this error as a ErrNotExist error

View File

@ -835,7 +835,7 @@ func GetUserByID(ctx context.Context, id int64) (*User, error) {
if err != nil {
return nil, err
} else if !has {
return nil, ErrUserNotExist{id, "", 0}
return nil, ErrUserNotExist{UID: id}
}
return u, nil
}
@ -885,14 +885,14 @@ func GetPossibleUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
// GetUserByNameCtx returns user by given name.
func GetUserByName(ctx context.Context, name string) (*User, error) {
if len(name) == 0 {
return nil, ErrUserNotExist{0, name, 0}
return nil, ErrUserNotExist{Name: name}
}
u := &User{LowerName: strings.ToLower(name), Type: UserTypeIndividual}
has, err := db.GetEngine(ctx).Get(u)
if err != nil {
return nil, err
} else if !has {
return nil, ErrUserNotExist{0, name, 0}
return nil, ErrUserNotExist{Name: name}
}
return u, nil
}
@ -1033,7 +1033,7 @@ func ValidateCommitsWithEmails(ctx context.Context, oldCommits []*git.Commit) []
// GetUserByEmail returns the user object by given e-mail if exists.
func GetUserByEmail(ctx context.Context, email string) (*User, error) {
if len(email) == 0 {
return nil, ErrUserNotExist{0, email, 0}
return nil, ErrUserNotExist{Name: email}
}
email = strings.ToLower(email)
@ -1060,7 +1060,7 @@ func GetUserByEmail(ctx context.Context, email string) (*User, error) {
}
}
return nil, ErrUserNotExist{0, email, 0}
return nil, ErrUserNotExist{Name: email}
}
// GetUser checks if a user already exists
@ -1071,7 +1071,7 @@ func GetUser(ctx context.Context, user *User) (bool, error) {
// GetUserByOpenID returns the user object by given OpenID if exists.
func GetUserByOpenID(ctx context.Context, uri string) (*User, error) {
if len(uri) == 0 {
return nil, ErrUserNotExist{0, uri, 0}
return nil, ErrUserNotExist{Name: uri}
}
uri, err := openid.Normalize(uri)
@ -1091,7 +1091,7 @@ func GetUserByOpenID(ctx context.Context, uri string) (*User, error) {
return GetUserByID(ctx, oid.UID)
}
return nil, ErrUserNotExist{0, uri, 0}
return nil, ErrUserNotExist{Name: uri}
}
// GetAdminUser returns the first administrator

View File

@ -8,6 +8,7 @@ import (
"context"
"crypto/rand"
"errors"
"html/template"
"math/big"
"strings"
"sync"
@ -121,15 +122,15 @@ func Generate(n int) (string, error) {
}
// BuildComplexityError builds the error message when password complexity checks fail
func BuildComplexityError(locale translation.Locale) string {
func BuildComplexityError(locale translation.Locale) template.HTML {
var buffer bytes.Buffer
buffer.WriteString(locale.Tr("form.password_complexity"))
buffer.WriteString(locale.TrString("form.password_complexity"))
buffer.WriteString("<ul>")
for _, c := range requiredList {
buffer.WriteString("<li>")
buffer.WriteString(locale.Tr(c.TrNameOne))
buffer.WriteString(locale.TrString(c.TrNameOne))
buffer.WriteString("</li>")
}
buffer.WriteString("</ul>")
return buffer.String()
return template.HTML(buffer.String())
}

View File

@ -173,7 +173,7 @@ func (e *escapeStreamer) ambiguousRune(r, c rune) error {
Val: "ambiguous-code-point",
}, html.Attribute{
Key: "data-tooltip-content",
Val: e.locale.Tr("repo.ambiguous_character", r, c),
Val: e.locale.TrString("repo.ambiguous_character", r, c),
}); err != nil {
return err
}

View File

@ -245,7 +245,7 @@ func APIContexter() func(http.Handler) http.Handler {
// NotFound handles 404s for APIContext
// String will replace message, errors will be added to a slice
func (ctx *APIContext) NotFound(objs ...any) {
message := ctx.Tr("error.not_found")
message := ctx.Locale.TrString("error.not_found")
var errors []string
for _, obj := range objs {
// Ignore nil

View File

@ -6,6 +6,7 @@ package context
import (
"context"
"fmt"
"html/template"
"io"
"net/http"
"net/url"
@ -286,11 +287,11 @@ func (b *Base) cleanUp() {
}
}
func (b *Base) Tr(msg string, args ...any) string {
func (b *Base) Tr(msg string, args ...any) template.HTML {
return b.Locale.Tr(msg, args...)
}
func (b *Base) TrN(cnt any, key1, keyN string, args ...any) string {
func (b *Base) TrN(cnt any, key1, keyN string, args ...any) template.HTML {
return b.Locale.TrN(cnt, key1, keyN, args...)
}

View File

@ -6,7 +6,7 @@ package context
import (
"context"
"html"
"fmt"
"html/template"
"io"
"net/http"
@ -71,16 +71,6 @@ func init() {
})
}
// TrHTMLEscapeArgs runs ".Locale.Tr()" but pre-escapes all arguments with html.EscapeString.
// This is useful if the locale message is intended to only produce HTML content.
func (ctx *Context) TrHTMLEscapeArgs(msg string, args ...string) string {
trArgs := make([]any, len(args))
for i, arg := range args {
trArgs[i] = html.EscapeString(arg)
}
return ctx.Locale.Tr(msg, trArgs...)
}
type webContextKeyType struct{}
var WebContextKey = webContextKeyType{}
@ -253,6 +243,13 @@ func (ctx *Context) JSONOK() {
ctx.JSON(http.StatusOK, map[string]any{"ok": true}) // this is only a dummy response, frontend seldom uses it
}
func (ctx *Context) JSONError(msg string) {
ctx.JSON(http.StatusBadRequest, map[string]any{"errorMessage": msg})
func (ctx *Context) JSONError(msg any) {
switch v := msg.(type) {
case string:
ctx.JSON(http.StatusBadRequest, map[string]any{"errorMessage": v, "renderFormat": "text"})
case template.HTML:
ctx.JSON(http.StatusBadRequest, map[string]any{"errorMessage": v, "renderFormat": "html"})
default:
panic(fmt.Sprintf("unsupported type: %T", msg))
}
}

View File

@ -98,12 +98,11 @@ func (ctx *Context) RenderToString(name base.TplName, data map[string]any) (stri
}
// RenderWithErr used for page has form validation but need to prompt error to users.
func (ctx *Context) RenderWithErr(msg string, tpl base.TplName, form any) {
func (ctx *Context) RenderWithErr(msg any, tpl base.TplName, form any) {
if form != nil {
middleware.AssignForm(form, ctx.Data)
}
ctx.Flash.ErrorMsg = msg
ctx.Data["Flash"] = ctx.Flash
ctx.Flash.Error(msg, true)
ctx.HTML(http.StatusOK, tpl)
}

View File

@ -6,6 +6,7 @@ package context
import (
"context"
"errors"
"fmt"
"html"
"net/http"
@ -85,7 +86,7 @@ func (r *Repository) CanCreateBranch() bool {
func RepoMustNotBeArchived() func(ctx *Context) {
return func(ctx *Context) {
if ctx.Repo.Repository.IsArchived {
ctx.NotFound("IsArchived", fmt.Errorf(ctx.Tr("repo.archive.title")))
ctx.NotFound("IsArchived", errors.New(ctx.Locale.TrString("repo.archive.title")))
}
}
}

View File

@ -123,9 +123,9 @@ func guessDelimiter(data []byte) rune {
func FormatError(err error, locale translation.Locale) (string, error) {
if perr, ok := err.(*stdcsv.ParseError); ok {
if perr.Err == stdcsv.ErrFieldCount {
return locale.Tr("repo.error.csv.invalid_field_count", perr.Line), nil
return locale.TrString("repo.error.csv.invalid_field_count", perr.Line), nil
}
return locale.Tr("repo.error.csv.unexpected", perr.Line, perr.Column), nil
return locale.TrString("repo.error.csv.unexpected", perr.Line, perr.Column), nil
}
return "", err

View File

@ -96,7 +96,7 @@ func (err ErrBranchNotExist) Unwrap() error {
return util.ErrNotExist
}
// ErrPushOutOfDate represents an error if merging fails due to unrelated histories
// ErrPushOutOfDate represents an error if merging fails due to the base branch being updated
type ErrPushOutOfDate struct {
StdOut string
StdErr string

View File

@ -39,36 +39,37 @@ var (
gitVersion *version.Version
)
// loadGitVersion returns current Git version from shell. Internal usage only.
func loadGitVersion() (*version.Version, error) {
// loadGitVersion tries to get the current git version and stores it into a global variable
func loadGitVersion() error {
// doesn't need RWMutex because it's executed by Init()
if gitVersion != nil {
return gitVersion, nil
return nil
}
stdout, _, runErr := NewCommand(DefaultContext, "version").RunStdString(nil)
if runErr != nil {
return nil, runErr
return runErr
}
fields := strings.Fields(stdout)
ver, err := parseGitVersionLine(strings.TrimSpace(stdout))
if err == nil {
gitVersion = ver
}
return err
}
func parseGitVersionLine(s string) (*version.Version, error) {
fields := strings.Fields(s)
if len(fields) < 3 {
return nil, fmt.Errorf("invalid git version output: %s", stdout)
return nil, fmt.Errorf("invalid git version: %q", s)
}
var versionString string
// Handle special case on Windows.
i := strings.Index(fields[2], "windows")
if i >= 1 {
versionString = fields[2][:i-1]
} else {
versionString = fields[2]
// version string is like: "git version 2.29.3" or "git version 2.29.3.windows.1"
versionString := fields[2]
if pos := strings.Index(versionString, "windows"); pos >= 1 {
versionString = versionString[:pos-1]
}
var err error
gitVersion, err = version.NewVersion(versionString)
return gitVersion, err
return version.NewVersion(versionString)
}
// SetExecutablePath changes the path of git executable and checks the file permission and version.
@ -83,8 +84,7 @@ func SetExecutablePath(path string) error {
}
GitExecutable = absPath
_, err = loadGitVersion()
if err != nil {
if err = loadGitVersion(); err != nil {
return fmt.Errorf("unable to load git version: %w", err)
}
@ -105,6 +105,9 @@ func SetExecutablePath(path string) error {
return fmt.Errorf("installed git version %q is not supported, Gitea requires git version >= %q, %s", gitVersion.Original(), RequiredVersion, moreHint)
}
if err = checkGitVersionCompatibility(gitVersion); err != nil {
return fmt.Errorf("installed git version %s has a known compatibility issue with Gitea: %w, please upgrade (or downgrade) git", gitVersion.String(), err)
}
return nil
}
@ -262,19 +265,18 @@ func syncGitConfig() (err error) {
}
}
// Due to CVE-2022-24765, git now denies access to git directories which are not owned by current user
// however, some docker users and samba users find it difficult to configure their systems so that Gitea's git repositories are owned by the Gitea user. (Possibly Windows Service users - but ownership in this case should really be set correctly on the filesystem.)
// see issue: https://github.com/go-gitea/gitea/issues/19455
// Fundamentally the problem lies with the uid-gid-mapping mechanism for filesystems in docker on windows (and to a lesser extent samba).
// Docker's configuration mechanism for local filesystems provides no way of setting this mapping and although there is a mechanism for setting this uid through using cifs mounting it is complicated and essentially undocumented
// Thus the owner uid/gid for files on these filesystems will be marked as root.
// Due to CVE-2022-24765, git now denies access to git directories which are not owned by current user.
// However, some docker users and samba users find it difficult to configure their systems correctly,
// so that Gitea's git repositories are owned by the Gitea user.
// (Possibly Windows Service users - but ownership in this case should really be set correctly on the filesystem.)
// See issue: https://github.com/go-gitea/gitea/issues/19455
// As Gitea now always use its internal git config file, and access to the git repositories is managed through Gitea,
// it is now safe to set "safe.directory=*" for internal usage only.
// Please note: the wildcard "*" is only supported by Git 2.30.4/2.31.3/2.32.2/2.33.3/2.34.3/2.35.3/2.36 and later
// Although only supported by Git 2.30.4/2.31.3/2.32.2/2.33.3/2.34.3/2.35.3/2.36 and later - this setting is tolerated by earlier versions
// Although this setting is only supported by some new git versions, it is also tolerated by earlier versions
if err := configAddNonExist("safe.directory", "*"); err != nil {
return err
}
if runtime.GOOS == "windows" {
if err := configSet("core.longpaths", "true"); err != nil {
return err
@ -307,8 +309,8 @@ func syncGitConfig() (err error) {
// CheckGitVersionAtLeast check git version is at least the constraint version
func CheckGitVersionAtLeast(atLeast string) error {
if _, err := loadGitVersion(); err != nil {
return err
if gitVersion == nil {
panic("git module is not initialized") // it shouldn't happen
}
atLeastVersion, err := version.NewVersion(atLeast)
if err != nil {
@ -320,6 +322,21 @@ func CheckGitVersionAtLeast(atLeast string) error {
return nil
}
func checkGitVersionCompatibility(gitVer *version.Version) error {
badVersions := []struct {
Version *version.Version
Reason string
}{
{version.Must(version.NewVersion("2.43.1")), "regression bug of GIT_FLUSH"},
}
for _, bad := range badVersions {
if gitVer.Equal(bad.Version) {
return errors.New(bad.Reason)
}
}
return nil
}
func configSet(key, value string) error {
stdout, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil)
if err != nil && !err.IsExitCode(1) {

View File

@ -13,6 +13,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"github.com/hashicorp/go-version"
"github.com/stretchr/testify/assert"
)
@ -93,3 +94,25 @@ func TestSyncConfig(t *testing.T) {
assert.True(t, gitConfigContains("[sync-test]"))
assert.True(t, gitConfigContains("cfg-key-a = CfgValA"))
}
func TestParseGitVersion(t *testing.T) {
v, err := parseGitVersionLine("git version 2.29.3")
assert.NoError(t, err)
assert.Equal(t, "2.29.3", v.String())
v, err = parseGitVersionLine("git version 2.29.3.windows.1")
assert.NoError(t, err)
assert.Equal(t, "2.29.3", v.String())
_, err = parseGitVersionLine("git version")
assert.Error(t, err)
_, err = parseGitVersionLine("git version windows")
assert.Error(t, err)
}
func TestCheckGitVersionCompatibility(t *testing.T) {
assert.NoError(t, checkGitVersionCompatibility(version.Must(version.NewVersion("2.43.0"))))
assert.ErrorContains(t, checkGitVersionCompatibility(version.Must(version.NewVersion("2.43.1"))), "regression bug of GIT_FLUSH")
assert.NoError(t, checkGitVersionCompatibility(version.Must(version.NewVersion("2.43.2"))))
}

View File

@ -143,19 +143,19 @@ func (g *LogNameStatusRepoParser) Next(treepath string, paths2ids map[string]int
}
// Our "line" must look like: <commitid> SP (<parent> SP) * NUL
commitIds := string(g.next)
commitIDs := string(g.next)
if g.buffull {
more, err := g.rd.ReadString('\x00')
if err != nil {
return nil, err
}
commitIds += more
commitIDs += more
}
commitIds = commitIds[:len(commitIds)-1]
splitIds := strings.Split(commitIds, " ")
ret.CommitID = splitIds[0]
if len(splitIds) > 1 {
ret.ParentIDs = splitIds[1:]
commitIDs = commitIDs[:len(commitIDs)-1]
splitIDs := strings.Split(commitIDs, " ")
ret.CommitID = splitIDs[0]
if len(splitIDs) > 1 {
ret.ParentIDs = splitIDs[1:]
}
// now read the next "line"

View File

@ -804,7 +804,7 @@ func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) {
// indicate that in the text by appending (comment)
if m[4] != -1 && m[5] != -1 {
if locale, ok := ctx.Ctx.Value(translation.ContextKey).(translation.Locale); ok {
text += " " + locale.Tr("repo.from_comment")
text += " " + locale.TrString("repo.from_comment")
} else {
text += " (comment)"
}

View File

@ -21,7 +21,7 @@ func createTOCNode(toc []markup.Header, lang string, detailsAttrs map[string]str
details.SetAttributeString(k, []byte(v))
}
summary.AppendChild(summary, ast.NewString([]byte(translation.NewLocale(lang).Tr("toc"))))
summary.AppendChild(summary, ast.NewString([]byte(translation.NewLocale(lang).TrString("toc"))))
details.AppendChild(details, summary)
ul := ast.NewList('-')
details.AppendChild(details, ul)

View File

@ -3,7 +3,7 @@
package migration
// Messenger is a formatting function similar to i18n.Tr
// Messenger is a formatting function similar to i18n.TrString
type Messenger func(key string, args ...any)
// NilMessenger represents an empty formatting function

View File

@ -87,7 +87,11 @@ func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, re
units = append(units, repo_model.RepoUnit{
RepoID: repo.ID,
Type: tp,
Config: &repo_model.PullRequestsConfig{AllowMerge: true, AllowRebase: true, AllowRebaseMerge: true, AllowSquash: true, DefaultMergeStyle: repo_model.MergeStyle(setting.Repository.PullRequest.DefaultMergeStyle), AllowRebaseUpdate: true},
Config: &repo_model.PullRequestsConfig{
AllowMerge: true, AllowRebase: true, AllowRebaseMerge: true, AllowSquash: true, AllowFastForwardOnly: true,
DefaultMergeStyle: repo_model.MergeStyle(setting.Repository.PullRequest.DefaultMergeStyle),
AllowRebaseUpdate: true,
},
})
} else {
units = append(units, repo_model.RepoUnit{

View File

@ -94,7 +94,7 @@ type GiteaTemplate struct {
}
// Globs parses the .gitea/template globs or returns them if they were already parsed
func (gt GiteaTemplate) Globs() []glob.Glob {
func (gt *GiteaTemplate) Globs() []glob.Glob {
if gt.globs != nil {
return gt.globs
}

View File

@ -98,6 +98,7 @@ type Repository struct {
AllowRebase bool `json:"allow_rebase"`
AllowRebaseMerge bool `json:"allow_rebase_explicit"`
AllowSquash bool `json:"allow_squash_merge"`
AllowFastForwardOnly bool `json:"allow_fast_forward_only_merge"`
AllowRebaseUpdate bool `json:"allow_rebase_update"`
DefaultDeleteBranchAfterMerge bool `json:"default_delete_branch_after_merge"`
DefaultMergeStyle string `json:"default_merge_style"`
@ -195,6 +196,8 @@ type EditRepoOption struct {
AllowRebaseMerge *bool `json:"allow_rebase_explicit,omitempty"`
// either `true` to allow squash-merging pull requests, or `false` to prevent squash-merging.
AllowSquash *bool `json:"allow_squash_merge,omitempty"`
// either `true` to allow fast-forward-only merging pull requests, or `false` to prevent fast-forward-only merging.
AllowFastForwardOnly *bool `json:"allow_fast_forward_only_merge,omitempty"`
// either `true` to allow mark pr as merged manually, or `false` to prevent it.
AllowManualMerge *bool `json:"allow_manual_merge,omitempty"`
// either `true` to enable AutodetectManualMerge, or `false` to prevent it. Note: In some special cases, misjudgments can occur.
@ -203,7 +206,7 @@ type EditRepoOption struct {
AllowRebaseUpdate *bool `json:"allow_rebase_update,omitempty"`
// set to `true` to delete pr branch after merge by default
DefaultDeleteBranchAfterMerge *bool `json:"default_delete_branch_after_merge,omitempty"`
// set to a merge style to be used by this repository: "merge", "rebase", "rebase-merge", or "squash".
// set to a merge style to be used by this repository: "merge", "rebase", "rebase-merge", "squash", or "fast-forward-only".
DefaultMergeStyle *string `json:"default_merge_style,omitempty"`
// set to `true` to allow edits from maintainers by default
DefaultAllowMaintainerEdit *bool `json:"default_allow_maintainer_edit,omitempty"`

View File

@ -36,7 +36,7 @@ func NewFuncMap() template.FuncMap {
"dict": dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names.
"Eval": Eval,
"Safe": Safe,
"Escape": html.EscapeString,
"Escape": Escape,
"QueryEscape": url.QueryEscape,
"JSEscape": template.JSEscapeString,
"Str2html": Str2html, // TODO: rename it to SanitizeHTML
@ -159,7 +159,7 @@ func NewFuncMap() template.FuncMap {
"RenderCodeBlock": RenderCodeBlock,
"RenderIssueTitle": RenderIssueTitle,
"RenderEmoji": RenderEmoji,
"RenderEmojiPlain": emoji.ReplaceAliases,
"RenderEmojiPlain": RenderEmojiPlain,
"ReactionToEmoji": ReactionToEmoji,
"RenderMarkdownToHtml": RenderMarkdownToHtml,
@ -180,13 +180,45 @@ func NewFuncMap() template.FuncMap {
}
// Safe render raw as HTML
func Safe(raw string) template.HTML {
return template.HTML(raw)
func Safe(s any) template.HTML {
switch v := s.(type) {
case string:
return template.HTML(v)
case template.HTML:
return v
}
panic(fmt.Sprintf("unexpected type %T", s))
}
// Str2html render Markdown text to HTML
func Str2html(raw string) template.HTML {
return template.HTML(markup.Sanitize(raw))
// Str2html sanitizes the input by pre-defined markdown rules
func Str2html(s any) template.HTML {
switch v := s.(type) {
case string:
return template.HTML(markup.Sanitize(v))
case template.HTML:
return template.HTML(markup.Sanitize(string(v)))
}
panic(fmt.Sprintf("unexpected type %T", s))
}
func Escape(s any) template.HTML {
switch v := s.(type) {
case string:
return template.HTML(html.EscapeString(v))
case template.HTML:
return v
}
panic(fmt.Sprintf("unexpected type %T", s))
}
func RenderEmojiPlain(s any) any {
switch v := s.(type) {
case string:
return emoji.ReplaceAliases(v)
case template.HTML:
return template.HTML(emoji.ReplaceAliases(string(v)))
}
panic(fmt.Sprintf("unexpected type %T", s))
}
// DotEscape wraps a dots in names with ZWJ [U+200D] in order to prevent autolinkers from detecting these as urls

View File

@ -28,54 +28,54 @@ func computeTimeDiffFloor(diff int64, lang translation.Locale) (int64, string) {
switch {
case diff <= 0:
diff = 0
diffStr = lang.Tr("tool.now")
diffStr = lang.TrString("tool.now")
case diff < 2:
diff = 0
diffStr = lang.Tr("tool.1s")
diffStr = lang.TrString("tool.1s")
case diff < 1*Minute:
diffStr = lang.Tr("tool.seconds", diff)
diffStr = lang.TrString("tool.seconds", diff)
diff = 0
case diff < 2*Minute:
diff -= 1 * Minute
diffStr = lang.Tr("tool.1m")
diffStr = lang.TrString("tool.1m")
case diff < 1*Hour:
diffStr = lang.Tr("tool.minutes", diff/Minute)
diffStr = lang.TrString("tool.minutes", diff/Minute)
diff -= diff / Minute * Minute
case diff < 2*Hour:
diff -= 1 * Hour
diffStr = lang.Tr("tool.1h")
diffStr = lang.TrString("tool.1h")
case diff < 1*Day:
diffStr = lang.Tr("tool.hours", diff/Hour)
diffStr = lang.TrString("tool.hours", diff/Hour)
diff -= diff / Hour * Hour
case diff < 2*Day:
diff -= 1 * Day
diffStr = lang.Tr("tool.1d")
diffStr = lang.TrString("tool.1d")
case diff < 1*Week:
diffStr = lang.Tr("tool.days", diff/Day)
diffStr = lang.TrString("tool.days", diff/Day)
diff -= diff / Day * Day
case diff < 2*Week:
diff -= 1 * Week
diffStr = lang.Tr("tool.1w")
diffStr = lang.TrString("tool.1w")
case diff < 1*Month:
diffStr = lang.Tr("tool.weeks", diff/Week)
diffStr = lang.TrString("tool.weeks", diff/Week)
diff -= diff / Week * Week
case diff < 2*Month:
diff -= 1 * Month
diffStr = lang.Tr("tool.1mon")
diffStr = lang.TrString("tool.1mon")
case diff < 1*Year:
diffStr = lang.Tr("tool.months", diff/Month)
diffStr = lang.TrString("tool.months", diff/Month)
diff -= diff / Month * Month
case diff < 2*Year:
diff -= 1 * Year
diffStr = lang.Tr("tool.1y")
diffStr = lang.TrString("tool.1y")
default:
diffStr = lang.Tr("tool.years", diff/Year)
diffStr = lang.TrString("tool.years", diff/Year)
diff -= (diff / Year) * Year
}
return diff, diffStr
@ -97,10 +97,10 @@ func timeSincePro(then, now time.Time, lang translation.Locale) string {
diff := now.Unix() - then.Unix()
if then.After(now) {
return lang.Tr("tool.future")
return lang.TrString("tool.future")
}
if diff == 0 {
return lang.Tr("tool.now")
return lang.TrString("tool.now")
}
var timeStr, diffStr string
@ -115,7 +115,7 @@ func timeSincePro(then, now time.Time, lang translation.Locale) string {
return strings.TrimPrefix(timeStr, ", ")
}
func timeSinceUnix(then, now time.Time, lang translation.Locale) template.HTML {
func timeSinceUnix(then, now time.Time, _ translation.Locale) template.HTML {
friendlyText := then.Format("2006-01-02 15:04:05 -07:00")
// document: https://github.com/github/relative-time-element

View File

@ -4,26 +4,25 @@
package i18n
import (
"html/template"
"io"
)
var DefaultLocales = NewLocaleStore()
type Locale interface {
// Tr translates a given key and arguments for a language
Tr(trKey string, trArgs ...any) string
// Has reports if a locale has a translation for a given key
Has(trKey string) bool
// TrString translates a given key and arguments for a language
TrString(trKey string, trArgs ...any) string
// TrHTML translates a given key and arguments for a language, string arguments are escaped to HTML
TrHTML(trKey string, trArgs ...any) template.HTML
// HasKey reports if a locale has a translation for a given key
HasKey(trKey string) bool
}
// LocaleStore provides the functions common to all locale stores
type LocaleStore interface {
io.Closer
// Tr translates a given key and arguments for a language
Tr(lang, trKey string, trArgs ...any) string
// Has reports if a locale has a translation for a given key
Has(lang, trKey string) bool
// SetDefaultLang sets the default language to fall back to
SetDefaultLang(lang string)
// ListLangNameDesc provides paired slices of language names to descriptors
@ -45,7 +44,7 @@ func ResetDefaultLocales() {
DefaultLocales = NewLocaleStore()
}
// GetLocales returns the locale from the default locales
// GetLocale returns the locale from the default locales
func GetLocale(lang string) (Locale, bool) {
return DefaultLocales.Locale(lang)
}

View File

@ -17,7 +17,7 @@ fmt = %[1]s %[2]s
[section]
sub = Sub String
mixed = test value; <span style="color: red\; background: none;">more text</span>
mixed = test value; <span style="color: red\; background: none;">%s</span>
`)
testData2 := []byte(`
@ -32,29 +32,33 @@ sub = Changed Sub String
assert.NoError(t, ls.AddLocaleByIni("lang2", "Lang2", testData2, nil))
ls.SetDefaultLang("lang1")
result := ls.Tr("lang1", "fmt", "a", "b")
lang1, _ := ls.Locale("lang1")
lang2, _ := ls.Locale("lang2")
result := lang1.TrString("fmt", "a", "b")
assert.Equal(t, "a b", result)
result = ls.Tr("lang2", "fmt", "a", "b")
result = lang2.TrString("fmt", "a", "b")
assert.Equal(t, "b a", result)
result = ls.Tr("lang1", "section.sub")
result = lang1.TrString("section.sub")
assert.Equal(t, "Sub String", result)
result = ls.Tr("lang2", "section.sub")
result = lang2.TrString("section.sub")
assert.Equal(t, "Changed Sub String", result)
result = ls.Tr("", ".dot.name")
langNone, _ := ls.Locale("none")
result = langNone.TrString(".dot.name")
assert.Equal(t, "Dot Name", result)
result = ls.Tr("lang2", "section.mixed")
assert.Equal(t, `test value; <span style="color: red; background: none;">more text</span>`, result)
result2 := lang2.TrHTML("section.mixed", "a&b")
assert.EqualValues(t, `test value; <span style="color: red; background: none;">a&amp;b</span>`, result2)
langs, descs := ls.ListLangNameDesc()
assert.ElementsMatch(t, []string{"lang1", "lang2"}, langs)
assert.ElementsMatch(t, []string{"Lang1", "Lang2"}, descs)
found := ls.Has("lang1", "no-such")
found := lang1.HasKey("no-such")
assert.False(t, found)
assert.NoError(t, ls.Close())
}
@ -72,9 +76,10 @@ c=22
ls := NewLocaleStore()
assert.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", testData1, testData2))
assert.Equal(t, "11", ls.Tr("lang1", "a"))
assert.Equal(t, "21", ls.Tr("lang1", "b"))
assert.Equal(t, "22", ls.Tr("lang1", "c"))
lang1, _ := ls.Locale("lang1")
assert.Equal(t, "11", lang1.TrString("a"))
assert.Equal(t, "21", lang1.TrString("b"))
assert.Equal(t, "22", lang1.TrString("c"))
}
func TestLocaleStoreQuirks(t *testing.T) {
@ -110,8 +115,9 @@ func TestLocaleStoreQuirks(t *testing.T) {
for _, testData := range testDataList {
ls := NewLocaleStore()
err := ls.AddLocaleByIni("lang1", "Lang1", []byte("a="+testData.in), nil)
lang1, _ := ls.Locale("lang1")
assert.NoError(t, err, testData.hint)
assert.Equal(t, testData.out, ls.Tr("lang1", "a"), testData.hint)
assert.Equal(t, testData.out, lang1.TrString("a"), testData.hint)
assert.NoError(t, ls.Close())
}

View File

@ -5,6 +5,8 @@ package i18n
import (
"fmt"
"html/template"
"slices"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
@ -18,6 +20,8 @@ type locale struct {
idxToMsgMap map[int]string // the map idx is generated by store's trKeyToIdxMap
}
var _ Locale = (*locale)(nil)
type localeStore struct {
// After initializing has finished, these fields are read-only.
langNames []string
@ -85,20 +89,6 @@ func (store *localeStore) SetDefaultLang(lang string) {
store.defaultLang = lang
}
// Tr translates content to target language. fall back to default language.
func (store *localeStore) Tr(lang, trKey string, trArgs ...any) string {
l, _ := store.Locale(lang)
return l.Tr(trKey, trArgs...)
}
// Has returns whether the given language has a translation for the provided key
func (store *localeStore) Has(lang, trKey string) bool {
l, _ := store.Locale(lang)
return l.Has(trKey)
}
// Locale returns the locale for the lang or the default language
func (store *localeStore) Locale(lang string) (Locale, bool) {
l, found := store.localeMap[lang]
@ -113,13 +103,11 @@ func (store *localeStore) Locale(lang string) (Locale, bool) {
return l, found
}
// Close implements io.Closer
func (store *localeStore) Close() error {
return nil
}
// Tr translates content to locale language. fall back to default language.
func (l *locale) Tr(trKey string, trArgs ...any) string {
func (l *locale) TrString(trKey string, trArgs ...any) string {
format := trKey
idx, ok := l.store.trKeyToIdxMap[trKey]
@ -141,8 +129,23 @@ func (l *locale) Tr(trKey string, trArgs ...any) string {
return msg
}
// Has returns whether a key is present in this locale or not
func (l *locale) Has(trKey string) bool {
func (l *locale) TrHTML(trKey string, trArgs ...any) template.HTML {
args := slices.Clone(trArgs)
for i, v := range args {
switch v := v.(type) {
case string:
args[i] = template.HTML(template.HTMLEscapeString(v))
case fmt.Stringer:
args[i] = template.HTMLEscapeString(v.String())
default: // int, float, include template.HTML
// do nothing, just use it
}
}
return template.HTML(l.TrString(trKey, args...))
}
// HasKey returns whether a key is present in this locale or not
func (l *locale) HasKey(trKey string) bool {
idx, ok := l.store.trKeyToIdxMap[trKey]
if !ok {
return false

View File

@ -3,7 +3,10 @@
package translation
import "fmt"
import (
"fmt"
"html/template"
)
// MockLocale provides a mocked locale without any translations
type MockLocale struct{}
@ -14,12 +17,16 @@ func (l MockLocale) Language() string {
return "en"
}
func (l MockLocale) Tr(s string, _ ...any) string {
func (l MockLocale) TrString(s string, _ ...any) string {
return s
}
func (l MockLocale) TrN(_cnt any, key1, _keyN string, _args ...any) string {
return key1
func (l MockLocale) Tr(s string, a ...any) template.HTML {
return template.HTML(s)
}
func (l MockLocale) TrN(cnt any, key1, keyN string, args ...any) template.HTML {
return template.HTML(key1)
}
func (l MockLocale) PrettyNumber(v any) string {

View File

@ -5,6 +5,7 @@ package translation
import (
"context"
"html/template"
"sort"
"strings"
"sync"
@ -27,8 +28,11 @@ var ContextKey any = &contextKey{}
// Locale represents an interface to translation
type Locale interface {
Language() string
Tr(string, ...any) string
TrN(cnt any, key1, keyN string, args ...any) string
TrString(string, ...any) string
Tr(key string, args ...any) template.HTML
TrN(cnt any, key1, keyN string, args ...any) template.HTML
PrettyNumber(v any) string
}
@ -144,6 +148,8 @@ type locale struct {
msgPrinter *message.Printer
}
var _ Locale = (*locale)(nil)
// NewLocale return a locale
func NewLocale(lang string) Locale {
if lock != nil {
@ -216,8 +222,12 @@ var trNLangRules = map[string]func(int64) int{
},
}
func (l *locale) Tr(s string, args ...any) template.HTML {
return l.TrHTML(s, args...)
}
// TrN returns translated message for plural text translation
func (l *locale) TrN(cnt any, key1, keyN string, args ...any) string {
func (l *locale) TrN(cnt any, key1, keyN string, args ...any) template.HTML {
var c int64
if t, ok := cnt.(int); ok {
c = int64(t)

View File

@ -104,40 +104,40 @@ func Validate(errs binding.Errors, data map[string]any, f Form, l translation.Lo
trName := field.Tag.Get("locale")
if len(trName) == 0 {
trName = l.Tr("form." + field.Name)
trName = l.TrString("form." + field.Name)
} else {
trName = l.Tr(trName)
trName = l.TrString(trName)
}
switch errs[0].Classification {
case binding.ERR_REQUIRED:
data["ErrorMsg"] = trName + l.Tr("form.require_error")
data["ErrorMsg"] = trName + l.TrString("form.require_error")
case binding.ERR_ALPHA_DASH:
data["ErrorMsg"] = trName + l.Tr("form.alpha_dash_error")
data["ErrorMsg"] = trName + l.TrString("form.alpha_dash_error")
case binding.ERR_ALPHA_DASH_DOT:
data["ErrorMsg"] = trName + l.Tr("form.alpha_dash_dot_error")
data["ErrorMsg"] = trName + l.TrString("form.alpha_dash_dot_error")
case validation.ErrGitRefName:
data["ErrorMsg"] = trName + l.Tr("form.git_ref_name_error")
data["ErrorMsg"] = trName + l.TrString("form.git_ref_name_error")
case binding.ERR_SIZE:
data["ErrorMsg"] = trName + l.Tr("form.size_error", GetSize(field))
data["ErrorMsg"] = trName + l.TrString("form.size_error", GetSize(field))
case binding.ERR_MIN_SIZE:
data["ErrorMsg"] = trName + l.Tr("form.min_size_error", GetMinSize(field))
data["ErrorMsg"] = trName + l.TrString("form.min_size_error", GetMinSize(field))
case binding.ERR_MAX_SIZE:
data["ErrorMsg"] = trName + l.Tr("form.max_size_error", GetMaxSize(field))
data["ErrorMsg"] = trName + l.TrString("form.max_size_error", GetMaxSize(field))
case binding.ERR_EMAIL:
data["ErrorMsg"] = trName + l.Tr("form.email_error")
data["ErrorMsg"] = trName + l.TrString("form.email_error")
case binding.ERR_URL:
data["ErrorMsg"] = trName + l.Tr("form.url_error", errs[0].Message)
data["ErrorMsg"] = trName + l.TrString("form.url_error", errs[0].Message)
case binding.ERR_INCLUDE:
data["ErrorMsg"] = trName + l.Tr("form.include_error", GetInclude(field))
data["ErrorMsg"] = trName + l.TrString("form.include_error", GetInclude(field))
case validation.ErrGlobPattern:
data["ErrorMsg"] = trName + l.Tr("form.glob_pattern_error", errs[0].Message)
data["ErrorMsg"] = trName + l.TrString("form.glob_pattern_error", errs[0].Message)
case validation.ErrRegexPattern:
data["ErrorMsg"] = trName + l.Tr("form.regex_pattern_error", errs[0].Message)
data["ErrorMsg"] = trName + l.TrString("form.regex_pattern_error", errs[0].Message)
case validation.ErrUsername:
data["ErrorMsg"] = trName + l.Tr("form.username_error")
data["ErrorMsg"] = trName + l.TrString("form.username_error")
case validation.ErrInvalidGroupTeamMap:
data["ErrorMsg"] = trName + l.Tr("form.invalid_group_team_map_error", errs[0].Message)
data["ErrorMsg"] = trName + l.TrString("form.invalid_group_team_map_error", errs[0].Message)
default:
msg := errs[0].Classification
if msg != "" && errs[0].Message != "" {
@ -146,7 +146,7 @@ func Validate(errs binding.Errors, data map[string]any, f Form, l translation.Lo
msg += errs[0].Message
if msg == "" {
msg = l.Tr("form.unknown_error")
msg = l.TrString("form.unknown_error")
}
data["ErrorMsg"] = trName + ": " + msg
}

View File

@ -3,7 +3,11 @@
package middleware
import "net/url"
import (
"fmt"
"html/template"
"net/url"
)
// Flash represents a one time data transfer between two requests.
type Flash struct {
@ -26,26 +30,36 @@ func (f *Flash) set(name, msg string, current ...bool) {
}
}
func flashMsgStringOrHTML(msg any) string {
switch v := msg.(type) {
case string:
return v
case template.HTML:
return string(v)
}
panic(fmt.Sprintf("unknown type: %T", msg))
}
// Error sets error message
func (f *Flash) Error(msg string, current ...bool) {
f.ErrorMsg = msg
f.set("error", msg, current...)
func (f *Flash) Error(msg any, current ...bool) {
f.ErrorMsg = flashMsgStringOrHTML(msg)
f.set("error", f.ErrorMsg, current...)
}
// Warning sets warning message
func (f *Flash) Warning(msg string, current ...bool) {
f.WarningMsg = msg
f.set("warning", msg, current...)
func (f *Flash) Warning(msg any, current ...bool) {
f.WarningMsg = flashMsgStringOrHTML(msg)
f.set("warning", f.WarningMsg, current...)
}
// Info sets info message
func (f *Flash) Info(msg string, current ...bool) {
f.InfoMsg = msg
f.set("info", msg, current...)
func (f *Flash) Info(msg any, current ...bool) {
f.InfoMsg = flashMsgStringOrHTML(msg)
f.set("info", f.InfoMsg, current...)
}
// Success sets success message
func (f *Flash) Success(msg string, current ...bool) {
f.SuccessMsg = msg
f.set("success", msg, current...)
func (f *Flash) Success(msg any, current ...bool) {
f.SuccessMsg = flashMsgStringOrHTML(msg)
f.set("success", f.SuccessMsg, current...)
}

File diff suppressed because it is too large Load Diff

View File

@ -420,6 +420,7 @@ authorize_title=Εξουσιοδότηση του "%s" για έχει πρόσ
authorization_failed=Αποτυχία εξουσιοδότησης
authorization_failed_desc=Η εξουσιοδότηση απέτυχε επειδή εντοπίστηκε μια μη έγκυρη αίτηση. Παρακαλούμε επικοινωνήστε με το συντηρητή της εφαρμογής που προσπαθήσατε να εξουσιοδοτήσετε.
sspi_auth_failed=Αποτυχία ταυτοποίησης SSPI
password_pwned=Ο κωδικός πρόσβασης που επιλέξατε είναι σε μια λίστα <a target="_blank" rel="noopener noreferrer" href="https://haveibeenpwned.com/Passwords">κλεμμένων κωδικών πρόσβασης</a> που προηγουμένως εκτέθηκαν σε παραβίαση δημόσιων δεδομένων. Παρακαλώ δοκιμάστε ξανά με διαφορετικό κωδικό πρόσβασης και σκεφτείτε να αλλάξετε αυτόν τον κωδικό πρόσβασης όπου αλλού χρησιμοποιείται.
password_pwned_err=Δεν ήταν δυνατή η ολοκλήρωση του αιτήματος προς το HaveIBeenPwned
[mail]
@ -633,6 +634,7 @@ webauthn=Κλειδιά Ασφαλείας
public_profile=Δημόσιο Προφίλ
biography_placeholder=Πείτε μας λίγο για τον εαυτό σας! (Μπορείτε να γράψετε με Markdown)
location_placeholder=Μοιραστείτε την κατά προσέγγιση τοποθεσία σας με άλλους
profile_desc=Ελέγξτε πώς εμφανίζεται το προφίλ σας σε άλλους χρήστες. Η κύρια διεύθυνση email σας θα χρησιμοποιηθεί για ειδοποιήσεις, ανάκτηση κωδικού πρόσβασης και λειτουργίες Git που βασίζονται στο web.
password_username_disabled=Οι μη τοπικοί χρήστες δεν επιτρέπεται να αλλάξουν το όνομα χρήστη τους. Επικοινωνήστε με το διαχειριστή σας για περισσότερες λεπτομέρειες.
full_name=Πλήρες Όνομα
website=Ιστοσελίδα
@ -644,6 +646,7 @@ update_language_not_found=Η γλώσσα "%s" δεν είναι διαθέσι
update_language_success=Η γλώσσα ενημερώθηκε.
update_profile_success=Το προφίλ σας έχει ενημερωθεί.
change_username=Το όνομα χρήστη σας έχει αλλάξει.
change_username_prompt=Σημείωση: Αλλάζοντας το όνομα χρήστη σας αλλάζει επίσης το URL του λογαριασμού σας.
change_username_redirect_prompt=Το παλιό όνομα χρήστη θα ανακατευθύνει μέχρι να ζητηθεί ξανά.
continue=Συνέχεια
cancel=Ακύρωση
@ -722,6 +725,7 @@ add_email_success=Η νέα διεύθυνση email έχει προστεθεί
email_preference_set_success=Οι προτιμήσεις email έχουν οριστεί επιτυχώς.
add_openid_success=Προστέθηκε η νέα διεύθυνση OpenID.
keep_email_private=Απόκρυψη Διεύθυνσης Email
keep_email_private_popup=Αυτό θα κρύψει τη διεύθυνση ηλεκτρονικού ταχυδρομείου σας από το προφίλ σας, καθώς και όταν κάνετε ένα pull request ή επεξεργαστείτε ένα αρχείο χρησιμοποιώντας τη διεπαφή ιστού. Οι ωθούμενες υποβολές δεν θα τροποποιηθούν. Χρησιμοποιήστε το %s στις υποβολές για να τις συσχετίσετε με το λογαριασμό σας.
openid_desc=Το OpenID σας επιτρέπει να αναθέσετε τον έλεγχο ταυτότητας σε έναν εξωτερικό πάροχο.
manage_ssh_keys=Διαχείριση SSH Κλειδιών
@ -800,7 +804,9 @@ ssh_disabled=SSH Απενεργοποιημένο
ssh_signonly=Το SSH είναι απενεργοποιημένο αυτή τη στιγμή, έτσι αυτά τα κλειδιά είναι μόνο για την επαλήθευση υπογραφής των υποβολών.
ssh_externally_managed=Αυτό το κλειδί SSH διαχειρίζεται εξωτερικά για αυτόν το χρήστη
manage_social=Διαχείριση Συσχετιζόμενων Λογαριασμών Κοινωνικών Δικτύων
social_desc=Αυτοί οι κοινωνικοί λογαριασμοί μπορούν να χρησιμοποιηθούν για να συνδεθείτε στο λογαριασμό σας. Βεβαιωθείτε ότι τους αναγνωρίζετε όλους.
unbind=Αποσύνδεση
unbind_success=Ο κοινωνικός λογαριασμός έχει διαγραφεί επιτυχώς.
manage_access_token=Διαχείριση Διακριτικών Πρόσβασης
generate_new_token=Δημιουργία Νέου Διακριτικού
@ -822,6 +828,7 @@ select_permissions=Επιλέξτε δικαιώματα
permission_no_access=Καμία Πρόσβαση
permission_read=Αναγνωσμένες
permission_write=Ανάγνωση και Εγγραφή
access_token_desc=Τα επιλεγμένα δικαιώματα διακριτικών περιορίζουν την άδεια μόνο στις αντίστοιχες διαδρομές <a %s>API</a>. Διαβάστε την τεκμηρίωση <a %s></a> για περισσότερες πληροφορίες.
at_least_one_permission=Πρέπει να επιλέξετε τουλάχιστον ένα δικαίωμα για να δημιουργήσετε ένα διακριτικό
permissions_list=Δικαιώματα:
@ -833,6 +840,8 @@ remove_oauth2_application_desc=Η αφαίρεση μιας εφαρμογής O
remove_oauth2_application_success=Η εφαρμογή έχει διαγραφεί.
create_oauth2_application=Δημιουργία νέας εφαρμογής OAuth2
create_oauth2_application_button=Δημιουργία Εφαρμογής
create_oauth2_application_success=Έχετε δημιουργήσει με επιτυχία μια νέα εφαρμογή OAuth2.
update_oauth2_application_success=Έχετε ενημερώσει με επιτυχία την εφαρμογή OAuth2.
oauth2_application_name=Όνομα Εφαρμογής
oauth2_confidential_client=Εμπιστευτικός Πελάτης. Επιλέξτε το για εφαρμογές που διατηρούν το μυστικό κωδικό κρυφό, όπως πχ οι εφαρμογές ιστού. Μην επιλέγετε για εγγενείς εφαρμογές, συμπεριλαμβανομένων εφαρμογών επιφάνειας εργασίας και εφαρμογών για κινητά.
oauth2_redirect_uris=URI Ανακατεύθυνσης. Χρησιμοποιήστε μια νέα γραμμή για κάθε URI.
@ -841,10 +850,14 @@ oauth2_client_id=Ταυτότητα Πελάτη
oauth2_client_secret=Μυστικό Πελάτη
oauth2_regenerate_secret=Αναδημιουργία Μυστικού
oauth2_regenerate_secret_hint=Χάσατε το μυστικό σας;
oauth2_client_secret_hint=Το μυστικό δε θα εμφανιστεί ξανά αν κλείσετε ή ανανεώσετε αυτή τη σελίδα. Παρακαλώ βεβαιωθείτε ότι το έχετε αποθηκεύσει.
oauth2_application_edit=Επεξεργασία
oauth2_application_create_description=Οι εφαρμογές OAuth2 δίνει πρόσβαση στην εξωτερική εφαρμογή σας σε λογαριασμούς χρηστών σε αυτή την υπηρεσία.
oauth2_application_remove_description=Αφαιρώντας μια εφαρμογή OAuth2 θα αποτραπεί η πρόσβαση αυτής, σε εξουσιοδοτημένους λογαριασμούς χρηστών σε αυτή την υπηρεσία. Συνέχεια;
oauth2_application_locked=Το Gitea κάνει προεγγραφή σε μερικές εφαρμογές OAuth2 κατά την εκκίνηση αν είναι ενεργοποιημένες στις ρυθμίσεις. Για την αποφυγή απροσδόκητης συμπεριφοράς, αυτές δεν μπορούν ούτε να επεξεργαστούν ούτε να καταργηθούν. Παρακαλούμε ανατρέξτε στην τεκμηρίωση OAuth2 για περισσότερες πληροφορίες.
authorized_oauth2_applications=Εξουσιοδοτημένες Εφαρμογές OAuth2
authorized_oauth2_applications_description=Έχετε χορηγήσει πρόσβαση στον προσωπικό σας λογαριασμό σε αυτές τις εφαρμογές τρίτων. Ανακαλέστε την πρόσβαση για εφαρμογές που δεν χρειάζεστε πλέον.
revoke_key=Ανάκληση
revoke_oauth2_grant=Ανάκληση Πρόσβασης
revoke_oauth2_grant_description=Η ανάκληση πρόσβασης για αυτή την εξωτερική εφαρμογή θα αποτρέψει αυτή την εφαρμογή από την πρόσβαση στα δεδομένα σας. Σίγουρα;
@ -964,6 +977,8 @@ mirror_interval_invalid=Το χρονικό διάστημα του ειδώλο
mirror_sync_on_commit=Συγχρονισμός κατά την ώθηση
mirror_address=Κλωνοποίηση Από Το URL
mirror_address_desc=Τοποθετήστε όλα τα απαιτούμενα διαπιστευτήρια στην ενότητα Εξουσιοδότηση.
mirror_address_url_invalid=Η διεύθυνση URL που δόθηκε δεν είναι έγκυρη. Πρέπει να κάνετε escape όλα τα στοιχεία του url σωστά.
mirror_address_protocol_invalid=Η παρεχόμενη διεύθυνση URL δεν είναι έγκυρη. Μόνο οι τοποθεσίες http(s):// ή git:// μπορούν να χρησιμοποιηθούν για τη δημιουργία ειδώλου.
mirror_lfs=Large File Storage (LFS)
mirror_lfs_desc=Ενεργοποίηση αντικατοπτρισμού δεδομένων LFS.
mirror_lfs_endpoint=Άκρο LFS
@ -1736,6 +1751,7 @@ pulls.rebase_conflict_summary=Μήνυμα Σφάλματος
pulls.unrelated_histories=H Συγχώνευση Απέτυχε: Η κεφαλή και η βάση της συγχώνευσης δεν μοιράζονται μια κοινή ιστορία. Συμβουλή: Δοκιμάστε μια διαφορετική στρατηγική
pulls.merge_out_of_date=Η συγχώνευση απέτυχε: Κατά τη δημιουργία της συγχώνευσης, η βάση ενημερώθηκε. Συμβουλή: Δοκιμάστε ξανά.
pulls.head_out_of_date=Η συγχώνευση απέτυχε: Κατά τη δημιουργία της συγχώνευσης, το HEAD ενημερώθηκε. Συμβουλή: Δοκιμάστε ξανά.
pulls.has_merged=Αποτυχία: Το pull request έχει συγχωνευθεί, δεν είναι δυνατή η συγχώνευση ξανά ή να αλλάξει ο κλάδος προορισμού.
pulls.push_rejected=Η συγχώνευση απέτυχε: Η ώθηση απορρίφθηκε. Ελέγξτε τα Άγκιστρα Git για αυτό το αποθετήριο.
pulls.push_rejected_summary=Μήνυμα Πλήρους Απόρριψης
pulls.push_rejected_no_message=H Συγχώνευση Aπέτυχε: Η ώθηση απορρίφθηκε, αλλά δεν υπήρχε απομακρυσμένο μήνυμα.<br>Ελέγξτε τα Άγκιστρα Git για αυτό το αποθετήριο
@ -1757,7 +1773,11 @@ pulls.outdated_with_base_branch=Αυτός ο κλάδος δεν είναι ε
pulls.close=Κλείσιμο Pull Request
pulls.closed_at=`έκλεισε αυτό το pull request <a id="%[1]s" href="#%[1]s">%[2]s</a>`
pulls.reopened_at=`άνοιξε ξανά αυτό το pull request <a id="%[1]s" href="#%[1]s">%[2]s</a>`
pulls.cmd_instruction_hint=`Δείτε τις <a class="show-instruction">οδηγίες γραμμής εντολών</a>.`
pulls.cmd_instruction_checkout_title=Έλεγχος
pulls.cmd_instruction_checkout_desc=Από το αποθετήριο του έργου σας, ελέγξτε έναν νέο κλάδο και δοκιμάστε τις αλλαγές.
pulls.cmd_instruction_merge_title=Συγχώνευση
pulls.cmd_instruction_merge_desc=Συγχώνευση των αλλαγών και ενημέρωση στο Gitea.
pulls.clear_merge_message=Εκκαθάριση μηνύματος συγχώνευσης
pulls.clear_merge_message_hint=Η εκκαθάριση του μηνύματος συγχώνευσης θα αφαιρέσει μόνο το περιεχόμενο του μηνύματος υποβολής και θα διατηρήσει τα παραγόμενα git trailers όπως "Co-Authored-By …".
@ -1776,6 +1796,7 @@ pulls.auto_merge_canceled_schedule_comment=`ακύρωσε την αυτόματ
pulls.delete.title=Διαγραφή αυτού του pull request;
pulls.delete.text=Θέλετε πραγματικά να διαγράψετε αυτό το pull request; (Αυτό θα σβήσει οριστικά όλο το περιεχόμενο του. Εξετάστε αν θέλετε να το κλείσετε, αν σκοπεύεται να το αρχειοθετήσετε)
pulls.recently_pushed_new_branches=Ωθήσατε στο κλάδο <strong>%[1]s</strong> %[2]s
pull.deleted_branch=(διαγράφηκε):%s
@ -1809,8 +1830,13 @@ milestones.filter_sort.most_complete=Περισσότερο πλήρη
milestones.filter_sort.most_issues=Περισσότερα ζητήματα
milestones.filter_sort.least_issues=Λιγότερα ζητήματα
signing.will_sign=Αυτή η υποβολή θα υπογραφεί με το κλειδί "%s".
signing.wont_sign.error=Παρουσιάστηκε σφάλμα κατά τον έλεγχο για το αν η υποβολή μπορεί να υπογραφεί.
signing.wont_sign.never=Οι υποβολές δεν υπογράφονται ποτέ.
signing.wont_sign.always=Οι υποβολές υπογράφονται πάντα.
signing.wont_sign.parentsigned=Η υποβολή δε θα υπογραφεί καθώς η γονική υποβολή δεν έχει υπογραφεί.
signing.wont_sign.basesigned=Η συγχώνευση δε θα υπογραφεί καθώς η βασική υποβολή δεν έχει υπογραφή της βάσης.
signing.wont_sign.headsigned=Η συγχώνευση δε θα υπογραφεί καθώς δεν έχει υπογραφή η υποβολή της κεφαλής.
signing.wont_sign.not_signed_in=Δεν είστε συνδεδεμένοι.
ext_wiki=Πρόσβαση στο Εξωτερικό Wiki
@ -1951,6 +1977,7 @@ settings.mirror_settings.last_update=Τελευταία ενημέρωση
settings.mirror_settings.push_mirror.none=Δεν έχουν ρυθμιστεί είδωλα ώθησης
settings.mirror_settings.push_mirror.remote_url=URL Απομακρυσμένου Αποθετηρίου Git
settings.mirror_settings.push_mirror.add=Προσθήκη Είδωλου Push
settings.mirror_settings.push_mirror.edit_sync_time=Επεξεργασία διαστήματος συγχρονισμού ειδώλου
settings.sync_mirror=Συγχρονισμός Τώρα
settings.site=Ιστοσελίδα
@ -2085,12 +2112,14 @@ settings.webhook_deletion_desc=Η αφαίρεση ενός webhook διαγρά
settings.webhook_deletion_success=Το webhook έχει αφαιρεθεί.
settings.webhook.test_delivery=Δοκιμή Παράδοσης
settings.webhook.test_delivery_desc=Δοκιμάστε αυτό το webhook με ένα ψεύτικο συμβάν.
settings.webhook.test_delivery_desc_disabled=Για να δοκιμάσετε αυτό το webhook με μια ψεύτικη κλήση, ενεργοποιήστε το.
settings.webhook.request=Αίτημα
settings.webhook.response=Απάντηση
settings.webhook.headers=Κεφαλίδες
settings.webhook.payload=Περιεχόμενο
settings.webhook.body=Σώμα
settings.webhook.replay.description=Επανάληψη αυτού του webhook.
settings.webhook.replay.description_disabled=Για να επαναλάβετε αυτό το webhook, ενεργοποιήστε το.
settings.webhook.delivery.success=Ένα γεγονός έχει προστεθεί στην ουρά παράδοσης. Μπορεί να χρειαστούν λίγα δευτερόλεπτα μέχρι να εμφανιστεί στο ιστορικό.
settings.githooks_desc=Τα Άγκιστρα Git παρέχονται από το ίδιο το Git. Μπορείτε να επεξεργαστείτε τα αρχεία αγκίστρων παρακάτω για να ρυθμίσετε προσαρμοσμένες λειτουργίες.
settings.githook_edit_desc=Αν το hook είναι ανενεργό, θα παρουσιαστεί ένα παράδειγμα. Αφήνοντας το περιεχόμενο του hook κενό θα το απενεργοποιήσετε.
@ -2250,6 +2279,7 @@ settings.dismiss_stale_approvals_desc=Όταν οι νέες υποβολές π
settings.require_signed_commits=Απαιτούνται Υπογεγραμμένες Υποβολές
settings.require_signed_commits_desc=Απόρριψη νέων υποβολών σε αυτόν τον κλάδο εάν είναι μη υπογεγραμμένες ή μη επαληθεύσιμες.
settings.protect_branch_name_pattern=Μοτίβο Προστατευμένου Ονόματος Κλάδου
settings.protect_branch_name_pattern_desc=Μοτίβα ονόματος προστατευμένων κλάδων. Δείτε <a href="https://github.com/gobwas/glob">την τεκμηρίωση</a> για σύνταξη μοτίβου. Παραδείγματα: main, release/**
settings.protect_patterns=Μοτίβα
settings.protect_protected_file_patterns=Μοτίβα προστατευμένων αρχείων (διαχωρισμένα με ερωτηματικό ';'):
settings.protect_protected_file_patterns_desc=Τα προστατευόμενα αρχεία δεν επιτρέπεται να αλλάξουν άμεσα, ακόμη και αν ο χρήστης έχει δικαιώματα να προσθέσει, να επεξεργαστεί ή να διαγράψει αρχεία σε αυτόν τον κλάδο. Επιπλέων μοτίβα μπορούν να διαχωριστούν με ερωτηματικό (';'). Δείτε την τεκμηρίωση <a href='https://pkg.go.dev/github.com/gobwas/glob#Compile'>github.com/gobwas/glob</a> για τη σύνταξη του μοτίβου. Πχ: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>.
@ -2286,6 +2316,7 @@ settings.tags.protection.allowed.teams=Επιτρεπόμενες ομάδες
settings.tags.protection.allowed.noone=Καμία
settings.tags.protection.create=Προστασία Ετικέτας
settings.tags.protection.none=Δεν υπάρχουν προστατευμένες ετικέτες.
settings.tags.protection.pattern.description=Μπορείτε να χρησιμοποιήσετε ένα μόνο όνομα ή ένα μοτίβο τύπου glob ή κανονική έκφραση για να ταιριάξετε πολλαπλές ετικέτες. Διαβάστε περισσότερα στον <a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/protected-tags">οδηγό προστατευμένων ετικετών</a>.
settings.bot_token=Διακριτικό Bot
settings.chat_id=ID Συνομιλίας
settings.thread_id=ID Νήματος
@ -2302,6 +2333,8 @@ settings.archive.branchsettings_unavailable=Οι ρυθμίσεις του κλ
settings.archive.tagsettings_unavailable=Οι ρυθμίσεις της ετικέτας δεν είναι διαθέσιμες αν το αποθετήριο είναι αρχειοθετημένο.
settings.unarchive.button=Απο-Αρχειοθέτηση αποθετηρίου
settings.unarchive.header=Απο-Αρχειοθέτηση του αποθετηρίου
settings.unarchive.text=Η απο-αρχειοθέτηση του αποθετηρίου θα αποκαταστήσει την ικανότητά του να λαμβάνει υποβολές και ωθήσεις, καθώς και νέα ζητήματα και pull-requests.
settings.unarchive.success=Το αποθετήριο απο-αρχειοθετήθηκε με επιτυχία.
settings.update_avatar_success=Η εικόνα του αποθετηρίου έχει ενημερωθεί.
settings.lfs=LFS
settings.lfs_filelist=Αρχεία LFS σε αυτό το αποθετήριο
@ -2484,6 +2517,7 @@ tag.create_success=Η ετικέτα "%s" δημιουργήθηκε.
topic.manage_topics=Διαχείριση Θεμάτων
topic.done=Ολοκληρώθηκε
topic.count_prompt=Δεν μπορείτε να επιλέξετε περισσότερα από 25 θέματα
topic.format_prompt=Τα θέματα πρέπει να ξεκινούν με γράμμα ή αριθμό, μπορούν να περιλαμβάνουν παύλες ('-') και τελείες ('.'), μπορεί να είναι μέχρι 35 χαρακτήρες. Τα γράμματα πρέπει να είναι πεζά.
find_file.go_to_file=Αναζήτηση αρχείου
find_file.no_matching=Δεν ταιριάζει κανένα αρχείο
@ -2646,11 +2680,13 @@ dashboard.clean_unbind_oauth=Εκκαθάριση μη δεσμευμένων σ
dashboard.clean_unbind_oauth_success=Όλες οι μη δεσμευμένες συνδέσεις OAuth διαγράφηκαν.
dashboard.task.started=Εκκίνηση Εργασίας: %[1]s
dashboard.task.process=Εργασία: %[1]s
dashboard.task.cancelled=Εργασία: %[1]ακυρώθηκε: %[3]s
dashboard.task.error=Σφάλμα στην Εργασία: %[1]s: %[3]s
dashboard.task.finished=Εργασία: %[1]s που εκκινήθηκε από %[2]s τελείωσε
dashboard.task.unknown=Άγνωστη εργασία: %[1]s
dashboard.cron.started=Εκκίνηση Προγραμματισμένης Εργασίας: %[1]s
dashboard.cron.process=Προγραμματισμένη Εργασία: %[1]s
dashboard.cron.cancelled=Προγραμματισμένη εργασία: %[1]s ακυρώθηκε: %[3]s
dashboard.cron.error=Σφάλμα στη Προγραμματισμένη Εργασία: %s: %[3]s
dashboard.cron.finished=Προγραμματισμένη Εργασία: %[1]s τελείωσε
dashboard.delete_inactive_accounts=Διαγραφή όλων των μη ενεργοποιημένων λογαριασμών
@ -2660,6 +2696,7 @@ dashboard.delete_repo_archives.started=Η διαγραφή όλων των αρ
dashboard.delete_missing_repos=Διαγραφή όλων των αποθετηρίων που δεν έχουν τα αρχεία Git τους
dashboard.delete_missing_repos.started=Η διαγραφή όλων των αποθετηρίων που δεν έχουν αρχεία Git τους, ξεκίνησε.
dashboard.delete_generated_repository_avatars=Διαγραφή δημιουργημένων εικόνων αποθετηρίων
dashboard.sync_repo_branches=Συγχρονισμός κλάδων που λείπουν, από τα δεδομένα git στις βάσεις δεδομένων
dashboard.update_mirrors=Ενημέρωση Ειδώλων
dashboard.repo_health_check=Έλεγχος υγείας σε όλα τα αποθετήρια
dashboard.check_repo_stats=Έλεγχος όλων των στατιστικών αποθετηρίων
@ -2712,6 +2749,7 @@ dashboard.stop_zombie_tasks=Διακοπή εργασιών ζόμπι
dashboard.stop_endless_tasks=Διακοπή ατελείωτων εργασιών
dashboard.cancel_abandoned_jobs=Ακύρωση εγκαταλελειμμένων εργασιών
dashboard.start_schedule_tasks=Έναρξη προγραμματισμένων εργασιών
dashboard.sync_branch.started=Ο Συγχρονισμός των Κλάδων ξεκίνησε
dashboard.rebuild_issue_indexer=Αναδόμηση ευρετηρίου ζητημάτων
users.user_manage_panel=Διαχείριση Λογαριασμών Χρηστών

View File

@ -1775,6 +1775,7 @@ pulls.merge_pull_request = Create merge commit
pulls.rebase_merge_pull_request = Rebase then fast-forward
pulls.rebase_merge_commit_pull_request = Rebase then create merge commit
pulls.squash_merge_pull_request = Create squash commit
pulls.fast_forward_only_merge_pull_request = Fast-forward only
pulls.merge_manually = Manually merged
pulls.merge_commit_id = The merge commit ID
pulls.require_signed_wont_sign = The branch requires signed commits but this merge will not be signed
@ -1911,6 +1912,8 @@ wiki.page_name_desc = Enter a name for this Wiki page. Some special names are: '
wiki.original_git_entry_tooltip = View original Git file instead of using friendly link.
activity = Activity
activity.navbar.pulse = Pulse
activity.navbar.contributors = Contributors
activity.period.filter_label = Period:
activity.period.daily = 1 day
activity.period.halfweekly = 3 days
@ -1976,6 +1979,16 @@ activity.git_stats_and_deletions = and
activity.git_stats_deletion_1 = %d deletion
activity.git_stats_deletion_n = %d deletions
contributors = Contributors
contributors.contribution_type.filter_label = Contribution type:
contributors.contribution_type.commits = Commits
contributors.contribution_type.additions = Additions
contributors.contribution_type.deletions = Deletions
contributors.loading_title = Loading contributions...
contributors.loading_title_failed = Could not load contributions
contributors.loading_info = This might take a bit…
contributors.component_failed_to_load = An unexpected error happened.
search = Search
search.search_repo = Search repository
search.type.tooltip = Search type

786
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,15 +17,20 @@
"@webcomponents/custom-elements": "1.6.0",
"add-asset-webpack-plugin": "2.0.1",
"ansi_up": "6.0.2",
"asciinema-player": "3.6.3",
"asciinema-player": "3.6.4",
"chart.js": "4.4.1",
"chartjs-adapter-dayjs-4": "1.0.4",
"chartjs-plugin-zoom": "2.0.1",
"clippie": "4.0.6",
"css-loader": "6.10.0",
"dayjs": "1.11.10",
"dropzone": "6.0.0-beta.2",
"easymde": "2.18.0",
"esbuild-loader": "4.0.3",
"escape-goat": "4.0.0",
"fast-glob": "3.3.2",
"htmx.org": "1.9.10",
"idiomorph": "0.3.0",
"jquery": "3.7.1",
"katex": "0.16.9",
"license-checker-webpack-plugin": "0.2.1",
@ -34,21 +39,22 @@
"minimatch": "9.0.3",
"monaco-editor": "0.46.0",
"monaco-editor-webpack-plugin": "7.1.0",
"pdfobject": "2.2.12",
"pdfobject": "2.3.0",
"pretty-ms": "9.0.0",
"sortablejs": "1.15.2",
"swagger-ui-dist": "5.11.3",
"swagger-ui-dist": "5.11.6",
"throttle-debounce": "5.0.0",
"tinycolor2": "1.6.0",
"tippy.js": "6.3.7",
"toastify-js": "1.12.0",
"tributejs": "5.1.3",
"uint8-to-base64": "0.2.0",
"vue": "3.4.18",
"vue": "3.4.19",
"vue-bar-graph": "2.0.0",
"vue-chartjs": "5.3.0",
"vue-loader": "17.4.2",
"vue3-calendar-heatmap": "2.0.5",
"webpack": "5.90.1",
"webpack": "5.90.2",
"webpack-cli": "5.1.4",
"wrap-ansi": "9.0.0"
},
@ -56,7 +62,7 @@
"@eslint-community/eslint-plugin-eslint-comments": "4.1.0",
"@playwright/test": "1.41.2",
"@stoplight/spectral-cli": "6.11.0",
"@stylistic/eslint-plugin-js": "1.6.1",
"@stylistic/eslint-plugin-js": "1.6.2",
"@stylistic/stylelint-plugin": "2.0.0",
"@vitejs/plugin-vue": "5.0.4",
"eslint": "8.56.0",
@ -66,7 +72,7 @@
"eslint-plugin-no-jquery": "2.7.0",
"eslint-plugin-no-use-extend-native": "0.5.0",
"eslint-plugin-regexp": "2.2.0",
"eslint-plugin-sonarjs": "0.23.0",
"eslint-plugin-sonarjs": "0.24.0",
"eslint-plugin-unicorn": "51.0.1",
"eslint-plugin-vitest": "0.3.22",
"eslint-plugin-vitest-globals": "1.4.0",

8
poetry.lock generated
View File

@ -342,13 +342,13 @@ telegram = ["requests"]
[[package]]
name = "yamllint"
version = "1.34.0"
version = "1.35.0"
description = "A linter for YAML files."
optional = false
python-versions = ">=3.8"
files = [
{file = "yamllint-1.34.0-py3-none-any.whl", hash = "sha256:33b813f6ff2ffad2e57a288281098392b85f7463ce1f3d5cd45aa848b916a806"},
{file = "yamllint-1.34.0.tar.gz", hash = "sha256:7f0a6a41e8aab3904878da4ae34b6248b6bc74634e0d3a90f0fb2d7e723a3d4f"},
{file = "yamllint-1.35.0-py3-none-any.whl", hash = "sha256:601b0adaaac6d9bacb16a2e612e7ee8d23caf941ceebf9bfe2cff0f196266004"},
{file = "yamllint-1.35.0.tar.gz", hash = "sha256:9bc99c3e9fe89b4c6ee26e17aa817cf2d14390de6577cb6e2e6ed5f72120c835"},
]
[package.dependencies]
@ -361,4 +361,4 @@ dev = ["doc8", "flake8", "flake8-import-order", "rstcheck[sphinx]", "sphinx"]
[metadata]
lock-version = "2.0"
python-versions = "^3.8"
content-hash = "e4ea4301a70487379fce7008493d15c005af3aada7d88fbf0bd3167147ec6502"
content-hash = "ba1c2c4235872f67354b5f52aa5bf0cd616354961530d9dc907f9fba28cc1ece"

View File

@ -9,7 +9,7 @@ python = "^3.8"
[tool.poetry.group.dev.dependencies]
djlint = "1.34.1"
yamllint = "1.34.0"
yamllint = "1.35.0"
[tool.djlint]
profile="golang"

View File

@ -63,6 +63,7 @@ package actions
import (
"crypto/md5"
"errors"
"fmt"
"net/http"
"strconv"
@ -426,7 +427,19 @@ func (ar artifactRoutes) getDownloadArtifactURL(ctx *ArtifactContext) {
var items []downloadArtifactResponseItem
for _, artifact := range artifacts {
downloadURL := ar.buildArtifactURL(runID, strconv.FormatInt(artifact.ID, 10), "download")
var downloadURL string
if setting.Actions.ArtifactStorage.MinioConfig.ServeDirect {
u, err := ar.fs.URL(artifact.StoragePath, artifact.ArtifactName)
if err != nil && !errors.Is(err, storage.ErrURLNotSupported) {
log.Error("Error getting serve direct url: %v", err)
}
if u != nil {
downloadURL = u.String()
}
}
if downloadURL == "" {
downloadURL = ar.buildArtifactURL(runID, strconv.FormatInt(artifact.ID, 10), "download")
}
item := downloadArtifactResponseItem{
Path: util.PathJoinRel(itemPath, artifact.ArtifactPath),
ItemType: "file",

View File

@ -811,7 +811,7 @@ func individualPermsChecker(ctx *context.APIContext) {
// check for and warn against deprecated authentication options
func checkDeprecatedAuthMethods(ctx *context.APIContext) {
if ctx.FormString("token") != "" || ctx.FormString("access_token") != "" {
ctx.Resp.Header().Set("Warning", "token and access_token API authentication is deprecated and will be removed in gitea 1.23. Please use AuthorizationHeaderToken instead. Existing queries will continue to work but without authorization.")
ctx.Resp.Header().Set("X-Gitea-Warning", "token and access_token API authentication is deprecated and will be removed in gitea 1.23. Please use AuthorizationHeaderToken instead. Existing queries will continue to work but without authorization.")
}
}

View File

@ -762,13 +762,13 @@ func changeFilesCommitMessage(ctx *context.APIContext, files []*files_service.Ch
}
message := ""
if len(createFiles) != 0 {
message += ctx.Tr("repo.editor.add", strings.Join(createFiles, ", ")+"\n")
message += ctx.Locale.TrString("repo.editor.add", strings.Join(createFiles, ", ")+"\n")
}
if len(updateFiles) != 0 {
message += ctx.Tr("repo.editor.update", strings.Join(updateFiles, ", ")+"\n")
message += ctx.Locale.TrString("repo.editor.update", strings.Join(updateFiles, ", ")+"\n")
}
if len(deleteFiles) != 0 {
message += ctx.Tr("repo.editor.delete", strings.Join(deleteFiles, ", "))
message += ctx.Locale.TrString("repo.editor.delete", strings.Join(deleteFiles, ", "))
}
return strings.Trim(message, "\n")
}

View File

@ -395,7 +395,7 @@ func CreateIssueComment(ctx *context.APIContext) {
}
if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin {
ctx.Error(http.StatusForbidden, "CreateIssueComment", errors.New(ctx.Tr("repo.issues.comment_on_locked")))
ctx.Error(http.StatusForbidden, "CreateIssueComment", errors.New(ctx.Locale.TrString("repo.issues.comment_on_locked")))
return
}

View File

@ -8,6 +8,7 @@ import (
"fmt"
"net/http"
"slices"
"strconv"
"strings"
"time"
@ -884,6 +885,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
AllowRebase: true,
AllowRebaseMerge: true,
AllowSquash: true,
AllowFastForwardOnly: true,
AllowManualMerge: true,
AutodetectManualMerge: false,
AllowRebaseUpdate: true,
@ -910,6 +912,9 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
if opts.AllowSquash != nil {
config.AllowSquash = *opts.AllowSquash
}
if opts.AllowFastForwardOnly != nil {
config.AllowFastForwardOnly = *opts.AllowFastForwardOnly
}
if opts.AllowManualMerge != nil {
config.AllowManualMerge = *opts.AllowManualMerge
}
@ -1161,12 +1166,11 @@ func GetIssueTemplates(ctx *context.APIContext) {
// "$ref": "#/responses/IssueTemplates"
// "404":
// "$ref": "#/responses/notFound"
ret, err := issue.GetTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetTemplatesFromDefaultBranch", err)
return
ret := issue.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
if cnt := len(ret.TemplateErrors); cnt != 0 {
ctx.Resp.Header().Add("X-Gitea-Warning", "error occurs when parsing issue template: count="+strconv.Itoa(cnt))
}
ctx.JSON(http.StatusOK, ret)
ctx.JSON(http.StatusOK, ret.IssueTemplates)
}
// GetIssueConfig returns the issue config for a repo

View File

@ -35,6 +35,7 @@ func TestRepoEdit(t *testing.T) {
allowRebase := false
allowRebaseMerge := false
allowSquashMerge := false
allowFastForwardOnlyMerge := false
archived := true
opts := api.EditRepoOption{
Name: &ctx.Repo.Repository.Name,
@ -50,6 +51,7 @@ func TestRepoEdit(t *testing.T) {
AllowRebase: &allowRebase,
AllowRebaseMerge: &allowRebaseMerge,
AllowSquash: &allowSquashMerge,
AllowFastForwardOnly: &allowFastForwardOnlyMerge,
Archived: &archived,
}

View File

@ -28,13 +28,14 @@ import (
)
const (
tplDashboard base.TplName = "admin/dashboard"
tplSelfCheck base.TplName = "admin/self_check"
tplCron base.TplName = "admin/cron"
tplQueue base.TplName = "admin/queue"
tplStacktrace base.TplName = "admin/stacktrace"
tplQueueManage base.TplName = "admin/queue_manage"
tplStats base.TplName = "admin/stats"
tplDashboard base.TplName = "admin/dashboard"
tplSystemStatus base.TplName = "admin/system_status"
tplSelfCheck base.TplName = "admin/self_check"
tplCron base.TplName = "admin/cron"
tplQueue base.TplName = "admin/queue"
tplStacktrace base.TplName = "admin/stacktrace"
tplQueueManage base.TplName = "admin/queue_manage"
tplStats base.TplName = "admin/stats"
)
var sysStatus struct {
@ -72,7 +73,7 @@ var sysStatus struct {
// Garbage collector statistics.
NextGC string // next run in HeapAlloc time (bytes)
LastGC string // last run in absolute time (ns)
LastGCTime string // last run time
PauseTotalNs string
PauseNs string // circular buffer of recent GC pause times, most recent at [(NumGC+255)%256]
NumGC uint32
@ -110,7 +111,7 @@ func updateSystemStatus() {
sysStatus.OtherSys = base.FileSize(int64(m.OtherSys))
sysStatus.NextGC = base.FileSize(int64(m.NextGC))
sysStatus.LastGC = fmt.Sprintf("%.1fs", float64(time.Now().UnixNano()-int64(m.LastGC))/1000/1000/1000)
sysStatus.LastGCTime = time.Unix(0, int64(m.LastGC)).Format(time.RFC3339)
sysStatus.PauseTotalNs = fmt.Sprintf("%.1fs", float64(m.PauseTotalNs)/1000/1000/1000)
sysStatus.PauseNs = fmt.Sprintf("%.3fs", float64(m.PauseNs[(m.NumGC+255)%256])/1000/1000/1000)
sysStatus.NumGC = m.NumGC
@ -132,7 +133,6 @@ func Dashboard(ctx *context.Context) {
ctx.Data["PageIsAdminDashboard"] = true
ctx.Data["NeedUpdate"] = updatechecker.GetNeedUpdate(ctx)
ctx.Data["RemoteVersion"] = updatechecker.GetRemoteVersion(ctx)
// FIXME: update periodically
updateSystemStatus()
ctx.Data["SysStatus"] = sysStatus
ctx.Data["SSH"] = setting.SSH
@ -140,6 +140,12 @@ func Dashboard(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplDashboard)
}
func SystemStatus(ctx *context.Context) {
updateSystemStatus()
ctx.Data["SysStatus"] = sysStatus
ctx.HTML(http.StatusOK, tplSystemStatus)
}
// DashboardPost run an admin operation
func DashboardPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.AdminDashboardForm)

View File

@ -210,16 +210,16 @@ func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source {
func parseSSPIConfig(ctx *context.Context, form forms.AuthenticationForm) (*sspi.Source, error) {
if util.IsEmptyString(form.SSPISeparatorReplacement) {
ctx.Data["Err_SSPISeparatorReplacement"] = true
return nil, errors.New(ctx.Tr("form.SSPISeparatorReplacement") + ctx.Tr("form.require_error"))
return nil, errors.New(ctx.Locale.TrString("form.SSPISeparatorReplacement") + ctx.Locale.TrString("form.require_error"))
}
if separatorAntiPattern.MatchString(form.SSPISeparatorReplacement) {
ctx.Data["Err_SSPISeparatorReplacement"] = true
return nil, errors.New(ctx.Tr("form.SSPISeparatorReplacement") + ctx.Tr("form.alpha_dash_dot_error"))
return nil, errors.New(ctx.Locale.TrString("form.SSPISeparatorReplacement") + ctx.Locale.TrString("form.alpha_dash_dot_error"))
}
if form.SSPIDefaultLanguage != "" && !langCodePattern.MatchString(form.SSPIDefaultLanguage) {
ctx.Data["Err_SSPIDefaultLanguage"] = true
return nil, errors.New(ctx.Tr("form.lang_select_error"))
return nil, errors.New(ctx.Locale.TrString("form.lang_select_error"))
}
return &sspi.Source{

View File

@ -37,7 +37,7 @@ func ForgotPasswd(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("auth.forgot_password_title")
if setting.MailService == nil {
log.Warn(ctx.Tr("auth.disable_forgot_password_mail_admin"))
log.Warn("no mail service configured")
ctx.Data["IsResetDisable"] = true
ctx.HTML(http.StatusOK, tplForgotPassword)
return

View File

@ -6,6 +6,7 @@ package feed
import (
"fmt"
"html"
"html/template"
"net/http"
"net/url"
"strconv"
@ -79,119 +80,120 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
// title
title = act.ActUser.DisplayName() + " "
var titleExtra template.HTML
switch act.OpType {
case activities_model.ActionCreateRepo:
title += ctx.TrHTMLEscapeArgs("action.create_repo", act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx))
titleExtra = ctx.Locale.Tr("action.create_repo", act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx))
link.Href = act.GetRepoAbsoluteLink(ctx)
case activities_model.ActionRenameRepo:
title += ctx.TrHTMLEscapeArgs("action.rename_repo", act.GetContent(), act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx))
titleExtra = ctx.Locale.Tr("action.rename_repo", act.GetContent(), act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx))
link.Href = act.GetRepoAbsoluteLink(ctx)
case activities_model.ActionCommitRepo:
link.Href = toBranchLink(ctx, act)
if len(act.Content) != 0 {
title += ctx.TrHTMLEscapeArgs("action.commit_repo", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetBranch(), act.ShortRepoPath(ctx))
titleExtra = ctx.Locale.Tr("action.commit_repo", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetBranch(), act.ShortRepoPath(ctx))
} else {
title += ctx.TrHTMLEscapeArgs("action.create_branch", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetBranch(), act.ShortRepoPath(ctx))
titleExtra = ctx.Locale.Tr("action.create_branch", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetBranch(), act.ShortRepoPath(ctx))
}
case activities_model.ActionCreateIssue:
link.Href = toIssueLink(ctx, act)
title += ctx.TrHTMLEscapeArgs("action.create_issue", link.Href, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
titleExtra = ctx.Locale.Tr("action.create_issue", link.Href, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionCreatePullRequest:
link.Href = toPullLink(ctx, act)
title += ctx.TrHTMLEscapeArgs("action.create_pull_request", link.Href, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
titleExtra = ctx.Locale.Tr("action.create_pull_request", link.Href, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionTransferRepo:
link.Href = act.GetRepoAbsoluteLink(ctx)
title += ctx.TrHTMLEscapeArgs("action.transfer_repo", act.GetContent(), act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx))
titleExtra = ctx.Locale.Tr("action.transfer_repo", act.GetContent(), act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx))
case activities_model.ActionPushTag:
link.Href = toTagLink(ctx, act)
title += ctx.TrHTMLEscapeArgs("action.push_tag", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetTag(), act.ShortRepoPath(ctx))
titleExtra = ctx.Locale.Tr("action.push_tag", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetTag(), act.ShortRepoPath(ctx))
case activities_model.ActionCommentIssue:
issueLink := toIssueLink(ctx, act)
if link.Href == "#" {
link.Href = issueLink
}
title += ctx.TrHTMLEscapeArgs("action.comment_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
titleExtra = ctx.Locale.Tr("action.comment_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionMergePullRequest:
pullLink := toPullLink(ctx, act)
if link.Href == "#" {
link.Href = pullLink
}
title += ctx.TrHTMLEscapeArgs("action.merge_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
titleExtra = ctx.Locale.Tr("action.merge_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionAutoMergePullRequest:
pullLink := toPullLink(ctx, act)
if link.Href == "#" {
link.Href = pullLink
}
title += ctx.TrHTMLEscapeArgs("action.auto_merge_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
titleExtra = ctx.Locale.Tr("action.auto_merge_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionCloseIssue:
issueLink := toIssueLink(ctx, act)
if link.Href == "#" {
link.Href = issueLink
}
title += ctx.TrHTMLEscapeArgs("action.close_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
titleExtra = ctx.Locale.Tr("action.close_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionReopenIssue:
issueLink := toIssueLink(ctx, act)
if link.Href == "#" {
link.Href = issueLink
}
title += ctx.TrHTMLEscapeArgs("action.reopen_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
titleExtra = ctx.Locale.Tr("action.reopen_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionClosePullRequest:
pullLink := toPullLink(ctx, act)
if link.Href == "#" {
link.Href = pullLink
}
title += ctx.TrHTMLEscapeArgs("action.close_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
titleExtra = ctx.Locale.Tr("action.close_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionReopenPullRequest:
pullLink := toPullLink(ctx, act)
if link.Href == "#" {
link.Href = pullLink
}
title += ctx.TrHTMLEscapeArgs("action.reopen_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
titleExtra = ctx.Locale.Tr("action.reopen_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionDeleteTag:
link.Href = act.GetRepoAbsoluteLink(ctx)
title += ctx.TrHTMLEscapeArgs("action.delete_tag", act.GetRepoAbsoluteLink(ctx), act.GetTag(), act.ShortRepoPath(ctx))
titleExtra = ctx.Locale.Tr("action.delete_tag", act.GetRepoAbsoluteLink(ctx), act.GetTag(), act.ShortRepoPath(ctx))
case activities_model.ActionDeleteBranch:
link.Href = act.GetRepoAbsoluteLink(ctx)
title += ctx.TrHTMLEscapeArgs("action.delete_branch", act.GetRepoAbsoluteLink(ctx), html.EscapeString(act.GetBranch()), act.ShortRepoPath(ctx))
titleExtra = ctx.Locale.Tr("action.delete_branch", act.GetRepoAbsoluteLink(ctx), html.EscapeString(act.GetBranch()), act.ShortRepoPath(ctx))
case activities_model.ActionMirrorSyncPush:
srcLink := toSrcLink(ctx, act)
if link.Href == "#" {
link.Href = srcLink
}
title += ctx.TrHTMLEscapeArgs("action.mirror_sync_push", act.GetRepoAbsoluteLink(ctx), srcLink, act.GetBranch(), act.ShortRepoPath(ctx))
titleExtra = ctx.Locale.Tr("action.mirror_sync_push", act.GetRepoAbsoluteLink(ctx), srcLink, act.GetBranch(), act.ShortRepoPath(ctx))
case activities_model.ActionMirrorSyncCreate:
srcLink := toSrcLink(ctx, act)
if link.Href == "#" {
link.Href = srcLink
}
title += ctx.TrHTMLEscapeArgs("action.mirror_sync_create", act.GetRepoAbsoluteLink(ctx), srcLink, act.GetBranch(), act.ShortRepoPath(ctx))
titleExtra = ctx.Locale.Tr("action.mirror_sync_create", act.GetRepoAbsoluteLink(ctx), srcLink, act.GetBranch(), act.ShortRepoPath(ctx))
case activities_model.ActionMirrorSyncDelete:
link.Href = act.GetRepoAbsoluteLink(ctx)
title += ctx.TrHTMLEscapeArgs("action.mirror_sync_delete", act.GetRepoAbsoluteLink(ctx), act.GetBranch(), act.ShortRepoPath(ctx))
titleExtra = ctx.Locale.Tr("action.mirror_sync_delete", act.GetRepoAbsoluteLink(ctx), act.GetBranch(), act.ShortRepoPath(ctx))
case activities_model.ActionApprovePullRequest:
pullLink := toPullLink(ctx, act)
title += ctx.TrHTMLEscapeArgs("action.approve_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
titleExtra = ctx.Locale.Tr("action.approve_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionRejectPullRequest:
pullLink := toPullLink(ctx, act)
title += ctx.TrHTMLEscapeArgs("action.reject_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
titleExtra = ctx.Locale.Tr("action.reject_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionCommentPull:
pullLink := toPullLink(ctx, act)
title += ctx.TrHTMLEscapeArgs("action.comment_pull", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
titleExtra = ctx.Locale.Tr("action.comment_pull", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionPublishRelease:
releaseLink := toReleaseLink(ctx, act)
if link.Href == "#" {
link.Href = releaseLink
}
title += ctx.TrHTMLEscapeArgs("action.publish_release", act.GetRepoAbsoluteLink(ctx), releaseLink, act.ShortRepoPath(ctx), act.Content)
titleExtra = ctx.Locale.Tr("action.publish_release", act.GetRepoAbsoluteLink(ctx), releaseLink, act.ShortRepoPath(ctx), act.Content)
case activities_model.ActionPullReviewDismissed:
pullLink := toPullLink(ctx, act)
title += ctx.TrHTMLEscapeArgs("action.review_dismissed", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx), act.GetIssueInfos()[1])
titleExtra = ctx.Locale.Tr("action.review_dismissed", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx), act.GetIssueInfos()[1])
case activities_model.ActionStarRepo:
link.Href = act.GetRepoAbsoluteLink(ctx)
title += ctx.TrHTMLEscapeArgs("action.starred_repo", act.GetRepoAbsoluteLink(ctx), act.GetRepoPath(ctx))
titleExtra = ctx.Locale.Tr("action.starred_repo", act.GetRepoAbsoluteLink(ctx), act.GetRepoPath(ctx))
case activities_model.ActionWatchRepo:
link.Href = act.GetRepoAbsoluteLink(ctx)
title += ctx.TrHTMLEscapeArgs("action.watched_repo", act.GetRepoAbsoluteLink(ctx), act.GetRepoPath(ctx))
titleExtra = ctx.Locale.Tr("action.watched_repo", act.GetRepoAbsoluteLink(ctx), act.GetRepoPath(ctx))
default:
return nil, fmt.Errorf("unknown action type: %v", act.OpType)
}
@ -233,7 +235,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
case activities_model.ActionCloseIssue, activities_model.ActionReopenIssue, activities_model.ActionClosePullRequest, activities_model.ActionReopenPullRequest:
desc = act.GetIssueTitle(ctx)
case activities_model.ActionPullReviewDismissed:
desc = ctx.Tr("action.review_dismissed_reason") + "\n\n" + act.GetIssueInfos()[2]
desc = ctx.Locale.TrString("action.review_dismissed_reason") + "\n\n" + act.GetIssueInfos()[2]
}
}
if len(content) == 0 {
@ -241,7 +243,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
}
items = append(items, &feeds.Item{
Title: title,
Title: template.HTMLEscapeString(title) + string(titleExtra),
Link: link,
Description: desc,
IsPermaLink: "false",

View File

@ -56,7 +56,7 @@ func showUserFeed(ctx *context.Context, formatType string) {
}
feed := &feeds.Feed{
Title: ctx.Tr("home.feed_of", ctx.ContextUser.DisplayName()),
Title: ctx.Locale.TrString("home.feed_of", ctx.ContextUser.DisplayName()),
Link: &feeds.Link{Href: ctx.ContextUser.HTMLURL()},
Description: ctxUserDescription,
Created: time.Now(),

View File

@ -28,10 +28,10 @@ func ShowReleaseFeed(ctx *context.Context, repo *repo_model.Repository, isReleas
var link *feeds.Link
if isReleasesOnly {
title = ctx.Tr("repo.release.releases_for", repo.FullName())
title = ctx.Locale.TrString("repo.release.releases_for", repo.FullName())
link = &feeds.Link{Href: repo.HTMLURL() + "/release"}
} else {
title = ctx.Tr("repo.release.tags_for", repo.FullName())
title = ctx.Locale.TrString("repo.release.tags_for", repo.FullName())
link = &feeds.Link{Href: repo.HTMLURL() + "/tags"}
}

View File

@ -27,7 +27,7 @@ func ShowRepoFeed(ctx *context.Context, repo *repo_model.Repository, formatType
}
feed := &feeds.Feed{
Title: ctx.Tr("home.feed_of", repo.FullName()),
Title: ctx.Locale.TrString("home.feed_of", repo.FullName()),
Link: &feeds.Link{Href: repo.HTMLURL()},
Description: repo.Description,
Created: time.Now(),

View File

@ -29,7 +29,7 @@ func Create(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("new_org")
ctx.Data["DefaultOrgVisibilityMode"] = setting.Service.DefaultOrgVisibilityMode
if !ctx.Doer.CanCreateOrganization() {
ctx.ServerError("Not allowed", errors.New(ctx.Tr("org.form.create_org_not_allowed")))
ctx.ServerError("Not allowed", errors.New(ctx.Locale.TrString("org.form.create_org_not_allowed")))
return
}
ctx.HTML(http.StatusOK, tplCreateOrg)
@ -41,7 +41,7 @@ func CreatePost(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("new_org")
if !ctx.Doer.CanCreateOrganization() {
ctx.ServerError("Not allowed", errors.New(ctx.Tr("org.form.create_org_not_allowed")))
ctx.ServerError("Not allowed", errors.New(ctx.Locale.TrString("org.form.create_org_not_allowed")))
return
}

View File

@ -353,7 +353,7 @@ func ViewProject(ctx *context.Context) {
}
if boards[0].ID == 0 {
boards[0].Title = ctx.Tr("repo.projects.type.uncategorized")
boards[0].Title = ctx.Locale.TrString("repo.projects.type.uncategorized")
}
issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards)
@ -377,16 +377,16 @@ func ViewProject(ctx *context.Context) {
linkedPrsMap := make(map[int64][]*issues_model.Issue)
for _, issuesList := range issuesMap {
for _, issue := range issuesList {
var referencedIds []int64
var referencedIDs []int64
for _, comment := range issue.Comments {
if comment.RefIssueID != 0 && comment.RefIsPull {
referencedIds = append(referencedIds, comment.RefIssueID)
referencedIDs = append(referencedIDs, comment.RefIssueID)
}
}
if len(referencedIds) > 0 {
if len(referencedIDs) > 0 {
if linkedPrs, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{
IssueIDs: referencedIds,
IssueIDs: referencedIDs,
IsPull: util.OptionalBoolTrue,
}); err == nil {
linkedPrsMap[issue.ID] = linkedPrs
@ -679,7 +679,7 @@ func MoveIssues(ctx *context.Context) {
board = &project_model.Board{
ID: 0,
ProjectID: project.ID,
Title: ctx.Tr("repo.projects.type.uncategorized"),
Title: ctx.Locale.TrString("repo.projects.type.uncategorized"),
}
} else {
board, err = project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))

24
routers/web/passkey.go Normal file
View File

@ -0,0 +1,24 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package web
import (
"net/http"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
)
type passkeyEndpointsType struct {
Enroll string `json:"enroll"`
Manage string `json:"manage"`
}
func passkeyEndpoints(ctx *context.Context) {
url := setting.AppURL + "user/settings/security"
ctx.JSON(http.StatusOK, passkeyEndpointsType{
Enroll: url,
Manage: url,
})
}

View File

@ -100,7 +100,7 @@ func List(ctx *context.Context) {
}
wf, err := model.ReadWorkflow(bytes.NewReader(content))
if err != nil {
workflow.ErrMsg = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", err.Error())
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.invalid_workflow_helper", err.Error())
workflows = append(workflows, workflow)
continue
}
@ -115,7 +115,7 @@ func List(ctx *context.Context) {
continue
}
if !allRunnerLabels.Contains(ro) {
workflow.ErrMsg = ctx.Locale.Tr("actions.runs.no_matching_online_runner_helper", ro)
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_matching_online_runner_helper", ro)
break
}
}

View File

@ -168,8 +168,8 @@ func ViewPost(ctx *context_module.Context) {
Link: run.RefLink(),
}
resp.State.Run.Commit = ViewCommit{
LocaleCommit: ctx.Tr("actions.runs.commit"),
LocalePushedBy: ctx.Tr("actions.runs.pushed_by"),
LocaleCommit: ctx.Locale.TrString("actions.runs.commit"),
LocalePushedBy: ctx.Locale.TrString("actions.runs.pushed_by"),
ShortSha: base.ShortSha(run.CommitSHA),
Link: fmt.Sprintf("%s/commit/%s", run.Repo.Link(), run.CommitSHA),
Pusher: pusher,
@ -194,7 +194,7 @@ func ViewPost(ctx *context_module.Context) {
resp.State.CurrentJob.Title = current.Name
resp.State.CurrentJob.Detail = current.Status.LocaleString(ctx.Locale)
if run.NeedApproval {
resp.State.CurrentJob.Detail = ctx.Locale.Tr("actions.need_approval_desc")
resp.State.CurrentJob.Detail = ctx.Locale.TrString("actions.need_approval_desc")
}
resp.State.CurrentJob.Steps = make([]*ViewJobStep, 0) // marshal to '[]' instead fo 'null' in json
resp.Logs.StepsLog = make([]*ViewStepLog, 0) // marshal to '[]' instead fo 'null' in json

View File

@ -22,6 +22,8 @@ func Activity(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.activity")
ctx.Data["PageIsActivity"] = true
ctx.Data["PageIsPulse"] = true
ctx.Data["Period"] = ctx.Params("period")
timeUntil := time.Now()

View File

@ -20,6 +20,7 @@ import (
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
files_service "code.gitea.io/gitea/services/repository/files"
)
type blameRow struct {
@ -247,31 +248,11 @@ func processBlameParts(ctx *context.Context, blameParts []*git.BlamePart) map[st
func renderBlame(ctx *context.Context, blameParts []*git.BlamePart, commitNames map[string]*user_model.UserCommit) {
repoLink := ctx.Repo.RepoLink
language := ""
indexFilename, worktree, deleteTemporaryFile, err := ctx.Repo.GitRepo.ReadTreeToTemporaryIndex(ctx.Repo.CommitID)
if err == nil {
defer deleteTemporaryFile()
filename2attribute2info, err := ctx.Repo.GitRepo.CheckAttribute(git.CheckAttributeOpts{
CachedOnly: true,
Attributes: []string{"linguist-language", "gitlab-language"},
Filenames: []string{ctx.Repo.TreePath},
IndexFile: indexFilename,
WorkTree: worktree,
})
if err != nil {
log.Error("Unable to load attributes for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err)
}
language = filename2attribute2info[ctx.Repo.TreePath]["linguist-language"]
if language == "" || language == "unspecified" {
language = filename2attribute2info[ctx.Repo.TreePath]["gitlab-language"]
}
if language == "unspecified" {
language = ""
}
language, err := files_service.TryGetContentLanguage(ctx.Repo.GitRepo, ctx.Repo.CommitID, ctx.Repo.TreePath)
if err != nil {
log.Error("Unable to get file language for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err)
}
lines := make([]string, 0)
rows := make([]*blameRow, 0)
escapeStatus := &charset.EscapeStatus{}

View File

@ -104,9 +104,9 @@ func CherryPickPost(ctx *context.Context) {
message := strings.TrimSpace(form.CommitSummary)
if message == "" {
if form.Revert {
message = ctx.Tr("repo.commit.revert-header", sha)
message = ctx.Locale.TrString("repo.commit.revert-header", sha)
} else {
message = ctx.Tr("repo.commit.cherry-pick-header", sha)
message = ctx.Locale.TrString("repo.commit.cherry-pick-header", sha)
}
}

View File

@ -126,7 +126,7 @@ func setCsvCompareContext(ctx *context.Context) {
return CsvDiffResult{nil, ""}
}
errTooLarge := errors.New(ctx.Locale.Tr("repo.error.csv.too_large"))
errTooLarge := errors.New(ctx.Locale.TrString("repo.error.csv.too_large"))
csvReaderFromCommit := func(ctx *markup.RenderContext, blob *git.Blob) (*csv.Reader, io.Closer, error) {
if blob == nil {

View File

@ -0,0 +1,44 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"errors"
"net/http"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
contributors_service "code.gitea.io/gitea/services/repository"
)
const (
tplContributors base.TplName = "repo/activity"
)
// Contributors render the page to show repository contributors graph
func Contributors(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.contributors")
ctx.Data["PageIsActivity"] = true
ctx.Data["PageIsContributors"] = true
ctx.PageData["contributionType"] = "commits"
ctx.PageData["repoLink"] = ctx.Repo.RepoLink
ctx.HTML(http.StatusOK, tplContributors)
}
// ContributorsData renders JSON of contributors along with their weekly commit statistics
func ContributorsData(ctx *context.Context) {
if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil {
if errors.Is(err, contributors_service.ErrAwaitGeneration) {
ctx.Status(http.StatusAccepted)
return
}
ctx.ServerError("GetContributorStats", err)
} else {
ctx.JSON(http.StatusOK, contributorStats)
}
}

View File

@ -262,9 +262,9 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
message := strings.TrimSpace(form.CommitSummary)
if len(message) == 0 {
if isNewFile {
message = ctx.Tr("repo.editor.add", form.TreePath)
message = ctx.Locale.TrString("repo.editor.add", form.TreePath)
} else {
message = ctx.Tr("repo.editor.update", form.TreePath)
message = ctx.Locale.TrString("repo.editor.update", form.TreePath)
}
}
form.CommitMessage = strings.TrimSpace(form.CommitMessage)
@ -415,7 +415,7 @@ func DiffPreviewPost(ctx *context.Context) {
}
if diff.NumFiles == 0 {
ctx.PlainText(http.StatusOK, ctx.Tr("repo.editor.no_changes_to_show"))
ctx.PlainText(http.StatusOK, ctx.Locale.TrString("repo.editor.no_changes_to_show"))
return
}
ctx.Data["File"] = diff.Files[0]
@ -482,7 +482,7 @@ func DeleteFilePost(ctx *context.Context) {
message := strings.TrimSpace(form.CommitSummary)
if len(message) == 0 {
message = ctx.Tr("repo.editor.delete", ctx.Repo.TreePath)
message = ctx.Locale.TrString("repo.editor.delete", ctx.Repo.TreePath)
}
form.CommitMessage = strings.TrimSpace(form.CommitMessage)
if len(form.CommitMessage) > 0 {
@ -691,7 +691,7 @@ func UploadFilePost(ctx *context.Context) {
if dir == "" {
dir = "/"
}
message = ctx.Tr("repo.editor.upload_files_to_dir", dir)
message = ctx.Locale.TrString("repo.editor.upload_files_to_dir", dir)
}
form.CommitMessage = strings.TrimSpace(form.CommitMessage)

View File

@ -993,17 +993,17 @@ func NewIssue(ctx *context.Context) {
}
ctx.Data["Tags"] = tags
_, templateErrs := issue_service.GetTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
ret := issue_service.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
templateLoaded, errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates)
for k, v := range errs {
templateErrs[k] = v
ret.TemplateErrors[k] = v
}
if ctx.Written() {
return
}
if len(templateErrs) > 0 {
ctx.Flash.Warning(renderErrorOfTemplates(ctx, templateErrs), true)
if len(ret.TemplateErrors) > 0 {
ctx.Flash.Warning(renderErrorOfTemplates(ctx, ret.TemplateErrors), true)
}
ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWrite(unit.TypeIssues)
@ -1036,7 +1036,7 @@ func renderErrorOfTemplates(ctx *context.Context, errs map[string]error) string
})
if err != nil {
log.Debug("render flash error: %v", err)
flashError = ctx.Tr("repo.issues.choose.ignore_invalid_templates")
flashError = ctx.Locale.TrString("repo.issues.choose.ignore_invalid_templates")
}
return flashError
}
@ -1046,11 +1046,11 @@ func NewIssueChooseTemplate(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.issues.new")
ctx.Data["PageIsIssueList"] = true
issueTemplates, errs := issue_service.GetTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
ctx.Data["IssueTemplates"] = issueTemplates
ret := issue_service.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
ctx.Data["IssueTemplates"] = ret.IssueTemplates
if len(errs) > 0 {
ctx.Flash.Warning(renderErrorOfTemplates(ctx, errs), true)
if len(ret.TemplateErrors) > 0 {
ctx.Flash.Warning(renderErrorOfTemplates(ctx, ret.TemplateErrors), true)
}
if !issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo) {
@ -1655,7 +1655,7 @@ func ViewIssue(ctx *context.Context) {
}
ghostMilestone := &issues_model.Milestone{
ID: -1,
Name: ctx.Tr("repo.issues.deleted_milestone"),
Name: ctx.Locale.TrString("repo.issues.deleted_milestone"),
}
if comment.OldMilestoneID > 0 && comment.OldMilestone == nil {
comment.OldMilestone = ghostMilestone
@ -1672,7 +1672,7 @@ func ViewIssue(ctx *context.Context) {
ghostProject := &project_model.Project{
ID: -1,
Title: ctx.Tr("repo.issues.deleted_project"),
Title: ctx.Locale.TrString("repo.issues.deleted_project"),
}
if comment.OldProjectID > 0 && comment.OldProject == nil {
@ -1862,6 +1862,8 @@ func ViewIssue(ctx *context.Context) {
mergeStyle = repo_model.MergeStyleRebaseMerge
} else if prConfig.AllowSquash {
mergeStyle = repo_model.MergeStyleSquash
} else if prConfig.AllowFastForwardOnly {
mergeStyle = repo_model.MergeStyleFastForwardOnly
} else if prConfig.AllowManualMerge {
mergeStyle = repo_model.MergeStyleManuallyMerged
}

View File

@ -56,12 +56,12 @@ func GetContentHistoryList(ctx *context.Context) {
for _, item := range items {
var actionText string
if item.IsDeleted {
actionTextDeleted := ctx.Locale.Tr("repo.issues.content_history.deleted")
actionTextDeleted := ctx.Locale.TrString("repo.issues.content_history.deleted")
actionText = "<i data-history-is-deleted='1'>" + actionTextDeleted + "</i>"
} else if item.IsFirstCreated {
actionText = ctx.Locale.Tr("repo.issues.content_history.created")
actionText = ctx.Locale.TrString("repo.issues.content_history.created")
} else {
actionText = ctx.Locale.Tr("repo.issues.content_history.edited")
actionText = ctx.Locale.TrString("repo.issues.content_history.edited")
}
username := item.UserName

View File

@ -123,7 +123,7 @@ func TestDeleteLabel(t *testing.T) {
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
unittest.AssertNotExistsBean(t, &issues_model.Label{ID: 2})
unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{LabelID: 2})
assert.Equal(t, ctx.Tr("repo.issues.label_deletion_success"), ctx.Flash.SuccessMsg)
assert.EqualValues(t, ctx.Tr("repo.issues.label_deletion_success"), ctx.Flash.SuccessMsg)
}
func TestUpdateIssueLabel_Clear(t *testing.T) {

View File

@ -294,8 +294,8 @@ func MilestoneIssuesAndPulls(ctx *context.Context) {
issues(ctx, milestoneID, projectID, util.OptionalBoolNone)
ret, _ := issue.GetTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
ctx.Data["NewIssueChooseTemplate"] = len(ret) > 0
ret := issue.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
ctx.Data["NewIssueChooseTemplate"] = len(ret.IssueTemplates) > 0
ctx.Data["CanWriteIssues"] = ctx.Repo.CanWriteIssuesOrPulls(false)
ctx.Data["CanWritePulls"] = ctx.Repo.CanWriteIssuesOrPulls(true)

View File

@ -79,7 +79,7 @@ func NewDiffPatchPost(ctx *context.Context) {
// `message` will be both the summary and message combined
message := strings.TrimSpace(form.CommitSummary)
if len(message) == 0 {
message = ctx.Tr("repo.editor.patch")
message = ctx.Locale.TrString("repo.editor.patch")
}
form.CommitMessage = strings.TrimSpace(form.CommitMessage)

View File

@ -315,7 +315,7 @@ func ViewProject(ctx *context.Context) {
}
if boards[0].ID == 0 {
boards[0].Title = ctx.Tr("repo.projects.type.uncategorized")
boards[0].Title = ctx.Locale.TrString("repo.projects.type.uncategorized")
}
issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards)
@ -339,16 +339,16 @@ func ViewProject(ctx *context.Context) {
linkedPrsMap := make(map[int64][]*issues_model.Issue)
for _, issuesList := range issuesMap {
for _, issue := range issuesList {
var referencedIds []int64
var referencedIDs []int64
for _, comment := range issue.Comments {
if comment.RefIssueID != 0 && comment.RefIsPull {
referencedIds = append(referencedIds, comment.RefIssueID)
referencedIDs = append(referencedIDs, comment.RefIssueID)
}
}
if len(referencedIds) > 0 {
if len(referencedIDs) > 0 {
if linkedPrs, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{
IssueIDs: referencedIds,
IssueIDs: referencedIDs,
IsPull: util.OptionalBoolTrue,
}); err == nil {
linkedPrsMap[issue.ID] = linkedPrs
@ -633,7 +633,7 @@ func MoveIssues(ctx *context.Context) {
board = &project_model.Board{
ID: 0,
ProjectID: project.ID,
Title: ctx.Tr("repo.projects.type.uncategorized"),
Title: ctx.Locale.TrString("repo.projects.type.uncategorized"),
}
} else {
board, err = project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))

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