mirror of
https://github.com/go-gitea/gitea.git
synced 2025-07-20 21:18:30 +02:00
Merge branch 'main' into lunny/move_wikipath
This commit is contained in:
commit
a7d5d972e9
1
.gitattributes
vendored
1
.gitattributes
vendored
@ -4,6 +4,7 @@
|
||||
/assets/*.json linguist-generated
|
||||
/public/assets/img/svg/*.svg linguist-generated
|
||||
/templates/swagger/v1_json.tmpl linguist-generated
|
||||
/options/fileicon/** linguist-generated
|
||||
/vendor/** -text -eol linguist-vendored
|
||||
/web_src/js/vendor/** -text -eol linguist-vendored
|
||||
Dockerfile.* linguist-language=Dockerfile
|
||||
|
24
.github/workflows/release-nightly.yml
vendored
24
.github/workflows/release-nightly.yml
vendored
@ -59,6 +59,8 @@ jobs:
|
||||
aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
|
||||
nightly-docker-rootful:
|
||||
runs-on: namespace-profile-gitea-release-docker
|
||||
permissions:
|
||||
packages: write # to publish to ghcr.io
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
@ -85,6 +87,12 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to GHCR using PAT
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: fetch go modules
|
||||
run: make vendor
|
||||
- name: build rootful docker image
|
||||
@ -93,9 +101,13 @@ jobs:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: gitea/gitea:${{ steps.clean_name.outputs.branch }}
|
||||
tags: |-
|
||||
gitea/gitea:${{ steps.clean_name.outputs.branch }}
|
||||
ghcr.io/go-gitea/gitea:${{ steps.clean_name.outputs.branch }}
|
||||
nightly-docker-rootless:
|
||||
runs-on: namespace-profile-gitea-release-docker
|
||||
permissions:
|
||||
packages: write # to publish to ghcr.io
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
@ -122,6 +134,12 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to GHCR using PAT
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: fetch go modules
|
||||
run: make vendor
|
||||
- name: build rootless docker image
|
||||
@ -131,4 +149,6 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
file: Dockerfile.rootless
|
||||
tags: gitea/gitea:${{ steps.clean_name.outputs.branch }}-rootless
|
||||
tags: |-
|
||||
gitea/gitea:${{ steps.clean_name.outputs.branch }}-rootless
|
||||
ghcr.io/go-gitea/gitea:${{ steps.clean_name.outputs.branch }}-rootless
|
||||
|
24
.github/workflows/release-tag-rc.yml
vendored
24
.github/workflows/release-tag-rc.yml
vendored
@ -69,6 +69,8 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
docker-rootful:
|
||||
runs-on: namespace-profile-gitea-release-docker
|
||||
permissions:
|
||||
packages: write # to publish to ghcr.io
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
@ -79,7 +81,9 @@ jobs:
|
||||
- uses: docker/metadata-action@v5
|
||||
id: meta
|
||||
with:
|
||||
images: gitea/gitea
|
||||
images: |-
|
||||
gitea/gitea
|
||||
ghcr.io/go-gitea/gitea
|
||||
flavor: |
|
||||
latest=false
|
||||
# 1.2.3-rc0
|
||||
@ -90,6 +94,12 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to GHCR using PAT
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: build rootful docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
@ -100,6 +110,8 @@ jobs:
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
docker-rootless:
|
||||
runs-on: namespace-profile-gitea-release-docker
|
||||
permissions:
|
||||
packages: write # to publish to ghcr.io
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
@ -110,7 +122,9 @@ jobs:
|
||||
- uses: docker/metadata-action@v5
|
||||
id: meta
|
||||
with:
|
||||
images: gitea/gitea
|
||||
images: |-
|
||||
gitea/gitea
|
||||
ghcr.io/go-gitea/gitea
|
||||
# each tag below will have the suffix of -rootless
|
||||
flavor: |
|
||||
latest=false
|
||||
@ -123,6 +137,12 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to GHCR using PAT
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: build rootless docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
|
24
.github/workflows/release-tag-version.yml
vendored
24
.github/workflows/release-tag-version.yml
vendored
@ -14,6 +14,8 @@ concurrency:
|
||||
jobs:
|
||||
binary:
|
||||
runs-on: namespace-profile-gitea-release-binary
|
||||
permissions:
|
||||
packages: write # to publish to ghcr.io
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
@ -71,6 +73,8 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
docker-rootful:
|
||||
runs-on: namespace-profile-gitea-release-docker
|
||||
permissions:
|
||||
packages: write # to publish to ghcr.io
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
@ -81,7 +85,9 @@ jobs:
|
||||
- uses: docker/metadata-action@v5
|
||||
id: meta
|
||||
with:
|
||||
images: gitea/gitea
|
||||
images: |-
|
||||
gitea/gitea
|
||||
ghcr.io/go-gitea/gitea
|
||||
# this will generate tags in the following format:
|
||||
# latest
|
||||
# 1
|
||||
@ -96,6 +102,12 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to GHCR using PAT
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: build rootful docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
@ -116,7 +128,9 @@ jobs:
|
||||
- uses: docker/metadata-action@v5
|
||||
id: meta
|
||||
with:
|
||||
images: gitea/gitea
|
||||
images: |-
|
||||
gitea/gitea
|
||||
ghcr.io/go-gitea/gitea
|
||||
# each tag below will have the suffix of -rootless
|
||||
flavor: |
|
||||
suffix=-rootless,onlatest=true
|
||||
@ -134,6 +148,12 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to GHCR using PAT
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: build rootless docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
|
@ -13,14 +13,17 @@ linters:
|
||||
- gocritic
|
||||
- govet
|
||||
- ineffassign
|
||||
- mirror
|
||||
- nakedret
|
||||
- nolintlint
|
||||
- perfsprint
|
||||
- revive
|
||||
- staticcheck
|
||||
- testifylint
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- usestdlibvars
|
||||
- usetesting
|
||||
- wastedassign
|
||||
settings:
|
||||
|
@ -11,7 +11,7 @@
|
||||
[](https://gitpod.io/#https://github.com/go-gitea/gitea)
|
||||
[](https://translate.gitea.com "Crowdin")
|
||||
|
||||
[View this document in Chinese](./README_ZH.md)
|
||||
[繁體中文](./README.zh-tw.md) | [简体中文](./README.zh-cn.md)
|
||||
|
||||
## Purpose
|
||||
|
||||
|
206
README.zh-cn.md
Normal file
206
README.zh-cn.md
Normal file
@ -0,0 +1,206 @@
|
||||
# Gitea
|
||||
|
||||
[](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly")
|
||||
[](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
|
||||
[](https://goreportcard.com/report/code.gitea.io/gitea "Go Report Card")
|
||||
[](https://pkg.go.dev/code.gitea.io/gitea "GoDoc")
|
||||
[](https://github.com/go-gitea/gitea/releases/latest "GitHub release")
|
||||
[](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source")
|
||||
[](https://opencollective.com/gitea "Become a backer/sponsor of gitea")
|
||||
[](https://opensource.org/licenses/MIT "License: MIT")
|
||||
[](https://gitpod.io/#https://github.com/go-gitea/gitea)
|
||||
[](https://translate.gitea.com "Crowdin")
|
||||
|
||||
[English](./README.md) | [繁體中文](./README.zh-tw.md)
|
||||
|
||||
## 目的
|
||||
|
||||
这个项目的目标是提供最简单、最快速、最无痛的方式来设置自托管的 Git 服务。
|
||||
|
||||
由于 Gitea 是用 Go 语言编写的,它可以在 Go 支持的所有平台和架构上运行,包括 Linux、macOS 和 Windows 的 x86、amd64、ARM 和 PowerPC 架构。这个项目自 2016 年 11 月从 [Gogs](https://gogs.io) [分叉](https://blog.gitea.com/welcome-to-gitea/) 而来,但已经有了很多变化。
|
||||
|
||||
在线演示可以访问 [demo.gitea.com](https://demo.gitea.com)。
|
||||
|
||||
要访问免费的 Gitea 服务(有一定数量的仓库限制),可以访问 [gitea.com](https://gitea.com/user/login)。
|
||||
|
||||
要快速部署您自己的专用 Gitea 实例,可以在 [cloud.gitea.com](https://cloud.gitea.com) 开始免费试用。
|
||||
|
||||
## 文件
|
||||
|
||||
您可以在我们的官方 [文件网站](https://docs.gitea.com/) 上找到全面的文件。
|
||||
|
||||
它包括安装、管理、使用、开发、贡献指南等,帮助您快速入门并有效地探索所有功能。
|
||||
|
||||
如果您有任何建议或想要贡献,可以访问 [文件仓库](https://gitea.com/gitea/docs)
|
||||
|
||||
## 构建
|
||||
|
||||
从源代码树的根目录运行:
|
||||
|
||||
TAGS="bindata" make build
|
||||
|
||||
如果需要 SQLite 支持:
|
||||
|
||||
TAGS="bindata sqlite sqlite_unlock_notify" make build
|
||||
|
||||
`build` 目标分为两个子目标:
|
||||
|
||||
- `make backend` 需要 [Go Stable](https://go.dev/dl/),所需版本在 [go.mod](/go.mod) 中定义。
|
||||
- `make frontend` 需要 [Node.js LTS](https://nodejs.org/en/download/) 或更高版本。
|
||||
|
||||
需要互联网连接来下载 go 和 npm 模块。从包含预构建前端文件的官方源代码压缩包构建时,不会触发 `frontend` 目标,因此可以在没有 Node.js 的情况下构建。
|
||||
|
||||
更多信息:https://docs.gitea.com/installation/install-from-source
|
||||
|
||||
## 使用
|
||||
|
||||
构建后,默认情况下会在源代码树的根目录生成一个名为 `gitea` 的二进制文件。要运行它,请使用:
|
||||
|
||||
./gitea web
|
||||
|
||||
> [!注意]
|
||||
> 如果您对使用我们的 API 感兴趣,我们提供了实验性支持,并附有 [文件](https://docs.gitea.com/api)。
|
||||
|
||||
## 贡献
|
||||
|
||||
预期的工作流程是:Fork -> Patch -> Push -> Pull Request
|
||||
|
||||
> [!注意]
|
||||
>
|
||||
> 1. **在开始进行 Pull Request 之前,您必须阅读 [贡献者指南](CONTRIBUTING.md)。**
|
||||
> 2. 如果您在项目中发现了漏洞,请私下写信给 **security@gitea.io**。谢谢!
|
||||
|
||||
## 翻译
|
||||
|
||||
[](https://translate.gitea.com)
|
||||
|
||||
翻译通过 [Crowdin](https://translate.gitea.com) 进行。如果您想翻译成新的语言,请在 Crowdin 项目中请求管理员添加新语言。
|
||||
|
||||
您也可以创建一个 issue 来添加语言,或者在 discord 的 #translation 频道上询问。如果您需要上下文或发现一些翻译问题,可以在字符串上留言或在 Discord 上询问。对于一般的翻译问题,文档中有一个部分。目前有点空,但我们希望随着问题的出现而填充它。
|
||||
|
||||
更多信息请参阅 [文件](https://docs.gitea.com/contributing/localization)。
|
||||
|
||||
## 官方和第三方项目
|
||||
|
||||
我们提供了一个官方的 [go-sdk](https://gitea.com/gitea/go-sdk),一个名为 [tea](https://gitea.com/gitea/tea) 的 CLI 工具和一个 Gitea Action 的 [action runner](https://gitea.com/gitea/act_runner)。
|
||||
|
||||
我们在 [gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea) 维护了一个 Gitea 相关项目的列表,您可以在那里发现更多的第三方项目,包括 SDK、插件、主题等。
|
||||
|
||||
## 通讯
|
||||
|
||||
[](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
|
||||
|
||||
如果您有任何文件未涵盖的问题,可以在我们的 [Discord 服务器](https://discord.gg/Gitea) 上与我们联系,或者在 [discourse 论坛](https://forum.gitea.com/) 上创建帖子。
|
||||
|
||||
## 作者
|
||||
|
||||
- [维护者](https://github.com/orgs/go-gitea/people)
|
||||
- [贡献者](https://github.com/go-gitea/gitea/graphs/contributors)
|
||||
- [翻译者](options/locale/TRANSLATORS)
|
||||
|
||||
## 支持者
|
||||
|
||||
感谢所有支持者! 🙏 [[成为支持者](https://opencollective.com/gitea#backer)]
|
||||
|
||||
<a href="https://opencollective.com/gitea#backers" target="_blank"><img src="https://opencollective.com/gitea/backers.svg?width=890"></a>
|
||||
|
||||
## 赞助商
|
||||
|
||||
通过成为赞助商来支持这个项目。您的标志将显示在这里,并带有链接到您的网站。 [[成为赞助商](https://opencollective.com/gitea#sponsor)]
|
||||
|
||||
<a href="https://opencollective.com/gitea/sponsor/0/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/0/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/1/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/1/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/2/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/2/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/3/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/3/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/4/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/4/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/5/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/5/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/6/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/6/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/7/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/7/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/8/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/8/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/9/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/9/avatar.svg"></a>
|
||||
|
||||
## 常见问题
|
||||
|
||||
**Gitea 怎么发音?**
|
||||
|
||||
Gitea 的发音是 [/ɡɪ’ti:/](https://youtu.be/EM71-2uDAoY),就像 "gi-tea" 一样,g 是硬音。
|
||||
|
||||
**为什么这个项目没有托管在 Gitea 实例上?**
|
||||
|
||||
我们正在 [努力](https://github.com/go-gitea/gitea/issues/1029)。
|
||||
|
||||
**在哪里可以找到安全补丁?**
|
||||
|
||||
在 [发布日志](https://github.com/go-gitea/gitea/releases) 或 [变更日志](https://github.com/go-gitea/gitea/blob/main/CHANGELOG.md) 中,搜索关键词 `SECURITY` 以找到安全补丁。
|
||||
|
||||
## 许可证
|
||||
|
||||
这个项目是根据 MIT 许可证授权的。
|
||||
请参阅 [LICENSE](https://github.com/go-gitea/gitea/blob/main/LICENSE) 文件以获取完整的许可证文本。
|
||||
|
||||
## 进一步信息
|
||||
|
||||
<details>
|
||||
<summary>寻找界面概述?查看这里!</summary>
|
||||
|
||||
### 登录/注册页面
|
||||
|
||||

|
||||

|
||||
|
||||
### 用户仪表板
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
### 用户资料
|
||||
|
||||

|
||||
|
||||
### 探索
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
### 仓库
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
#### 仓库问题
|
||||
|
||||

|
||||

|
||||
|
||||
#### 仓库拉取请求
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
#### 仓库操作
|
||||
|
||||

|
||||

|
||||
|
||||
#### 仓库活动
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
### 组织
|
||||
|
||||

|
||||
|
||||
</details>
|
206
README.zh-tw.md
Normal file
206
README.zh-tw.md
Normal file
@ -0,0 +1,206 @@
|
||||
# Gitea
|
||||
|
||||
[](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly")
|
||||
[](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
|
||||
[](https://goreportcard.com/report/code.gitea.io/gitea "Go Report Card")
|
||||
[](https://pkg.go.dev/code.gitea.io/gitea "GoDoc")
|
||||
[](https://github.com/go-gitea/gitea/releases/latest "GitHub release")
|
||||
[](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source")
|
||||
[](https://opencollective.com/gitea "Become a backer/sponsor of gitea")
|
||||
[](https://opensource.org/licenses/MIT "License: MIT")
|
||||
[](https://gitpod.io/#https://github.com/go-gitea/gitea)
|
||||
[](https://translate.gitea.com "Crowdin")
|
||||
|
||||
[English](./README.md) | [简体中文](./README.zh-cn.md)
|
||||
|
||||
## 目的
|
||||
|
||||
這個項目的目標是提供最簡單、最快速、最無痛的方式來設置自託管的 Git 服務。
|
||||
|
||||
由於 Gitea 是用 Go 語言編寫的,它可以在 Go 支援的所有平台和架構上運行,包括 Linux、macOS 和 Windows 的 x86、amd64、ARM 和 PowerPC 架構。這個項目自 2016 年 11 月從 [Gogs](https://gogs.io) [分叉](https://blog.gitea.com/welcome-to-gitea/) 而來,但已經有了很多變化。
|
||||
|
||||
在線演示可以訪問 [demo.gitea.com](https://demo.gitea.com)。
|
||||
|
||||
要訪問免費的 Gitea 服務(有一定數量的倉庫限制),可以訪問 [gitea.com](https://gitea.com/user/login)。
|
||||
|
||||
要快速部署您自己的專用 Gitea 實例,可以在 [cloud.gitea.com](https://cloud.gitea.com) 開始免費試用。
|
||||
|
||||
## 文件
|
||||
|
||||
您可以在我們的官方 [文件網站](https://docs.gitea.com/) 上找到全面的文件。
|
||||
|
||||
它包括安裝、管理、使用、開發、貢獻指南等,幫助您快速入門並有效地探索所有功能。
|
||||
|
||||
如果您有任何建議或想要貢獻,可以訪問 [文件倉庫](https://gitea.com/gitea/docs)
|
||||
|
||||
## 構建
|
||||
|
||||
從源代碼樹的根目錄運行:
|
||||
|
||||
TAGS="bindata" make build
|
||||
|
||||
如果需要 SQLite 支援:
|
||||
|
||||
TAGS="bindata sqlite sqlite_unlock_notify" make build
|
||||
|
||||
`build` 目標分為兩個子目標:
|
||||
|
||||
- `make backend` 需要 [Go Stable](https://go.dev/dl/),所需版本在 [go.mod](/go.mod) 中定義。
|
||||
- `make frontend` 需要 [Node.js LTS](https://nodejs.org/en/download/) 或更高版本。
|
||||
|
||||
需要互聯網連接來下載 go 和 npm 模塊。從包含預構建前端文件的官方源代碼壓縮包構建時,不會觸發 `frontend` 目標,因此可以在沒有 Node.js 的情況下構建。
|
||||
|
||||
更多信息:https://docs.gitea.com/installation/install-from-source
|
||||
|
||||
## 使用
|
||||
|
||||
構建後,默認情況下會在源代碼樹的根目錄生成一個名為 `gitea` 的二進制文件。要運行它,請使用:
|
||||
|
||||
./gitea web
|
||||
|
||||
> [!注意]
|
||||
> 如果您對使用我們的 API 感興趣,我們提供了實驗性支援,並附有 [文件](https://docs.gitea.com/api)。
|
||||
|
||||
## 貢獻
|
||||
|
||||
預期的工作流程是:Fork -> Patch -> Push -> Pull Request
|
||||
|
||||
> [!注意]
|
||||
>
|
||||
> 1. **在開始進行 Pull Request 之前,您必須閱讀 [貢獻者指南](CONTRIBUTING.md)。**
|
||||
> 2. 如果您在項目中發現了漏洞,請私下寫信給 **security@gitea.io**。謝謝!
|
||||
|
||||
## 翻譯
|
||||
|
||||
[](https://translate.gitea.com)
|
||||
|
||||
翻譯通過 [Crowdin](https://translate.gitea.com) 進行。如果您想翻譯成新的語言,請在 Crowdin 項目中請求管理員添加新語言。
|
||||
|
||||
您也可以創建一個 issue 來添加語言,或者在 discord 的 #translation 頻道上詢問。如果您需要上下文或發現一些翻譯問題,可以在字符串上留言或在 Discord 上詢問。對於一般的翻譯問題,文檔中有一個部分。目前有點空,但我們希望隨著問題的出現而填充它。
|
||||
|
||||
更多信息請參閱 [文件](https://docs.gitea.com/contributing/localization)。
|
||||
|
||||
## 官方和第三方項目
|
||||
|
||||
我們提供了一個官方的 [go-sdk](https://gitea.com/gitea/go-sdk),一個名為 [tea](https://gitea.com/gitea/tea) 的 CLI 工具和一個 Gitea Action 的 [action runner](https://gitea.com/gitea/act_runner)。
|
||||
|
||||
我們在 [gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea) 維護了一個 Gitea 相關項目的列表,您可以在那裡發現更多的第三方項目,包括 SDK、插件、主題等。
|
||||
|
||||
## 通訊
|
||||
|
||||
[](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
|
||||
|
||||
如果您有任何文件未涵蓋的問題,可以在我們的 [Discord 服務器](https://discord.gg/Gitea) 上與我們聯繫,或者在 [discourse 論壇](https://forum.gitea.com/) 上創建帖子。
|
||||
|
||||
## 作者
|
||||
|
||||
- [維護者](https://github.com/orgs/go-gitea/people)
|
||||
- [貢獻者](https://github.com/go-gitea/gitea/graphs/contributors)
|
||||
- [翻譯者](options/locale/TRANSLATORS)
|
||||
|
||||
## 支持者
|
||||
|
||||
感謝所有支持者! 🙏 [[成為支持者](https://opencollective.com/gitea#backer)]
|
||||
|
||||
<a href="https://opencollective.com/gitea#backers" target="_blank"><img src="https://opencollective.com/gitea/backers.svg?width=890"></a>
|
||||
|
||||
## 贊助商
|
||||
|
||||
通過成為贊助商來支持這個項目。您的標誌將顯示在這裡,並帶有鏈接到您的網站。 [[成為贊助商](https://opencollective.com/gitea#sponsor)]
|
||||
|
||||
<a href="https://opencollective.com/gitea/sponsor/0/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/0/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/1/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/1/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/2/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/2/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/3/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/3/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/4/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/4/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/5/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/5/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/6/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/6/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/7/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/7/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/8/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/8/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/9/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/9/avatar.svg"></a>
|
||||
|
||||
## 常見問題
|
||||
|
||||
**Gitea 怎麼發音?**
|
||||
|
||||
Gitea 的發音是 [/ɡɪ’ti:/](https://youtu.be/EM71-2uDAoY),就像 "gi-tea" 一樣,g 是硬音。
|
||||
|
||||
**為什麼這個項目沒有託管在 Gitea 實例上?**
|
||||
|
||||
我們正在 [努力](https://github.com/go-gitea/gitea/issues/1029)。
|
||||
|
||||
**在哪裡可以找到安全補丁?**
|
||||
|
||||
在 [發佈日誌](https://github.com/go-gitea/gitea/releases) 或 [變更日誌](https://github.com/go-gitea/gitea/blob/main/CHANGELOG.md) 中,搜索關鍵詞 `SECURITY` 以找到安全補丁。
|
||||
|
||||
## 許可證
|
||||
|
||||
這個項目是根據 MIT 許可證授權的。
|
||||
請參閱 [LICENSE](https://github.com/go-gitea/gitea/blob/main/LICENSE) 文件以獲取完整的許可證文本。
|
||||
|
||||
## 進一步信息
|
||||
|
||||
<details>
|
||||
<summary>尋找界面概述?查看這裡!</summary>
|
||||
|
||||
### 登錄/註冊頁面
|
||||
|
||||

|
||||

|
||||
|
||||
### 用戶儀表板
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
### 用戶資料
|
||||
|
||||

|
||||
|
||||
### 探索
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
### 倉庫
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
#### 倉庫問題
|
||||
|
||||

|
||||

|
||||
|
||||
#### 倉庫拉取請求
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
#### 倉庫操作
|
||||
|
||||

|
||||

|
||||
|
||||
#### 倉庫活動
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
### 組織
|
||||
|
||||

|
||||
|
||||
</details>
|
156
README_ZH.md
156
README_ZH.md
@ -1,156 +0,0 @@
|
||||
# Gitea
|
||||
|
||||
[](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly")
|
||||
[](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
|
||||
[](https://goreportcard.com/report/code.gitea.io/gitea "Go Report Card")
|
||||
[](https://pkg.go.dev/code.gitea.io/gitea "GoDoc")
|
||||
[](https://github.com/go-gitea/gitea/releases/latest "GitHub release")
|
||||
[](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source")
|
||||
[](https://opencollective.com/gitea "Become a backer/sponsor of gitea")
|
||||
[](https://opensource.org/licenses/MIT "License: MIT")
|
||||
[](https://gitpod.io/#https://github.com/go-gitea/gitea)
|
||||
[](https://translate.gitea.com "Crowdin")
|
||||
|
||||
[View this document in English](./README.md)
|
||||
|
||||
## 目标
|
||||
|
||||
Gitea 的首要目标是创建一个极易安装,运行非常快速,安装和使用体验良好的自建 Git 服务。我们采用 Go 作为后端语言,这使我们只要生成一个可执行程序即可。并且他还支持跨平台,支持 Linux、macOS 和 Windows 以及各种架构,除了 x86 和 amd64,还包括 ARM 和 PowerPC。
|
||||
|
||||
如果你想试用在线演示和报告问题,请访问 [demo.gitea.com](https://demo.gitea.com/)。
|
||||
|
||||
如果你想使用免费的 Gitea 服务(有仓库数量限制),请访问 [gitea.com](https://gitea.com/user/login)。
|
||||
|
||||
如果你想在 Gitea Cloud 上快速部署你自己独享的 Gitea 实例,请访问 [cloud.gitea.com](https://cloud.gitea.com) 开始免费试用。
|
||||
|
||||
## 文档
|
||||
|
||||
关于如何安装请访问我们的 [文档站](https://docs.gitea.com/zh-cn/category/installation),如果没有找到对应的文档,你也可以通过 [Discord - 英文](https://discord.gg/gitea) 和 QQ群 328432459 来和我们交流。
|
||||
|
||||
## 编译
|
||||
|
||||
在源代码的根目录下执行:
|
||||
|
||||
TAGS="bindata" make build
|
||||
|
||||
或者如果需要SQLite支持:
|
||||
|
||||
TAGS="bindata sqlite sqlite_unlock_notify" make build
|
||||
|
||||
编译过程会分成2个子任务:
|
||||
|
||||
- `make backend`,需要 [Go Stable](https://go.dev/dl/),最低版本需求可查看 [go.mod](/go.mod)。
|
||||
- `make frontend`,需要 [Node.js LTS](https://nodejs.org/en/download/) 或更高版本。
|
||||
|
||||
你需要连接网络来下载 go 和 npm modules。当从 tar 格式的源文件编译时,其中包含了预编译的前端文件,因此 `make frontend` 将不会被执行。这允许编译时不需要 Node.js。
|
||||
|
||||
更多信息: https://docs.gitea.com/installation/install-from-source
|
||||
|
||||
## 使用
|
||||
|
||||
编译之后,默认会在根目录下生成一个名为 `gitea` 的文件。你可以这样执行它:
|
||||
|
||||
./gitea web
|
||||
|
||||
> [!注意]
|
||||
> 如果你要使用API,请参见 [API 文档](https://godoc.org/code.gitea.io/sdk/gitea)。
|
||||
|
||||
## 贡献
|
||||
|
||||
贡献流程:Fork -> Patch -> Push -> Pull Request
|
||||
|
||||
> [!注意]
|
||||
>
|
||||
> 1. **开始贡献代码之前请确保你已经看过了 [贡献者向导(英文)](CONTRIBUTING.md)**。
|
||||
> 2. 所有的安全问题,请私下发送邮件给 **security@gitea.io**。 谢谢!
|
||||
|
||||
## 翻译
|
||||
|
||||
[](https://translate.gitea.com)
|
||||
|
||||
多语言翻译是基于Crowdin进行的。
|
||||
|
||||
从 [文档](https://docs.gitea.com/contributing/localization) 中获取更多信息。
|
||||
|
||||
## 官方和第三方项目
|
||||
|
||||
Gitea 提供官方的 [go-sdk](https://gitea.com/gitea/go-sdk),以及名为 [tea](https://gitea.com/gitea/tea) 的 CLI 工具 和 用于 Gitea Action 的 [action runner](https://gitea.com/gitea/act_runner)。
|
||||
|
||||
[gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea) 是一个 Gitea 相关项目的列表,你可以在这里找到更多的第三方项目,包括 SDK、插件、主题等等。
|
||||
|
||||
## 作者
|
||||
|
||||
- [Maintainers](https://github.com/orgs/go-gitea/people)
|
||||
- [Contributors](https://github.com/go-gitea/gitea/graphs/contributors)
|
||||
- [Translators](options/locale/TRANSLATORS)
|
||||
|
||||
## 授权许可
|
||||
|
||||
本项目采用 MIT 开源授权许可证,完整的授权说明已放置在 [LICENSE](https://github.com/go-gitea/gitea/blob/main/LICENSE) 文件中。
|
||||
|
||||
## 更多信息
|
||||
|
||||
<details>
|
||||
<summary>截图</summary>
|
||||
|
||||
### 登录界面
|
||||
|
||||

|
||||

|
||||
|
||||
### 用户首页
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
### 用户资料
|
||||
|
||||

|
||||
|
||||
### 探索
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
### 仓库
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
#### 仓库工单
|
||||
|
||||

|
||||

|
||||
|
||||
#### 仓库合并请求
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
#### 仓库 Actions
|
||||
|
||||

|
||||

|
||||
|
||||
#### 仓库动态
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
### 组织
|
||||
|
||||

|
||||
|
||||
</details>
|
@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
@ -66,6 +67,16 @@ var microcmdUserCreate = &cli.Command{
|
||||
Name: "access-token",
|
||||
Usage: "Generate access token for the user",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "access-token-name",
|
||||
Usage: `Name of the generated access token`,
|
||||
Value: "gitea-admin",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "access-token-scopes",
|
||||
Usage: `Scopes of the generated access token, comma separated. Examples: "all", "public-only,read:issue", "write:repository,write:user"`,
|
||||
Value: "all",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "restricted",
|
||||
Usage: "Make a restricted user account",
|
||||
@ -187,23 +198,40 @@ func runCreateUser(c *cli.Context) error {
|
||||
IsRestricted: restricted,
|
||||
}
|
||||
|
||||
var accessTokenName string
|
||||
var accessTokenScope auth_model.AccessTokenScope
|
||||
if c.IsSet("access-token") {
|
||||
accessTokenName = strings.TrimSpace(c.String("access-token-name"))
|
||||
if accessTokenName == "" {
|
||||
return errors.New("access-token-name cannot be empty")
|
||||
}
|
||||
var err error
|
||||
accessTokenScope, err = auth_model.AccessTokenScope(c.String("access-token-scopes")).Normalize()
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid access token scope provided: %w", err)
|
||||
}
|
||||
if !accessTokenScope.HasPermissionScope() {
|
||||
return errors.New("access token does not have any permission")
|
||||
}
|
||||
} else if c.IsSet("access-token-name") || c.IsSet("access-token-scopes") {
|
||||
return errors.New("access-token-name and access-token-scopes flags are only valid when access-token flag is set")
|
||||
}
|
||||
|
||||
// arguments should be prepared before creating the user & access token, in case there is anything wrong
|
||||
|
||||
// create the user
|
||||
if err := user_model.CreateUser(ctx, u, &user_model.Meta{}, overwriteDefault); err != nil {
|
||||
return fmt.Errorf("CreateUser: %w", err)
|
||||
}
|
||||
fmt.Printf("New user '%s' has been successfully created!\n", username)
|
||||
|
||||
if c.Bool("access-token") {
|
||||
t := &auth_model.AccessToken{
|
||||
Name: "gitea-admin",
|
||||
UID: u.ID,
|
||||
}
|
||||
|
||||
// create the access token
|
||||
if accessTokenScope != "" {
|
||||
t := &auth_model.AccessToken{Name: accessTokenName, UID: u.ID, Scope: accessTokenScope}
|
||||
if err := auth_model.NewAccessToken(ctx, t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Access token was successfully created... %s\n", t.Token)
|
||||
}
|
||||
|
||||
fmt.Printf("New user '%s' has been successfully created!\n", username)
|
||||
return nil
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
@ -22,6 +23,7 @@ func TestAdminUserCreate(t *testing.T) {
|
||||
reset := func() {
|
||||
require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
|
||||
require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{}))
|
||||
require.NoError(t, db.TruncateBeans(db.DefaultContext, &auth_model.AccessToken{}))
|
||||
}
|
||||
|
||||
t.Run("MustChangePassword", func(t *testing.T) {
|
||||
@ -48,11 +50,11 @@ func TestAdminUserCreate(t *testing.T) {
|
||||
assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u5", "--must-change-password=false"))
|
||||
})
|
||||
|
||||
t.Run("UserType", func(t *testing.T) {
|
||||
createUser := func(name, args string) error {
|
||||
return app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s", name, name, args)))
|
||||
}
|
||||
createUser := func(name, args string) error {
|
||||
return app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s", name, name, args)))
|
||||
}
|
||||
|
||||
t.Run("UserType", func(t *testing.T) {
|
||||
reset()
|
||||
assert.ErrorContains(t, createUser("u", "--user-type invalid"), "invalid user type")
|
||||
assert.ErrorContains(t, createUser("u", "--user-type bot --password 123"), "can only be set for individual users")
|
||||
@ -63,4 +65,56 @@ func TestAdminUserCreate(t *testing.T) {
|
||||
assert.Equal(t, user_model.UserTypeBot, u.Type)
|
||||
assert.Empty(t, u.Passwd)
|
||||
})
|
||||
|
||||
t.Run("AccessToken", func(t *testing.T) {
|
||||
// no generated access token
|
||||
reset()
|
||||
assert.NoError(t, createUser("u", "--random-password"))
|
||||
assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
|
||||
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
|
||||
|
||||
// using "--access-token" only means "all" access
|
||||
reset()
|
||||
assert.NoError(t, createUser("u", "--random-password --access-token"))
|
||||
assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
|
||||
assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{}))
|
||||
accessToken := unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{Name: "gitea-admin"})
|
||||
hasScopes, err := accessToken.Scope.HasScope(auth_model.AccessTokenScopeWriteAdmin, auth_model.AccessTokenScopeWriteRepository)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, hasScopes)
|
||||
|
||||
// using "--access-token" with name & scopes
|
||||
reset()
|
||||
assert.NoError(t, createUser("u", "--random-password --access-token --access-token-name new-token-name --access-token-scopes read:issue,read:user"))
|
||||
assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
|
||||
assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{}))
|
||||
accessToken = unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{Name: "new-token-name"})
|
||||
hasScopes, err = accessToken.Scope.HasScope(auth_model.AccessTokenScopeReadIssue, auth_model.AccessTokenScopeReadUser)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, hasScopes)
|
||||
hasScopes, err = accessToken.Scope.HasScope(auth_model.AccessTokenScopeWriteAdmin, auth_model.AccessTokenScopeWriteRepository)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, hasScopes)
|
||||
|
||||
// using "--access-token-name" without "--access-token"
|
||||
reset()
|
||||
err = createUser("u", "--random-password --access-token-name new-token-name")
|
||||
assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
|
||||
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
|
||||
assert.ErrorContains(t, err, "access-token-name and access-token-scopes flags are only valid when access-token flag is set")
|
||||
|
||||
// using "--access-token-scopes" without "--access-token"
|
||||
reset()
|
||||
err = createUser("u", "--random-password --access-token-scopes read:issue")
|
||||
assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
|
||||
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
|
||||
assert.ErrorContains(t, err, "access-token-name and access-token-scopes flags are only valid when access-token flag is set")
|
||||
|
||||
// empty permission
|
||||
reset()
|
||||
err = createUser("u", "--random-password --access-token --access-token-scopes public-only")
|
||||
assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
|
||||
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
|
||||
assert.ErrorContains(t, err, "access token does not have any permission")
|
||||
})
|
||||
}
|
||||
|
@ -34,8 +34,8 @@ var microcmdUserGenerateAccessToken = &cli.Command{
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "scopes",
|
||||
Value: "",
|
||||
Usage: "Comma separated list of scopes to apply to access token",
|
||||
Value: "all",
|
||||
Usage: `Comma separated list of scopes to apply to access token, examples: "all", "public-only,read:issue", "write:repository,write:user"`,
|
||||
},
|
||||
},
|
||||
Action: runGenerateAccessToken,
|
||||
@ -43,7 +43,7 @@ var microcmdUserGenerateAccessToken = &cli.Command{
|
||||
|
||||
func runGenerateAccessToken(c *cli.Context) error {
|
||||
if !c.IsSet("username") {
|
||||
return errors.New("You must provide a username to generate a token for")
|
||||
return errors.New("you must provide a username to generate a token for")
|
||||
}
|
||||
|
||||
ctx, cancel := installSignals()
|
||||
@ -77,6 +77,9 @@ func runGenerateAccessToken(c *cli.Context) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid access token scope provided: %w", err)
|
||||
}
|
||||
if !accessTokenScope.HasPermissionScope() {
|
||||
return errors.New("access token does not have any permission")
|
||||
}
|
||||
t.Scope = accessTokenScope
|
||||
|
||||
// create the token
|
||||
|
@ -5,7 +5,6 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
@ -93,7 +92,7 @@ var CmdDump = &cli.Command{
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "type",
|
||||
Usage: fmt.Sprintf(`Dump output format, default to "zip", supported types: %s`, strings.Join(dump.SupportedOutputTypes, ", ")),
|
||||
Usage: `Dump output format, default to "zip", supported types: ` + strings.Join(dump.SupportedOutputTypes, ", "),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
@ -127,7 +128,7 @@ func TestCliCmd(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCliCmdError(t *testing.T) {
|
||||
app := newTestApp(func(ctx *cli.Context) error { return fmt.Errorf("normal error") })
|
||||
app := newTestApp(func(ctx *cli.Context) error { return errors.New("normal error") })
|
||||
r, err := runTestApp(app, "./gitea", "test-cmd")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 1, r.ExitCode)
|
||||
|
@ -173,7 +173,7 @@ func getLFSAuthToken(ctx context.Context, lfsVerb string, results *private.ServC
|
||||
if err != nil {
|
||||
return "", fail(ctx, "Failed to sign JWT Token", "Failed to sign JWT token: %v", err)
|
||||
}
|
||||
return fmt.Sprintf("Bearer %s", tokenString), nil
|
||||
return "Bearer " + tokenString, nil
|
||||
}
|
||||
|
||||
func runServ(c *cli.Context) error {
|
||||
@ -372,9 +372,9 @@ func runServ(c *cli.Context) error {
|
||||
repo_module.EnvPusherEmail+"="+results.UserEmail,
|
||||
repo_module.EnvPusherID+"="+strconv.FormatInt(results.UserID, 10),
|
||||
repo_module.EnvRepoID+"="+strconv.FormatInt(results.RepoID, 10),
|
||||
repo_module.EnvPRID+"="+fmt.Sprintf("%d", 0),
|
||||
repo_module.EnvDeployKeyID+"="+fmt.Sprintf("%d", results.DeployKeyID),
|
||||
repo_module.EnvKeyID+"="+fmt.Sprintf("%d", results.KeyID),
|
||||
repo_module.EnvPRID+"="+strconv.Itoa(0),
|
||||
repo_module.EnvDeployKeyID+"="+strconv.FormatInt(results.DeployKeyID, 10),
|
||||
repo_module.EnvKeyID+"="+strconv.FormatInt(results.KeyID, 10),
|
||||
repo_module.EnvAppURL+"="+setting.AppURL,
|
||||
)
|
||||
// to avoid breaking, here only use the minimal environment variables for the "gitea serv" command.
|
||||
|
@ -213,6 +213,10 @@ func serveInstalled(ctx *cli.Context) error {
|
||||
log.Fatal("Can not find APP_DATA_PATH %q", setting.AppDataPath)
|
||||
}
|
||||
|
||||
// the AppDataTempDir is fully managed by us with a safe sub-path
|
||||
// so it's safe to automatically remove the outdated files
|
||||
setting.AppDataTempDir("").RemoveOutdated(3 * 24 * time.Hour)
|
||||
|
||||
// Override the provided port number within the configuration
|
||||
if ctx.IsSet("port") {
|
||||
if err := setPort(ctx.String("port")); err != nil {
|
||||
|
@ -136,7 +136,7 @@ func runACME(listenAddr string, m http.Handler) error {
|
||||
}
|
||||
|
||||
func runLetsEncryptFallbackHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" && r.Method != "HEAD" {
|
||||
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
||||
http.Error(w, "Use HTTPS", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
@ -158,7 +159,7 @@ func runBackport(c *cli.Context) error {
|
||||
|
||||
args := c.Args().Slice()
|
||||
if len(args) == 0 && pr == "" {
|
||||
return fmt.Errorf("no PR number provided\nProvide a PR number to backport")
|
||||
return errors.New("no PR number provided\nProvide a PR number to backport")
|
||||
} else if len(args) != 1 && pr == "" {
|
||||
return fmt.Errorf("multiple PRs provided %v\nOnly a single PR can be backported at a time", args)
|
||||
}
|
||||
|
@ -197,13 +197,6 @@ RUN_USER = ; git
|
||||
;; relative paths are made absolute relative to the APP_DATA_PATH
|
||||
;SSH_SERVER_HOST_KEYS=ssh/gitea.rsa, ssh/gogs.rsa
|
||||
;;
|
||||
;; Directory to create temporary files in when testing public keys using ssh-keygen,
|
||||
;; default is the system temporary directory.
|
||||
;SSH_KEY_TEST_PATH =
|
||||
;;
|
||||
;; Use `ssh-keygen` to parse public SSH keys. The value is passed to the shell. By default, Gitea does the parsing itself.
|
||||
;SSH_KEYGEN_PATH =
|
||||
;;
|
||||
;; Enable SSH Authorized Key Backup when rewriting all keys, default is false
|
||||
;SSH_AUTHORIZED_KEYS_BACKUP = false
|
||||
;;
|
||||
@ -294,6 +287,9 @@ RUN_USER = ; git
|
||||
;; Default path for App data
|
||||
;APP_DATA_PATH = data ; relative paths will be made absolute with _`AppWorkPath`_
|
||||
;;
|
||||
;; Base path for App's temp files, leave empty to use the managed tmp directory in APP_DATA_PATH
|
||||
;APP_TEMP_PATH =
|
||||
;;
|
||||
;; Enable gzip compression for runtime-generated content, static resources excluded
|
||||
;ENABLE_GZIP = false
|
||||
;;
|
||||
@ -1069,15 +1065,6 @@ LEVEL = Info
|
||||
;; Separate extensions with a comma. To line wrap files without an extension, just put a comma
|
||||
;LINE_WRAP_EXTENSIONS = .txt,.md,.markdown,.mdown,.mkd,.livemd,
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;[repository.local]
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;
|
||||
;; Path for local repository copy. Defaults to `tmp/local-repo` (content gets deleted on gitea restart)
|
||||
;LOCAL_COPY_PATH = tmp/local-repo
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;[repository.upload]
|
||||
@ -1087,9 +1074,6 @@ LEVEL = Info
|
||||
;; Whether repository file uploads are enabled. Defaults to `true`
|
||||
;ENABLED = true
|
||||
;;
|
||||
;; Path for uploads. Defaults to `data/tmp/uploads` (content gets deleted on gitea restart)
|
||||
;TEMP_PATH = data/tmp/uploads
|
||||
;;
|
||||
;; Comma-separated list of allowed file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types.
|
||||
;ALLOWED_TYPES =
|
||||
;;
|
||||
@ -1413,14 +1397,14 @@ LEVEL = Info
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;
|
||||
;; Render soft line breaks as hard line breaks, which means a single newline character between
|
||||
;; paragraphs will cause a line break and adding trailing whitespace to paragraphs is not
|
||||
;; necessary to force a line break.
|
||||
;; Render soft line breaks as hard line breaks for comments
|
||||
;ENABLE_HARD_LINE_BREAK_IN_COMMENTS = true
|
||||
;;
|
||||
;; Render soft line breaks as hard line breaks for markdown documents
|
||||
;ENABLE_HARD_LINE_BREAK_IN_DOCUMENTS = false
|
||||
;; Customize render options for different contexts. Set to "none" to disable the defaults, or use comma separated list:
|
||||
;; * short-issue-pattern: recognized "#123" issue reference and render it as a link to the issue
|
||||
;; * new-line-hard-break: render soft line breaks as hard line breaks, which means a single newline character between
|
||||
;; paragraphs will cause a line break and adding trailing whitespace to paragraphs is not
|
||||
;; necessary to force a line break.
|
||||
;RENDER_OPTIONS_COMMENT = short-issue-pattern, new-line-hard-break
|
||||
;RENDER_OPTIONS_WIKI = short-issue-pattern
|
||||
;RENDER_OPTIONS_REPO_FILE =
|
||||
;;
|
||||
;; Comma separated list of custom URL-Schemes that are allowed as links when rendering Markdown
|
||||
;; for example git,magnet,ftp (more at https://en.wikipedia.org/wiki/List_of_URI_schemes)
|
||||
@ -1434,6 +1418,12 @@ LEVEL = Info
|
||||
;;
|
||||
;; Enables math inline and block detection
|
||||
;ENABLE_MATH = true
|
||||
;;
|
||||
;; Enable delimiters for math code block detection. Set to "none" to disable all,
|
||||
;; or use comma separated list: inline-dollar, inline-parentheses, block-dollar, block-square-brackets
|
||||
;; Defaults to "inline-dollar,block-dollar" to follow GitHub's behavior.
|
||||
;MATH_CODE_BLOCK_DETECTION =
|
||||
;;
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@ -2467,7 +2457,7 @@ LEVEL = Info
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Set the maximum number of characters in a mermaid source. (Set to -1 to disable limits)
|
||||
;MERMAID_MAX_SOURCE_CHARACTERS = 5000
|
||||
;MERMAID_MAX_SOURCE_CHARACTERS = 50000
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@ -2588,9 +2578,6 @@ LEVEL = Info
|
||||
;; Currently, only `minio` and `azureblob` is supported.
|
||||
;SERVE_DIRECT = false
|
||||
;;
|
||||
;; Path for chunked uploads. Defaults to APP_DATA_PATH + `tmp/package-upload`
|
||||
;CHUNKED_UPLOAD_PATH = tmp/package-upload
|
||||
;;
|
||||
;; Maximum count of package versions a single owner can have (`-1` means no limits)
|
||||
;LIMIT_TOTAL_OWNER_COUNT = -1
|
||||
;; Maximum size of packages a single owner can use (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
|
@ -31,6 +31,21 @@ if [ -e /data/ssh/ssh_host_ecdsa_cert ]; then
|
||||
SSH_ECDSA_CERT=${SSH_ECDSA_CERT:-"/data/ssh/ssh_host_ecdsa_cert"}
|
||||
fi
|
||||
|
||||
# In case someone wants to sign the `{keyname}.pub` key by `ssh-keygen -s ca -I identity ...` to
|
||||
# make use of the ssh-key certificate authority feature (see ssh-keygen CERTIFICATES section),
|
||||
# the generated key file name is `{keyname}-cert.pub`
|
||||
if [ -e /data/ssh/ssh_host_ed25519_key-cert.pub ]; then
|
||||
SSH_ED25519_CERT=${SSH_ED25519_CERT:-"/data/ssh/ssh_host_ed25519_key-cert.pub"}
|
||||
fi
|
||||
|
||||
if [ -e /data/ssh/ssh_host_rsa_key-cert.pub ]; then
|
||||
SSH_RSA_CERT=${SSH_RSA_CERT:-"/data/ssh/ssh_host_rsa_key-cert.pub"}
|
||||
fi
|
||||
|
||||
if [ -e /data/ssh/ssh_host_ecdsa_key-cert.pub ]; then
|
||||
SSH_ECDSA_CERT=${SSH_ECDSA_CERT:-"/data/ssh/ssh_host_ecdsa_key-cert.pub"}
|
||||
fi
|
||||
|
||||
if [ -d /etc/ssh ]; then
|
||||
SSH_PORT=${SSH_PORT:-"22"} \
|
||||
SSH_LISTEN_PORT=${SSH_LISTEN_PORT:-"${SSH_PORT}"} \
|
||||
|
@ -5,6 +5,7 @@ package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
@ -245,7 +246,7 @@ func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID strin
|
||||
|
||||
// If the update affected 0 rows, it means the job has changed in the meantime, so we need to try again.
|
||||
if n == 0 {
|
||||
return cancelledJobs, fmt.Errorf("job has changed, try again")
|
||||
return cancelledJobs, errors.New("job has changed, try again")
|
||||
}
|
||||
|
||||
cancelledJobs = append(cancelledJobs, job)
|
||||
@ -412,7 +413,7 @@ func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error {
|
||||
return err
|
||||
}
|
||||
if affected == 0 {
|
||||
return fmt.Errorf("run has changed")
|
||||
return errors.New("run has changed")
|
||||
// It's impossible that the run is not found, since Gitea never deletes runs.
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@ package actions
|
||||
import (
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@ -361,7 +362,7 @@ func UpdateTaskByState(ctx context.Context, runnerID int64, state *runnerv1.Task
|
||||
} else if !has {
|
||||
return nil, util.ErrNotExist
|
||||
} else if runnerID != task.RunnerID {
|
||||
return nil, fmt.Errorf("invalid runner for task")
|
||||
return nil, errors.New("invalid runner for task")
|
||||
}
|
||||
|
||||
if task.Status.IsDone() {
|
||||
|
@ -5,6 +5,7 @@ package activities
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
@ -205,7 +206,7 @@ func (actions ActionList) LoadIssues(ctx context.Context) error {
|
||||
// GetFeeds returns actions according to the provided options
|
||||
func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, error) {
|
||||
if opts.RequestedUser == nil && opts.RequestedTeam == nil && opts.RequestedRepo == nil {
|
||||
return nil, 0, fmt.Errorf("need at least one of these filters: RequestedUser, RequestedTeam, RequestedRepo")
|
||||
return nil, 0, errors.New("need at least one of these filters: RequestedUser, RequestedTeam, RequestedRepo")
|
||||
}
|
||||
|
||||
var err error
|
||||
|
@ -132,7 +132,7 @@ func IsErrGPGKeyParsing(err error) bool {
|
||||
}
|
||||
|
||||
func (err ErrGPGKeyParsing) Error() string {
|
||||
return fmt.Sprintf("failed to parse gpg key %s", err.ParseError.Error())
|
||||
return "failed to parse gpg key " + err.ParseError.Error()
|
||||
}
|
||||
|
||||
// ErrGPGKeyNotExist represents a "GPGKeyNotExist" kind of error.
|
||||
|
@ -5,6 +5,7 @@ package asymkey
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
@ -207,7 +208,7 @@ func parseGPGKey(ctx context.Context, ownerID int64, e *openpgp.Entity, verified
|
||||
// deleteGPGKey does the actual key deletion
|
||||
func deleteGPGKey(ctx context.Context, keyID string) (int64, error) {
|
||||
if keyID == "" {
|
||||
return 0, fmt.Errorf("empty KeyId forbidden") // Should never happen but just to be sure
|
||||
return 0, errors.New("empty KeyId forbidden") // Should never happen but just to be sure
|
||||
}
|
||||
// Delete imported key
|
||||
n, err := db.GetEngine(ctx).Where("key_id=?", keyID).Delete(new(GPGKeyImport))
|
||||
@ -239,3 +240,10 @@ func DeleteGPGKey(ctx context.Context, doer *user_model.User, id int64) (err err
|
||||
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
func FindGPGKeyWithSubKeys(ctx context.Context, keyID string) ([]*GPGKey, error) {
|
||||
return db.Find[GPGKey](ctx, FindGPGKeyOptions{
|
||||
KeyID: keyID,
|
||||
IncludeSubKeys: true,
|
||||
})
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
package asymkey
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
|
||||
@ -68,7 +69,7 @@ const (
|
||||
func verifySign(s *packet.Signature, h hash.Hash, k *GPGKey) error {
|
||||
// Check if key can sign
|
||||
if !k.CanSign {
|
||||
return fmt.Errorf("key can not sign")
|
||||
return errors.New("key can not sign")
|
||||
}
|
||||
// Decode key
|
||||
pkey, err := base64DecPubKey(k.Content)
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
@ -75,7 +76,7 @@ func base64DecPubKey(content string) (*packet.PublicKey, error) {
|
||||
// Check type
|
||||
pkey, ok := p.(*packet.PublicKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("key is not a public key")
|
||||
return nil, errors.New("key is not a public key")
|
||||
}
|
||||
return pkey, nil
|
||||
}
|
||||
@ -122,15 +123,15 @@ func readArmoredSign(r io.Reader) (body io.Reader, err error) {
|
||||
func ExtractSignature(s string) (*packet.Signature, error) {
|
||||
r, err := readArmoredSign(strings.NewReader(s))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to read signature armor")
|
||||
return nil, errors.New("Failed to read signature armor")
|
||||
}
|
||||
p, err := packet.Read(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to read signature packet")
|
||||
return nil, errors.New("Failed to read signature packet")
|
||||
}
|
||||
sig, ok := p.(*packet.Signature)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Packet is not a signature")
|
||||
return nil, errors.New("Packet is not a signature")
|
||||
}
|
||||
return sig, nil
|
||||
}
|
||||
|
@ -1,80 +0,0 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package asymkey
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
"github.com/42wim/sshsig"
|
||||
)
|
||||
|
||||
// ParseCommitWithSSHSignature check if signature is good against keystore.
|
||||
func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer *user_model.User) *CommitVerification {
|
||||
// Now try to associate the signature with the committer, if present
|
||||
if committer.ID != 0 {
|
||||
keys, err := db.Find[PublicKey](ctx, FindPublicKeyOptions{
|
||||
OwnerID: committer.ID,
|
||||
NotKeytype: KeyTypePrincipal,
|
||||
})
|
||||
if err != nil { // Skipping failed to get ssh keys of user
|
||||
log.Error("ListPublicKeys: %v", err)
|
||||
return &CommitVerification{
|
||||
CommittingUser: committer,
|
||||
Verified: false,
|
||||
Reason: "gpg.error.failed_retrieval_gpg_keys",
|
||||
}
|
||||
}
|
||||
|
||||
committerEmailAddresses, err := user_model.GetEmailAddresses(ctx, committer.ID)
|
||||
if err != nil {
|
||||
log.Error("GetEmailAddresses: %v", err)
|
||||
}
|
||||
|
||||
activated := false
|
||||
for _, e := range committerEmailAddresses {
|
||||
if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) {
|
||||
activated = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for _, k := range keys {
|
||||
if k.Verified && activated {
|
||||
commitVerification := verifySSHCommitVerification(c.Signature.Signature, c.Signature.Payload, k, committer, committer, c.Committer.Email)
|
||||
if commitVerification != nil {
|
||||
return commitVerification
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &CommitVerification{
|
||||
CommittingUser: committer,
|
||||
Verified: false,
|
||||
Reason: NoKeyFound,
|
||||
}
|
||||
}
|
||||
|
||||
func verifySSHCommitVerification(sig, payload string, k *PublicKey, committer, signer *user_model.User, email string) *CommitVerification {
|
||||
if err := sshsig.Verify(bytes.NewBuffer([]byte(payload)), []byte(sig), []byte(k.Content), "git"); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &CommitVerification{ // Everything is ok
|
||||
CommittingUser: committer,
|
||||
Verified: true,
|
||||
Reason: fmt.Sprintf("%s / %s", signer.Name, k.Fingerprint),
|
||||
SigningUser: signer,
|
||||
SigningSSHKey: k,
|
||||
SigningEmail: email,
|
||||
}
|
||||
}
|
@ -6,27 +6,13 @@ package asymkey
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/process"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// ___________.__ .__ __
|
||||
// \_ _____/|__| ____ ____ ________________________|__| _____/ |_
|
||||
// | __) | |/ \ / ___\_/ __ \_ __ \____ \_ __ \ |/ \ __\
|
||||
// | \ | | | \/ /_/ > ___/| | \/ |_> > | \/ | | \ |
|
||||
// \___ / |__|___| /\___ / \___ >__| | __/|__| |__|___| /__|
|
||||
// \/ \//_____/ \/ |__| \/
|
||||
//
|
||||
// This file contains functions for fingerprinting SSH keys
|
||||
//
|
||||
// The database is used in checkKeyFingerprint however most of these functions probably belong in a module
|
||||
|
||||
// checkKeyFingerprint only checks if key fingerprint has been used as public key,
|
||||
@ -41,29 +27,6 @@ func checkKeyFingerprint(ctx context.Context, fingerprint string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func calcFingerprintSSHKeygen(publicKeyContent string) (string, error) {
|
||||
// Calculate fingerprint.
|
||||
tmpPath, err := writeTmpKeyFile(publicKeyContent)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer func() {
|
||||
if err := util.Remove(tmpPath); err != nil {
|
||||
log.Warn("Unable to remove temporary key file: %s: Error: %v", tmpPath, err)
|
||||
}
|
||||
}()
|
||||
stdout, stderr, err := process.GetManager().Exec("AddPublicKey", "ssh-keygen", "-lf", tmpPath)
|
||||
if err != nil {
|
||||
if strings.Contains(stderr, "is not a public key file") {
|
||||
return "", ErrKeyUnableVerify{stderr}
|
||||
}
|
||||
return "", util.NewInvalidArgumentErrorf("'ssh-keygen -lf %s' failed with error '%s': %s", tmpPath, err, stderr)
|
||||
} else if len(stdout) < 2 {
|
||||
return "", util.NewInvalidArgumentErrorf("not enough output for calculating fingerprint: %s", stdout)
|
||||
}
|
||||
return strings.Split(stdout, " ")[1], nil
|
||||
}
|
||||
|
||||
func calcFingerprintNative(publicKeyContent string) (string, error) {
|
||||
// Calculate fingerprint.
|
||||
pk, _, _, _, err := ssh.ParseAuthorizedKey([]byte(publicKeyContent))
|
||||
@ -75,15 +38,12 @@ func calcFingerprintNative(publicKeyContent string) (string, error) {
|
||||
|
||||
// CalcFingerprint calculate public key's fingerprint
|
||||
func CalcFingerprint(publicKeyContent string) (string, error) {
|
||||
// Call the method based on configuration
|
||||
useNative := setting.SSH.KeygenPath == ""
|
||||
calcFn := util.Iif(useNative, calcFingerprintNative, calcFingerprintSSHKeygen)
|
||||
fp, err := calcFn(publicKeyContent)
|
||||
fp, err := calcFingerprintNative(publicKeyContent)
|
||||
if err != nil {
|
||||
if IsErrKeyUnableVerify(err) {
|
||||
return "", err
|
||||
}
|
||||
return "", fmt.Errorf("CalcFingerprint(%s): %w", util.Iif(useNative, "native", "ssh-keygen"), err)
|
||||
return "", fmt.Errorf("CalcFingerprint: %w", err)
|
||||
}
|
||||
return fp, nil
|
||||
}
|
||||
|
@ -10,14 +10,12 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/process"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
@ -93,7 +91,7 @@ func parseKeyString(content string) (string, error) {
|
||||
|
||||
block, _ := pem.Decode([]byte(content))
|
||||
if block == nil {
|
||||
return "", fmt.Errorf("failed to parse PEM block containing the public key")
|
||||
return "", errors.New("failed to parse PEM block containing the public key")
|
||||
}
|
||||
if strings.Contains(block.Type, "PRIVATE") {
|
||||
return "", ErrKeyIsPrivate
|
||||
@ -174,20 +172,9 @@ func CheckPublicKeyString(content string) (_ string, err error) {
|
||||
return content, nil
|
||||
}
|
||||
|
||||
var (
|
||||
fnName string
|
||||
keyType string
|
||||
length int
|
||||
)
|
||||
if len(setting.SSH.KeygenPath) == 0 {
|
||||
fnName = "SSHNativeParsePublicKey"
|
||||
keyType, length, err = SSHNativeParsePublicKey(content)
|
||||
} else {
|
||||
fnName = "SSHKeyGenParsePublicKey"
|
||||
keyType, length, err = SSHKeyGenParsePublicKey(content)
|
||||
}
|
||||
keyType, length, err := SSHNativeParsePublicKey(content)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%s: %w", fnName, err)
|
||||
return "", fmt.Errorf("SSHNativeParsePublicKey: %w", err)
|
||||
}
|
||||
log.Trace("Key info [native: %v]: %s-%d", setting.SSH.StartBuiltinServer, keyType, length)
|
||||
|
||||
@ -257,56 +244,3 @@ func SSHNativeParsePublicKey(keyLine string) (string, int, error) {
|
||||
}
|
||||
return "", 0, fmt.Errorf("unsupported key length detection for type: %s", pkey.Type())
|
||||
}
|
||||
|
||||
// writeTmpKeyFile writes key content to a temporary file
|
||||
// and returns the name of that file, along with any possible errors.
|
||||
func writeTmpKeyFile(content string) (string, error) {
|
||||
tmpFile, err := os.CreateTemp(setting.SSH.KeyTestPath, "gitea_keytest")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("TempFile: %w", err)
|
||||
}
|
||||
defer tmpFile.Close()
|
||||
|
||||
if _, err = tmpFile.WriteString(content); err != nil {
|
||||
return "", fmt.Errorf("WriteString: %w", err)
|
||||
}
|
||||
return tmpFile.Name(), nil
|
||||
}
|
||||
|
||||
// SSHKeyGenParsePublicKey extracts key type and length using ssh-keygen.
|
||||
func SSHKeyGenParsePublicKey(key string) (string, int, error) {
|
||||
tmpName, err := writeTmpKeyFile(key)
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("writeTmpKeyFile: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := util.Remove(tmpName); err != nil {
|
||||
log.Warn("Unable to remove temporary key file: %s: Error: %v", tmpName, err)
|
||||
}
|
||||
}()
|
||||
|
||||
keygenPath := setting.SSH.KeygenPath
|
||||
if len(keygenPath) == 0 {
|
||||
keygenPath = "ssh-keygen"
|
||||
}
|
||||
|
||||
stdout, stderr, err := process.GetManager().Exec("SSHKeyGenParsePublicKey", keygenPath, "-lf", tmpName)
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("fail to parse public key: %s - %s", err, stderr)
|
||||
}
|
||||
if strings.Contains(stdout, "is not a public key file") {
|
||||
return "", 0, ErrKeyUnableVerify{stdout}
|
||||
}
|
||||
|
||||
fields := strings.Split(stdout, " ")
|
||||
if len(fields) < 4 {
|
||||
return "", 0, fmt.Errorf("invalid public key line: %s", stdout)
|
||||
}
|
||||
|
||||
keyType := strings.Trim(fields[len(fields)-1], "()\r\n")
|
||||
length, err := strconv.ParseInt(fields[0], 10, 32)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
return strings.ToLower(keyType), int(length), nil
|
||||
}
|
||||
|
@ -18,7 +18,6 @@ import (
|
||||
|
||||
"github.com/42wim/sshsig"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_SSHParsePublicKey(t *testing.T) {
|
||||
@ -45,27 +44,6 @@ func Test_SSHParsePublicKey(t *testing.T) {
|
||||
assert.Equal(t, tc.keyType, keyTypeN)
|
||||
assert.Equal(t, tc.length, lengthN)
|
||||
})
|
||||
if tc.skipSSHKeygen {
|
||||
return
|
||||
}
|
||||
t.Run("SSHKeygen", func(t *testing.T) {
|
||||
keyTypeK, lengthK, err := SSHKeyGenParsePublicKey(tc.content)
|
||||
if err != nil {
|
||||
// Some servers do not support ecdsa format.
|
||||
if !strings.Contains(err.Error(), "line 1 too long:") {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
assert.Equal(t, tc.keyType, keyTypeK)
|
||||
assert.Equal(t, tc.length, lengthK)
|
||||
})
|
||||
t.Run("SSHParseKeyNative", func(t *testing.T) {
|
||||
keyTypeK, lengthK, err := SSHNativeParsePublicKey(tc.content)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tc.keyType, keyTypeK)
|
||||
assert.Equal(t, tc.length, lengthK)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -186,14 +164,6 @@ func Test_calcFingerprint(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tc.fp, fpN)
|
||||
})
|
||||
if tc.skipSSHKeygen {
|
||||
return
|
||||
}
|
||||
t.Run("SSHKeygen", func(t *testing.T) {
|
||||
fpK, err := calcFingerprintSSHKeygen(tc.content)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tc.fp, fpK)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -4,8 +4,8 @@
|
||||
package asymkey
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@ -30,11 +30,11 @@ func VerifySSHKey(ctx context.Context, ownerID int64, fingerprint, token, signat
|
||||
return "", ErrKeyNotExist{}
|
||||
}
|
||||
|
||||
err = sshsig.Verify(bytes.NewBuffer([]byte(token)), []byte(signature), []byte(key.Content), "gitea")
|
||||
err = sshsig.Verify(strings.NewReader(token), []byte(signature), []byte(key.Content), "gitea")
|
||||
if err != nil {
|
||||
// edge case for Windows based shells that will add CR LF if piped to ssh-keygen command
|
||||
// see https://github.com/PowerShell/PowerShell/issues/5974
|
||||
if sshsig.Verify(bytes.NewBuffer([]byte(token+"\r\n")), []byte(signature), []byte(key.Content), "gitea") != nil {
|
||||
if sshsig.Verify(strings.NewReader(token+"\r\n"), []byte(signature), []byte(key.Content), "gitea") != nil {
|
||||
log.Error("Unable to validate token signature. Error: %v", err)
|
||||
return "", ErrSSHInvalidTokenSignature{
|
||||
Fingerprint: key.Fingerprint,
|
||||
|
@ -295,6 +295,10 @@ func (s AccessTokenScope) Normalize() (AccessTokenScope, error) {
|
||||
return bitmap.toScope(), nil
|
||||
}
|
||||
|
||||
func (s AccessTokenScope) HasPermissionScope() bool {
|
||||
return s != "" && s != AccessTokenScopePublicOnly
|
||||
}
|
||||
|
||||
// PublicOnly checks if this token scope is limited to public resources
|
||||
func (s AccessTokenScope) PublicOnly() (bool, error) {
|
||||
bitmap, err := s.parse()
|
||||
|
@ -28,11 +28,11 @@ func TestAccessTokenScope_Normalize(t *testing.T) {
|
||||
|
||||
for _, scope := range GetAccessTokenCategories() {
|
||||
tests = append(tests,
|
||||
scopeTestNormalize{AccessTokenScope(fmt.Sprintf("read:%s", scope)), AccessTokenScope(fmt.Sprintf("read:%s", scope)), nil},
|
||||
scopeTestNormalize{AccessTokenScope(fmt.Sprintf("write:%s", scope)), AccessTokenScope(fmt.Sprintf("write:%s", scope)), nil},
|
||||
scopeTestNormalize{AccessTokenScope(fmt.Sprintf("write:%[1]s,read:%[1]s", scope)), AccessTokenScope(fmt.Sprintf("write:%s", scope)), nil},
|
||||
scopeTestNormalize{AccessTokenScope(fmt.Sprintf("read:%[1]s,write:%[1]s", scope)), AccessTokenScope(fmt.Sprintf("write:%s", scope)), nil},
|
||||
scopeTestNormalize{AccessTokenScope(fmt.Sprintf("read:%[1]s,write:%[1]s,write:%[1]s", scope)), AccessTokenScope(fmt.Sprintf("write:%s", scope)), nil},
|
||||
scopeTestNormalize{AccessTokenScope("read:" + scope), AccessTokenScope("read:" + scope), nil},
|
||||
scopeTestNormalize{AccessTokenScope("write:" + scope), AccessTokenScope("write:" + scope), nil},
|
||||
scopeTestNormalize{AccessTokenScope(fmt.Sprintf("write:%[1]s,read:%[1]s", scope)), AccessTokenScope("write:" + scope), nil},
|
||||
scopeTestNormalize{AccessTokenScope(fmt.Sprintf("read:%[1]s,write:%[1]s", scope)), AccessTokenScope("write:" + scope), nil},
|
||||
scopeTestNormalize{AccessTokenScope(fmt.Sprintf("read:%[1]s,write:%[1]s,write:%[1]s", scope)), AccessTokenScope("write:" + scope), nil},
|
||||
)
|
||||
}
|
||||
|
||||
@ -63,20 +63,20 @@ func TestAccessTokenScope_HasScope(t *testing.T) {
|
||||
for _, scope := range GetAccessTokenCategories() {
|
||||
tests = append(tests,
|
||||
scopeTestHasScope{
|
||||
AccessTokenScope(fmt.Sprintf("read:%s", scope)),
|
||||
AccessTokenScope(fmt.Sprintf("read:%s", scope)), true, nil,
|
||||
AccessTokenScope("read:" + scope),
|
||||
AccessTokenScope("read:" + scope), true, nil,
|
||||
},
|
||||
scopeTestHasScope{
|
||||
AccessTokenScope(fmt.Sprintf("write:%s", scope)),
|
||||
AccessTokenScope(fmt.Sprintf("write:%s", scope)), true, nil,
|
||||
AccessTokenScope("write:" + scope),
|
||||
AccessTokenScope("write:" + scope), true, nil,
|
||||
},
|
||||
scopeTestHasScope{
|
||||
AccessTokenScope(fmt.Sprintf("write:%s", scope)),
|
||||
AccessTokenScope(fmt.Sprintf("read:%s", scope)), true, nil,
|
||||
AccessTokenScope("write:" + scope),
|
||||
AccessTokenScope("read:" + scope), true, nil,
|
||||
},
|
||||
scopeTestHasScope{
|
||||
AccessTokenScope(fmt.Sprintf("read:%s", scope)),
|
||||
AccessTokenScope(fmt.Sprintf("write:%s", scope)), false, nil,
|
||||
AccessTokenScope("read:" + scope),
|
||||
AccessTokenScope("write:" + scope), false, nil,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -127,7 +127,7 @@ func IsTableNotEmpty(beanOrTableName any) (bool, error) {
|
||||
|
||||
// DeleteAllRecords will delete all the records of this table
|
||||
func DeleteAllRecords(tableName string) error {
|
||||
_, err := xormEngine.Exec(fmt.Sprintf("DELETE FROM %s", tableName))
|
||||
_, err := xormEngine.Exec("DELETE FROM " + tableName)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -65,7 +65,7 @@ func (err ErrNotExist) Error() string {
|
||||
if err.ID != 0 {
|
||||
return fmt.Sprintf("%s does not exist [id: %d]", name, err.ID)
|
||||
}
|
||||
return fmt.Sprintf("%s does not exist", name)
|
||||
return name + " does not exist"
|
||||
}
|
||||
|
||||
// Unwrap unwraps this as a ErrNotExist err
|
||||
|
@ -93,3 +93,111 @@
|
||||
is_deleted: false
|
||||
deleted_by_id: 0
|
||||
deleted_unix: 0
|
||||
|
||||
-
|
||||
id: 16
|
||||
repo_id: 16
|
||||
name: 'master'
|
||||
commit_id: '69554a64c1e6030f051e5c3f94bfbd773cd6a324'
|
||||
commit_message: 'not signed commit'
|
||||
commit_time: 1502042309
|
||||
pusher_id: 2
|
||||
is_deleted: false
|
||||
deleted_by_id: 0
|
||||
deleted_unix: 0
|
||||
|
||||
-
|
||||
id: 17
|
||||
repo_id: 16
|
||||
name: 'not-signed'
|
||||
commit_id: '69554a64c1e6030f051e5c3f94bfbd773cd6a324'
|
||||
commit_message: 'not signed commit'
|
||||
commit_time: 1502042309
|
||||
pusher_id: 2
|
||||
is_deleted: false
|
||||
deleted_by_id: 0
|
||||
deleted_unix: 0
|
||||
|
||||
-
|
||||
id: 18
|
||||
repo_id: 16
|
||||
name: 'good-sign-not-yet-validated'
|
||||
commit_id: '27566bd5738fc8b4e3fef3c5e72cce608537bd95'
|
||||
commit_message: 'good signed commit (with not yet validated email)'
|
||||
commit_time: 1502042234
|
||||
pusher_id: 2
|
||||
is_deleted: false
|
||||
deleted_by_id: 0
|
||||
deleted_unix: 0
|
||||
|
||||
-
|
||||
id: 19
|
||||
repo_id: 16
|
||||
name: 'good-sign'
|
||||
commit_id: 'f27c2b2b03dcab38beaf89b0ab4ff61f6de63441'
|
||||
commit_message: 'good signed commit'
|
||||
commit_time: 1502042101
|
||||
pusher_id: 2
|
||||
is_deleted: false
|
||||
deleted_by_id: 0
|
||||
deleted_unix: 0
|
||||
|
||||
-
|
||||
id: 20
|
||||
repo_id: 1
|
||||
name: 'feature/1'
|
||||
commit_id: '65f1bf27bc3bf70f64657658635e66094edbcb4d'
|
||||
commit_message: 'Initial commit'
|
||||
commit_time: 1489950479
|
||||
pusher_id: 2
|
||||
is_deleted: false
|
||||
deleted_by_id: 0
|
||||
deleted_unix: 0
|
||||
|
||||
-
|
||||
id: 21
|
||||
repo_id: 49
|
||||
name: 'master'
|
||||
commit_id: 'aacbdfe9e1c4b47f60abe81849045fa4e96f1d75'
|
||||
commit_message: "Add 'test/test.txt'"
|
||||
commit_time: 1572535577
|
||||
pusher_id: 2
|
||||
is_deleted: false
|
||||
deleted_by_id: 0
|
||||
deleted_unix: 0
|
||||
|
||||
-
|
||||
id: 22
|
||||
repo_id: 1
|
||||
name: 'develop'
|
||||
commit_id: '65f1bf27bc3bf70f64657658635e66094edbcb4d'
|
||||
commit_message: "Initial commit"
|
||||
commit_time: 1489927679
|
||||
pusher_id: 1
|
||||
is_deleted: false
|
||||
deleted_by_id: 0
|
||||
deleted_unix: 0
|
||||
|
||||
-
|
||||
id: 23
|
||||
repo_id: 3
|
||||
name: 'master'
|
||||
commit_id: '2a47ca4b614a9f5a43abbd5ad851a54a616ffee6'
|
||||
commit_message: "init project"
|
||||
commit_time: 1497448461
|
||||
pusher_id: 1
|
||||
is_deleted: false
|
||||
deleted_by_id: 0
|
||||
deleted_unix: 0
|
||||
|
||||
-
|
||||
id: 24
|
||||
repo_id: 3
|
||||
name: 'test_branch'
|
||||
commit_id: 'd22b4d4daa5be07329fcef6ed458f00cf3392da0'
|
||||
commit_message: "test commit"
|
||||
commit_time: 1602935385
|
||||
pusher_id: 1
|
||||
is_deleted: false
|
||||
deleted_by_id: 0
|
||||
deleted_unix: 0
|
||||
|
@ -21,3 +21,11 @@
|
||||
repo_id: 32
|
||||
created_unix: 1553610671
|
||||
updated_unix: 1553610671
|
||||
|
||||
-
|
||||
id: 4
|
||||
doer_id: 3
|
||||
recipient_id: 1
|
||||
repo_id: 5
|
||||
created_unix: 1553610671
|
||||
updated_unix: 1553610671
|
||||
|
@ -1,7 +1,7 @@
|
||||
-
|
||||
id: 1
|
||||
repo_id: 1
|
||||
url: www.example.com/url1
|
||||
url: https://www.example.com/url1
|
||||
content_type: 1 # json
|
||||
events: '{"push_only":true,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":false}}'
|
||||
is_active: true
|
||||
@ -9,7 +9,7 @@
|
||||
-
|
||||
id: 2
|
||||
repo_id: 1
|
||||
url: www.example.com/url2
|
||||
url: https://www.example.com/url2
|
||||
content_type: 1 # json
|
||||
events: '{"push_only":false,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":true}}'
|
||||
is_active: false
|
||||
@ -18,7 +18,7 @@
|
||||
id: 3
|
||||
owner_id: 3
|
||||
repo_id: 3
|
||||
url: www.example.com/url3
|
||||
url: https://www.example.com/url3
|
||||
content_type: 1 # json
|
||||
events: '{"push_only":false,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":true}}'
|
||||
is_active: true
|
||||
@ -26,7 +26,7 @@
|
||||
-
|
||||
id: 4
|
||||
repo_id: 2
|
||||
url: www.example.com/url4
|
||||
url: https://www.example.com/url4
|
||||
content_type: 1 # json
|
||||
events: '{"push_only":true,"branch_filter":"{master,feature*}"}'
|
||||
is_active: true
|
||||
@ -35,7 +35,7 @@
|
||||
id: 5
|
||||
repo_id: 0
|
||||
owner_id: 0
|
||||
url: www.example.com/url5
|
||||
url: https://www.example.com/url5
|
||||
content_type: 1 # json
|
||||
events: '{"push_only":true,"branch_filter":"{master,feature*}"}'
|
||||
is_active: true
|
||||
@ -45,7 +45,7 @@
|
||||
id: 6
|
||||
repo_id: 0
|
||||
owner_id: 0
|
||||
url: www.example.com/url6
|
||||
url: https://www.example.com/url6
|
||||
content_type: 1 # json
|
||||
events: '{"push_only":true,"branch_filter":"{master,feature*}"}'
|
||||
is_active: true
|
||||
|
@ -173,6 +173,18 @@ func GetBranch(ctx context.Context, repoID int64, branchName string) (*Branch, e
|
||||
return &branch, nil
|
||||
}
|
||||
|
||||
// IsBranchExist returns true if the branch exists in the repository.
|
||||
func IsBranchExist(ctx context.Context, repoID int64, branchName string) (bool, error) {
|
||||
var branch Branch
|
||||
has, err := db.GetEngine(ctx).Where("repo_id=?", repoID).And("name=?", branchName).Get(&branch)
|
||||
if err != nil {
|
||||
return false, err
|
||||
} else if !has {
|
||||
return false, nil
|
||||
}
|
||||
return !branch.IsDeleted, nil
|
||||
}
|
||||
|
||||
func GetBranches(ctx context.Context, repoID int64, branchNames []string, includeDeleted bool) ([]*Branch, error) {
|
||||
branches := make([]*Branch, 0, len(branchNames))
|
||||
|
||||
@ -223,6 +235,11 @@ func GetDeletedBranchByID(ctx context.Context, repoID, branchID int64) (*Branch,
|
||||
return &branch, nil
|
||||
}
|
||||
|
||||
func DeleteRepoBranches(ctx context.Context, repoID int64) error {
|
||||
_, err := db.GetEngine(ctx).Where("repo_id=?", repoID).Delete(new(Branch))
|
||||
return err
|
||||
}
|
||||
|
||||
func DeleteBranches(ctx context.Context, repoID, doerID int64, branchIDs []int64) error {
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
branches := make([]*Branch, 0, len(branchIDs))
|
||||
|
@ -222,7 +222,7 @@ func (status *CommitStatus) HideActionsURL(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
prefix := fmt.Sprintf("%s/actions", status.Repo.Link())
|
||||
prefix := status.Repo.Link() + "/actions"
|
||||
if strings.HasPrefix(status.TargetURL, prefix) {
|
||||
status.TargetURL = ""
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"html/template"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
project_model "code.gitea.io/gitea/models/project"
|
||||
@ -815,7 +816,7 @@ func ChangeIssueTimeEstimate(ctx context.Context, issue *Issue, doer *user_model
|
||||
Doer: doer,
|
||||
Repo: issue.Repo,
|
||||
Issue: issue,
|
||||
Content: fmt.Sprintf("%d", timeEstimate),
|
||||
Content: strconv.FormatInt(timeEstimate, 10),
|
||||
}
|
||||
if _, err := CreateComment(ctx, opts); err != nil {
|
||||
return fmt.Errorf("createComment: %w", err)
|
||||
|
@ -21,6 +21,8 @@ import (
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
const ScopeSortPrefix = "scope-"
|
||||
|
||||
// IssuesOptions represents options of an issue.
|
||||
type IssuesOptions struct { //nolint
|
||||
Paginator *db.ListOptions
|
||||
@ -70,6 +72,17 @@ func (o *IssuesOptions) Copy(edit ...func(options *IssuesOptions)) *IssuesOption
|
||||
// applySorts sort an issues-related session based on the provided
|
||||
// sortType string
|
||||
func applySorts(sess *xorm.Session, sortType string, priorityRepoID int64) {
|
||||
// Since this sortType is dynamically created, it has to be treated specially.
|
||||
if strings.HasPrefix(sortType, ScopeSortPrefix) {
|
||||
scope := strings.TrimPrefix(sortType, ScopeSortPrefix)
|
||||
sess.Join("LEFT", "issue_label", "issue.id = issue_label.issue_id")
|
||||
// "exclusive_order=0" means "no order is set", so exclude it from the JOIN criteria and then "LEFT JOIN" result is also null
|
||||
sess.Join("LEFT", "label", "label.id = issue_label.label_id AND label.exclusive_order <> 0 AND label.name LIKE ?", scope+"/%")
|
||||
// Use COALESCE to make sure we sort NULL last regardless of backend DB (2147483647 == max int)
|
||||
sess.OrderBy("COALESCE(label.exclusive_order, 2147483647) ASC").Desc("issue.id")
|
||||
return
|
||||
}
|
||||
|
||||
switch sortType {
|
||||
case "oldest":
|
||||
sess.Asc("issue.created_unix").Asc("issue.id")
|
||||
|
@ -5,12 +5,12 @@ package issues
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
project_model "code.gitea.io/gitea/models/project"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
@ -386,10 +386,10 @@ func NewIssueWithIndex(ctx context.Context, doer *user_model.User, opts NewIssue
|
||||
}
|
||||
|
||||
if opts.Issue.Index <= 0 {
|
||||
return fmt.Errorf("no issue index provided")
|
||||
return errors.New("no issue index provided")
|
||||
}
|
||||
if opts.Issue.ID > 0 {
|
||||
return fmt.Errorf("issue exist")
|
||||
return errors.New("issue exist")
|
||||
}
|
||||
|
||||
if _, err := e.Insert(opts.Issue); err != nil {
|
||||
@ -611,7 +611,7 @@ func ResolveIssueMentionsByVisibility(ctx context.Context, issue *Issue, doer *u
|
||||
unittype = unit.TypePullRequests
|
||||
}
|
||||
for _, team := range teams {
|
||||
if team.AccessMode >= perm.AccessModeAdmin {
|
||||
if team.HasAdminAccess() {
|
||||
checked = append(checked, team.ID)
|
||||
resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true
|
||||
continue
|
||||
@ -845,6 +845,7 @@ func DeleteOrphanedIssues(ctx context.Context) error {
|
||||
|
||||
// Remove issue attachment files.
|
||||
for i := range attachmentPaths {
|
||||
// FIXME: it's not right, because the attachment might not be on local filesystem
|
||||
system_model.RemoveAllWithNotice(ctx, "Delete issue attachment", attachmentPaths[i])
|
||||
}
|
||||
return nil
|
||||
|
@ -87,6 +87,7 @@ type Label struct {
|
||||
OrgID int64 `xorm:"INDEX"`
|
||||
Name string
|
||||
Exclusive bool
|
||||
ExclusiveOrder int `xorm:"DEFAULT 0"` // 0 means no exclusive order
|
||||
Description string
|
||||
Color string `xorm:"VARCHAR(7)"`
|
||||
NumIssues int
|
||||
@ -236,7 +237,7 @@ func UpdateLabel(ctx context.Context, l *Label) error {
|
||||
}
|
||||
l.Color = color
|
||||
|
||||
return updateLabelCols(ctx, l, "name", "description", "color", "exclusive", "archived_unix")
|
||||
return updateLabelCols(ctx, l, "name", "description", "color", "exclusive", "exclusive_order", "archived_unix")
|
||||
}
|
||||
|
||||
// DeleteLabel delete a label
|
||||
|
@ -6,6 +6,7 @@ package issues
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
@ -732,7 +733,7 @@ func (pr *PullRequest) GetWorkInProgressPrefix(ctx context.Context) string {
|
||||
// UpdateCommitDivergence update Divergence of a pull request
|
||||
func (pr *PullRequest) UpdateCommitDivergence(ctx context.Context, ahead, behind int) error {
|
||||
if pr.ID == 0 {
|
||||
return fmt.Errorf("pull ID is 0")
|
||||
return errors.New("pull ID is 0")
|
||||
}
|
||||
pr.CommitsAhead = ahead
|
||||
pr.CommitsBehind = behind
|
||||
@ -925,7 +926,7 @@ func ParseCodeOwnersLine(ctx context.Context, tokens []string) (*CodeOwnerRule,
|
||||
if strings.Contains(user, "/") {
|
||||
s := strings.Split(user, "/")
|
||||
if len(s) != 2 {
|
||||
warnings = append(warnings, fmt.Sprintf("incorrect codeowner group: %s", user))
|
||||
warnings = append(warnings, "incorrect codeowner group: "+user)
|
||||
continue
|
||||
}
|
||||
orgName := s[0]
|
||||
@ -933,12 +934,12 @@ func ParseCodeOwnersLine(ctx context.Context, tokens []string) (*CodeOwnerRule,
|
||||
|
||||
org, err := org_model.GetOrgByName(ctx, orgName)
|
||||
if err != nil {
|
||||
warnings = append(warnings, fmt.Sprintf("incorrect codeowner organization: %s", user))
|
||||
warnings = append(warnings, "incorrect codeowner organization: "+user)
|
||||
continue
|
||||
}
|
||||
teams, err := org.LoadTeams(ctx)
|
||||
if err != nil {
|
||||
warnings = append(warnings, fmt.Sprintf("incorrect codeowner team: %s", user))
|
||||
warnings = append(warnings, "incorrect codeowner team: "+user)
|
||||
continue
|
||||
}
|
||||
|
||||
@ -950,7 +951,7 @@ func ParseCodeOwnersLine(ctx context.Context, tokens []string) (*CodeOwnerRule,
|
||||
} else {
|
||||
u, err := user_model.GetUserByName(ctx, user)
|
||||
if err != nil {
|
||||
warnings = append(warnings, fmt.Sprintf("incorrect codeowner user: %s", user))
|
||||
warnings = append(warnings, "incorrect codeowner user: "+user)
|
||||
continue
|
||||
}
|
||||
rule.Users = append(rule.Users, u)
|
||||
|
@ -5,6 +5,7 @@ package issues
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
@ -374,7 +375,7 @@ func CreateReview(ctx context.Context, opts CreateReviewOptions) (*Review, error
|
||||
review.Type = ReviewTypeRequest
|
||||
review.ReviewerTeamID = opts.ReviewerTeam.ID
|
||||
} else {
|
||||
return nil, fmt.Errorf("provide either reviewer or reviewer team")
|
||||
return nil, errors.New("provide either reviewer or reviewer team")
|
||||
}
|
||||
|
||||
if _, err := sess.Insert(review); err != nil {
|
||||
@ -933,7 +934,7 @@ func MarkConversation(ctx context.Context, comment *Comment, doer *user_model.Us
|
||||
// the PR writer , official reviewer and poster can do it
|
||||
func CanMarkConversation(ctx context.Context, issue *Issue, doer *user_model.User) (permResult bool, err error) {
|
||||
if doer == nil || issue == nil {
|
||||
return false, fmt.Errorf("issue or doer is nil")
|
||||
return false, errors.New("issue or doer is nil")
|
||||
}
|
||||
|
||||
if err = issue.LoadRepo(ctx); err != nil {
|
||||
@ -972,11 +973,11 @@ func DeleteReview(ctx context.Context, r *Review) error {
|
||||
defer committer.Close()
|
||||
|
||||
if r.ID == 0 {
|
||||
return fmt.Errorf("review is not allowed to be 0")
|
||||
return errors.New("review is not allowed to be 0")
|
||||
}
|
||||
|
||||
if r.Type == ReviewTypeRequest {
|
||||
return fmt.Errorf("review request can not be deleted using this method")
|
||||
return errors.New("review request can not be deleted using this method")
|
||||
}
|
||||
|
||||
opts := FindCommentsOptions{
|
||||
|
@ -52,7 +52,7 @@ func RecreateTable(sess *xorm.Session, bean any) error {
|
||||
// TODO: This will not work if there are foreign keys
|
||||
|
||||
tableName := sess.Engine().TableName(bean)
|
||||
tempTableName := fmt.Sprintf("tmp_recreate__%s", tableName)
|
||||
tempTableName := "tmp_recreate__" + tableName
|
||||
|
||||
// We need to move the old table away and create a new one with the correct columns
|
||||
// We will need to do this in stages to prevent data loss
|
||||
@ -82,7 +82,7 @@ func RecreateTable(sess *xorm.Session, bean any) error {
|
||||
}
|
||||
newTableColumns := table.Columns()
|
||||
if len(newTableColumns) == 0 {
|
||||
return fmt.Errorf("no columns in new table")
|
||||
return errors.New("no columns in new table")
|
||||
}
|
||||
hasID := false
|
||||
for _, column := range newTableColumns {
|
||||
@ -552,11 +552,11 @@ func deleteDB() error {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
if _, err = db.Exec(fmt.Sprintf("DROP DATABASE IF EXISTS %s", setting.Database.Name)); err != nil {
|
||||
if _, err = db.Exec("DROP DATABASE IF EXISTS " + setting.Database.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = db.Exec(fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s", setting.Database.Name)); err != nil {
|
||||
if _, err = db.Exec("CREATE DATABASE IF NOT EXISTS " + setting.Database.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@ -568,11 +568,11 @@ func deleteDB() error {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
if _, err = db.Exec(fmt.Sprintf("DROP DATABASE IF EXISTS %s", setting.Database.Name)); err != nil {
|
||||
if _, err = db.Exec("DROP DATABASE IF EXISTS " + setting.Database.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = db.Exec(fmt.Sprintf("CREATE DATABASE %s", setting.Database.Name)); err != nil {
|
||||
if _, err = db.Exec("CREATE DATABASE " + setting.Database.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
db.Close()
|
||||
@ -594,7 +594,7 @@ func deleteDB() error {
|
||||
|
||||
if !schrows.Next() {
|
||||
// Create and setup a DB schema
|
||||
_, err = db.Exec(fmt.Sprintf("CREATE SCHEMA %s", setting.Database.Schema))
|
||||
_, err = db.Exec("CREATE SCHEMA " + setting.Database.Schema)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/tempdir"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/modules/testlogger"
|
||||
|
||||
@ -114,15 +115,16 @@ func MainTest(m *testing.M) {
|
||||
setting.CustomConf = giteaConf
|
||||
}
|
||||
|
||||
tmpDataPath, err := os.MkdirTemp("", "data")
|
||||
tmpDataPath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("data")
|
||||
if err != nil {
|
||||
testlogger.Fatalf("Unable to create temporary data path %v\n", err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
setting.CustomPath = filepath.Join(setting.AppWorkPath, "custom")
|
||||
setting.AppDataPath = tmpDataPath
|
||||
|
||||
unittest.InitSettings()
|
||||
unittest.InitSettingsForTesting()
|
||||
if err = git.InitFull(context.Background()); err != nil {
|
||||
testlogger.Fatalf("Unable to InitFull: %v\n", err)
|
||||
}
|
||||
@ -134,8 +136,5 @@ func MainTest(m *testing.M) {
|
||||
if err := removeAllWithRetry(setting.RepoRootPath); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "os.RemoveAll: %v\n", err)
|
||||
}
|
||||
if err := removeAllWithRetry(tmpDataPath); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "os.RemoveAll: %v\n", err)
|
||||
}
|
||||
os.Exit(exitStatus)
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/migrations/v1_10"
|
||||
@ -379,6 +380,7 @@ func prepareMigrationTasks() []*migration {
|
||||
newMigration(316, "Add description for secrets and variables", v1_24.AddDescriptionForSecretsAndVariables),
|
||||
newMigration(317, "Add new index for action for heatmap", v1_24.AddNewIndexForUserDashboard),
|
||||
newMigration(318, "Add anonymous_access_mode for repo_unit", v1_24.AddRepoUnitAnonymousAccessMode),
|
||||
newMigration(319, "Add ExclusiveOrder to Label table", v1_24.AddExclusiveOrderColumnToLabelTable),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
@ -424,7 +426,7 @@ func EnsureUpToDate(ctx context.Context, x *xorm.Engine) error {
|
||||
}
|
||||
|
||||
if currentDB < 0 {
|
||||
return fmt.Errorf("database has not been initialized")
|
||||
return errors.New("database has not been initialized")
|
||||
}
|
||||
|
||||
if minDBVersion > currentDB {
|
||||
|
@ -46,7 +46,7 @@ func FixLanguageStatsToSaveSize(x *xorm.Engine) error {
|
||||
}
|
||||
|
||||
// Delete language stats
|
||||
if _, err := x.Exec(fmt.Sprintf("%s language_stat", truncExpr)); err != nil {
|
||||
if _, err := x.Exec(truncExpr + " language_stat"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -42,7 +42,7 @@ func IncreaseLanguageField(x *xorm.Engine) error {
|
||||
|
||||
switch {
|
||||
case setting.Database.Type.IsMySQL():
|
||||
if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE language_stat MODIFY COLUMN language %s", sqlType)); err != nil {
|
||||
if _, err := sess.Exec("ALTER TABLE language_stat MODIFY COLUMN language " + sqlType); err != nil {
|
||||
return err
|
||||
}
|
||||
case setting.Database.Type.IsMSSQL():
|
||||
@ -64,7 +64,7 @@ func IncreaseLanguageField(x *xorm.Engine) error {
|
||||
return fmt.Errorf("Drop table `language_stat` constraint `%s`: %w", constraint, err)
|
||||
}
|
||||
}
|
||||
if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE language_stat ALTER COLUMN language %s", sqlType)); err != nil {
|
||||
if _, err := sess.Exec("ALTER TABLE language_stat ALTER COLUMN language " + sqlType); err != nil {
|
||||
return err
|
||||
}
|
||||
// Finally restore the constraint
|
||||
@ -72,7 +72,7 @@ func IncreaseLanguageField(x *xorm.Engine) error {
|
||||
return err
|
||||
}
|
||||
case setting.Database.Type.IsPostgreSQL():
|
||||
if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE language_stat ALTER COLUMN language TYPE %s", sqlType)); err != nil {
|
||||
if _, err := sess.Exec("ALTER TABLE language_stat ALTER COLUMN language TYPE " + sqlType); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ package v1_13 //nolint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@ -113,7 +114,7 @@ func SetDefaultPasswordToArgon2(x *xorm.Engine) error {
|
||||
|
||||
newTableColumns := table.Columns()
|
||||
if len(newTableColumns) == 0 {
|
||||
return fmt.Errorf("no columns in new table")
|
||||
return errors.New("no columns in new table")
|
||||
}
|
||||
hasID := false
|
||||
for _, column := range newTableColumns {
|
||||
|
@ -4,7 +4,7 @@
|
||||
package v1_14 //nolint
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@ -82,7 +82,7 @@ func UpdateCodeCommentReplies(x *xorm.Engine) error {
|
||||
sqlCmd = "SELECT TOP " + strconv.Itoa(batchSize) + " * FROM #temp_comments WHERE " +
|
||||
"(id NOT IN ( SELECT TOP " + strconv.Itoa(start) + " id FROM #temp_comments ORDER BY id )) ORDER BY id"
|
||||
default:
|
||||
return fmt.Errorf("Unsupported database type")
|
||||
return errors.New("Unsupported database type")
|
||||
}
|
||||
|
||||
if err := sess.SQL(sqlCmd).Find(&comments); err != nil {
|
||||
|
@ -5,6 +5,7 @@ package v1_17 //nolint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/migrations/base"
|
||||
@ -29,7 +30,7 @@ func DropOldCredentialIDColumn(x *xorm.Engine) error {
|
||||
}
|
||||
if !credentialIDBytesExists {
|
||||
// looks like 221 hasn't properly run
|
||||
return fmt.Errorf("webauthn_credential does not have a credential_id_bytes column... it is not safe to run this migration")
|
||||
return errors.New("webauthn_credential does not have a credential_id_bytes column... it is not safe to run this migration")
|
||||
}
|
||||
|
||||
// Create webauthnCredential table
|
||||
|
@ -5,7 +5,6 @@ package v1_20 //nolint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/migrations/base"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@ -57,7 +56,7 @@ func RenameWebhookOrgToOwner(x *xorm.Engine) error {
|
||||
return err
|
||||
}
|
||||
sqlType := x.Dialect().SQLType(inferredTable.GetColumn("org_id"))
|
||||
if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `webhook` CHANGE org_id owner_id %s", sqlType)); err != nil {
|
||||
if _, err := sess.Exec("ALTER TABLE `webhook` CHANGE org_id owner_id " + sqlType); err != nil {
|
||||
return err
|
||||
}
|
||||
case setting.Database.Type.IsMSSQL():
|
||||
|
@ -5,7 +5,7 @@ package v1_21 //nolint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"errors"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
@ -57,7 +57,7 @@ func AddBranchTable(x *xorm.Engine) error {
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !has {
|
||||
return fmt.Errorf("no admin user found")
|
||||
return errors.New("no admin user found")
|
||||
}
|
||||
|
||||
branches := make([]Branch, 0, 100)
|
||||
|
@ -4,7 +4,7 @@
|
||||
package v1_22 //nolint
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/migrations/base"
|
||||
@ -50,7 +50,7 @@ func Test_UpdateBadgeColName(t *testing.T) {
|
||||
for i, e := range oldBadges {
|
||||
got := got[i+1] // 1 is in the badge.yml
|
||||
assert.Equal(t, e.ID, got.ID)
|
||||
assert.Equal(t, fmt.Sprintf("%d", e.ID), got.Slug)
|
||||
assert.Equal(t, strconv.FormatInt(e.ID, 10), got.Slug)
|
||||
}
|
||||
|
||||
// TODO: check if badges have been updated
|
||||
|
16
models/migrations/v1_24/v319.go
Normal file
16
models/migrations/v1_24/v319.go
Normal file
@ -0,0 +1,16 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_24 //nolint
|
||||
|
||||
import (
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func AddExclusiveOrderColumnToLabelTable(x *xorm.Engine) error {
|
||||
type Label struct {
|
||||
ExclusiveOrder int `xorm:"DEFAULT 0"`
|
||||
}
|
||||
|
||||
return x.Sync(new(Label))
|
||||
}
|
@ -178,12 +178,6 @@ func (org *Organization) HomeLink() string {
|
||||
return org.AsUser().HomeLink()
|
||||
}
|
||||
|
||||
// CanCreateRepo returns if user login can create a repository
|
||||
// NOTE: functions calling this assume a failure due to repository count limit; if new checks are added, those functions should be revised
|
||||
func (org *Organization) CanCreateRepo() bool {
|
||||
return org.AsUser().CanCreateRepo()
|
||||
}
|
||||
|
||||
// FindOrgMembersOpts represensts find org members conditions
|
||||
type FindOrgMembersOpts struct {
|
||||
db.ListOptions
|
||||
|
@ -78,7 +78,7 @@ func IsOrganizationAdmin(ctx context.Context, orgID, uid int64) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
for _, t := range teams {
|
||||
if t.AccessMode >= perm.AccessModeAdmin {
|
||||
if t.HasAdminAccess() {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
@ -113,7 +113,7 @@ func (t *Team) LoadUnits(ctx context.Context) (err error) {
|
||||
|
||||
// GetUnitNames returns the team units names
|
||||
func (t *Team) GetUnitNames() (res []string) {
|
||||
if t.AccessMode >= perm.AccessModeAdmin {
|
||||
if t.HasAdminAccess() {
|
||||
return unit.AllUnitKeyNames()
|
||||
}
|
||||
|
||||
@ -126,7 +126,7 @@ func (t *Team) GetUnitNames() (res []string) {
|
||||
// GetUnitsMap returns the team units permissions
|
||||
func (t *Team) GetUnitsMap() map[string]string {
|
||||
m := make(map[string]string)
|
||||
if t.AccessMode >= perm.AccessModeAdmin {
|
||||
if t.HasAdminAccess() {
|
||||
for _, u := range unit.Units {
|
||||
m[u.NameKey] = t.AccessMode.ToString()
|
||||
}
|
||||
@ -153,6 +153,10 @@ func (t *Team) IsMember(ctx context.Context, userID int64) bool {
|
||||
return isMember
|
||||
}
|
||||
|
||||
func (t *Team) HasAdminAccess() bool {
|
||||
return t.AccessMode >= perm.AccessModeAdmin
|
||||
}
|
||||
|
||||
// LoadMembers returns paginated members in team of organization.
|
||||
func (t *Team) LoadMembers(ctx context.Context) (err error) {
|
||||
t.Members, err = GetTeamMembers(ctx, &SearchMembersOptions{
|
||||
@ -238,22 +242,6 @@ func GetTeamByID(ctx context.Context, teamID int64) (*Team, error) {
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// GetTeamNamesByID returns team's lower name from a list of team ids.
|
||||
func GetTeamNamesByID(ctx context.Context, teamIDs []int64) ([]string, error) {
|
||||
if len(teamIDs) == 0 {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
var teamNames []string
|
||||
err := db.GetEngine(ctx).Table("team").
|
||||
Select("lower_name").
|
||||
In("id", teamIDs).
|
||||
Asc("name").
|
||||
Find(&teamNames)
|
||||
|
||||
return teamNames, err
|
||||
}
|
||||
|
||||
// IncrTeamRepoNum increases the number of repos for the given team by 1
|
||||
func IncrTeamRepoNum(ctx context.Context, teamID int64) error {
|
||||
_, err := db.GetEngine(ctx).Incr("num_repos").ID(teamID).Update(new(Team))
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/cache"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/packages/alpine"
|
||||
"code.gitea.io/gitea/modules/packages/arch"
|
||||
@ -102,22 +103,26 @@ func (pd *PackageDescriptor) CalculateBlobSize() int64 {
|
||||
|
||||
// GetPackageDescriptor gets the package description for a version
|
||||
func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDescriptor, error) {
|
||||
p, err := GetPackageByID(ctx, pv.PackageID)
|
||||
return getPackageDescriptor(ctx, pv, cache.NewEphemeralCache())
|
||||
}
|
||||
|
||||
func getPackageDescriptor(ctx context.Context, pv *PackageVersion, c *cache.EphemeralCache) (*PackageDescriptor, error) {
|
||||
p, err := cache.GetWithEphemeralCache(ctx, c, "package", pv.PackageID, GetPackageByID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
o, err := user_model.GetUserByID(ctx, p.OwnerID)
|
||||
o, err := cache.GetWithEphemeralCache(ctx, c, "user", p.OwnerID, user_model.GetUserByID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var repository *repo_model.Repository
|
||||
if p.RepoID > 0 {
|
||||
repository, err = repo_model.GetRepositoryByID(ctx, p.RepoID)
|
||||
repository, err = cache.GetWithEphemeralCache(ctx, c, "repo", p.RepoID, repo_model.GetRepositoryByID)
|
||||
if err != nil && !repo_model.IsErrRepoNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
creator, err := user_model.GetUserByID(ctx, pv.CreatorID)
|
||||
creator, err := cache.GetWithEphemeralCache(ctx, c, "user", pv.CreatorID, user_model.GetUserByID)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
creator = user_model.NewGhostUser()
|
||||
@ -145,9 +150,13 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pfds, err := GetPackageFileDescriptors(ctx, pfs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
pfds := make([]*PackageFileDescriptor, 0, len(pfs))
|
||||
for _, pf := range pfs {
|
||||
pfd, err := getPackageFileDescriptor(ctx, pf, c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pfds = append(pfds, pfd)
|
||||
}
|
||||
|
||||
var metadata any
|
||||
@ -197,7 +206,7 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc
|
||||
case TypeVagrant:
|
||||
metadata = &vagrant.Metadata{}
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown package type: %s", string(p.Type)))
|
||||
panic("unknown package type: " + string(p.Type))
|
||||
}
|
||||
if metadata != nil {
|
||||
if err := json.Unmarshal([]byte(pv.MetadataJSON), &metadata); err != nil {
|
||||
@ -221,7 +230,11 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc
|
||||
|
||||
// GetPackageFileDescriptor gets a package file descriptor for a package file
|
||||
func GetPackageFileDescriptor(ctx context.Context, pf *PackageFile) (*PackageFileDescriptor, error) {
|
||||
pb, err := GetBlobByID(ctx, pf.BlobID)
|
||||
return getPackageFileDescriptor(ctx, pf, cache.NewEphemeralCache())
|
||||
}
|
||||
|
||||
func getPackageFileDescriptor(ctx context.Context, pf *PackageFile, c *cache.EphemeralCache) (*PackageFileDescriptor, error) {
|
||||
pb, err := cache.GetWithEphemeralCache(ctx, c, "package_file_blob", pf.BlobID, GetBlobByID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -251,9 +264,13 @@ func GetPackageFileDescriptors(ctx context.Context, pfs []*PackageFile) ([]*Pack
|
||||
|
||||
// GetPackageDescriptors gets the package descriptions for the versions
|
||||
func GetPackageDescriptors(ctx context.Context, pvs []*PackageVersion) ([]*PackageDescriptor, error) {
|
||||
return getPackageDescriptors(ctx, pvs, cache.NewEphemeralCache())
|
||||
}
|
||||
|
||||
func getPackageDescriptors(ctx context.Context, pvs []*PackageVersion, c *cache.EphemeralCache) ([]*PackageDescriptor, error) {
|
||||
pds := make([]*PackageDescriptor, 0, len(pvs))
|
||||
for _, pv := range pvs {
|
||||
pd, err := GetPackageDescriptor(ctx, pv)
|
||||
pd, err := getPackageDescriptor(ctx, pv, c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -127,7 +127,7 @@ func (pt Type) Name() string {
|
||||
case TypeVagrant:
|
||||
return "Vagrant"
|
||||
}
|
||||
panic(fmt.Sprintf("unknown package type: %s", string(pt)))
|
||||
panic("unknown package type: " + string(pt))
|
||||
}
|
||||
|
||||
// SVGName gets the name of the package type svg image
|
||||
@ -178,7 +178,7 @@ func (pt Type) SVGName() string {
|
||||
case TypeVagrant:
|
||||
return "gitea-vagrant"
|
||||
}
|
||||
panic(fmt.Sprintf("unknown package type: %s", string(pt)))
|
||||
panic("unknown package type: " + string(pt))
|
||||
}
|
||||
|
||||
// Package represents a package
|
||||
|
@ -331,7 +331,7 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
|
||||
|
||||
// if user in an owner team
|
||||
for _, team := range teams {
|
||||
if team.AccessMode >= perm_model.AccessModeAdmin {
|
||||
if team.HasAdminAccess() {
|
||||
perm.AccessMode = perm_model.AccessModeOwner
|
||||
perm.unitsMode = nil
|
||||
return perm, nil
|
||||
@ -399,7 +399,7 @@ func IsUserRepoAdmin(ctx context.Context, repo *repo_model.Repository, user *use
|
||||
}
|
||||
|
||||
for _, team := range teams {
|
||||
if team.AccessMode >= perm_model.AccessModeAdmin {
|
||||
if team.HasAdminAccess() {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
@ -147,7 +147,7 @@ func NewColumn(ctx context.Context, column *Column) error {
|
||||
return err
|
||||
}
|
||||
if res.ColumnCount >= maxProjectColumns {
|
||||
return fmt.Errorf("NewBoard: maximum number of columns reached")
|
||||
return errors.New("NewBoard: maximum number of columns reached")
|
||||
}
|
||||
column.Sorting = int8(util.Iif(res.ColumnCount > 0, res.MaxSorting+1, 0))
|
||||
_, err := db.GetEngine(ctx).Insert(column)
|
||||
@ -172,7 +172,7 @@ func deleteColumnByID(ctx context.Context, columnID int64) error {
|
||||
}
|
||||
|
||||
if column.Default {
|
||||
return fmt.Errorf("deleteColumnByID: cannot delete default column")
|
||||
return errors.New("deleteColumnByID: cannot delete default column")
|
||||
}
|
||||
|
||||
// move all issues to the default column
|
||||
|
@ -5,7 +5,7 @@ package project
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"errors"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
@ -35,7 +35,7 @@ func deleteProjectIssuesByProjectID(ctx context.Context, projectID int64) error
|
||||
|
||||
func (c *Column) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Column) error {
|
||||
if c.ProjectID != newColumn.ProjectID {
|
||||
return fmt.Errorf("columns have to be in the same project")
|
||||
return errors.New("columns have to be in the same project")
|
||||
}
|
||||
|
||||
if c.ID == newColumn.ID {
|
||||
|
@ -28,14 +28,14 @@ func (r *RepoComment) IsCommitIDExisting(commitID string) bool {
|
||||
return r.commitChecker.IsCommitIDExisting(commitID)
|
||||
}
|
||||
|
||||
func (r *RepoComment) ResolveLink(link string, likeType markup.LinkType) (finalLink string) {
|
||||
switch likeType {
|
||||
case markup.LinkTypeApp:
|
||||
finalLink = r.ctx.ResolveLinkApp(link)
|
||||
func (r *RepoComment) ResolveLink(link, preferLinkType string) string {
|
||||
linkType, link := markup.ParseRenderedLink(link, preferLinkType)
|
||||
switch linkType {
|
||||
case markup.LinkTypeRoot:
|
||||
return r.ctx.ResolveLinkRoot(link)
|
||||
default:
|
||||
finalLink = r.ctx.ResolveLinkRelative(r.repoLink, r.opts.CurrentRefPath, link)
|
||||
return r.ctx.ResolveLinkRelative(r.repoLink, r.opts.CurrentRefPath, link)
|
||||
}
|
||||
return finalLink
|
||||
}
|
||||
|
||||
var _ markup.RenderHelper = (*RepoComment)(nil)
|
||||
@ -56,7 +56,7 @@ func NewRenderContextRepoComment(ctx context.Context, repo *repo_model.Repositor
|
||||
if repo != nil {
|
||||
helper.repoLink = repo.Link()
|
||||
helper.commitChecker = newCommitChecker(ctx, repo)
|
||||
rctx = rctx.WithMetas(repo.ComposeMetas(ctx))
|
||||
rctx = rctx.WithMetas(repo.ComposeCommentMetas(ctx))
|
||||
} else {
|
||||
// this is almost dead code, only to pass the incorrect tests
|
||||
helper.repoLink = fmt.Sprintf("%s/%s", helper.opts.DeprecatedOwnerName, helper.opts.DeprecatedRepoName)
|
||||
@ -64,7 +64,7 @@ func NewRenderContextRepoComment(ctx context.Context, repo *repo_model.Repositor
|
||||
"user": helper.opts.DeprecatedOwnerName,
|
||||
"repo": helper.opts.DeprecatedRepoName,
|
||||
|
||||
"markdownLineBreakStyle": "comment",
|
||||
"markdownNewLineHardBreak": "true",
|
||||
"markupAllowShortIssuePattern": "true",
|
||||
})
|
||||
}
|
||||
|
@ -29,17 +29,17 @@ func (r *RepoFile) IsCommitIDExisting(commitID string) bool {
|
||||
return r.commitChecker.IsCommitIDExisting(commitID)
|
||||
}
|
||||
|
||||
func (r *RepoFile) ResolveLink(link string, likeType markup.LinkType) string {
|
||||
finalLink := link
|
||||
switch likeType {
|
||||
case markup.LinkTypeApp:
|
||||
finalLink = r.ctx.ResolveLinkApp(link)
|
||||
case markup.LinkTypeDefault:
|
||||
finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "src", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link)
|
||||
func (r *RepoFile) ResolveLink(link, preferLinkType string) (finalLink string) {
|
||||
linkType, link := markup.ParseRenderedLink(link, preferLinkType)
|
||||
switch linkType {
|
||||
case markup.LinkTypeRoot:
|
||||
finalLink = r.ctx.ResolveLinkRoot(link)
|
||||
case markup.LinkTypeRaw:
|
||||
finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "raw", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link)
|
||||
case markup.LinkTypeMedia:
|
||||
finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "media", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link)
|
||||
default:
|
||||
finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "src", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link)
|
||||
}
|
||||
return finalLink
|
||||
}
|
||||
@ -61,15 +61,13 @@ func NewRenderContextRepoFile(ctx context.Context, repo *repo_model.Repository,
|
||||
if repo != nil {
|
||||
helper.repoLink = repo.Link()
|
||||
helper.commitChecker = newCommitChecker(ctx, repo)
|
||||
rctx = rctx.WithMetas(repo.ComposeDocumentMetas(ctx))
|
||||
rctx = rctx.WithMetas(repo.ComposeRepoFileMetas(ctx))
|
||||
} else {
|
||||
// this is almost dead code, only to pass the incorrect tests
|
||||
helper.repoLink = fmt.Sprintf("%s/%s", helper.opts.DeprecatedOwnerName, helper.opts.DeprecatedRepoName)
|
||||
rctx = rctx.WithMetas(map[string]string{
|
||||
"user": helper.opts.DeprecatedOwnerName,
|
||||
"repo": helper.opts.DeprecatedRepoName,
|
||||
|
||||
"markdownLineBreakStyle": "document",
|
||||
})
|
||||
}
|
||||
rctx = rctx.WithHelper(helper)
|
||||
|
@ -48,8 +48,8 @@ func TestRepoFile(t *testing.T) {
|
||||
assert.Equal(t,
|
||||
`<p><a href="/user2/repo1/src/branch/main/test" rel="nofollow">/test</a>
|
||||
<a href="/user2/repo1/src/branch/main/test" rel="nofollow">./test</a>
|
||||
<a href="/user2/repo1/media/branch/main/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/branch/main/image" alt="/image"/></a>
|
||||
<a href="/user2/repo1/media/branch/main/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/branch/main/image" alt="./image"/></a></p>
|
||||
<a href="/user2/repo1/src/branch/main/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/branch/main/image" alt="/image"/></a>
|
||||
<a href="/user2/repo1/src/branch/main/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/branch/main/image" alt="./image"/></a></p>
|
||||
`, rendered)
|
||||
})
|
||||
|
||||
@ -62,7 +62,7 @@ func TestRepoFile(t *testing.T) {
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `<p><a href="/user2/repo1/src/commit/1234/test" rel="nofollow">/test</a>
|
||||
<a href="/user2/repo1/media/commit/1234/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/commit/1234/image" alt="/image"/></a></p>
|
||||
<a href="/user2/repo1/src/commit/1234/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/commit/1234/image" alt="/image"/></a></p>
|
||||
`, rendered)
|
||||
})
|
||||
|
||||
@ -77,7 +77,7 @@ func TestRepoFile(t *testing.T) {
|
||||
<video src="LINK">
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `<a href="/user2/repo1/media/commit/1234/my-dir/LINK" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/commit/1234/my-dir/LINK"/></a>
|
||||
assert.Equal(t, `<a href="/user2/repo1/src/commit/1234/my-dir/LINK" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/commit/1234/my-dir/LINK"/></a>
|
||||
<video src="/user2/repo1/media/commit/1234/my-dir/LINK">
|
||||
</video>`, rendered)
|
||||
})
|
||||
@ -100,7 +100,7 @@ func TestRepoFileOrgMode(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `<p>
|
||||
<a href="https://google.com/" rel="nofollow">https://google.com/</a>
|
||||
<a href="/user2/repo1/media/commit/1234/my-dir/ImageLink.svg" rel="nofollow">The Image Desc</a></p>
|
||||
<a href="/user2/repo1/src/commit/1234/my-dir/ImageLink.svg" rel="nofollow">The Image Desc</a></p>
|
||||
`, rendered)
|
||||
})
|
||||
|
||||
|
@ -30,18 +30,16 @@ func (r *RepoWiki) IsCommitIDExisting(commitID string) bool {
|
||||
return r.commitChecker.IsCommitIDExisting(commitID)
|
||||
}
|
||||
|
||||
func (r *RepoWiki) ResolveLink(link string, likeType markup.LinkType) string {
|
||||
finalLink := link
|
||||
switch likeType {
|
||||
case markup.LinkTypeApp:
|
||||
finalLink = r.ctx.ResolveLinkApp(link)
|
||||
case markup.LinkTypeDefault:
|
||||
finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "wiki", r.opts.currentRefPath), r.opts.currentTreePath, link)
|
||||
case markup.LinkTypeMedia:
|
||||
func (r *RepoWiki) ResolveLink(link, preferLinkType string) (finalLink string) {
|
||||
linkType, link := markup.ParseRenderedLink(link, preferLinkType)
|
||||
switch linkType {
|
||||
case markup.LinkTypeRoot:
|
||||
finalLink = r.ctx.ResolveLinkRoot(link)
|
||||
case markup.LinkTypeMedia, markup.LinkTypeRaw:
|
||||
finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "wiki/raw", r.opts.currentRefPath), r.opts.currentTreePath, link)
|
||||
case markup.LinkTypeRaw: // wiki doesn't use it
|
||||
default:
|
||||
finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "wiki", r.opts.currentRefPath), r.opts.currentTreePath, link)
|
||||
}
|
||||
|
||||
return finalLink
|
||||
}
|
||||
|
||||
@ -70,7 +68,6 @@ func NewRenderContextRepoWiki(ctx context.Context, repo *repo_model.Repository,
|
||||
"user": helper.opts.DeprecatedOwnerName,
|
||||
"repo": helper.opts.DeprecatedRepoName,
|
||||
|
||||
"markdownLineBreakStyle": "document",
|
||||
"markupAllowShortIssuePattern": "true",
|
||||
})
|
||||
}
|
||||
|
@ -45,8 +45,8 @@ func TestRepoWiki(t *testing.T) {
|
||||
assert.Equal(t,
|
||||
`<p><a href="/user2/repo1/wiki/test" rel="nofollow">/test</a>
|
||||
<a href="/user2/repo1/wiki/test" rel="nofollow">./test</a>
|
||||
<a href="/user2/repo1/wiki/raw/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/wiki/raw/image" alt="/image"/></a>
|
||||
<a href="/user2/repo1/wiki/raw/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/wiki/raw/image" alt="./image"/></a></p>
|
||||
<a href="/user2/repo1/wiki/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/wiki/raw/image" alt="/image"/></a>
|
||||
<a href="/user2/repo1/wiki/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/wiki/raw/image" alt="./image"/></a></p>
|
||||
`, rendered)
|
||||
})
|
||||
|
||||
@ -57,7 +57,7 @@ func TestRepoWiki(t *testing.T) {
|
||||
<video src="LINK">
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `<a href="/user2/repo1/wiki/raw/LINK" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/wiki/raw/LINK"/></a>
|
||||
assert.Equal(t, `<a href="/user2/repo1/wiki/LINK" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/wiki/raw/LINK"/></a>
|
||||
<video src="/user2/repo1/wiki/raw/LINK">
|
||||
</video>`, rendered)
|
||||
})
|
||||
|
@ -15,8 +15,14 @@ type SimpleDocument struct {
|
||||
baseLink string
|
||||
}
|
||||
|
||||
func (r *SimpleDocument) ResolveLink(link string, likeType markup.LinkType) string {
|
||||
return r.ctx.ResolveLinkRelative(r.baseLink, "", link)
|
||||
func (r *SimpleDocument) ResolveLink(link, preferLinkType string) string {
|
||||
linkType, link := markup.ParseRenderedLink(link, preferLinkType)
|
||||
switch linkType {
|
||||
case markup.LinkTypeRoot:
|
||||
return r.ctx.ResolveLinkRoot(link)
|
||||
default:
|
||||
return r.ctx.ResolveLinkRelative(r.baseLink, "", link)
|
||||
}
|
||||
}
|
||||
|
||||
var _ markup.RenderHelper = (*SimpleDocument)(nil)
|
||||
|
@ -30,7 +30,7 @@ func TestSimpleDocument(t *testing.T) {
|
||||
assert.Equal(t,
|
||||
`<p>65f1bf27bc3bf70f64657658635e66094edbcb4d
|
||||
#1
|
||||
<a href="/base/user2" rel="nofollow">@user2</a></p>
|
||||
<a href="/user2" rel="nofollow">@user2</a></p>
|
||||
<p><a href="/base/test" rel="nofollow">/test</a>
|
||||
<a href="/base/test" rel="nofollow">./test</a>
|
||||
<a href="/base/image" target="_blank" rel="nofollow noopener"><img src="/base/image" alt="/image"/></a>
|
||||
|
@ -224,7 +224,7 @@ func DeleteAttachmentsByComment(ctx context.Context, commentID int64, remove boo
|
||||
// UpdateAttachmentByUUID Updates attachment via uuid
|
||||
func UpdateAttachmentByUUID(ctx context.Context, attach *Attachment, cols ...string) error {
|
||||
if attach.UUID == "" {
|
||||
return fmt.Errorf("attachment uuid should be not blank")
|
||||
return errors.New("attachment uuid should be not blank")
|
||||
}
|
||||
_, err := db.GetEngine(ctx).Where("uuid=?", attach.UUID).Cols(cols...).Update(attach)
|
||||
return err
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"image/png"
|
||||
"io"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/avatar"
|
||||
@ -37,7 +38,7 @@ func (repo *Repository) RelAvatarLink(ctx context.Context) string {
|
||||
|
||||
// generateRandomAvatar generates a random avatar for repository.
|
||||
func generateRandomAvatar(ctx context.Context, repo *Repository) error {
|
||||
idToString := fmt.Sprintf("%d", repo.ID)
|
||||
idToString := strconv.FormatInt(repo.ID, 10)
|
||||
|
||||
seed := idToString
|
||||
img, err := avatar.RandomImage([]byte(seed))
|
||||
|
@ -558,3 +558,8 @@ func FindTagsByCommitIDs(ctx context.Context, repoID int64, commitIDs ...string)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func DeleteRepoReleases(ctx context.Context, repoID int64) error {
|
||||
_, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Delete(new(Release))
|
||||
return err
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"maps"
|
||||
@ -59,7 +60,7 @@ type ErrRepoIsArchived struct {
|
||||
}
|
||||
|
||||
func (err ErrRepoIsArchived) Error() string {
|
||||
return fmt.Sprintf("%s is archived", err.Repo.LogString())
|
||||
return err.Repo.LogString() + " is archived"
|
||||
}
|
||||
|
||||
type globalVarsStruct struct {
|
||||
@ -515,15 +516,15 @@ func (repo *Repository) composeCommonMetas(ctx context.Context) map[string]strin
|
||||
"repo": repo.Name,
|
||||
}
|
||||
|
||||
unit, err := repo.GetUnit(ctx, unit.TypeExternalTracker)
|
||||
unitExternalTracker, err := repo.GetUnit(ctx, unit.TypeExternalTracker)
|
||||
if err == nil {
|
||||
metas["format"] = unit.ExternalTrackerConfig().ExternalTrackerFormat
|
||||
switch unit.ExternalTrackerConfig().ExternalTrackerStyle {
|
||||
metas["format"] = unitExternalTracker.ExternalTrackerConfig().ExternalTrackerFormat
|
||||
switch unitExternalTracker.ExternalTrackerConfig().ExternalTrackerStyle {
|
||||
case markup.IssueNameStyleAlphanumeric:
|
||||
metas["style"] = markup.IssueNameStyleAlphanumeric
|
||||
case markup.IssueNameStyleRegexp:
|
||||
metas["style"] = markup.IssueNameStyleRegexp
|
||||
metas["regexp"] = unit.ExternalTrackerConfig().ExternalTrackerRegexpPattern
|
||||
metas["regexp"] = unitExternalTracker.ExternalTrackerConfig().ExternalTrackerRegexpPattern
|
||||
default:
|
||||
metas["style"] = markup.IssueNameStyleNumeric
|
||||
}
|
||||
@ -547,11 +548,11 @@ func (repo *Repository) composeCommonMetas(ctx context.Context) map[string]strin
|
||||
return repo.commonRenderingMetas
|
||||
}
|
||||
|
||||
// ComposeMetas composes a map of metas for properly rendering comments or comment-like contents (commit message)
|
||||
func (repo *Repository) ComposeMetas(ctx context.Context) map[string]string {
|
||||
// ComposeCommentMetas composes a map of metas for properly rendering comments or comment-like contents (commit message)
|
||||
func (repo *Repository) ComposeCommentMetas(ctx context.Context) map[string]string {
|
||||
metas := maps.Clone(repo.composeCommonMetas(ctx))
|
||||
metas["markdownLineBreakStyle"] = "comment"
|
||||
metas["markupAllowShortIssuePattern"] = "true"
|
||||
metas["markdownNewLineHardBreak"] = strconv.FormatBool(setting.Markdown.RenderOptionsComment.NewLineHardBreak)
|
||||
metas["markupAllowShortIssuePattern"] = strconv.FormatBool(setting.Markdown.RenderOptionsComment.ShortIssuePattern)
|
||||
return metas
|
||||
}
|
||||
|
||||
@ -559,16 +560,17 @@ func (repo *Repository) ComposeMetas(ctx context.Context) map[string]string {
|
||||
func (repo *Repository) ComposeWikiMetas(ctx context.Context) map[string]string {
|
||||
// does wiki need the "teams" and "org" from common metas?
|
||||
metas := maps.Clone(repo.composeCommonMetas(ctx))
|
||||
metas["markdownLineBreakStyle"] = "document"
|
||||
metas["markupAllowShortIssuePattern"] = "true"
|
||||
metas["markdownNewLineHardBreak"] = strconv.FormatBool(setting.Markdown.RenderOptionsWiki.NewLineHardBreak)
|
||||
metas["markupAllowShortIssuePattern"] = strconv.FormatBool(setting.Markdown.RenderOptionsWiki.ShortIssuePattern)
|
||||
return metas
|
||||
}
|
||||
|
||||
// ComposeDocumentMetas composes a map of metas for properly rendering documents (repo files)
|
||||
func (repo *Repository) ComposeDocumentMetas(ctx context.Context) map[string]string {
|
||||
// ComposeRepoFileMetas composes a map of metas for properly rendering documents (repo files)
|
||||
func (repo *Repository) ComposeRepoFileMetas(ctx context.Context) map[string]string {
|
||||
// does document(file) need the "teams" and "org" from common metas?
|
||||
metas := maps.Clone(repo.composeCommonMetas(ctx))
|
||||
metas["markdownLineBreakStyle"] = "document"
|
||||
metas["markdownNewLineHardBreak"] = strconv.FormatBool(setting.Markdown.RenderOptionsRepoFile.NewLineHardBreak)
|
||||
metas["markupAllowShortIssuePattern"] = strconv.FormatBool(setting.Markdown.RenderOptionsRepoFile.ShortIssuePattern)
|
||||
return metas
|
||||
}
|
||||
|
||||
@ -824,7 +826,7 @@ func GetRepositoryByName(ctx context.Context, ownerID int64, name string) (*Repo
|
||||
func GetRepositoryByURL(ctx context.Context, repoURL string) (*Repository, error) {
|
||||
ret, err := giturl.ParseRepositoryURL(ctx, repoURL)
|
||||
if err != nil || ret.OwnerName == "" {
|
||||
return nil, fmt.Errorf("unknown or malformed repository URL")
|
||||
return nil, errors.New("unknown or malformed repository URL")
|
||||
}
|
||||
return GetRepositoryByOwnerAndName(ctx, ret.OwnerName, ret.RepoName)
|
||||
}
|
||||
|
@ -86,7 +86,7 @@ func TestMetas(t *testing.T) {
|
||||
|
||||
repo.Units = nil
|
||||
|
||||
metas := repo.ComposeMetas(db.DefaultContext)
|
||||
metas := repo.ComposeCommentMetas(db.DefaultContext)
|
||||
assert.Equal(t, "testRepo", metas["repo"])
|
||||
assert.Equal(t, "testOwner", metas["user"])
|
||||
|
||||
@ -100,7 +100,7 @@ func TestMetas(t *testing.T) {
|
||||
testSuccess := func(expectedStyle string) {
|
||||
repo.Units = []*RepoUnit{&externalTracker}
|
||||
repo.commonRenderingMetas = nil
|
||||
metas := repo.ComposeMetas(db.DefaultContext)
|
||||
metas := repo.ComposeCommentMetas(db.DefaultContext)
|
||||
assert.Equal(t, expectedStyle, metas["style"])
|
||||
assert.Equal(t, "testRepo", metas["repo"])
|
||||
assert.Equal(t, "testOwner", metas["user"])
|
||||
@ -121,7 +121,7 @@ func TestMetas(t *testing.T) {
|
||||
repo, err := GetRepositoryByID(db.DefaultContext, 3)
|
||||
assert.NoError(t, err)
|
||||
|
||||
metas = repo.ComposeMetas(db.DefaultContext)
|
||||
metas = repo.ComposeCommentMetas(db.DefaultContext)
|
||||
assert.Contains(t, metas, "org")
|
||||
assert.Contains(t, metas, "teams")
|
||||
assert.Equal(t, "org3", metas["org"])
|
||||
|
@ -5,7 +5,6 @@ package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
@ -33,7 +32,7 @@ func IsErrUnitTypeNotExist(err error) bool {
|
||||
}
|
||||
|
||||
func (err ErrUnitTypeNotExist) Error() string {
|
||||
return fmt.Sprintf("Unit type does not exist: %s", err.UT.LogString())
|
||||
return "Unit type does not exist: " + err.UT.LogString()
|
||||
}
|
||||
|
||||
func (err ErrUnitTypeNotExist) Unwrap() error {
|
||||
|
@ -111,31 +111,31 @@ func (err ErrRepoFilesAlreadyExist) Unwrap() error {
|
||||
return util.ErrAlreadyExist
|
||||
}
|
||||
|
||||
// CheckCreateRepository check if could created a repository
|
||||
func CheckCreateRepository(ctx context.Context, doer, u *user_model.User, name string, overwriteOrAdopt bool) error {
|
||||
if !doer.CanCreateRepo() {
|
||||
return ErrReachLimitOfRepo{u.MaxRepoCreation}
|
||||
// CheckCreateRepository check if doer could create a repository in new owner
|
||||
func CheckCreateRepository(ctx context.Context, doer, owner *user_model.User, name string, overwriteOrAdopt bool) error {
|
||||
if !doer.CanCreateRepoIn(owner) {
|
||||
return ErrReachLimitOfRepo{owner.MaxRepoCreation}
|
||||
}
|
||||
|
||||
if err := IsUsableRepoName(name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
has, err := IsRepositoryModelOrDirExist(ctx, u, name)
|
||||
has, err := IsRepositoryModelOrDirExist(ctx, owner, name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("IsRepositoryExist: %w", err)
|
||||
} else if has {
|
||||
return ErrRepoAlreadyExist{u.Name, name}
|
||||
return ErrRepoAlreadyExist{owner.Name, name}
|
||||
}
|
||||
|
||||
repoPath := RepoPath(u.Name, name)
|
||||
repoPath := RepoPath(owner.Name, name)
|
||||
isExist, err := util.IsExist(repoPath)
|
||||
if err != nil {
|
||||
log.Error("Unable to check if %s exists. Error: %v", repoPath, err)
|
||||
return err
|
||||
}
|
||||
if !overwriteOrAdopt && isExist {
|
||||
return ErrRepoFilesAlreadyExist{u.Name, name}
|
||||
return ErrRepoFilesAlreadyExist{owner.Name, name}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -51,14 +51,10 @@ func init() {
|
||||
db.RegisterModel(new(Upload))
|
||||
}
|
||||
|
||||
// UploadLocalPath returns where uploads is stored in local file system based on given UUID.
|
||||
func UploadLocalPath(uuid string) string {
|
||||
return filepath.Join(setting.Repository.Upload.TempPath, uuid[0:1], uuid[1:2], uuid)
|
||||
}
|
||||
|
||||
// LocalPath returns where uploads are temporarily stored in local file system.
|
||||
// LocalPath returns where uploads are temporarily stored in local file system based on given UUID.
|
||||
func (upload *Upload) LocalPath() string {
|
||||
return UploadLocalPath(upload.UUID)
|
||||
uuid := upload.UUID
|
||||
return setting.AppDataTempDir("repo-uploads").JoinPath(uuid[0:1], uuid[1:2], uuid)
|
||||
}
|
||||
|
||||
// NewUpload creates a new upload object.
|
||||
|
@ -45,7 +45,7 @@ func IsErrWikiReservedName(err error) bool {
|
||||
}
|
||||
|
||||
func (err ErrWikiReservedName) Error() string {
|
||||
return fmt.Sprintf("wiki title is reserved: %s", err.Title)
|
||||
return "wiki title is reserved: " + err.Title
|
||||
}
|
||||
|
||||
func (err ErrWikiReservedName) Unwrap() error {
|
||||
@ -64,7 +64,7 @@ func IsErrWikiInvalidFileName(err error) bool {
|
||||
}
|
||||
|
||||
func (err ErrWikiInvalidFileName) Error() string {
|
||||
return fmt.Sprintf("Invalid wiki filename: %s", err.FileName)
|
||||
return "Invalid wiki filename: " + err.FileName
|
||||
}
|
||||
|
||||
func (err ErrWikiInvalidFileName) Unwrap() error {
|
||||
|
@ -20,17 +20,21 @@ type Type int
|
||||
|
||||
// Enumerate all the unit types
|
||||
const (
|
||||
TypeInvalid Type = iota // 0 invalid
|
||||
TypeCode // 1 code
|
||||
TypeIssues // 2 issues
|
||||
TypePullRequests // 3 PRs
|
||||
TypeReleases // 4 Releases
|
||||
TypeWiki // 5 Wiki
|
||||
TypeExternalWiki // 6 ExternalWiki
|
||||
TypeExternalTracker // 7 ExternalTracker
|
||||
TypeProjects // 8 Projects
|
||||
TypePackages // 9 Packages
|
||||
TypeActions // 10 Actions
|
||||
TypeInvalid Type = iota // 0 invalid
|
||||
|
||||
TypeCode // 1 code
|
||||
TypeIssues // 2 issues
|
||||
TypePullRequests // 3 PRs
|
||||
TypeReleases // 4 Releases
|
||||
TypeWiki // 5 Wiki
|
||||
TypeExternalWiki // 6 ExternalWiki
|
||||
TypeExternalTracker // 7 ExternalTracker
|
||||
TypeProjects // 8 Projects
|
||||
TypePackages // 9 Packages
|
||||
TypeActions // 10 Actions
|
||||
|
||||
// FIXME: TEAM-UNIT-PERMISSION: the team unit "admin" permission's design is not right, when a new unit is added in the future,
|
||||
// admin team won't inherit the correct admin permission for the new unit, need to have a complete fix before adding any new unit.
|
||||
)
|
||||
|
||||
// Value returns integer value for unit type (used by template)
|
||||
@ -380,20 +384,3 @@ func AllUnitKeyNames() []string {
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// MinUnitAccessMode returns the minial permission of the permission map
|
||||
func MinUnitAccessMode(unitsMap map[Type]perm.AccessMode) perm.AccessMode {
|
||||
res := perm.AccessModeNone
|
||||
for t, mode := range unitsMap {
|
||||
// Don't allow `TypeExternal{Tracker,Wiki}` to influence this as they can only be set to READ perms.
|
||||
if t == TypeExternalTracker || t == TypeExternalWiki {
|
||||
continue
|
||||
}
|
||||
|
||||
// get the minial permission great than AccessModeNone except all are AccessModeNone
|
||||
if mode > perm.AccessModeNone && (res == perm.AccessModeNone || mode < res) {
|
||||
res = mode
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
@ -120,7 +120,7 @@ func (f *fixturesLoaderInternal) loadFixtures(tx *sql.Tx, fixture *FixtureItem)
|
||||
}
|
||||
}
|
||||
|
||||
_, err = tx.Exec(fmt.Sprintf("DELETE FROM %s", fixture.tableNameQuoted)) // sqlite3 doesn't support truncate
|
||||
_, err = tx.Exec("DELETE FROM " + fixture.tableNameQuoted) // sqlite3 doesn't support truncate
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/setting/config"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/modules/tempdir"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
@ -35,8 +36,8 @@ func fatalTestError(fmtStr string, args ...any) {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// InitSettings initializes config provider and load common settings for tests
|
||||
func InitSettings() {
|
||||
// InitSettingsForTesting initializes config provider and load common settings for tests
|
||||
func InitSettingsForTesting() {
|
||||
setting.IsInTesting = true
|
||||
log.OsExiter = func(code int) {
|
||||
if code != 0 {
|
||||
@ -75,7 +76,7 @@ func MainTest(m *testing.M, testOptsArg ...*TestOptions) {
|
||||
testOpts := util.OptionalArg(testOptsArg, &TestOptions{})
|
||||
giteaRoot = test.SetupGiteaRoot()
|
||||
setting.CustomPath = filepath.Join(giteaRoot, "custom")
|
||||
InitSettings()
|
||||
InitSettingsForTesting()
|
||||
|
||||
fixturesOpts := FixturesOptions{Dir: filepath.Join(giteaRoot, "models", "fixtures"), Files: testOpts.FixtureFiles}
|
||||
if err := CreateTestEngine(fixturesOpts); err != nil {
|
||||
@ -92,15 +93,19 @@ func MainTest(m *testing.M, testOptsArg ...*TestOptions) {
|
||||
setting.SSH.Domain = "try.gitea.io"
|
||||
setting.Database.Type = "sqlite3"
|
||||
setting.Repository.DefaultBranch = "master" // many test code still assume that default branch is called "master"
|
||||
repoRootPath, err := os.MkdirTemp(os.TempDir(), "repos")
|
||||
repoRootPath, cleanup1, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("repos")
|
||||
if err != nil {
|
||||
fatalTestError("TempDir: %v\n", err)
|
||||
}
|
||||
defer cleanup1()
|
||||
|
||||
setting.RepoRootPath = repoRootPath
|
||||
appDataPath, err := os.MkdirTemp(os.TempDir(), "appdata")
|
||||
appDataPath, cleanup2, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("appdata")
|
||||
if err != nil {
|
||||
fatalTestError("TempDir: %v\n", err)
|
||||
}
|
||||
defer cleanup2()
|
||||
|
||||
setting.AppDataPath = appDataPath
|
||||
setting.AppWorkPath = giteaRoot
|
||||
setting.StaticRootPath = giteaRoot
|
||||
@ -153,13 +158,6 @@ func MainTest(m *testing.M, testOptsArg ...*TestOptions) {
|
||||
fatalTestError("tear down failed: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err = util.RemoveAll(repoRootPath); err != nil {
|
||||
fatalTestError("util.RemoveAll: %v\n", err)
|
||||
}
|
||||
if err = util.RemoveAll(appDataPath); err != nil {
|
||||
fatalTestError("util.RemoveAll: %v\n", err)
|
||||
}
|
||||
os.Exit(exitStatus)
|
||||
}
|
||||
|
||||
|
@ -153,9 +153,9 @@ func DumpQueryResult(t require.TestingT, sqlOrBean any, sqlArgs ...any) {
|
||||
goDB := x.DB().DB
|
||||
sql, ok := sqlOrBean.(string)
|
||||
if !ok {
|
||||
sql = fmt.Sprintf("SELECT * FROM %s", db.TableName(sqlOrBean))
|
||||
sql = "SELECT * FROM " + db.TableName(sqlOrBean)
|
||||
} else if !strings.Contains(sql, " ") {
|
||||
sql = fmt.Sprintf("SELECT * FROM %s", sql)
|
||||
sql = "SELECT * FROM " + sql
|
||||
}
|
||||
rows, err := goDB.Query(sql, sqlArgs...)
|
||||
require.NoError(t, err)
|
||||
|
@ -61,7 +61,9 @@ func GenerateRandomAvatar(ctx context.Context, u *User) error {
|
||||
|
||||
// AvatarLinkWithSize returns a link to the user's avatar with size. size <= 0 means default size
|
||||
func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string {
|
||||
if u.IsGhost() || u.IsGiteaActions() {
|
||||
// ghost user was deleted, Gitea actions is a bot user, 0 means the user should be a virtual user
|
||||
// which comes from git configure information
|
||||
if u.IsGhost() || u.IsGiteaActions() || u.ID <= 0 {
|
||||
return avatars.DefaultAvatarLink()
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@ -114,10 +115,10 @@ func GetUserAllSettings(ctx context.Context, uid int64) (map[string]*Setting, er
|
||||
|
||||
func validateUserSettingKey(key string) error {
|
||||
if len(key) == 0 {
|
||||
return fmt.Errorf("setting key must be set")
|
||||
return errors.New("setting key must be set")
|
||||
}
|
||||
if strings.ToLower(key) != key {
|
||||
return fmt.Errorf("setting key should be lowercase")
|
||||
return errors.New("setting key should be lowercase")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -247,19 +247,20 @@ func (u *User) MaxCreationLimit() int {
|
||||
return u.MaxRepoCreation
|
||||
}
|
||||
|
||||
// CanCreateRepo returns if user login can create a repository
|
||||
// NOTE: functions calling this assume a failure due to repository count limit; if new checks are added, those functions should be revised
|
||||
func (u *User) CanCreateRepo() bool {
|
||||
// CanCreateRepoIn checks whether the doer(u) can create a repository in the owner
|
||||
// NOTE: functions calling this assume a failure due to repository count limit; it ONLY checks the repo number LIMIT, if new checks are added, those functions should be revised
|
||||
func (u *User) CanCreateRepoIn(owner *User) bool {
|
||||
if u.IsAdmin {
|
||||
return true
|
||||
}
|
||||
if u.MaxRepoCreation <= -1 {
|
||||
if setting.Repository.MaxCreationLimit <= -1 {
|
||||
const noLimit = -1
|
||||
if owner.MaxRepoCreation == noLimit {
|
||||
if setting.Repository.MaxCreationLimit == noLimit {
|
||||
return true
|
||||
}
|
||||
return u.NumRepos < setting.Repository.MaxCreationLimit
|
||||
return owner.NumRepos < setting.Repository.MaxCreationLimit
|
||||
}
|
||||
return u.NumRepos < u.MaxRepoCreation
|
||||
return owner.NumRepos < owner.MaxRepoCreation
|
||||
}
|
||||
|
||||
// CanCreateOrganization returns true if user can create organisation.
|
||||
@ -272,13 +273,12 @@ func (u *User) CanEditGitHook() bool {
|
||||
return !setting.DisableGitHooks && (u.IsAdmin || u.AllowGitHook)
|
||||
}
|
||||
|
||||
// CanForkRepo returns if user login can fork a repository
|
||||
// It checks especially that the user can create repos, and potentially more
|
||||
func (u *User) CanForkRepo() bool {
|
||||
// CanForkRepoIn ONLY checks repository count limit
|
||||
func (u *User) CanForkRepoIn(owner *User) bool {
|
||||
if setting.Repository.AllowForkWithoutMaximumLimit {
|
||||
return true
|
||||
}
|
||||
return u.CanCreateRepo()
|
||||
return u.CanCreateRepoIn(owner)
|
||||
}
|
||||
|
||||
// CanImportLocal returns true if user can migrate repository by local path.
|
||||
@ -1169,8 +1169,8 @@ func GetUsersByEmails(ctx context.Context, emails []string) (map[string]*User, e
|
||||
needCheckEmails := make(container.Set[string])
|
||||
needCheckUserNames := make(container.Set[string])
|
||||
for _, email := range emails {
|
||||
if strings.HasSuffix(email, fmt.Sprintf("@%s", setting.Service.NoReplyAddress)) {
|
||||
username := strings.TrimSuffix(email, fmt.Sprintf("@%s", setting.Service.NoReplyAddress))
|
||||
if strings.HasSuffix(email, "@"+setting.Service.NoReplyAddress) {
|
||||
username := strings.TrimSuffix(email, "@"+setting.Service.NoReplyAddress)
|
||||
needCheckUserNames.Add(username)
|
||||
} else {
|
||||
needCheckEmails.Add(strings.ToLower(email))
|
||||
@ -1187,29 +1187,28 @@ func GetUsersByEmails(ctx context.Context, emails []string) (map[string]*User, e
|
||||
for _, email := range emailAddresses {
|
||||
userIDs.Add(email.UID)
|
||||
}
|
||||
users, err := GetUsersMapByIDs(ctx, userIDs.Values())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
results := make(map[string]*User, len(emails))
|
||||
for _, email := range emailAddresses {
|
||||
user := users[email.UID]
|
||||
if user != nil {
|
||||
if user.KeepEmailPrivate {
|
||||
results[user.LowerName+"@"+setting.Service.NoReplyAddress] = user
|
||||
} else {
|
||||
results[email.Email] = user
|
||||
|
||||
if len(userIDs) > 0 {
|
||||
users, err := GetUsersMapByIDs(ctx, userIDs.Values())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, email := range emailAddresses {
|
||||
user := users[email.UID]
|
||||
if user != nil {
|
||||
results[user.GetEmail()] = user
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
users = make(map[int64]*User, len(needCheckUserNames))
|
||||
users := make(map[int64]*User, len(needCheckUserNames))
|
||||
if err := db.GetEngine(ctx).In("lower_name", needCheckUserNames.Values()).Find(&users); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, user := range users {
|
||||
results[user.LowerName+"@"+setting.Service.NoReplyAddress] = user
|
||||
results[user.GetPlaceholderEmail()] = user
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
@ -1232,8 +1231,8 @@ func GetUserByEmail(ctx context.Context, email string) (*User, error) {
|
||||
}
|
||||
|
||||
// Finally, if email address is the protected email address:
|
||||
if strings.HasSuffix(email, fmt.Sprintf("@%s", setting.Service.NoReplyAddress)) {
|
||||
username := strings.TrimSuffix(email, fmt.Sprintf("@%s", setting.Service.NoReplyAddress))
|
||||
if strings.HasSuffix(email, "@"+setting.Service.NoReplyAddress) {
|
||||
username := strings.TrimSuffix(email, "@"+setting.Service.NoReplyAddress)
|
||||
user := &User{}
|
||||
has, err := db.GetEngine(ctx).Where("lower_name=?", username).Get(user)
|
||||
if err != nil {
|
||||
|
@ -19,6 +19,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -616,3 +617,37 @@ func TestGetInactiveUsers(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, users)
|
||||
}
|
||||
|
||||
func TestCanCreateRepo(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Repository.MaxCreationLimit)()
|
||||
const noLimit = -1
|
||||
doerNormal := &user_model.User{}
|
||||
doerAdmin := &user_model.User{IsAdmin: true}
|
||||
t.Run("NoGlobalLimit", func(t *testing.T) {
|
||||
setting.Repository.MaxCreationLimit = noLimit
|
||||
|
||||
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit}))
|
||||
assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0}))
|
||||
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100}))
|
||||
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit}))
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0}))
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100}))
|
||||
})
|
||||
|
||||
t.Run("GlobalLimit50", func(t *testing.T) {
|
||||
setting.Repository.MaxCreationLimit = 50
|
||||
|
||||
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit}))
|
||||
assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: noLimit})) // limited by global limit
|
||||
assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0}))
|
||||
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100}))
|
||||
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: 100}))
|
||||
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit}))
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: noLimit}))
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0}))
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100}))
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: 100}))
|
||||
})
|
||||
}
|
||||
|
@ -90,7 +90,7 @@ func TestWebhook_EventsArray(t *testing.T) {
|
||||
func TestCreateWebhook(t *testing.T) {
|
||||
hook := &Webhook{
|
||||
RepoID: 3,
|
||||
URL: "www.example.com/unit_test",
|
||||
URL: "https://www.example.com/unit_test",
|
||||
ContentType: ContentTypeJSON,
|
||||
Events: `{"push_only":false,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":true}}`,
|
||||
}
|
||||
|
@ -463,7 +463,7 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
|
||||
matchTimes++
|
||||
}
|
||||
case "paths":
|
||||
filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.Base.Ref)
|
||||
filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.MergeBase)
|
||||
if err != nil {
|
||||
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err)
|
||||
} else {
|
||||
@ -476,7 +476,7 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
|
||||
}
|
||||
}
|
||||
case "paths-ignore":
|
||||
filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.Base.Ref)
|
||||
filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.MergeBase)
|
||||
if err != nil {
|
||||
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err)
|
||||
} else {
|
||||
|
@ -8,6 +8,7 @@ package identicon
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
@ -29,7 +30,7 @@ type Identicon struct {
|
||||
// fore all possible foreground colors. only one foreground color will be picked randomly for one image
|
||||
func New(size int, back color.Color, fore ...color.Color) (*Identicon, error) {
|
||||
if len(fore) == 0 {
|
||||
return nil, fmt.Errorf("foreground is not set")
|
||||
return nil, errors.New("foreground is not set")
|
||||
}
|
||||
|
||||
if size < minImageSize {
|
||||
|
@ -5,6 +5,7 @@ package badge
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
"unicode"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
@ -49,23 +50,40 @@ func (b Badge) Width() int {
|
||||
return b.Label.width + b.Message.width
|
||||
}
|
||||
|
||||
// Style follows https://shields.io/badges
|
||||
const (
|
||||
StyleFlat = "flat"
|
||||
StyleFlatSquare = "flat-square"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultOffset = 10
|
||||
defaultFontSize = 11
|
||||
DefaultColor = "#9f9f9f" // Grey
|
||||
DefaultFontFamily = "DejaVu Sans,Verdana,Geneva,sans-serif"
|
||||
DefaultStyle = StyleFlat
|
||||
)
|
||||
|
||||
var StatusColorMap = map[actions_model.Status]string{
|
||||
actions_model.StatusSuccess: "#4c1", // Green
|
||||
actions_model.StatusSkipped: "#dfb317", // Yellow
|
||||
actions_model.StatusUnknown: "#97ca00", // Light Green
|
||||
actions_model.StatusFailure: "#e05d44", // Red
|
||||
actions_model.StatusCancelled: "#fe7d37", // Orange
|
||||
actions_model.StatusWaiting: "#dfb317", // Yellow
|
||||
actions_model.StatusRunning: "#dfb317", // Yellow
|
||||
actions_model.StatusBlocked: "#dfb317", // Yellow
|
||||
}
|
||||
var GlobalVars = sync.OnceValue(func() (ret struct {
|
||||
StatusColorMap map[actions_model.Status]string
|
||||
DejaVuGlyphWidthData map[rune]uint8
|
||||
AllStyles []string
|
||||
},
|
||||
) {
|
||||
ret.StatusColorMap = map[actions_model.Status]string{
|
||||
actions_model.StatusSuccess: "#4c1", // Green
|
||||
actions_model.StatusSkipped: "#dfb317", // Yellow
|
||||
actions_model.StatusUnknown: "#97ca00", // Light Green
|
||||
actions_model.StatusFailure: "#e05d44", // Red
|
||||
actions_model.StatusCancelled: "#fe7d37", // Orange
|
||||
actions_model.StatusWaiting: "#dfb317", // Yellow
|
||||
actions_model.StatusRunning: "#dfb317", // Yellow
|
||||
actions_model.StatusBlocked: "#dfb317", // Yellow
|
||||
}
|
||||
ret.DejaVuGlyphWidthData = dejaVuGlyphWidthDataFunc()
|
||||
ret.AllStyles = []string{StyleFlat, StyleFlatSquare}
|
||||
return ret
|
||||
})
|
||||
|
||||
// GenerateBadge generates badge with given template
|
||||
func GenerateBadge(label, message, color string) Badge {
|
||||
@ -93,7 +111,7 @@ func GenerateBadge(label, message, color string) Badge {
|
||||
|
||||
func calculateTextWidth(text string) int {
|
||||
width := 0
|
||||
widthData := DejaVuGlyphWidthData()
|
||||
widthData := GlobalVars().DejaVuGlyphWidthData
|
||||
for _, char := range strings.TrimSpace(text) {
|
||||
charWidth, ok := widthData[char]
|
||||
if !ok {
|
||||
|
@ -3,8 +3,6 @@
|
||||
|
||||
package badge
|
||||
|
||||
import "sync"
|
||||
|
||||
// DejaVuGlyphWidthData is generated by `sfnt.Face.GlyphAdvance(nil, <rune>, 11, font.HintingNone)` with DejaVu Sans
|
||||
// v2.37 (https://github.com/dejavu-fonts/dejavu-fonts/releases/download/version_2_37/dejavu-sans-ttf-2.37.zip).
|
||||
//
|
||||
@ -13,7 +11,7 @@ import "sync"
|
||||
//
|
||||
// A devtest page "/devtest/badge-actions-svg" could be used to check the rendered images.
|
||||
|
||||
var DejaVuGlyphWidthData = sync.OnceValue(func() map[rune]uint8 {
|
||||
func dejaVuGlyphWidthDataFunc() map[rune]uint8 {
|
||||
return map[rune]uint8{
|
||||
32: 3,
|
||||
33: 4,
|
||||
@ -205,4 +203,4 @@ var DejaVuGlyphWidthData = sync.OnceValue(func() map[rune]uint8 {
|
||||
254: 7,
|
||||
255: 7,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
10
modules/cache/cache.go
vendored
10
modules/cache/cache.go
vendored
@ -4,6 +4,8 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
@ -48,10 +50,10 @@ const (
|
||||
// returns
|
||||
func Test() (time.Duration, error) {
|
||||
if defaultCache == nil {
|
||||
return 0, fmt.Errorf("default cache not initialized")
|
||||
return 0, errors.New("default cache not initialized")
|
||||
}
|
||||
|
||||
testData := fmt.Sprintf("%x", make([]byte, 500))
|
||||
testData := hex.EncodeToString(make([]byte, 500))
|
||||
|
||||
start := time.Now()
|
||||
|
||||
@ -63,10 +65,10 @@ func Test() (time.Duration, error) {
|
||||
}
|
||||
testVal, hit := defaultCache.Get(testCacheKey)
|
||||
if !hit {
|
||||
return 0, fmt.Errorf("expect cache hit but got none")
|
||||
return 0, errors.New("expect cache hit but got none")
|
||||
}
|
||||
if testVal != testData {
|
||||
return 0, fmt.Errorf("expect cache to return same value as stored but got other")
|
||||
return 0, errors.New("expect cache to return same value as stored but got other")
|
||||
}
|
||||
|
||||
return time.Since(start), nil
|
||||
|
10
modules/cache/cache_test.go
vendored
10
modules/cache/cache_test.go
vendored
@ -4,7 +4,7 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -57,7 +57,7 @@ func TestGetString(t *testing.T) {
|
||||
createTestCache()
|
||||
|
||||
data, err := GetString("key", func() (string, error) {
|
||||
return "", fmt.Errorf("some error")
|
||||
return "", errors.New("some error")
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, data)
|
||||
@ -82,7 +82,7 @@ func TestGetString(t *testing.T) {
|
||||
assert.Equal(t, "some data", data)
|
||||
|
||||
data, err = GetString("key", func() (string, error) {
|
||||
return "", fmt.Errorf("some error")
|
||||
return "", errors.New("some error")
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "some data", data)
|
||||
@ -93,7 +93,7 @@ func TestGetInt64(t *testing.T) {
|
||||
createTestCache()
|
||||
|
||||
data, err := GetInt64("key", func() (int64, error) {
|
||||
return 0, fmt.Errorf("some error")
|
||||
return 0, errors.New("some error")
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.EqualValues(t, 0, data)
|
||||
@ -118,7 +118,7 @@ func TestGetInt64(t *testing.T) {
|
||||
assert.EqualValues(t, 100, data)
|
||||
|
||||
data, err = GetInt64("key", func() (int64, error) {
|
||||
return 0, fmt.Errorf("some error")
|
||||
return 0, errors.New("some error")
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 100, data)
|
||||
|
177
modules/cache/context.go
vendored
177
modules/cache/context.go
vendored
@ -5,176 +5,39 @@ package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
|
||||
// cacheContext is a context that can be used to cache data in a request level context
|
||||
// This is useful for caching data that is expensive to calculate and is likely to be
|
||||
// used multiple times in a request.
|
||||
type cacheContext struct {
|
||||
data map[any]map[any]any
|
||||
lock sync.RWMutex
|
||||
created time.Time
|
||||
discard bool
|
||||
}
|
||||
type cacheContextKeyType struct{}
|
||||
|
||||
func (cc *cacheContext) Get(tp, key any) any {
|
||||
cc.lock.RLock()
|
||||
defer cc.lock.RUnlock()
|
||||
return cc.data[tp][key]
|
||||
}
|
||||
var cacheContextKey = cacheContextKeyType{}
|
||||
|
||||
func (cc *cacheContext) Put(tp, key, value any) {
|
||||
cc.lock.Lock()
|
||||
defer cc.lock.Unlock()
|
||||
|
||||
if cc.discard {
|
||||
return
|
||||
}
|
||||
|
||||
d := cc.data[tp]
|
||||
if d == nil {
|
||||
d = make(map[any]any)
|
||||
cc.data[tp] = d
|
||||
}
|
||||
d[key] = value
|
||||
}
|
||||
|
||||
func (cc *cacheContext) Delete(tp, key any) {
|
||||
cc.lock.Lock()
|
||||
defer cc.lock.Unlock()
|
||||
delete(cc.data[tp], key)
|
||||
}
|
||||
|
||||
func (cc *cacheContext) Discard() {
|
||||
cc.lock.Lock()
|
||||
defer cc.lock.Unlock()
|
||||
cc.data = nil
|
||||
cc.discard = true
|
||||
}
|
||||
|
||||
func (cc *cacheContext) isDiscard() bool {
|
||||
cc.lock.RLock()
|
||||
defer cc.lock.RUnlock()
|
||||
return cc.discard
|
||||
}
|
||||
|
||||
// cacheContextLifetime is the max lifetime of cacheContext.
|
||||
// Since cacheContext is used to cache data in a request level context, 5 minutes is enough.
|
||||
// If a cacheContext is used more than 5 minutes, it's probably misuse.
|
||||
const cacheContextLifetime = 5 * time.Minute
|
||||
|
||||
var timeNow = time.Now
|
||||
|
||||
func (cc *cacheContext) Expired() bool {
|
||||
return timeNow().Sub(cc.created) > cacheContextLifetime
|
||||
}
|
||||
|
||||
var cacheContextKey = struct{}{}
|
||||
|
||||
/*
|
||||
Since there are both WithCacheContext and WithNoCacheContext,
|
||||
it may be confusing when there is nesting.
|
||||
|
||||
Some cases to explain the design:
|
||||
|
||||
When:
|
||||
- A, B or C means a cache context.
|
||||
- A', B' or C' means a discard cache context.
|
||||
- ctx means context.Backgrand().
|
||||
- A(ctx) means a cache context with ctx as the parent context.
|
||||
- B(A(ctx)) means a cache context with A(ctx) as the parent context.
|
||||
- With is alias of WithCacheContext.
|
||||
- WithNo is alias of WithNoCacheContext.
|
||||
|
||||
So:
|
||||
- With(ctx) -> A(ctx)
|
||||
- With(With(ctx)) -> A(ctx), not B(A(ctx)), always reuse parent cache context if possible.
|
||||
- With(With(With(ctx))) -> A(ctx), not C(B(A(ctx))), ditto.
|
||||
- WithNo(ctx) -> ctx, not A'(ctx), don't create new cache context if we don't have to.
|
||||
- WithNo(With(ctx)) -> A'(ctx)
|
||||
- WithNo(WithNo(With(ctx))) -> A'(ctx), not B'(A'(ctx)), don't create new cache context if we don't have to.
|
||||
- With(WithNo(With(ctx))) -> B(A'(ctx)), not A(ctx), never reuse a discard cache context.
|
||||
- WithNo(With(WithNo(With(ctx)))) -> B'(A'(ctx))
|
||||
- With(WithNo(With(WithNo(With(ctx))))) -> C(B'(A'(ctx))), so there's always only one not-discard cache context.
|
||||
*/
|
||||
// contextCacheLifetime is the max lifetime of context cache.
|
||||
// Since context cache is used to cache data in a request level context, 5 minutes is enough.
|
||||
// If a context cache is used more than 5 minutes, it's probably abused.
|
||||
const contextCacheLifetime = 5 * time.Minute
|
||||
|
||||
func WithCacheContext(ctx context.Context) context.Context {
|
||||
if c, ok := ctx.Value(cacheContextKey).(*cacheContext); ok {
|
||||
if !c.isDiscard() {
|
||||
// reuse parent context
|
||||
return ctx
|
||||
}
|
||||
}
|
||||
// FIXME: review the use of this nolint directive
|
||||
return context.WithValue(ctx, cacheContextKey, &cacheContext{ //nolint:staticcheck
|
||||
data: make(map[any]map[any]any),
|
||||
created: timeNow(),
|
||||
})
|
||||
}
|
||||
|
||||
func WithNoCacheContext(ctx context.Context) context.Context {
|
||||
if c, ok := ctx.Value(cacheContextKey).(*cacheContext); ok {
|
||||
// The caller want to run long-life tasks, but the parent context is a cache context.
|
||||
// So we should disable and clean the cache data, or it will be kept in memory for a long time.
|
||||
c.Discard()
|
||||
if c := GetContextCache(ctx); c != nil {
|
||||
return ctx
|
||||
}
|
||||
|
||||
return ctx
|
||||
return context.WithValue(ctx, cacheContextKey, NewEphemeralCache(contextCacheLifetime))
|
||||
}
|
||||
|
||||
func GetContextData(ctx context.Context, tp, key any) any {
|
||||
if c, ok := ctx.Value(cacheContextKey).(*cacheContext); ok {
|
||||
if c.Expired() {
|
||||
// The warning means that the cache context is misused for long-life task,
|
||||
// it can be resolved with WithNoCacheContext(ctx).
|
||||
log.Warn("cache context is expired, is highly likely to be misused for long-life tasks: %v", c)
|
||||
return nil
|
||||
}
|
||||
return c.Get(tp, key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func SetContextData(ctx context.Context, tp, key, value any) {
|
||||
if c, ok := ctx.Value(cacheContextKey).(*cacheContext); ok {
|
||||
if c.Expired() {
|
||||
// The warning means that the cache context is misused for long-life task,
|
||||
// it can be resolved with WithNoCacheContext(ctx).
|
||||
log.Warn("cache context is expired, is highly likely to be misused for long-life tasks: %v", c)
|
||||
return
|
||||
}
|
||||
c.Put(tp, key, value)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func RemoveContextData(ctx context.Context, tp, key any) {
|
||||
if c, ok := ctx.Value(cacheContextKey).(*cacheContext); ok {
|
||||
if c.Expired() {
|
||||
// The warning means that the cache context is misused for long-life task,
|
||||
// it can be resolved with WithNoCacheContext(ctx).
|
||||
log.Warn("cache context is expired, is highly likely to be misused for long-life tasks: %v", c)
|
||||
return
|
||||
}
|
||||
c.Delete(tp, key)
|
||||
}
|
||||
func GetContextCache(ctx context.Context) *EphemeralCache {
|
||||
c, _ := ctx.Value(cacheContextKey).(*EphemeralCache)
|
||||
return c
|
||||
}
|
||||
|
||||
// GetWithContextCache returns the cache value of the given key in the given context.
|
||||
func GetWithContextCache[T any](ctx context.Context, cacheGroupKey string, cacheTargetID any, f func() (T, error)) (T, error) {
|
||||
v := GetContextData(ctx, cacheGroupKey, cacheTargetID)
|
||||
if vv, ok := v.(T); ok {
|
||||
return vv, nil
|
||||
// FIXME: in some cases, the "context cache" should not be used, because it has uncontrollable behaviors
|
||||
// For example, these calls:
|
||||
// * GetWithContextCache(TargetID) -> OtherCodeCreateModel(TargetID) -> GetWithContextCache(TargetID)
|
||||
// Will cause the second call is not able to get the correct created target.
|
||||
// UNLESS it is certain that the target won't be changed during the request, DO NOT use it.
|
||||
func GetWithContextCache[T, K any](ctx context.Context, groupKey string, targetKey K, f func(context.Context, K) (T, error)) (T, error) {
|
||||
if c := GetContextCache(ctx); c != nil {
|
||||
return GetWithEphemeralCache(ctx, c, groupKey, targetKey, f)
|
||||
}
|
||||
t, err := f()
|
||||
if err != nil {
|
||||
return t, err
|
||||
}
|
||||
SetContextData(ctx, cacheGroupKey, cacheTargetID, t)
|
||||
return t, nil
|
||||
return f(ctx, targetKey)
|
||||
}
|
||||
|
61
modules/cache/context_test.go
vendored
61
modules/cache/context_test.go
vendored
@ -4,74 +4,47 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestWithCacheContext(t *testing.T) {
|
||||
ctx := WithCacheContext(t.Context())
|
||||
|
||||
v := GetContextData(ctx, "empty_field", "my_config1")
|
||||
c := GetContextCache(ctx)
|
||||
v, _ := c.Get("empty_field", "my_config1")
|
||||
assert.Nil(t, v)
|
||||
|
||||
const field = "system_setting"
|
||||
v = GetContextData(ctx, field, "my_config1")
|
||||
v, _ = c.Get(field, "my_config1")
|
||||
assert.Nil(t, v)
|
||||
SetContextData(ctx, field, "my_config1", 1)
|
||||
v = GetContextData(ctx, field, "my_config1")
|
||||
c.Put(field, "my_config1", 1)
|
||||
v, _ = c.Get(field, "my_config1")
|
||||
assert.NotNil(t, v)
|
||||
assert.Equal(t, 1, v.(int))
|
||||
|
||||
RemoveContextData(ctx, field, "my_config1")
|
||||
RemoveContextData(ctx, field, "my_config2") // remove a non-exist key
|
||||
c.Delete(field, "my_config1")
|
||||
c.Delete(field, "my_config2") // remove a non-exist key
|
||||
|
||||
v = GetContextData(ctx, field, "my_config1")
|
||||
v, _ = c.Get(field, "my_config1")
|
||||
assert.Nil(t, v)
|
||||
|
||||
vInt, err := GetWithContextCache(ctx, field, "my_config1", func() (int, error) {
|
||||
vInt, err := GetWithContextCache(ctx, field, "my_config1", func(context.Context, string) (int, error) {
|
||||
return 1, nil
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, vInt)
|
||||
|
||||
v = GetContextData(ctx, field, "my_config1")
|
||||
v, _ = c.Get(field, "my_config1")
|
||||
assert.EqualValues(t, 1, v)
|
||||
|
||||
now := timeNow
|
||||
defer func() {
|
||||
timeNow = now
|
||||
}()
|
||||
timeNow = func() time.Time {
|
||||
return now().Add(5 * time.Minute)
|
||||
}
|
||||
v = GetContextData(ctx, field, "my_config1")
|
||||
defer test.MockVariableValue(&timeNow, func() time.Time {
|
||||
return time.Now().Add(5 * time.Minute)
|
||||
})()
|
||||
v, _ = c.Get(field, "my_config1")
|
||||
assert.Nil(t, v)
|
||||
}
|
||||
|
||||
func TestWithNoCacheContext(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
const field = "system_setting"
|
||||
|
||||
v := GetContextData(ctx, field, "my_config1")
|
||||
assert.Nil(t, v)
|
||||
SetContextData(ctx, field, "my_config1", 1)
|
||||
v = GetContextData(ctx, field, "my_config1")
|
||||
assert.Nil(t, v) // still no cache
|
||||
|
||||
ctx = WithCacheContext(ctx)
|
||||
v = GetContextData(ctx, field, "my_config1")
|
||||
assert.Nil(t, v)
|
||||
SetContextData(ctx, field, "my_config1", 1)
|
||||
v = GetContextData(ctx, field, "my_config1")
|
||||
assert.NotNil(t, v)
|
||||
|
||||
ctx = WithNoCacheContext(ctx)
|
||||
v = GetContextData(ctx, field, "my_config1")
|
||||
assert.Nil(t, v)
|
||||
SetContextData(ctx, field, "my_config1", 1)
|
||||
v = GetContextData(ctx, field, "my_config1")
|
||||
assert.Nil(t, v) // still no cache
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user