Compare commits
54 commits
71bd71e45d
...
327bcf710d
Author | SHA1 | Date | |
---|---|---|---|
327bcf710d | |||
a094b48f42 | |||
|
248b8bb0fa | ||
|
90a650d2b0 | ||
|
7ddb1291ea | ||
|
1f8d01c74d | ||
|
12a8d65a5a | ||
|
004ec9026b | ||
|
bea2c52572 | ||
|
1c7c48d96e | ||
|
d350add668 | ||
|
40fa6a526a | ||
|
9693f08812 | ||
|
d15f9ee0b0 | ||
|
5d1d66ac3a | ||
|
2878d07926 | ||
|
cd5a0ec1c8 | ||
|
c5ac659d1b | ||
|
6cc170011b | ||
|
1475c1fcc4 | ||
|
2b991b32eb | ||
|
3c6edfa5e2 | ||
|
066c3f3baa | ||
|
2a82e2d216 | ||
|
a77398cd34 | ||
|
160ef74363 | ||
|
fdf950398b | ||
|
b0874a0912 | ||
|
580f29d9a2 | ||
|
cbe94214e9 | ||
|
401c2a3c3d | ||
|
cd4413bdca | ||
|
cfaa6dc2ed | ||
|
9515a0ea38 | ||
|
a86fa739dc | ||
|
76d58fa269 | ||
|
dbd896ce42 | ||
|
ce3d6b60bd | ||
|
c477780163 | ||
|
bd264e6aed | ||
|
6b48228500 | ||
|
3b672c9791 | ||
|
0e3a5abb69 | ||
|
a0300f0bce | ||
|
14750f3d11 | ||
|
e4dc14f070 | ||
|
69b4fd5fe2 | ||
|
ee1655d5b0 | ||
|
d6ae79f78f | ||
|
4148fb27db | ||
|
bbdb47dfa1 | ||
|
026a4bb02d | ||
|
3981e6fdf3 | ||
|
5258e8f63d |
97 changed files with 921 additions and 545 deletions
|
@ -1,45 +0,0 @@
|
||||||
name: upgrade
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_review:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- 'forgejo*'
|
|
||||||
- 'v*/forgejo*'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
upgrade:
|
|
||||||
runs-on: docker
|
|
||||||
container:
|
|
||||||
image: codeberg.org/forgejo/test_env:main
|
|
||||||
steps:
|
|
||||||
- run: apt-get install -y -qq zstd
|
|
||||||
|
|
||||||
- name: cache S3 binaries
|
|
||||||
id: S3
|
|
||||||
uses: https://code.forgejo.org/actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
/usr/local/bin/minio
|
|
||||||
/usr/local/bin/mc
|
|
||||||
/usr/local/bin/garage
|
|
||||||
key: S3
|
|
||||||
|
|
||||||
- name: skip if S3 cache hit
|
|
||||||
if: steps.S3.outputs.cache-hit != 'true'
|
|
||||||
run: echo no hit
|
|
||||||
|
|
||||||
- uses: https://code.forgejo.org/actions/checkout@v3
|
|
||||||
- uses: https://code.forgejo.org/actions/setup-go@v4
|
|
||||||
with:
|
|
||||||
go-version: "1.21"
|
|
||||||
- run: |
|
|
||||||
git config --add safe.directory '*'
|
|
||||||
chown -R gitea:gitea . /go
|
|
||||||
- run: |
|
|
||||||
su gitea -c 'make deps-backend'
|
|
||||||
- run: |
|
|
||||||
script=$(pwd)/.forgejo/upgrades/test-upgrade.sh
|
|
||||||
$script run dependencies
|
|
||||||
$script clobber
|
|
||||||
su gitea -c "$script test_upgrades"
|
|
|
@ -3,7 +3,7 @@ pipeline:
|
||||||
image: docker.io/woodpeckerci/plugin-docker-buildx
|
image: docker.io/woodpeckerci/plugin-docker-buildx
|
||||||
settings:
|
settings:
|
||||||
repo: gitea.nulo.in/nulo/forgejo
|
repo: gitea.nulo.in/nulo/forgejo
|
||||||
tag: v1.21.2-0
|
tag: v1.21.3-0
|
||||||
registry: https://gitea.nulo.in
|
registry: https://gitea.nulo.in
|
||||||
username: Nulo
|
username: Nulo
|
||||||
password:
|
password:
|
||||||
|
|
67
CHANGELOG.md
67
CHANGELOG.md
|
@ -4,6 +4,73 @@ This changelog goes through all the changes that have been made in each release
|
||||||
without substantial changes to our git log; to see the highlights of what has
|
without substantial changes to our git log; to see the highlights of what has
|
||||||
been added to each release, please refer to the [blog](https://blog.gitea.com).
|
been added to each release, please refer to the [blog](https://blog.gitea.com).
|
||||||
|
|
||||||
|
## [1.21.3](https://github.com/go-gitea/gitea/releases/tag/1.21.3) - 2023-12-21
|
||||||
|
|
||||||
|
* SECURITY
|
||||||
|
* Update golang.org/x/crypto (#28519)
|
||||||
|
* API
|
||||||
|
* chore(api): support ignore password if login source type is LDAP for creating user API (#28491) (#28525)
|
||||||
|
* Add endpoint for not implemented Docker auth (#28457) (#28462)
|
||||||
|
* ENHANCEMENTS
|
||||||
|
* Add option to disable ambiguous unicode characters detection (#28454) (#28499)
|
||||||
|
* Refactor SSH clone URL generation code (#28421) (#28480)
|
||||||
|
* Polyfill SubmitEvent for PaleMoon (#28441) (#28478)
|
||||||
|
* BUGFIXES
|
||||||
|
* Fix the issue ref rendering for wiki (#28556) (#28559)
|
||||||
|
* Fix duplicate ID when deleting repo (#28520) (#28528)
|
||||||
|
* Only check online runner when detecting matching runners in workflows (#28286) (#28512)
|
||||||
|
* Initalize stroage for orphaned repository doctor (#28487) (#28490)
|
||||||
|
* Fix possible nil pointer access (#28428) (#28440)
|
||||||
|
* Don't show unnecessary citation JS error on UI (#28433) (#28437)
|
||||||
|
* DOCS
|
||||||
|
* Update actions document about comparsion as Github Actions (#28560) (#28564)
|
||||||
|
* Fix documents for "custom/public/assets/" (#28465) (#28467)
|
||||||
|
* MISC
|
||||||
|
* Fix inperformant query on retrifing review from database. (#28552) (#28562)
|
||||||
|
* Improve the prompt for "ssh-keygen sign" (#28509) (#28510)
|
||||||
|
* Update docs for DISABLE_QUERY_AUTH_TOKEN (#28485) (#28488)
|
||||||
|
* Fix Chinese translation of config cheat sheet[API] (#28472) (#28473)
|
||||||
|
* Retry SSH key verification with additional CRLF if it failed (#28392) (#28464)
|
||||||
|
|
||||||
|
## [1.21.2](https://github.com/go-gitea/gitea/releases/tag/1.21.2) - 2023-12-12
|
||||||
|
|
||||||
|
* SECURITY
|
||||||
|
* Rebuild with recently released golang version
|
||||||
|
* Fix missing check (#28406) (#28411)
|
||||||
|
* Do some missing checks (#28423) (#28432)
|
||||||
|
* BUGFIXES
|
||||||
|
* Fix margin in server signed signature verification view (#28379) (#28381)
|
||||||
|
* Fix object does not exist error when checking citation file (#28314) (#28369)
|
||||||
|
* Use `filepath` instead of `path` to create SQLite3 database file (#28374) (#28378)
|
||||||
|
* Fix the runs will not be displayed bug when the main branch have no workflows but other branches have (#28359) (#28365)
|
||||||
|
* Handle repository.size column being NULL in migration v263 (#28336) (#28363)
|
||||||
|
* Convert git commit summary to valid UTF8. (#28356) (#28358)
|
||||||
|
* Fix migration panic due to an empty review comment diff (#28334) (#28362)
|
||||||
|
* Add `HEAD` support for rpm repo files (#28309) (#28360)
|
||||||
|
* Fix RPM/Debian signature key creation (#28352) (#28353)
|
||||||
|
* Keep profile tab when clicking on Language (#28320) (#28331)
|
||||||
|
* Fix missing issue search index update when changing status (#28325) (#28330)
|
||||||
|
* Fix wrong link in `protect_branch_name_pattern_desc` (#28313) (#28315)
|
||||||
|
* Read `previous` info from git blame (#28306) (#28310)
|
||||||
|
* Ignore "non-existing" errors when getDirectorySize calculates the size (#28276) (#28285)
|
||||||
|
* Use appSubUrl for OAuth2 callback URL tip (#28266) (#28275)
|
||||||
|
* Meilisearch: require all query terms to be matched (#28293) (#28296)
|
||||||
|
* Fix required error for token name (#28267) (#28284)
|
||||||
|
* Fix issue will be detected as pull request when checking `First-time contributor` (#28237) (#28271)
|
||||||
|
* Use full width for project boards (#28225) (#28245)
|
||||||
|
* Increase "version" when update the setting value to a same value as before (#28243) (#28244)
|
||||||
|
* Also sync DB branches on push if necessary (#28361) (#28403)
|
||||||
|
* Make gogit Repository.GetBranchNames consistent (#28348) (#28386)
|
||||||
|
* Recover from panic in cron task (#28409) (#28425)
|
||||||
|
* Deprecate query string auth tokens (#28390) (#28430)
|
||||||
|
* ENHANCEMENTS
|
||||||
|
* Improve doctor cli behavior (#28422) (#28424)
|
||||||
|
* Fix margin in server signed signature verification view (#28379) (#28381)
|
||||||
|
* Refactor template empty checks (#28351) (#28354)
|
||||||
|
* Read `previous` info from git blame (#28306) (#28310)
|
||||||
|
* Use full width for project boards (#28225) (#28245)
|
||||||
|
* Enable system users search via the API (#28013) (#28018)
|
||||||
|
|
||||||
## [1.21.1](https://github.com/go-gitea/gitea/releases/tag/1.21.1) - 2023-11-26
|
## [1.21.1](https://github.com/go-gitea/gitea/releases/tag/1.21.1) - 2023-11-26
|
||||||
|
|
||||||
* SECURITY
|
* SECURITY
|
||||||
|
|
2
Makefile
2
Makefile
|
@ -89,7 +89,7 @@ endif
|
||||||
VERSION = ${GITEA_VERSION}
|
VERSION = ${GITEA_VERSION}
|
||||||
|
|
||||||
# SemVer
|
# SemVer
|
||||||
FORGEJO_VERSION := 6.0.1+0-gitea-1.21.2
|
FORGEJO_VERSION := 6.0.3+0-gitea-1.21.3
|
||||||
|
|
||||||
LDFLAGS := $(LDFLAGS) -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)" -X "code.gitea.io/gitea/routers/api/forgejo/v1.ForgejoVersion=$(FORGEJO_VERSION)" -X "main.ForgejoVersion=$(FORGEJO_VERSION)"
|
LDFLAGS := $(LDFLAGS) -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)" -X "code.gitea.io/gitea/routers/api/forgejo/v1.ForgejoVersion=$(FORGEJO_VERSION)" -X "main.ForgejoVersion=$(FORGEJO_VERSION)"
|
||||||
|
|
||||||
|
|
|
@ -1214,6 +1214,9 @@ LEVEL = Info
|
||||||
;; Max size of files to be displayed (default is 8MiB)
|
;; Max size of files to be displayed (default is 8MiB)
|
||||||
;MAX_DISPLAY_FILE_SIZE = 8388608
|
;MAX_DISPLAY_FILE_SIZE = 8388608
|
||||||
;;
|
;;
|
||||||
|
;; Detect ambiguous unicode characters in file contents and show warnings on the UI
|
||||||
|
;AMBIGUOUS_UNICODE_DETECTION = true
|
||||||
|
;;
|
||||||
;; Whether the email of the user should be shown in the Explore Users page
|
;; Whether the email of the user should be shown in the Explore Users page
|
||||||
;SHOW_USER_EMAIL = true
|
;SHOW_USER_EMAIL = true
|
||||||
;;
|
;;
|
||||||
|
|
|
@ -19,10 +19,10 @@ Some jurisdictions (such as EU), requires certain legal pages (e.g. Privacy Poli
|
||||||
|
|
||||||
## Getting Pages
|
## Getting Pages
|
||||||
|
|
||||||
Gitea source code ships with sample pages, available in `contrib/legal` directory. Copy them to `custom/public/`. For example, to add Privacy Policy:
|
Gitea source code ships with sample pages, available in `contrib/legal` directory. Copy them to `custom/public/assets/`. For example, to add Privacy Policy:
|
||||||
|
|
||||||
```
|
```
|
||||||
wget -O /path/to/custom/public/privacy.html https://raw.githubusercontent.com/go-gitea/gitea/main/contrib/legal/privacy.html.sample
|
wget -O /path/to/custom/public/assets/privacy.html https://raw.githubusercontent.com/go-gitea/gitea/main/contrib/legal/privacy.html.sample
|
||||||
```
|
```
|
||||||
|
|
||||||
Now you need to edit the page to meet your requirements. In particular you must change the email addresses, web addresses and references to "Your Gitea Instance" to match your situation.
|
Now you need to edit the page to meet your requirements. In particular you must change the email addresses, web addresses and references to "Your Gitea Instance" to match your situation.
|
||||||
|
|
|
@ -19,10 +19,10 @@ menu:
|
||||||
|
|
||||||
## 获取页面
|
## 获取页面
|
||||||
|
|
||||||
Gitea 源代码附带了示例页面,位于 `contrib/legal` 目录中。将它们复制到 `custom/public/` 目录下。例如,如果要添加隐私政策:
|
Gitea 源代码附带了示例页面,位于 `contrib/legal` 目录中。将它们复制到 `custom/public/assets/` 目录下。例如,如果要添加隐私政策:
|
||||||
|
|
||||||
```
|
```
|
||||||
wget -O /path/to/custom/public/privacy.html https://raw.githubusercontent.com/go-gitea/gitea/main/contrib/legal/privacy.html.sample
|
wget -O /path/to/custom/public/assets/privacy.html https://raw.githubusercontent.com/go-gitea/gitea/main/contrib/legal/privacy.html.sample
|
||||||
```
|
```
|
||||||
|
|
||||||
现在,你需要编辑该页面以满足你的需求。特别是,你必须更改电子邮件地址、网址以及与 "Your Gitea Instance" 相关的引用,以匹配你的情况。
|
现在,你需要编辑该页面以满足你的需求。特别是,你必须更改电子邮件地址、网址以及与 "Your Gitea Instance" 相关的引用,以匹配你的情况。
|
||||||
|
|
|
@ -220,6 +220,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a
|
||||||
- `THEMES`: **auto,gitea,arc-green**: All available themes. Allow users select personalized themes.
|
- `THEMES`: **auto,gitea,arc-green**: All available themes. Allow users select personalized themes.
|
||||||
regardless of the value of `DEFAULT_THEME`.
|
regardless of the value of `DEFAULT_THEME`.
|
||||||
- `MAX_DISPLAY_FILE_SIZE`: **8388608**: Max size of files to be displayed (default is 8MiB)
|
- `MAX_DISPLAY_FILE_SIZE`: **8388608**: Max size of files to be displayed (default is 8MiB)
|
||||||
|
- `AMBIGUOUS_UNICODE_DETECTION`: **true**: Detect ambiguous unicode characters in file contents and show warnings on the UI
|
||||||
- `REACTIONS`: All available reactions users can choose on issues/prs and comments
|
- `REACTIONS`: All available reactions users can choose on issues/prs and comments
|
||||||
Values can be emoji alias (:smile:) or a unicode emoji.
|
Values can be emoji alias (:smile:) or a unicode emoji.
|
||||||
For custom reactions, add a tightly cropped square image to public/assets/img/emoji/reaction_name.png
|
For custom reactions, add a tightly cropped square image to public/assets/img/emoji/reaction_name.png
|
||||||
|
@ -573,6 +574,7 @@ And the following unique queues:
|
||||||
- off - do not check password complexity
|
- off - do not check password complexity
|
||||||
- `PASSWORD_CHECK_PWN`: **false**: Check [HaveIBeenPwned](https://haveibeenpwned.com/Passwords) to see if a password has been exposed.
|
- `PASSWORD_CHECK_PWN`: **false**: Check [HaveIBeenPwned](https://haveibeenpwned.com/Passwords) to see if a password has been exposed.
|
||||||
- `SUCCESSFUL_TOKENS_CACHE_SIZE`: **20**: Cache successful token hashes. API tokens are stored in the DB as pbkdf2 hashes however, this means that there is a potentially significant hashing load when there are multiple API operations. This cache will store the successfully hashed tokens in a LRU cache as a balance between performance and security.
|
- `SUCCESSFUL_TOKENS_CACHE_SIZE`: **20**: Cache successful token hashes. API tokens are stored in the DB as pbkdf2 hashes however, this means that there is a potentially significant hashing load when there are multiple API operations. This cache will store the successfully hashed tokens in a LRU cache as a balance between performance and security.
|
||||||
|
- `DISABLE_QUERY_AUTH_TOKEN`: **false**: Reject API tokens sent in URL query string (Accept Header-based API tokens only). This setting will default to `true` in Gitea 1.23 and be deprecated in Gitea 1.24.
|
||||||
|
|
||||||
## Camo (`camo`)
|
## Camo (`camo`)
|
||||||
|
|
||||||
|
|
|
@ -1040,10 +1040,11 @@ Gitea 创建以下非唯一队列:
|
||||||
|
|
||||||
## API (`api`)
|
## API (`api`)
|
||||||
|
|
||||||
- `ENABLE_SWAGGER`: **true**: 是否启用swagger路由 (`/api/swagger`, `/api/v1/swagger`, …)。
|
- `ENABLE_SWAGGER`: **true**: 启用API文档接口 (`/api/swagger`, `/api/v1/swagger`, …). True or false。
|
||||||
- `MAX_RESPONSE_ITEMS`: **50**: 单个页面的最大 Feed.
|
- `MAX_RESPONSE_ITEMS`: **50**: API分页的最大单页项目数。
|
||||||
- `ENABLE_OPENID_SIGNIN`: **false**: 允许使用OpenID登录,当设置为`true`时可以通过 `/user/login` 页面进行OpenID登录。
|
- `DEFAULT_PAGING_NUM`: **30**: API分页的默认分页数。
|
||||||
- `DISABLE_REGISTRATION`: **false**: 关闭用户注册。
|
- `DEFAULT_GIT_TREES_PER_PAGE`: **1000**: Git trees API的默认单页项目数。
|
||||||
|
- `DEFAULT_MAX_BLOB_SIZE`: **10485760** (10MiB): blobs API的默认最大文件大小。
|
||||||
|
|
||||||
## OAuth2 (`oauth2`)
|
## OAuth2 (`oauth2`)
|
||||||
|
|
||||||
|
|
|
@ -42,11 +42,11 @@ Gitea 引用 `custom` 目录中的自定义配置文件来覆盖配置、模板
|
||||||
|
|
||||||
将自定义的公共文件(比如页面和图片)作为 webroot 放在 `custom/public/` 中来让 Gitea 提供这些自定义内容(符号链接将被追踪)。
|
将自定义的公共文件(比如页面和图片)作为 webroot 放在 `custom/public/` 中来让 Gitea 提供这些自定义内容(符号链接将被追踪)。
|
||||||
|
|
||||||
举例说明:`image.png` 存放在 `custom/public/`中,那么它可以通过链接 http://gitea.domain.tld/assets/image.png 访问。
|
举例说明:`image.png` 存放在 `custom/public/assets/`中,那么它可以通过链接 http://gitea.domain.tld/assets/image.png 访问。
|
||||||
|
|
||||||
## 修改默认头像
|
## 修改默认头像
|
||||||
|
|
||||||
替换以下目录中的 png 图片: `custom/public/img/avatar\_default.png`
|
替换以下目录中的 png 图片: `custom/public/assets/img/avatar\_default.png`
|
||||||
|
|
||||||
## 自定义 Gitea 页面
|
## 自定义 Gitea 页面
|
||||||
|
|
||||||
|
|
|
@ -194,7 +194,7 @@ ALLOW_DATA_URI_IMAGES = true
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
将您的样式表添加到自定义目录中,例如 `custom/public/css/my-style-XXXXX.css`,并使用自定义的头文件 `custom/templates/custom/header.tmpl` 进行导入:
|
将您的样式表添加到自定义目录中,例如 `custom/public/assets/css/my-style-XXXXX.css`,并使用自定义的头文件 `custom/templates/custom/header.tmpl` 进行导入:
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<link rel="stylesheet" href="{{AppSubUrl}}/assets/css/my-style-XXXXX.css" />
|
<link rel="stylesheet" href="{{AppSubUrl}}/assets/css/my-style-XXXXX.css" />
|
||||||
|
|
|
@ -33,7 +33,7 @@ CERT_FILE = cert.pem
|
||||||
KEY_FILE = key.pem
|
KEY_FILE = key.pem
|
||||||
```
|
```
|
||||||
|
|
||||||
请注意,如果您的证书由第三方证书颁发机构签名(即不是自签名的),则 cert.pem 应包含证书链。服务器证书必须是 cert.pem 中的第一个条目,后跟中介(如果有)。不必包含根证书,因为连接客户端必须已经拥有根证书才能建立信任关系。要了解有关配置值的更多信息,请查看 [配置备忘单](../config-cheat-sheet#server-server)。
|
请注意,如果您的证书由第三方证书颁发机构签名(即不是自签名的),则 cert.pem 应包含证书链。服务器证书必须是 cert.pem 中的第一个条目,后跟中介(如果有)。不必包含根证书,因为连接客户端必须已经拥有根证书才能建立信任关系。要了解有关配置值的更多信息,请查看 [配置备忘单](administration/config-cheat-sheet#server-server)。
|
||||||
|
|
||||||
对于“CERT_FILE”或“KEY_FILE”字段,当文件路径是相对路径时,文件路径相对于“GITEA_CUSTOM”环境变量。它也可以是绝对路径。
|
对于“CERT_FILE”或“KEY_FILE”字段,当文件路径是相对路径时,文件路径相对于“GITEA_CUSTOM”环境变量。它也可以是绝对路径。
|
||||||
|
|
||||||
|
|
|
@ -19,10 +19,7 @@ menu:
|
||||||
|
|
||||||
## Enabling/configuring API access
|
## Enabling/configuring API access
|
||||||
|
|
||||||
By default, `ENABLE_SWAGGER` is true, and
|
By default, `ENABLE_SWAGGER` is true, and `MAX_RESPONSE_ITEMS` is set to 50. See [Config Cheat Sheet](administration/config-cheat-sheet.md) for more information.
|
||||||
`MAX_RESPONSE_ITEMS` is set to 50. See [Config Cheat
|
|
||||||
Sheet](administration/config-cheat-sheet.md) for more
|
|
||||||
information.
|
|
||||||
|
|
||||||
## Authentication
|
## Authentication
|
||||||
|
|
||||||
|
|
|
@ -19,8 +19,7 @@ menu:
|
||||||
|
|
||||||
## 开启/配置 API 访问
|
## 开启/配置 API 访问
|
||||||
|
|
||||||
通常情况下, `ENABLE_SWAGGER` 默认开启并且参数 `MAX_RESPONSE_ITEMS` 默认为 50。您可以从 [Config Cheat
|
通常情况下, `ENABLE_SWAGGER` 默认开启并且参数 `MAX_RESPONSE_ITEMS` 默认为 50。您可以从 [Config Cheat Sheet](administration/config-cheat-sheet.md) 中获取更多配置相关信息。
|
||||||
Sheet](administration/config-cheat-sheet.md) 中获取更多配置相关信息。
|
|
||||||
|
|
||||||
## 通过 API 认证
|
## 通过 API 认证
|
||||||
|
|
||||||
|
|
|
@ -190,7 +190,7 @@ Gitea 目前支持三个官方主题,分别是 `gitea`(亮色)、`arc-gree
|
||||||
|
|
||||||
假设我们的主题是 `arc-blue`(这是一个真实的主题,可以在[此问题](https://github.com/go-gitea/gitea/issues/6011)中找到)
|
假设我们的主题是 `arc-blue`(这是一个真实的主题,可以在[此问题](https://github.com/go-gitea/gitea/issues/6011)中找到)
|
||||||
|
|
||||||
将`.css`文件命名为`theme-arc-blue.css`并将其添加到`custom/public/css`文件夹中
|
将`.css`文件命名为`theme-arc-blue.css`并将其添加到`custom/public/assets/css`文件夹中
|
||||||
|
|
||||||
通过将`arc-blue`添加到`app.ini`中的`THEMES`列表中,允许用户使用该主题
|
通过将`arc-blue`添加到`app.ini`中的`THEMES`列表中,允许用户使用该主题
|
||||||
|
|
||||||
|
|
|
@ -117,7 +117,7 @@ chmod 770 /etc/gitea
|
||||||
- 使用 `gitea generate secret` 创建 `SECRET_KEY` 和 `INTERNAL_TOKEN`
|
- 使用 `gitea generate secret` 创建 `SECRET_KEY` 和 `INTERNAL_TOKEN`
|
||||||
- 提供所有必要的密钥
|
- 提供所有必要的密钥
|
||||||
|
|
||||||
详情参考 [命令行文档](/zh-cn/command-line/) 中有关 `gitea generate secret` 的内容。
|
详情参考 [命令行文档](administration/command-line.md) 中有关 `gitea generate secret` 的内容。
|
||||||
|
|
||||||
### 配置 Gitea 工作路径
|
### 配置 Gitea 工作路径
|
||||||
|
|
||||||
|
@ -209,6 +209,6 @@ remote: ./hooks/pre-receive.d/gitea: line 2: [...]: No such file or directory
|
||||||
|
|
||||||
如果您没有使用 Gitea 内置的 SSH 服务器,您还需要通过在管理选项中运行任务 `Update the '.ssh/authorized_keys' file with Gitea SSH keys.` 来重新编写授权密钥文件。
|
如果您没有使用 Gitea 内置的 SSH 服务器,您还需要通过在管理选项中运行任务 `Update the '.ssh/authorized_keys' file with Gitea SSH keys.` 来重新编写授权密钥文件。
|
||||||
|
|
||||||
> 更多经验总结,请参考英文版 [Troubleshooting](/en-us/install-from-binary/#troubleshooting)
|
> 更多经验总结,请参考英文版 [Troubleshooting](https://docs.gitea.com/installation/install-from-binary#troubleshooting)
|
||||||
|
|
||||||
如果从本页中没有找到你需要的内容,请访问 [帮助页面](help/support.md)
|
如果从本页中没有找到你需要的内容,请访问 [帮助页面](help/support.md)
|
||||||
|
|
|
@ -64,7 +64,7 @@ git checkout v@version@ # or git checkout pr-xyz
|
||||||
|
|
||||||
- `go` @minGoVersion@ 或更高版本,请参阅 [这里](https://golang.org/dl/)
|
- `go` @minGoVersion@ 或更高版本,请参阅 [这里](https://golang.org/dl/)
|
||||||
- `node` @minNodeVersion@ 或更高版本,并且安装 `npm`, 请参阅 [这里](https://nodejs.org/zh-cn/download/)
|
- `node` @minNodeVersion@ 或更高版本,并且安装 `npm`, 请参阅 [这里](https://nodejs.org/zh-cn/download/)
|
||||||
- `make`, 请参阅 [这里](/zh-cn/hacking-on-gitea/)
|
- `make`, 请参阅 [这里](development/hacking-on-gitea.md)
|
||||||
|
|
||||||
为了尽可能简化编译过程,提供了各种 [make任务](https://github.com/go-gitea/gitea/blob/main/Makefile)。
|
为了尽可能简化编译过程,提供了各种 [make任务](https://github.com/go-gitea/gitea/blob/main/Makefile)。
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,10 @@ Like `uses: https://github.com/actions/checkout@v3` or `uses: http://your_gitea.
|
||||||
Gitea Actions supports writing actions in Go.
|
Gitea Actions supports writing actions in Go.
|
||||||
See [Creating Go Actions](https://blog.gitea.com/creating-go-actions/).
|
See [Creating Go Actions](https://blog.gitea.com/creating-go-actions/).
|
||||||
|
|
||||||
|
### Support the non-standard syntax @yearly, @monthly, @weekly, @daily, @hourly on schedule
|
||||||
|
|
||||||
|
Github Actions doesn't support that. https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule
|
||||||
|
|
||||||
## Unsupported workflows syntax
|
## Unsupported workflows syntax
|
||||||
|
|
||||||
### `concurrency`
|
### `concurrency`
|
||||||
|
@ -110,6 +114,10 @@ It's ignored by Gitea Actions now.
|
||||||
|
|
||||||
Pre and Post steps don't have their own section in the job log user interface.
|
Pre and Post steps don't have their own section in the job log user interface.
|
||||||
|
|
||||||
|
### Services steps
|
||||||
|
|
||||||
|
Services steps don't have their own section in the job log user interface.
|
||||||
|
|
||||||
## Different behavior
|
## Different behavior
|
||||||
|
|
||||||
### Downloading actions
|
### Downloading actions
|
||||||
|
|
|
@ -29,6 +29,10 @@ Gitea Actions支持通过URL绝对路径定义actions,这意味着您可以使
|
||||||
Gitea Actions支持使用Go编写Actions。
|
Gitea Actions支持使用Go编写Actions。
|
||||||
请参阅[创建Go Actions](https://blog.gitea.com/creating-go-actions/)。
|
请参阅[创建Go Actions](https://blog.gitea.com/creating-go-actions/)。
|
||||||
|
|
||||||
|
### 支持非标准的调度语法 @yearly, @monthly, @weekly, @daily, @hourly
|
||||||
|
|
||||||
|
Github Actions 不支持这些语法,详见: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule
|
||||||
|
|
||||||
## 不支持的工作流语法
|
## 不支持的工作流语法
|
||||||
|
|
||||||
### `concurrency`
|
### `concurrency`
|
||||||
|
@ -116,6 +120,10 @@ Gitea Actions目前不支持此功能。
|
||||||
|
|
||||||
预处理和后处理步骤在Job日志用户界面中没有自己的用户界面。
|
预处理和后处理步骤在Job日志用户界面中没有自己的用户界面。
|
||||||
|
|
||||||
|
### 服务步骤
|
||||||
|
|
||||||
|
服务步骤在Job日志用户界面中没有自己的用户界面。
|
||||||
|
|
||||||
## 不一样的行为
|
## 不一样的行为
|
||||||
|
|
||||||
### 下载Actions
|
### 下载Actions
|
||||||
|
|
6
go.mod
6
go.mod
|
@ -103,12 +103,12 @@ require (
|
||||||
github.com/yuin/goldmark v1.5.6
|
github.com/yuin/goldmark v1.5.6
|
||||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||||
github.com/yuin/goldmark-meta v1.1.0
|
github.com/yuin/goldmark-meta v1.1.0
|
||||||
golang.org/x/crypto v0.14.0
|
golang.org/x/crypto v0.17.0
|
||||||
golang.org/x/image v0.13.0
|
golang.org/x/image v0.13.0
|
||||||
golang.org/x/net v0.17.0
|
golang.org/x/net v0.17.0
|
||||||
golang.org/x/oauth2 v0.13.0
|
golang.org/x/oauth2 v0.13.0
|
||||||
golang.org/x/sys v0.13.0
|
golang.org/x/sys v0.15.0
|
||||||
golang.org/x/text v0.13.0
|
golang.org/x/text v0.14.0
|
||||||
golang.org/x/tools v0.14.0
|
golang.org/x/tools v0.14.0
|
||||||
google.golang.org/grpc v1.58.3
|
google.golang.org/grpc v1.58.3
|
||||||
google.golang.org/protobuf v1.31.0
|
google.golang.org/protobuf v1.31.0
|
||||||
|
|
15
go.sum
15
go.sum
|
@ -1143,8 +1143,8 @@ golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2Uz
|
||||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
||||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
|
@ -1336,8 +1336,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
@ -1347,8 +1347,8 @@ golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||||
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
|
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
|
||||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
@ -1363,8 +1363,9 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
|
||||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
|
|
@ -51,6 +51,11 @@ type ActionRunner struct {
|
||||||
Deleted timeutil.TimeStamp `xorm:"deleted"`
|
Deleted timeutil.TimeStamp `xorm:"deleted"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
RunnerOfflineTime = time.Minute
|
||||||
|
RunnerIdleTime = 10 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
// BelongsToOwnerName before calling, should guarantee that all attributes are loaded
|
// BelongsToOwnerName before calling, should guarantee that all attributes are loaded
|
||||||
func (r *ActionRunner) BelongsToOwnerName() string {
|
func (r *ActionRunner) BelongsToOwnerName() string {
|
||||||
if r.RepoID != 0 {
|
if r.RepoID != 0 {
|
||||||
|
@ -76,11 +81,12 @@ func (r *ActionRunner) BelongsToOwnerType() types.OwnerType {
|
||||||
return types.OwnerTypeSystemGlobal
|
return types.OwnerTypeSystemGlobal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if the logic here changed, you should also modify FindRunnerOptions.ToCond
|
||||||
func (r *ActionRunner) Status() runnerv1.RunnerStatus {
|
func (r *ActionRunner) Status() runnerv1.RunnerStatus {
|
||||||
if time.Since(r.LastOnline.AsTime()) > time.Minute {
|
if time.Since(r.LastOnline.AsTime()) > RunnerOfflineTime {
|
||||||
return runnerv1.RunnerStatus_RUNNER_STATUS_OFFLINE
|
return runnerv1.RunnerStatus_RUNNER_STATUS_OFFLINE
|
||||||
}
|
}
|
||||||
if time.Since(r.LastActive.AsTime()) > 10*time.Second {
|
if time.Since(r.LastActive.AsTime()) > RunnerIdleTime {
|
||||||
return runnerv1.RunnerStatus_RUNNER_STATUS_IDLE
|
return runnerv1.RunnerStatus_RUNNER_STATUS_IDLE
|
||||||
}
|
}
|
||||||
return runnerv1.RunnerStatus_RUNNER_STATUS_ACTIVE
|
return runnerv1.RunnerStatus_RUNNER_STATUS_ACTIVE
|
||||||
|
@ -153,6 +159,7 @@ type FindRunnerOptions struct {
|
||||||
OwnerID int64
|
OwnerID int64
|
||||||
Sort string
|
Sort string
|
||||||
Filter string
|
Filter string
|
||||||
|
IsOnline util.OptionalBool
|
||||||
WithAvailable bool // not only runners belong to, but also runners can be used
|
WithAvailable bool // not only runners belong to, but also runners can be used
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,6 +185,12 @@ func (opts FindRunnerOptions) toCond() builder.Cond {
|
||||||
if opts.Filter != "" {
|
if opts.Filter != "" {
|
||||||
cond = cond.And(builder.Like{"name", opts.Filter})
|
cond = cond.And(builder.Like{"name", opts.Filter})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.IsOnline.IsTrue() {
|
||||||
|
cond = cond.And(builder.Gt{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()})
|
||||||
|
} else if opts.IsOnline.IsFalse() {
|
||||||
|
cond = cond.And(builder.Lte{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()})
|
||||||
|
}
|
||||||
return cond
|
return cond
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,10 +29,15 @@ func VerifySSHKey(ownerID int64, fingerprint, token, signature string) (string,
|
||||||
return "", ErrKeyNotExist{}
|
return "", ErrKeyNotExist{}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := sshsig.Verify(bytes.NewBuffer([]byte(token)), []byte(signature), []byte(key.Content), "gitea"); err != nil {
|
err = sshsig.Verify(bytes.NewBuffer([]byte(token)), []byte(signature), []byte(key.Content), "gitea")
|
||||||
log.Error("Unable to validate token signature. Error: %v", err)
|
if err != nil {
|
||||||
return "", ErrSSHInvalidTokenSignature{
|
// edge case for Windows based shells that will add CR LF if piped to ssh-keygen command
|
||||||
Fingerprint: key.Fingerprint,
|
// see https://github.com/PowerShell/PowerShell/issues/5974
|
||||||
|
if sshsig.Verify(bytes.NewBuffer([]byte(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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -178,6 +178,15 @@ func GetByBean(ctx context.Context, bean any) (bool, error) {
|
||||||
return GetEngine(ctx).Get(bean)
|
return GetEngine(ctx).Get(bean)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Exist[T any](ctx context.Context, cond builder.Cond) (bool, error) {
|
||||||
|
if !cond.IsValid() {
|
||||||
|
return false, ErrConditionRequired{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var bean T
|
||||||
|
return GetEngine(ctx).Where(cond).NoAutoCondition().Exist(&bean)
|
||||||
|
}
|
||||||
|
|
||||||
// DeleteByBean deletes all records according non-empty fields of the bean as conditions.
|
// DeleteByBean deletes all records according non-empty fields of the bean as conditions.
|
||||||
func DeleteByBean(ctx context.Context, bean any) (int64, error) {
|
func DeleteByBean(ctx context.Context, bean any) (int64, error) {
|
||||||
return GetEngine(ctx).Delete(bean)
|
return GetEngine(ctx).Delete(bean)
|
||||||
|
|
|
@ -72,3 +72,21 @@ func (err ErrNotExist) Error() string {
|
||||||
func (err ErrNotExist) Unwrap() error {
|
func (err ErrNotExist) Unwrap() error {
|
||||||
return util.ErrNotExist
|
return util.ErrNotExist
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ErrConditionRequired represents an error which require condition.
|
||||||
|
type ErrConditionRequired struct{}
|
||||||
|
|
||||||
|
// IsErrConditionRequired checks if an error is an ErrConditionRequired
|
||||||
|
func IsErrConditionRequired(err error) bool {
|
||||||
|
_, ok := err.(ErrConditionRequired)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrConditionRequired) Error() string {
|
||||||
|
return "condition is required"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap unwraps this as a ErrNotExist err
|
||||||
|
func (err ErrConditionRequired) Unwrap() error {
|
||||||
|
return util.ErrInvalidArgument
|
||||||
|
}
|
||||||
|
|
|
@ -205,10 +205,9 @@ func DeleteBranches(ctx context.Context, repoID, doerID int64, branchIDs []int64
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateBranch updates the branch information in the database. If the branch exist, it will update latest commit of this branch information
|
// UpdateBranch updates the branch information in the database.
|
||||||
// If it doest not exist, insert a new record into database
|
func UpdateBranch(ctx context.Context, repoID, pusherID int64, branchName string, commit *git.Commit) (int64, error) {
|
||||||
func UpdateBranch(ctx context.Context, repoID, pusherID int64, branchName string, commit *git.Commit) error {
|
return db.GetEngine(ctx).Where("repo_id=? AND name=?", repoID, branchName).
|
||||||
cnt, err := db.GetEngine(ctx).Where("repo_id=? AND name=?", repoID, branchName).
|
|
||||||
Cols("commit_id, commit_message, pusher_id, commit_time, is_deleted, updated_unix").
|
Cols("commit_id, commit_message, pusher_id, commit_time, is_deleted, updated_unix").
|
||||||
Update(&Branch{
|
Update(&Branch{
|
||||||
CommitID: commit.ID.String(),
|
CommitID: commit.ID.String(),
|
||||||
|
@ -217,21 +216,6 @@ func UpdateBranch(ctx context.Context, repoID, pusherID int64, branchName string
|
||||||
CommitTime: timeutil.TimeStamp(commit.Committer.When.Unix()),
|
CommitTime: timeutil.TimeStamp(commit.Committer.When.Unix()),
|
||||||
IsDeleted: false,
|
IsDeleted: false,
|
||||||
})
|
})
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if cnt > 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return db.Insert(ctx, &Branch{
|
|
||||||
RepoID: repoID,
|
|
||||||
Name: branchName,
|
|
||||||
CommitID: commit.ID.String(),
|
|
||||||
CommitMessage: commit.Summary(),
|
|
||||||
PusherID: pusherID,
|
|
||||||
CommitTime: timeutil.TimeStamp(commit.Committer.When.Unix()),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddDeletedBranch adds a deleted branch to the database
|
// AddDeletedBranch adds a deleted branch to the database
|
||||||
|
@ -308,6 +292,17 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str
|
||||||
|
|
||||||
sess := db.GetEngine(ctx)
|
sess := db.GetEngine(ctx)
|
||||||
|
|
||||||
|
var branch Branch
|
||||||
|
exist, err := db.GetEngine(ctx).Where("repo_id=? AND name=?", repo.ID, from).Get(&branch)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if !exist || branch.IsDeleted {
|
||||||
|
return ErrBranchNotExist{
|
||||||
|
RepoID: repo.ID,
|
||||||
|
BranchName: from,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 1. update branch in database
|
// 1. update branch in database
|
||||||
if n, err := sess.Where("repo_id=? AND name=?", repo.ID, from).Update(&Branch{
|
if n, err := sess.Where("repo_id=? AND name=?", repo.ID, from).Update(&Branch{
|
||||||
Name: to,
|
Name: to,
|
||||||
|
|
|
@ -73,7 +73,7 @@ type FindBranchOptions struct {
|
||||||
Keyword string
|
Keyword string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opts *FindBranchOptions) Cond() builder.Cond {
|
func (opts FindBranchOptions) ToConds() builder.Cond {
|
||||||
cond := builder.NewCond()
|
cond := builder.NewCond()
|
||||||
if opts.RepoID > 0 {
|
if opts.RepoID > 0 {
|
||||||
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
|
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
|
||||||
|
@ -92,7 +92,7 @@ func (opts *FindBranchOptions) Cond() builder.Cond {
|
||||||
}
|
}
|
||||||
|
|
||||||
func CountBranches(ctx context.Context, opts FindBranchOptions) (int64, error) {
|
func CountBranches(ctx context.Context, opts FindBranchOptions) (int64, error) {
|
||||||
return db.GetEngine(ctx).Where(opts.Cond()).Count(&Branch{})
|
return db.GetEngine(ctx).Where(opts.ToConds()).Count(&Branch{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func orderByBranches(sess *xorm.Session, opts FindBranchOptions) *xorm.Session {
|
func orderByBranches(sess *xorm.Session, opts FindBranchOptions) *xorm.Session {
|
||||||
|
@ -108,7 +108,7 @@ func orderByBranches(sess *xorm.Session, opts FindBranchOptions) *xorm.Session {
|
||||||
}
|
}
|
||||||
|
|
||||||
func FindBranches(ctx context.Context, opts FindBranchOptions) (BranchList, error) {
|
func FindBranches(ctx context.Context, opts FindBranchOptions) (BranchList, error) {
|
||||||
sess := db.GetEngine(ctx).Where(opts.Cond())
|
sess := db.GetEngine(ctx).Where(opts.ToConds())
|
||||||
if opts.PageSize > 0 && !opts.IsListAll() {
|
if opts.PageSize > 0 && !opts.IsListAll() {
|
||||||
sess = db.SetSessionPagination(sess, &opts.ListOptions)
|
sess = db.SetSessionPagination(sess, &opts.ListOptions)
|
||||||
}
|
}
|
||||||
|
@ -119,7 +119,7 @@ func FindBranches(ctx context.Context, opts FindBranchOptions) (BranchList, erro
|
||||||
}
|
}
|
||||||
|
|
||||||
func FindBranchNames(ctx context.Context, opts FindBranchOptions) ([]string, error) {
|
func FindBranchNames(ctx context.Context, opts FindBranchOptions) ([]string, error) {
|
||||||
sess := db.GetEngine(ctx).Select("name").Where(opts.Cond())
|
sess := db.GetEngine(ctx).Select("name").Where(opts.ToConds())
|
||||||
if opts.PageSize > 0 && !opts.IsListAll() {
|
if opts.PageSize > 0 && !opts.IsListAll() {
|
||||||
sess = db.SetSessionPagination(sess, &opts.ListOptions)
|
sess = db.SetSessionPagination(sess, &opts.ListOptions)
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ func TestAddDeletedBranch(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := git_model.UpdateBranch(db.DefaultContext, repo.ID, secondBranch.PusherID, secondBranch.Name, commit)
|
_, err := git_model.UpdateBranch(db.DefaultContext, repo.ID, secondBranch.PusherID, secondBranch.Name, commit)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -461,8 +461,10 @@ func SubmitReview(ctx context.Context, doer *user_model.User, issue *Issue, revi
|
||||||
func GetReviewByIssueIDAndUserID(ctx context.Context, issueID, userID int64) (*Review, error) {
|
func GetReviewByIssueIDAndUserID(ctx context.Context, issueID, userID int64) (*Review, error) {
|
||||||
review := new(Review)
|
review := new(Review)
|
||||||
|
|
||||||
has, err := db.GetEngine(ctx).SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_id = ? AND original_author_id = 0 AND type in (?, ?, ?))",
|
has, err := db.GetEngine(ctx).Where(
|
||||||
issueID, userID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest).
|
builder.In("type", ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest).
|
||||||
|
And(builder.Eq{"issue_id": issueID, "reviewer_id": userID, "original_author_id": 0})).
|
||||||
|
Desc("id").
|
||||||
Get(review)
|
Get(review)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -476,13 +478,13 @@ func GetReviewByIssueIDAndUserID(ctx context.Context, issueID, userID int64) (*R
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTeamReviewerByIssueIDAndTeamID get the latest review request of reviewer team for a pull request
|
// GetTeamReviewerByIssueIDAndTeamID get the latest review request of reviewer team for a pull request
|
||||||
func GetTeamReviewerByIssueIDAndTeamID(ctx context.Context, issueID, teamID int64) (review *Review, err error) {
|
func GetTeamReviewerByIssueIDAndTeamID(ctx context.Context, issueID, teamID int64) (*Review, error) {
|
||||||
review = new(Review)
|
review := new(Review)
|
||||||
|
|
||||||
var has bool
|
has, err := db.GetEngine(ctx).Where(builder.Eq{"issue_id": issueID, "reviewer_team_id": teamID}).
|
||||||
if has, err = db.GetEngine(ctx).SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_team_id = ?)",
|
Desc("id").
|
||||||
issueID, teamID).
|
Get(review)
|
||||||
Get(review); err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -74,7 +74,7 @@ func GetScheduledMergeByPullID(ctx context.Context, pullID int64) (bool, *AutoMe
|
||||||
return false, nil, err
|
return false, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
doer, err := user_model.GetUserByID(ctx, scheduledPRM.DoerID)
|
doer, err := user_model.GetPossibleUserByID(ctx, scheduledPRM.DoerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, nil, err
|
return false, nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,14 @@ func (err ErrUserDoesNotHaveAccessToRepo) Unwrap() error {
|
||||||
return util.ErrPermissionDenied
|
return util.ErrPermissionDenied
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ErrRepoIsArchived struct {
|
||||||
|
Repo *Repository
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrRepoIsArchived) Error() string {
|
||||||
|
return fmt.Sprintf("%s is archived", err.Repo.LogString())
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
reservedRepoNames = []string{".", "..", "-"}
|
reservedRepoNames = []string{".", "..", "-"}
|
||||||
reservedRepoPatterns = []string{"*.git", "*.wiki", "*.rss", "*.atom"}
|
reservedRepoPatterns = []string{"*.git", "*.wiki", "*.rss", "*.atom"}
|
||||||
|
@ -594,25 +602,23 @@ func ComposeHTTPSCloneURL(owner, repo string) string {
|
||||||
|
|
||||||
func ComposeSSHCloneURL(ownerName, repoName string) string {
|
func ComposeSSHCloneURL(ownerName, repoName string) string {
|
||||||
sshUser := setting.SSH.User
|
sshUser := setting.SSH.User
|
||||||
|
|
||||||
// if we have a ipv6 literal we need to put brackets around it
|
|
||||||
// for the git cloning to work.
|
|
||||||
sshDomain := setting.SSH.Domain
|
sshDomain := setting.SSH.Domain
|
||||||
ip := net.ParseIP(setting.SSH.Domain)
|
|
||||||
if ip != nil && ip.To4() == nil {
|
// non-standard port, it must use full URI
|
||||||
sshDomain = "[" + setting.SSH.Domain + "]"
|
if setting.SSH.Port != 22 {
|
||||||
|
sshHost := net.JoinHostPort(sshDomain, strconv.Itoa(setting.SSH.Port))
|
||||||
|
return fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName))
|
||||||
}
|
}
|
||||||
|
|
||||||
if setting.SSH.Port != 22 {
|
// for standard port, it can use a shorter URI (without the port)
|
||||||
return fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser,
|
sshHost := sshDomain
|
||||||
net.JoinHostPort(setting.SSH.Domain, strconv.Itoa(setting.SSH.Port)),
|
if ip := net.ParseIP(sshHost); ip != nil && ip.To4() == nil {
|
||||||
url.PathEscape(ownerName),
|
sshHost = "[" + sshHost + "]" // for IPv6 address, wrap it with brackets
|
||||||
url.PathEscape(repoName))
|
|
||||||
}
|
}
|
||||||
if setting.Repository.UseCompatSSHURI {
|
if setting.Repository.UseCompatSSHURI {
|
||||||
return fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, sshDomain, url.PathEscape(ownerName), url.PathEscape(repoName))
|
return fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName))
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%s@%s:%s/%s.git", sshUser, sshDomain, url.PathEscape(ownerName), url.PathEscape(repoName))
|
return fmt.Sprintf("%s@%s:%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo *Repository) cloneLink(isWiki bool) *CloneLink {
|
func (repo *Repository) cloneLink(isWiki bool) *CloneLink {
|
||||||
|
@ -654,6 +660,14 @@ func (repo *Repository) GetTrustModel() TrustModelType {
|
||||||
return trustModel
|
return trustModel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MustNotBeArchived returns ErrRepoIsArchived if the repo is archived
|
||||||
|
func (repo *Repository) MustNotBeArchived() error {
|
||||||
|
if repo.IsArchived {
|
||||||
|
return ErrRepoIsArchived{Repo: repo}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// __________ .__ __
|
// __________ .__ __
|
||||||
// \______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__.
|
// \______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__.
|
||||||
// | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | |
|
// | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | |
|
||||||
|
|
|
@ -12,6 +12,8 @@ import (
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/markup"
|
"code.gitea.io/gitea/modules/markup"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -186,3 +188,32 @@ func TestGetRepositoryByURL(t *testing.T) {
|
||||||
test(t, "try.gitea.io:user2/repo2.git")
|
test(t, "try.gitea.io:user2/repo2.git")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestComposeSSHCloneURL(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&setting.SSH, setting.SSH)()
|
||||||
|
defer test.MockVariableValue(&setting.Repository, setting.Repository)()
|
||||||
|
|
||||||
|
setting.SSH.User = "git"
|
||||||
|
|
||||||
|
// test SSH_DOMAIN
|
||||||
|
setting.SSH.Domain = "domain"
|
||||||
|
setting.SSH.Port = 22
|
||||||
|
setting.Repository.UseCompatSSHURI = false
|
||||||
|
assert.Equal(t, "git@domain:user/repo.git", repo_model.ComposeSSHCloneURL("user", "repo"))
|
||||||
|
setting.Repository.UseCompatSSHURI = true
|
||||||
|
assert.Equal(t, "ssh://git@domain/user/repo.git", repo_model.ComposeSSHCloneURL("user", "repo"))
|
||||||
|
// test SSH_DOMAIN while use non-standard SSH port
|
||||||
|
setting.SSH.Port = 123
|
||||||
|
setting.Repository.UseCompatSSHURI = false
|
||||||
|
assert.Equal(t, "ssh://git@domain:123/user/repo.git", repo_model.ComposeSSHCloneURL("user", "repo"))
|
||||||
|
setting.Repository.UseCompatSSHURI = true
|
||||||
|
assert.Equal(t, "ssh://git@domain:123/user/repo.git", repo_model.ComposeSSHCloneURL("user", "repo"))
|
||||||
|
|
||||||
|
// test IPv6 SSH_DOMAIN
|
||||||
|
setting.Repository.UseCompatSSHURI = false
|
||||||
|
setting.SSH.Domain = "::1"
|
||||||
|
setting.SSH.Port = 22
|
||||||
|
assert.Equal(t, "git@[::1]:user/repo.git", repo_model.ComposeSSHCloneURL("user", "repo"))
|
||||||
|
setting.SSH.Port = 123
|
||||||
|
assert.Equal(t, "ssh://git@[::1]:123/user/repo.git", repo_model.ComposeSSHCloneURL("user", "repo"))
|
||||||
|
}
|
||||||
|
|
|
@ -8,11 +8,12 @@
|
||||||
package charset
|
package charset
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/translation"
|
"code.gitea.io/gitea/modules/translation"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -20,20 +21,18 @@ import (
|
||||||
const RuneNBSP = 0xa0
|
const RuneNBSP = 0xa0
|
||||||
|
|
||||||
// EscapeControlHTML escapes the unicode control sequences in a provided html document
|
// EscapeControlHTML escapes the unicode control sequences in a provided html document
|
||||||
func EscapeControlHTML(text string, locale translation.Locale, allowed ...rune) (escaped *EscapeStatus, output string) {
|
func EscapeControlHTML(html template.HTML, locale translation.Locale, allowed ...rune) (escaped *EscapeStatus, output template.HTML) {
|
||||||
sb := &strings.Builder{}
|
sb := &strings.Builder{}
|
||||||
outputStream := &HTMLStreamerWriter{Writer: sb}
|
escaped, _ = EscapeControlReader(strings.NewReader(string(html)), sb, locale, allowed...) // err has been handled in EscapeControlReader
|
||||||
streamer := NewEscapeStreamer(locale, outputStream, allowed...).(*escapeStreamer)
|
return escaped, template.HTML(sb.String())
|
||||||
|
|
||||||
if err := StreamHTML(strings.NewReader(text), streamer); err != nil {
|
|
||||||
streamer.escaped.HasError = true
|
|
||||||
log.Error("Error whilst escaping: %v", err)
|
|
||||||
}
|
|
||||||
return streamer.escaped, sb.String()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// EscapeControlReaders escapes the unicode control sequences in a provided reader of HTML content and writer in a locale and returns the findings as an EscapeStatus and the escaped []byte
|
// EscapeControlReader escapes the unicode control sequences in a provided reader of HTML content and writer in a locale and returns the findings as an EscapeStatus
|
||||||
func EscapeControlReader(reader io.Reader, writer io.Writer, locale translation.Locale, allowed ...rune) (escaped *EscapeStatus, err error) {
|
func EscapeControlReader(reader io.Reader, writer io.Writer, locale translation.Locale, allowed ...rune) (escaped *EscapeStatus, err error) {
|
||||||
|
if !setting.UI.AmbiguousUnicodeDetection {
|
||||||
|
_, err = io.Copy(writer, reader)
|
||||||
|
return &EscapeStatus{}, err
|
||||||
|
}
|
||||||
outputStream := &HTMLStreamerWriter{Writer: writer}
|
outputStream := &HTMLStreamerWriter{Writer: writer}
|
||||||
streamer := NewEscapeStreamer(locale, outputStream, allowed...).(*escapeStreamer)
|
streamer := NewEscapeStreamer(locale, outputStream, allowed...).(*escapeStreamer)
|
||||||
|
|
||||||
|
@ -43,41 +42,3 @@ func EscapeControlReader(reader io.Reader, writer io.Writer, locale translation.
|
||||||
}
|
}
|
||||||
return streamer.escaped, err
|
return streamer.escaped, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// EscapeControlStringReader escapes the unicode control sequences in a provided reader of string content and writer in a locale and returns the findings as an EscapeStatus and the escaped []byte. HTML line breaks are not inserted after every newline by this method.
|
|
||||||
func EscapeControlStringReader(reader io.Reader, writer io.Writer, locale translation.Locale, allowed ...rune) (escaped *EscapeStatus, err error) {
|
|
||||||
bufRd := bufio.NewReader(reader)
|
|
||||||
outputStream := &HTMLStreamerWriter{Writer: writer}
|
|
||||||
streamer := NewEscapeStreamer(locale, outputStream, allowed...).(*escapeStreamer)
|
|
||||||
|
|
||||||
for {
|
|
||||||
line, rdErr := bufRd.ReadString('\n')
|
|
||||||
if len(line) > 0 {
|
|
||||||
if err := streamer.Text(line); err != nil {
|
|
||||||
streamer.escaped.HasError = true
|
|
||||||
log.Error("Error whilst escaping: %v", err)
|
|
||||||
return streamer.escaped, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if rdErr != nil {
|
|
||||||
if rdErr != io.EOF {
|
|
||||||
err = rdErr
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return streamer.escaped, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// EscapeControlString escapes the unicode control sequences in a provided string and returns the findings as an EscapeStatus and the escaped string
|
|
||||||
func EscapeControlString(text string, locale translation.Locale, allowed ...rune) (escaped *EscapeStatus, output string) {
|
|
||||||
sb := &strings.Builder{}
|
|
||||||
outputStream := &HTMLStreamerWriter{Writer: sb}
|
|
||||||
streamer := NewEscapeStreamer(locale, outputStream, allowed...).(*escapeStreamer)
|
|
||||||
|
|
||||||
if err := streamer.Text(text); err != nil {
|
|
||||||
streamer.escaped.HasError = true
|
|
||||||
log.Error("Error whilst escaping: %v", err)
|
|
||||||
}
|
|
||||||
return streamer.escaped, sb.String()
|
|
||||||
}
|
|
||||||
|
|
|
@ -64,7 +64,7 @@ func (e *escapeStreamer) Text(data string) error {
|
||||||
until, next = nextIdxs[0]+pos, nextIdxs[1]+pos
|
until, next = nextIdxs[0]+pos, nextIdxs[1]+pos
|
||||||
}
|
}
|
||||||
|
|
||||||
// from pos until until we know that the runes are not \r\t\n or even ' '
|
// from pos until we know that the runes are not \r\t\n or even ' '
|
||||||
runes := make([]rune, 0, next-until)
|
runes := make([]rune, 0, next-until)
|
||||||
positions := make([]int, 0, next-until+1)
|
positions := make([]int, 0, next-until+1)
|
||||||
|
|
||||||
|
|
|
@ -4,11 +4,14 @@
|
||||||
package charset
|
package charset
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
"code.gitea.io/gitea/modules/translation"
|
"code.gitea.io/gitea/modules/translation"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
type escapeControlTest struct {
|
type escapeControlTest struct {
|
||||||
|
@ -132,22 +135,8 @@ then resh (ר), and finally heh (ה) (which should appear leftmost).`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEscapeControlString(t *testing.T) {
|
|
||||||
for _, tt := range escapeControlTests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
status, result := EscapeControlString(tt.text, &translation.MockLocale{})
|
|
||||||
if !reflect.DeepEqual(*status, tt.status) {
|
|
||||||
t.Errorf("EscapeControlString() status = %v, wanted= %v", status, tt.status)
|
|
||||||
}
|
|
||||||
if result != tt.result {
|
|
||||||
t.Errorf("EscapeControlString()\nresult= %v,\nwanted= %v", result, tt.result)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEscapeControlReader(t *testing.T) {
|
func TestEscapeControlReader(t *testing.T) {
|
||||||
// lets add some control characters to the tests
|
// add some control characters to the tests
|
||||||
tests := make([]escapeControlTest, 0, len(escapeControlTests)*3)
|
tests := make([]escapeControlTest, 0, len(escapeControlTests)*3)
|
||||||
copy(tests, escapeControlTests)
|
copy(tests, escapeControlTests)
|
||||||
|
|
||||||
|
@ -169,29 +158,20 @@ func TestEscapeControlReader(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
input := strings.NewReader(tt.text)
|
|
||||||
output := &strings.Builder{}
|
output := &strings.Builder{}
|
||||||
status, err := EscapeControlReader(input, output, &translation.MockLocale{})
|
status, err := EscapeControlReader(strings.NewReader(tt.text), output, &translation.MockLocale{})
|
||||||
result := output.String()
|
assert.NoError(t, err)
|
||||||
if err != nil {
|
assert.Equal(t, tt.status, *status)
|
||||||
t.Errorf("EscapeControlReader(): err = %v", err)
|
assert.Equal(t, tt.result, output.String())
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(*status, tt.status) {
|
|
||||||
t.Errorf("EscapeControlReader() status = %v, wanted= %v", status, tt.status)
|
|
||||||
}
|
|
||||||
if result != tt.result {
|
|
||||||
t.Errorf("EscapeControlReader()\nresult= %v,\nwanted= %v", result, tt.result)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEscapeControlReader_panic(t *testing.T) {
|
func TestSettingAmbiguousUnicodeDetection(t *testing.T) {
|
||||||
bs := make([]byte, 0, 20479)
|
defer test.MockVariableValue(&setting.UI.AmbiguousUnicodeDetection, true)()
|
||||||
bs = append(bs, 'A')
|
_, out := EscapeControlHTML("a test", &translation.MockLocale{})
|
||||||
for i := 0; i < 6826; i++ {
|
assert.EqualValues(t, `a<span class="escaped-code-point" data-escaped="[U+00A0]"><span class="char"> </span></span>test`, out)
|
||||||
bs = append(bs, []byte("—")...)
|
setting.UI.AmbiguousUnicodeDetection = false
|
||||||
}
|
_, out = EscapeControlHTML("a test", &translation.MockLocale{})
|
||||||
_, _ = EscapeControlString(string(bs), &translation.MockLocale{})
|
assert.EqualValues(t, `a test`, out)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/storage"
|
||||||
repo_service "code.gitea.io/gitea/services/repository"
|
repo_service "code.gitea.io/gitea/services/repository"
|
||||||
|
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
|
@ -31,6 +32,10 @@ func countOrphanedRepos(ctx context.Context) (int64, error) {
|
||||||
|
|
||||||
// deleteOrphanedRepos delete repository where user of owner_id do not exist
|
// deleteOrphanedRepos delete repository where user of owner_id do not exist
|
||||||
func deleteOrphanedRepos(ctx context.Context) (int64, error) {
|
func deleteOrphanedRepos(ctx context.Context) (int64, error) {
|
||||||
|
if err := storage.Init(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
batchSize := db.MaxBatchInsertSize("repository")
|
batchSize := db.MaxBatchInsertSize("repository")
|
||||||
e := db.GetEngine(ctx)
|
e := db.GetEngine(ctx)
|
||||||
var deleted int64
|
var deleted int64
|
||||||
|
|
|
@ -14,7 +14,6 @@ import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git/internal" //nolint:depguard // only this file can use the internal type CmdArg, other files and packages should use AddXxx functions
|
"code.gitea.io/gitea/modules/git/internal" //nolint:depguard // only this file can use the internal type CmdArg, other files and packages should use AddXxx functions
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
@ -389,15 +388,11 @@ func (r *runStdError) IsExitCode(code int) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func bytesToString(b []byte) string {
|
|
||||||
return *(*string)(unsafe.Pointer(&b)) // that's what Golang's strings.Builder.String() does (go/src/strings/builder.go)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RunStdString runs the command with options and returns stdout/stderr as string. and store stderr to returned error (err combined with stderr).
|
// RunStdString runs the command with options and returns stdout/stderr as string. and store stderr to returned error (err combined with stderr).
|
||||||
func (c *Command) RunStdString(opts *RunOpts) (stdout, stderr string, runErr RunStdError) {
|
func (c *Command) RunStdString(opts *RunOpts) (stdout, stderr string, runErr RunStdError) {
|
||||||
stdoutBytes, stderrBytes, err := c.RunStdBytes(opts)
|
stdoutBytes, stderrBytes, err := c.RunStdBytes(opts)
|
||||||
stdout = bytesToString(stdoutBytes)
|
stdout = util.UnsafeBytesToString(stdoutBytes)
|
||||||
stderr = bytesToString(stderrBytes)
|
stderr = util.UnsafeBytesToString(stderrBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return stdout, stderr, &runStdError{err: err, stderr: stderr}
|
return stdout, stderr, &runStdError{err: err, stderr: stderr}
|
||||||
}
|
}
|
||||||
|
@ -432,7 +427,7 @@ func (c *Command) RunStdBytes(opts *RunOpts) (stdout, stderr []byte, runErr RunS
|
||||||
err := c.Run(newOpts)
|
err := c.Run(newOpts)
|
||||||
stderr = stderrBuf.Bytes()
|
stderr = stderrBuf.Bytes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, stderr, &runStdError{err: err, stderr: bytesToString(stderr)}
|
return nil, stderr, &runStdError{err: err, stderr: util.UnsafeBytesToString(stderr)}
|
||||||
}
|
}
|
||||||
// even if there is no err, there could still be some stderr output
|
// even if there is no err, there could still be some stderr output
|
||||||
return stdoutBuf.Bytes(), stderr, nil
|
return stdoutBuf.Bytes(), stderr, nil
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
gohtml "html"
|
gohtml "html"
|
||||||
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -55,7 +56,7 @@ func NewContext() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Code returns a HTML version of code string with chroma syntax highlighting classes and the matched lexer name
|
// Code returns a HTML version of code string with chroma syntax highlighting classes and the matched lexer name
|
||||||
func Code(fileName, language, code string) (string, string) {
|
func Code(fileName, language, code string) (output template.HTML, lexerName string) {
|
||||||
NewContext()
|
NewContext()
|
||||||
|
|
||||||
// diff view newline will be passed as empty, change to literal '\n' so it can be copied
|
// diff view newline will be passed as empty, change to literal '\n' so it can be copied
|
||||||
|
@ -65,7 +66,7 @@ func Code(fileName, language, code string) (string, string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(code) > sizeLimit {
|
if len(code) > sizeLimit {
|
||||||
return code, ""
|
return template.HTML(template.HTMLEscapeString(code)), ""
|
||||||
}
|
}
|
||||||
|
|
||||||
var lexer chroma.Lexer
|
var lexer chroma.Lexer
|
||||||
|
@ -102,13 +103,11 @@ func Code(fileName, language, code string) (string, string) {
|
||||||
cache.Add(fileName, lexer)
|
cache.Add(fileName, lexer)
|
||||||
}
|
}
|
||||||
|
|
||||||
lexerName := formatLexerName(lexer.Config().Name)
|
return CodeFromLexer(lexer, code), formatLexerName(lexer.Config().Name)
|
||||||
|
|
||||||
return CodeFromLexer(lexer, code), lexerName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CodeFromLexer returns a HTML version of code string with chroma syntax highlighting classes
|
// CodeFromLexer returns a HTML version of code string with chroma syntax highlighting classes
|
||||||
func CodeFromLexer(lexer chroma.Lexer, code string) string {
|
func CodeFromLexer(lexer chroma.Lexer, code string) template.HTML {
|
||||||
formatter := html.New(html.WithClasses(true),
|
formatter := html.New(html.WithClasses(true),
|
||||||
html.WithLineNumbers(false),
|
html.WithLineNumbers(false),
|
||||||
html.PreventSurroundingPre(true),
|
html.PreventSurroundingPre(true),
|
||||||
|
@ -120,23 +119,23 @@ func CodeFromLexer(lexer chroma.Lexer, code string) string {
|
||||||
iterator, err := lexer.Tokenise(nil, code)
|
iterator, err := lexer.Tokenise(nil, code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Can't tokenize code: %v", err)
|
log.Error("Can't tokenize code: %v", err)
|
||||||
return code
|
return template.HTML(template.HTMLEscapeString(code))
|
||||||
}
|
}
|
||||||
// style not used for live site but need to pass something
|
// style not used for live site but need to pass something
|
||||||
err = formatter.Format(htmlw, githubStyles, iterator)
|
err = formatter.Format(htmlw, githubStyles, iterator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Can't format code: %v", err)
|
log.Error("Can't format code: %v", err)
|
||||||
return code
|
return template.HTML(template.HTMLEscapeString(code))
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = htmlw.Flush()
|
_ = htmlw.Flush()
|
||||||
// Chroma will add newlines for certain lexers in order to highlight them properly
|
// Chroma will add newlines for certain lexers in order to highlight them properly
|
||||||
// Once highlighted, strip them here, so they don't cause copy/paste trouble in HTML output
|
// Once highlighted, strip them here, so they don't cause copy/paste trouble in HTML output
|
||||||
return strings.TrimSuffix(htmlbuf.String(), "\n")
|
return template.HTML(strings.TrimSuffix(htmlbuf.String(), "\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// File returns a slice of chroma syntax highlighted HTML lines of code and the matched lexer name
|
// File returns a slice of chroma syntax highlighted HTML lines of code and the matched lexer name
|
||||||
func File(fileName, language string, code []byte) ([]string, string, error) {
|
func File(fileName, language string, code []byte) ([]template.HTML, string, error) {
|
||||||
NewContext()
|
NewContext()
|
||||||
|
|
||||||
if len(code) > sizeLimit {
|
if len(code) > sizeLimit {
|
||||||
|
@ -183,14 +182,14 @@ func File(fileName, language string, code []byte) ([]string, string, error) {
|
||||||
tokensLines := chroma.SplitTokensIntoLines(iterator.Tokens())
|
tokensLines := chroma.SplitTokensIntoLines(iterator.Tokens())
|
||||||
htmlBuf := &bytes.Buffer{}
|
htmlBuf := &bytes.Buffer{}
|
||||||
|
|
||||||
lines := make([]string, 0, len(tokensLines))
|
lines := make([]template.HTML, 0, len(tokensLines))
|
||||||
for _, tokens := range tokensLines {
|
for _, tokens := range tokensLines {
|
||||||
iterator = chroma.Literator(tokens...)
|
iterator = chroma.Literator(tokens...)
|
||||||
err = formatter.Format(htmlBuf, githubStyles, iterator)
|
err = formatter.Format(htmlBuf, githubStyles, iterator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", fmt.Errorf("can't format code: %w", err)
|
return nil, "", fmt.Errorf("can't format code: %w", err)
|
||||||
}
|
}
|
||||||
lines = append(lines, htmlBuf.String())
|
lines = append(lines, template.HTML(htmlBuf.String()))
|
||||||
htmlBuf.Reset()
|
htmlBuf.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,9 +197,9 @@ func File(fileName, language string, code []byte) ([]string, string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// PlainText returns non-highlighted HTML for code
|
// PlainText returns non-highlighted HTML for code
|
||||||
func PlainText(code []byte) []string {
|
func PlainText(code []byte) []template.HTML {
|
||||||
r := bufio.NewReader(bytes.NewReader(code))
|
r := bufio.NewReader(bytes.NewReader(code))
|
||||||
m := make([]string, 0, bytes.Count(code, []byte{'\n'})+1)
|
m := make([]template.HTML, 0, bytes.Count(code, []byte{'\n'})+1)
|
||||||
for {
|
for {
|
||||||
content, err := r.ReadString('\n')
|
content, err := r.ReadString('\n')
|
||||||
if err != nil && err != io.EOF {
|
if err != nil && err != io.EOF {
|
||||||
|
@ -210,7 +209,7 @@ func PlainText(code []byte) []string {
|
||||||
if content == "" && err == io.EOF {
|
if content == "" && err == io.EOF {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
s := gohtml.EscapeString(content)
|
s := template.HTML(gohtml.EscapeString(content))
|
||||||
m = append(m, s)
|
m = append(m, s)
|
||||||
}
|
}
|
||||||
return m
|
return m
|
||||||
|
|
|
@ -4,21 +4,36 @@
|
||||||
package highlight
|
package highlight
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"html/template"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func lines(s string) []string {
|
func lines(s string) (out []template.HTML) {
|
||||||
return strings.Split(strings.ReplaceAll(strings.TrimSpace(s), `\n`, "\n"), "\n")
|
// "" => [], "a" => ["a"], "a\n" => ["a\n"], "a\nb" => ["a\n", "b"] (each line always includes EOL "\n" if it exists)
|
||||||
|
out = make([]template.HTML, 0)
|
||||||
|
s = strings.ReplaceAll(strings.ReplaceAll(strings.TrimSpace(s), "\n", ""), `\n`, "\n")
|
||||||
|
for {
|
||||||
|
if p := strings.IndexByte(s, '\n'); p != -1 {
|
||||||
|
out = append(out, template.HTML(s[:p+1]))
|
||||||
|
s = s[p+1:]
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s != "" {
|
||||||
|
out = append(out, template.HTML(s))
|
||||||
|
}
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFile(t *testing.T) {
|
func TestFile(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
code string
|
code string
|
||||||
want []string
|
want []template.HTML
|
||||||
lexerName string
|
lexerName string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
@ -99,10 +114,7 @@ c=2
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
out, lexerName, err := File(tt.name, "", []byte(tt.code))
|
out, lexerName, err := File(tt.name, "", []byte(tt.code))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
expected := strings.Join(tt.want, "\n")
|
assert.EqualValues(t, tt.want, out)
|
||||||
actual := strings.Join(out, "\n")
|
|
||||||
assert.Equal(t, strings.Count(actual, "<span"), strings.Count(actual, "</span>"))
|
|
||||||
assert.EqualValues(t, expected, actual)
|
|
||||||
assert.Equal(t, tt.lexerName, lexerName)
|
assert.Equal(t, tt.lexerName, lexerName)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -112,7 +124,7 @@ func TestPlainText(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
code string
|
code string
|
||||||
want []string
|
want []template.HTML
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "empty.py",
|
name: "empty.py",
|
||||||
|
@ -165,9 +177,7 @@ c=2`),
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
out := PlainText([]byte(tt.code))
|
out := PlainText([]byte(tt.code))
|
||||||
expected := strings.Join(tt.want, "\n")
|
assert.EqualValues(t, tt.want, out)
|
||||||
actual := strings.Join(out, "\n")
|
|
||||||
assert.EqualValues(t, expected, actual)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ package code
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"html/template"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/highlight"
|
"code.gitea.io/gitea/modules/highlight"
|
||||||
|
@ -22,7 +23,7 @@ type Result struct {
|
||||||
Language string
|
Language string
|
||||||
Color string
|
Color string
|
||||||
LineNumbers []int
|
LineNumbers []int
|
||||||
FormattedLines string
|
FormattedLines template.HTML
|
||||||
}
|
}
|
||||||
|
|
||||||
type SearchResultLanguages = internal.SearchResultLanguages
|
type SearchResultLanguages = internal.SearchResultLanguages
|
||||||
|
|
|
@ -29,12 +29,17 @@ func CleanValue(value []byte) []byte {
|
||||||
value = bytes.TrimSpace(value)
|
value = bytes.TrimSpace(value)
|
||||||
rs := bytes.Runes(value)
|
rs := bytes.Runes(value)
|
||||||
result := make([]rune, 0, len(rs))
|
result := make([]rune, 0, len(rs))
|
||||||
|
needsDash := false
|
||||||
for _, r := range rs {
|
for _, r := range rs {
|
||||||
if unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_' || r == '-' {
|
switch {
|
||||||
|
case unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_':
|
||||||
|
if needsDash && len(result) > 0 {
|
||||||
|
result = append(result, '-')
|
||||||
|
}
|
||||||
|
needsDash = false
|
||||||
result = append(result, unicode.ToLower(r))
|
result = append(result, unicode.ToLower(r))
|
||||||
}
|
default:
|
||||||
if unicode.IsSpace(r) {
|
needsDash = true
|
||||||
result = append(result, '-')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return []byte(string(result))
|
return []byte(string(result))
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// Copyright 2023 The Forgejo Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
package common
|
package common
|
||||||
|
|
||||||
|
@ -15,44 +16,45 @@ func TestCleanValue(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
// Github behavior test cases
|
// Github behavior test cases
|
||||||
{"", ""},
|
{"", ""},
|
||||||
{"test(0)", "test0"},
|
{"test.0.1", "test-0-1"},
|
||||||
{"test!1", "test1"},
|
{"test(0)", "test-0"},
|
||||||
{"test:2", "test2"},
|
{"test!1", "test-1"},
|
||||||
{"test*3", "test3"},
|
{"test:2", "test-2"},
|
||||||
{"test!4", "test4"},
|
{"test*3", "test-3"},
|
||||||
{"test:5", "test5"},
|
{"test!4", "test-4"},
|
||||||
{"test*6", "test6"},
|
{"test:5", "test-5"},
|
||||||
{"test:6 a", "test6-a"},
|
{"test*6", "test-6"},
|
||||||
{"test:6 !b", "test6-b"},
|
{"test:6 a", "test-6-a"},
|
||||||
{"test:ad # df", "testad--df"},
|
{"test:6 !b", "test-6-b"},
|
||||||
{"test:ad #23 df 2*/*", "testad-23-df-2"},
|
{"test:ad # df", "test-ad-df"},
|
||||||
{"test:ad 23 df 2*/*", "testad-23-df-2"},
|
{"test:ad #23 df 2*/*", "test-ad-23-df-2"},
|
||||||
{"test:ad # 23 df 2*/*", "testad--23-df-2"},
|
{"test:ad 23 df 2*/*", "test-ad-23-df-2"},
|
||||||
|
{"test:ad # 23 df 2*/*", "test-ad-23-df-2"},
|
||||||
{"Anchors in Markdown", "anchors-in-markdown"},
|
{"Anchors in Markdown", "anchors-in-markdown"},
|
||||||
{"a_b_c", "a_b_c"},
|
{"a_b_c", "a_b_c"},
|
||||||
{"a-b-c", "a-b-c"},
|
{"a-b-c", "a-b-c"},
|
||||||
{"a-b-c----", "a-b-c----"},
|
{"a-b-c----", "a-b-c"},
|
||||||
{"test:6a", "test6a"},
|
{"test:6a", "test-6a"},
|
||||||
{"test:a6", "testa6"},
|
{"test:a6", "test-a6"},
|
||||||
{"tes a a a a", "tes-a-a---a--a"},
|
{"tes a a a a", "tes-a-a-a-a"},
|
||||||
{" tes a a a a ", "tes-a-a---a--a"},
|
{" tes a a a a ", "tes-a-a-a-a"},
|
||||||
{"Header with \"double quotes\"", "header-with-double-quotes"},
|
{"Header with \"double quotes\"", "header-with-double-quotes"},
|
||||||
{"Placeholder to force scrolling on link's click", "placeholder-to-force-scrolling-on-links-click"},
|
{"Placeholder to force scrolling on link's click", "placeholder-to-force-scrolling-on-link-s-click"},
|
||||||
{"tes()", "tes"},
|
{"tes()", "tes"},
|
||||||
{"tes(0)", "tes0"},
|
{"tes(0)", "tes-0"},
|
||||||
{"tes{0}", "tes0"},
|
{"tes{0}", "tes-0"},
|
||||||
{"tes[0]", "tes0"},
|
{"tes[0]", "tes-0"},
|
||||||
{"test【0】", "test0"},
|
{"test【0】", "test-0"},
|
||||||
{"tes…@a", "tesa"},
|
{"tes…@a", "tes-a"},
|
||||||
{"tes¥& a", "tes-a"},
|
{"tes¥& a", "tes-a"},
|
||||||
{"tes= a", "tes-a"},
|
{"tes= a", "tes-a"},
|
||||||
{"tes|a", "tesa"},
|
{"tes|a", "tes-a"},
|
||||||
{"tes\\a", "tesa"},
|
{"tes\\a", "tes-a"},
|
||||||
{"tes/a", "tesa"},
|
{"tes/a", "tes-a"},
|
||||||
{"a啊啊b", "a啊啊b"},
|
{"a啊啊b", "a啊啊b"},
|
||||||
{"c🤔️🤔️d", "cd"},
|
{"c🤔️🤔️d", "c-d"},
|
||||||
{"a⚡a", "aa"},
|
{"a⚡a", "a-a"},
|
||||||
{"e.~f", "ef"},
|
{"e.~f", "e-f"},
|
||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
assert.Equal(t, []byte(test.expect), CleanValue([]byte(test.param)), test.param)
|
assert.Equal(t, []byte(test.expect), CleanValue([]byte(test.param)), test.param)
|
||||||
|
|
|
@ -852,7 +852,9 @@ func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
|
func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||||
if ctx.Metas == nil || ctx.Metas["mode"] == "document" {
|
// FIXME: the use of "mode" is quite dirty and hacky, for example: what is a "document"? how should it be rendered?
|
||||||
|
// The "mode" approach should be refactored to some other more clear&reliable way.
|
||||||
|
if ctx.Metas == nil || (ctx.Metas["mode"] == "document" && !ctx.IsWiki) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var (
|
var (
|
||||||
|
|
|
@ -87,7 +87,7 @@ func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error
|
||||||
}
|
}
|
||||||
lexer = chroma.Coalesce(lexer)
|
lexer = chroma.Coalesce(lexer)
|
||||||
|
|
||||||
if _, err := w.WriteString(highlight.CodeFromLexer(lexer, source)); err != nil {
|
if _, err := w.WriteString(string(highlight.CodeFromLexer(lexer, source))); err != nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -150,7 +150,7 @@ var (
|
||||||
DefaultPrivate: RepoCreatingLastUserVisibility,
|
DefaultPrivate: RepoCreatingLastUserVisibility,
|
||||||
DefaultPushCreatePrivate: true,
|
DefaultPushCreatePrivate: true,
|
||||||
MaxCreationLimit: -1,
|
MaxCreationLimit: -1,
|
||||||
PreferredLicenses: []string{"Apache License 2.0", "MIT License"},
|
PreferredLicenses: []string{"Apache-2.0", "MIT"},
|
||||||
DisableHTTPGit: false,
|
DisableHTTPGit: false,
|
||||||
AccessControlAllowOrigin: "",
|
AccessControlAllowOrigin: "",
|
||||||
UseCompatSSHURI: false,
|
UseCompatSSHURI: false,
|
||||||
|
|
|
@ -34,6 +34,8 @@ var UI = struct {
|
||||||
SearchRepoDescription bool
|
SearchRepoDescription bool
|
||||||
OnlyShowRelevantRepos bool
|
OnlyShowRelevantRepos bool
|
||||||
|
|
||||||
|
AmbiguousUnicodeDetection bool
|
||||||
|
|
||||||
Notification struct {
|
Notification struct {
|
||||||
MinTimeout time.Duration
|
MinTimeout time.Duration
|
||||||
TimeoutStep time.Duration
|
TimeoutStep time.Duration
|
||||||
|
@ -81,6 +83,9 @@ var UI = struct {
|
||||||
Reactions: []string{`+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, `rocket`, `eyes`},
|
Reactions: []string{`+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, `rocket`, `eyes`},
|
||||||
CustomEmojis: []string{`git`, `gitea`, `codeberg`, `gitlab`, `github`, `gogs`, `forgejo`},
|
CustomEmojis: []string{`git`, `gitea`, `codeberg`, `gitlab`, `github`, `gogs`, `forgejo`},
|
||||||
CustomEmojisMap: map[string]string{"git": ":git:", "gitea": ":gitea:", "codeberg": ":codeberg:", "gitlab": ":gitlab:", "github": ":github:", "gogs": ":gogs:", "forgejo": ":forgejo:"},
|
CustomEmojisMap: map[string]string{"git": ":git:", "gitea": ":gitea:", "codeberg": ":codeberg:", "gitlab": ":gitlab:", "github": ":github:", "gogs": ":gogs:", "forgejo": ":forgejo:"},
|
||||||
|
|
||||||
|
AmbiguousUnicodeDetection: true,
|
||||||
|
|
||||||
Notification: struct {
|
Notification: struct {
|
||||||
MinTimeout time.Duration
|
MinTimeout time.Duration
|
||||||
TimeoutStep time.Duration
|
TimeoutStep time.Duration
|
||||||
|
|
|
@ -15,9 +15,8 @@ type CreateUserOption struct {
|
||||||
FullName string `json:"full_name" binding:"MaxSize(100)"`
|
FullName string `json:"full_name" binding:"MaxSize(100)"`
|
||||||
// required: true
|
// required: true
|
||||||
// swagger:strfmt email
|
// swagger:strfmt email
|
||||||
Email string `json:"email" binding:"Required;Email;MaxSize(254)"`
|
Email string `json:"email" binding:"Required;Email;MaxSize(254)"`
|
||||||
// required: true
|
Password string `json:"password" binding:"MaxSize(255)"`
|
||||||
Password string `json:"password" binding:"Required;MaxSize(255)"`
|
|
||||||
MustChangePassword *bool `json:"must_change_password"`
|
MustChangePassword *bool `json:"must_change_password"`
|
||||||
SendNotify bool `json:"send_notify"`
|
SendNotify bool `json:"send_notify"`
|
||||||
Restricted *bool `json:"restricted"`
|
Restricted *bool `json:"restricted"`
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
package util
|
package util
|
||||||
|
|
||||||
import "github.com/yuin/goldmark/util"
|
import "unsafe"
|
||||||
|
|
||||||
func isSnakeCaseUpper(c byte) bool {
|
func isSnakeCaseUpper(c byte) bool {
|
||||||
return 'A' <= c && c <= 'Z'
|
return 'A' <= c && c <= 'Z'
|
||||||
|
@ -83,5 +83,15 @@ func ToSnakeCase(input string) string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return util.BytesToReadOnlyString(res)
|
return UnsafeBytesToString(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsafeBytesToString uses Go's unsafe package to convert a byte slice to a string.
|
||||||
|
// TODO: replace all "goldmark/util.BytesToReadOnlyString" with this official approach
|
||||||
|
func UnsafeBytesToString(b []byte) string {
|
||||||
|
return unsafe.String(unsafe.SliceData(b), len(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func UnsafeStringToBytes(s string) []byte {
|
||||||
|
return unsafe.Slice(unsafe.StringData(s), len(s))
|
||||||
}
|
}
|
||||||
|
|
|
@ -3551,7 +3551,7 @@ runs.commit = Commit
|
||||||
runs.scheduled = Scheduled
|
runs.scheduled = Scheduled
|
||||||
runs.pushed_by = pushed by
|
runs.pushed_by = pushed by
|
||||||
runs.invalid_workflow_helper = Workflow config file is invalid. Please check your config file: %s
|
runs.invalid_workflow_helper = Workflow config file is invalid. Please check your config file: %s
|
||||||
runs.no_matching_runner_helper = No matching runner: %s
|
runs.no_matching_online_runner_helper = No matching online runner with label: %s
|
||||||
runs.actor = Actor
|
runs.actor = Actor
|
||||||
runs.status = Status
|
runs.status = Status
|
||||||
runs.actors_no_select = All actors
|
runs.actors_no_select = All actors
|
||||||
|
|
18
package-lock.json
generated
18
package-lock.json
generated
|
@ -30,7 +30,7 @@
|
||||||
"katex": "0.16.9",
|
"katex": "0.16.9",
|
||||||
"license-checker-webpack-plugin": "0.2.1",
|
"license-checker-webpack-plugin": "0.2.1",
|
||||||
"lightningcss-loader": "2.1.0",
|
"lightningcss-loader": "2.1.0",
|
||||||
"mermaid": "10.5.0",
|
"mermaid": "10.6.1",
|
||||||
"mini-css-extract-plugin": "2.7.6",
|
"mini-css-extract-plugin": "2.7.6",
|
||||||
"minimatch": "9.0.3",
|
"minimatch": "9.0.3",
|
||||||
"monaco-editor": "0.44.0",
|
"monaco-editor": "0.44.0",
|
||||||
|
@ -999,11 +999,6 @@
|
||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@github/combobox-nav": {
|
|
||||||
"version": "2.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@github/combobox-nav/-/combobox-nav-2.3.0.tgz",
|
|
||||||
"integrity": "sha512-5CX03DbsLZ41dX5hKHyQKtg133U6lruX4TD9G0Zs4W8BpWy7lN8DJ6TYaeZN/V7x8K34coaqNYk/Y5ic7stfkg=="
|
|
||||||
},
|
|
||||||
"node_modules/@github/markdown-toolbar-element": {
|
"node_modules/@github/markdown-toolbar-element": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@github/markdown-toolbar-element/-/markdown-toolbar-element-2.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@github/markdown-toolbar-element/-/markdown-toolbar-element-2.2.1.tgz",
|
||||||
|
@ -1022,6 +1017,11 @@
|
||||||
"@github/combobox-nav": "^2.0.2"
|
"@github/combobox-nav": "^2.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@github/text-expander-element/node_modules/@github/combobox-nav": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@github/combobox-nav/-/combobox-nav-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-28kJUfzzPDYNyYsFCP/be8aXvEpjcEuciVENZlopcaUynS/4Pt9ll88Kl9l1D1Vy6a9+k+Km/YGJQ1e+gzG7SQ=="
|
||||||
|
},
|
||||||
"node_modules/@humanwhocodes/config-array": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.11.11",
|
"version": "0.11.11",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz",
|
||||||
|
@ -7530,9 +7530,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mermaid": {
|
"node_modules/mermaid": {
|
||||||
"version": "10.5.0",
|
"version": "10.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-10.5.0.tgz",
|
"resolved": "https://registry.npmmirror.com/mermaid/-/mermaid-10.6.1.tgz",
|
||||||
"integrity": "sha512-9l0o1uUod78D3/FVYPGSsgV+Z0tSnzLBDiC9rVzvelPxuO80HbN1oDr9ofpPETQy9XpypPQa26fr09VzEPfvWA==",
|
"integrity": "sha512-Hky0/RpOw/1il9X8AvzOEChfJtVvmXm+y7JML5C//ePYMy0/9jCEmW1E1g86x9oDfW9+iVEdTV/i+M6KWRNs4A==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@braintree/sanitize-url": "^6.0.1",
|
"@braintree/sanitize-url": "^6.0.1",
|
||||||
"@types/d3-scale": "^4.0.3",
|
"@types/d3-scale": "^4.0.3",
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
"katex": "0.16.9",
|
"katex": "0.16.9",
|
||||||
"license-checker-webpack-plugin": "0.2.1",
|
"license-checker-webpack-plugin": "0.2.1",
|
||||||
"lightningcss-loader": "2.1.0",
|
"lightningcss-loader": "2.1.0",
|
||||||
"mermaid": "10.5.0",
|
"mermaid": "10.6.1",
|
||||||
"mini-css-extract-plugin": "2.7.6",
|
"mini-css-extract-plugin": "2.7.6",
|
||||||
"minimatch": "9.0.3",
|
"minimatch": "9.0.3",
|
||||||
"monaco-editor": "0.44.0",
|
"monaco-editor": "0.44.0",
|
||||||
|
@ -82,6 +82,9 @@
|
||||||
"vite-string-plugin": "1.1.2",
|
"vite-string-plugin": "1.1.2",
|
||||||
"vitest": "0.34.6"
|
"vitest": "0.34.6"
|
||||||
},
|
},
|
||||||
|
"overrides": {
|
||||||
|
"@github/combobox-nav": "2.2.0"
|
||||||
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"defaults",
|
"defaults",
|
||||||
"not ie > 0",
|
"not ie > 0",
|
||||||
|
|
|
@ -25,10 +25,11 @@ func saveUploadChunk(st storage.ObjectStorage, ctx *ArtifactContext,
|
||||||
contentRange := ctx.Req.Header.Get("Content-Range")
|
contentRange := ctx.Req.Header.Get("Content-Range")
|
||||||
start, end, length := int64(0), int64(0), int64(0)
|
start, end, length := int64(0), int64(0), int64(0)
|
||||||
if _, err := fmt.Sscanf(contentRange, "bytes %d-%d/%d", &start, &end, &length); err != nil {
|
if _, err := fmt.Sscanf(contentRange, "bytes %d-%d/%d", &start, &end, &length); err != nil {
|
||||||
|
log.Warn("parse content range error: %v, content-range: %s", err, contentRange)
|
||||||
return -1, fmt.Errorf("parse content range error: %v", err)
|
return -1, fmt.Errorf("parse content range error: %v", err)
|
||||||
}
|
}
|
||||||
// build chunk store path
|
// build chunk store path
|
||||||
storagePath := fmt.Sprintf("tmp%d/%d-%d-%d.chunk", runID, artifact.ID, start, end)
|
storagePath := fmt.Sprintf("tmp%d/%d-%d-%d-%d.chunk", runID, runID, artifact.ID, start, end)
|
||||||
// use io.TeeReader to avoid reading all body to md5 sum.
|
// use io.TeeReader to avoid reading all body to md5 sum.
|
||||||
// it writes data to hasher after reading end
|
// it writes data to hasher after reading end
|
||||||
// if hash is not matched, delete the read-end result
|
// if hash is not matched, delete the read-end result
|
||||||
|
@ -57,6 +58,7 @@ func saveUploadChunk(st storage.ObjectStorage, ctx *ArtifactContext,
|
||||||
}
|
}
|
||||||
|
|
||||||
type chunkFileItem struct {
|
type chunkFileItem struct {
|
||||||
|
RunID int64
|
||||||
ArtifactID int64
|
ArtifactID int64
|
||||||
Start int64
|
Start int64
|
||||||
End int64
|
End int64
|
||||||
|
@ -66,9 +68,12 @@ type chunkFileItem struct {
|
||||||
func listChunksByRunID(st storage.ObjectStorage, runID int64) (map[int64][]*chunkFileItem, error) {
|
func listChunksByRunID(st storage.ObjectStorage, runID int64) (map[int64][]*chunkFileItem, error) {
|
||||||
storageDir := fmt.Sprintf("tmp%d", runID)
|
storageDir := fmt.Sprintf("tmp%d", runID)
|
||||||
var chunks []*chunkFileItem
|
var chunks []*chunkFileItem
|
||||||
if err := st.IterateObjects(storageDir, func(path string, obj storage.Object) error {
|
if err := st.IterateObjects(storageDir, func(fpath string, obj storage.Object) error {
|
||||||
item := chunkFileItem{Path: path}
|
baseName := filepath.Base(fpath)
|
||||||
if _, err := fmt.Sscanf(path, filepath.Join(storageDir, "%d-%d-%d.chunk"), &item.ArtifactID, &item.Start, &item.End); err != nil {
|
// when read chunks from storage, it only contains storage dir and basename,
|
||||||
|
// no matter the subdirectory setting in storage config
|
||||||
|
item := chunkFileItem{Path: storageDir + "/" + baseName}
|
||||||
|
if _, err := fmt.Sscanf(baseName, "%d-%d-%d-%d.chunk", &item.RunID, &item.ArtifactID, &item.Start, &item.End); err != nil {
|
||||||
return fmt.Errorf("parse content range error: %v", err)
|
return fmt.Errorf("parse content range error: %v", err)
|
||||||
}
|
}
|
||||||
chunks = append(chunks, &item)
|
chunks = append(chunks, &item)
|
||||||
|
|
|
@ -603,7 +603,10 @@ func ContainerRoutes() *web.Route {
|
||||||
})
|
})
|
||||||
|
|
||||||
r.Get("", container.ReqContainerAccess, container.DetermineSupport)
|
r.Get("", container.ReqContainerAccess, container.DetermineSupport)
|
||||||
r.Get("/token", container.Authenticate)
|
r.Group("/token", func() {
|
||||||
|
r.Get("", container.Authenticate)
|
||||||
|
r.Post("", container.AuthenticateNotImplemented)
|
||||||
|
})
|
||||||
r.Get("/_catalog", container.ReqContainerAccess, container.GetRepositoryList)
|
r.Get("/_catalog", container.ReqContainerAccess, container.GetRepositoryList)
|
||||||
r.Group("/{username}", func() {
|
r.Group("/{username}", func() {
|
||||||
r.Group("/{image}", func() {
|
r.Group("/{image}", func() {
|
||||||
|
|
|
@ -156,6 +156,17 @@ func Authenticate(ctx *context.Context) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://distribution.github.io/distribution/spec/auth/oauth/
|
||||||
|
func AuthenticateNotImplemented(ctx *context.Context) {
|
||||||
|
// This optional endpoint can be used to authenticate a client.
|
||||||
|
// It must implement the specification described in:
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc6749
|
||||||
|
// https://distribution.github.io/distribution/spec/auth/oauth/
|
||||||
|
// Purpose of this stub is to respond with 404 Not Found instead of 405 Method Not Allowed.
|
||||||
|
|
||||||
|
ctx.Status(http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
// https://docs.docker.com/registry/spec/api/#listing-repositories
|
// https://docs.docker.com/registry/spec/api/#listing-repositories
|
||||||
func GetRepositoryList(ctx *context.Context) {
|
func GetRepositoryList(ctx *context.Context) {
|
||||||
n := ctx.FormInt("n")
|
n := ctx.FormInt("n")
|
||||||
|
|
|
@ -93,18 +93,28 @@ func CreateUser(ctx *context.APIContext) {
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !password.IsComplexEnough(form.Password) {
|
|
||||||
err := errors.New("PasswordComplexity")
|
if u.LoginType == auth.Plain {
|
||||||
ctx.Error(http.StatusBadRequest, "PasswordComplexity", err)
|
if len(form.Password) < setting.MinPasswordLength {
|
||||||
return
|
err := errors.New("PasswordIsRequired")
|
||||||
}
|
ctx.Error(http.StatusBadRequest, "PasswordIsRequired", err)
|
||||||
pwned, err := password.IsPwned(ctx, form.Password)
|
return
|
||||||
if pwned {
|
}
|
||||||
if err != nil {
|
|
||||||
log.Error(err.Error())
|
if !password.IsComplexEnough(form.Password) {
|
||||||
|
err := errors.New("PasswordComplexity")
|
||||||
|
ctx.Error(http.StatusBadRequest, "PasswordComplexity", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pwned, err := password.IsPwned(ctx, form.Password)
|
||||||
|
if pwned {
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err.Error())
|
||||||
|
}
|
||||||
|
ctx.Error(http.StatusBadRequest, "PasswordPwned", errors.New("PasswordPwned"))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
ctx.Error(http.StatusBadRequest, "PasswordPwned", errors.New("PasswordPwned"))
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
overwriteDefault := &user_model.CreateUserOverwriteOptions{
|
overwriteDefault := &user_model.CreateUserOverwriteOptions{
|
||||||
|
|
|
@ -787,6 +787,24 @@ func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.APIC
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func individualPermsChecker(ctx *context.APIContext) {
|
||||||
|
// org permissions have been checked in context.OrgAssignment(), but individual permissions haven't been checked.
|
||||||
|
if ctx.ContextUser.IsIndividual() {
|
||||||
|
switch {
|
||||||
|
case ctx.ContextUser.Visibility == api.VisibleTypePrivate:
|
||||||
|
if ctx.Doer == nil || (ctx.ContextUser.ID != ctx.Doer.ID && !ctx.Doer.IsAdmin) {
|
||||||
|
ctx.NotFound("Visit Project", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case ctx.ContextUser.Visibility == api.VisibleTypeLimited:
|
||||||
|
if ctx.Doer == nil {
|
||||||
|
ctx.NotFound("Visit Project", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Routes registers all v1 APIs routes to web application.
|
// Routes registers all v1 APIs routes to web application.
|
||||||
func Routes() *web.Route {
|
func Routes() *web.Route {
|
||||||
m := web.NewRoute()
|
m := web.NewRoute()
|
||||||
|
@ -887,7 +905,7 @@ func Routes() *web.Route {
|
||||||
}, reqSelfOrAdmin(), reqBasicOrRevProxyAuth())
|
}, reqSelfOrAdmin(), reqBasicOrRevProxyAuth())
|
||||||
|
|
||||||
m.Get("/activities/feeds", user.ListUserActivityFeeds)
|
m.Get("/activities/feeds", user.ListUserActivityFeeds)
|
||||||
}, context_service.UserAssignmentAPI())
|
}, context_service.UserAssignmentAPI(), individualPermsChecker)
|
||||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser))
|
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser))
|
||||||
|
|
||||||
// Users (requires user scope)
|
// Users (requires user scope)
|
||||||
|
|
|
@ -262,12 +262,11 @@ func CreateBranch(ctx *context.APIContext) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = repo_service.CreateNewBranchFromCommit(ctx, ctx.Doer, ctx.Repo.Repository, oldCommit.ID.String(), opt.BranchName)
|
err = repo_service.CreateNewBranchFromCommit(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, oldCommit.ID.String(), opt.BranchName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if git_model.IsErrBranchNotExist(err) {
|
if git_model.IsErrBranchNotExist(err) {
|
||||||
ctx.Error(http.StatusNotFound, "", "The old branch does not exist")
|
ctx.Error(http.StatusNotFound, "", "The old branch does not exist")
|
||||||
}
|
} else if models.IsErrTagAlreadyExists(err) {
|
||||||
if models.IsErrTagAlreadyExists(err) {
|
|
||||||
ctx.Error(http.StatusConflict, "", "The branch with the same tag already exists.")
|
ctx.Error(http.StatusConflict, "", "The branch with the same tag already exists.")
|
||||||
} else if git_model.IsErrBranchAlreadyExists(err) || git.IsErrPushOutOfDate(err) {
|
} else if git_model.IsErrBranchAlreadyExists(err) || git.IsErrPushOutOfDate(err) {
|
||||||
ctx.Error(http.StatusConflict, "", "The branch already exists.")
|
ctx.Error(http.StatusConflict, "", "The branch already exists.")
|
||||||
|
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/context"
|
"code.gitea.io/gitea/modules/context"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/routers/web/repo"
|
"code.gitea.io/gitea/routers/web/repo"
|
||||||
"code.gitea.io/gitea/services/convert"
|
"code.gitea.io/gitea/services/convert"
|
||||||
|
|
||||||
|
@ -77,6 +78,7 @@ func List(ctx *context.Context) {
|
||||||
// Get all runner labels
|
// Get all runner labels
|
||||||
opts := actions_model.FindRunnerOptions{
|
opts := actions_model.FindRunnerOptions{
|
||||||
RepoID: ctx.Repo.Repository.ID,
|
RepoID: ctx.Repo.Repository.ID,
|
||||||
|
IsOnline: util.OptionalBoolTrue,
|
||||||
WithAvailable: true,
|
WithAvailable: true,
|
||||||
}
|
}
|
||||||
runners, err := actions_model.FindRunners(ctx, opts)
|
runners, err := actions_model.FindRunners(ctx, opts)
|
||||||
|
@ -114,7 +116,7 @@ func List(ctx *context.Context) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !allRunnerLabels.Contains(ro) {
|
if !allRunnerLabels.Contains(ro) {
|
||||||
workflow.ErrMsg = ctx.Locale.Tr("actions.runs.no_matching_runner_helper", ro)
|
workflow.ErrMsg = ctx.Locale.Tr("actions.runs.no_matching_online_runner_helper", ro)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -125,7 +125,7 @@ func RefBlame(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
type blameResult struct {
|
type blameResult struct {
|
||||||
Parts []git.BlamePart
|
Parts []*git.BlamePart
|
||||||
UsesIgnoreRevs bool
|
UsesIgnoreRevs bool
|
||||||
FaultyIgnoreRevsFile bool
|
FaultyIgnoreRevsFile bool
|
||||||
}
|
}
|
||||||
|
@ -170,7 +170,9 @@ func performBlame(ctx *context.Context, repoPath string, commit *git.Commit, fil
|
||||||
func fillBlameResult(br *git.BlameReader, r *blameResult) error {
|
func fillBlameResult(br *git.BlameReader, r *blameResult) error {
|
||||||
r.UsesIgnoreRevs = br.UsesIgnoreRevs()
|
r.UsesIgnoreRevs = br.UsesIgnoreRevs()
|
||||||
|
|
||||||
r.Parts = make([]git.BlamePart, 0, 5)
|
previousHelper := make(map[string]*git.BlamePart)
|
||||||
|
|
||||||
|
r.Parts = make([]*git.BlamePart, 0, 5)
|
||||||
for {
|
for {
|
||||||
blamePart, err := br.NextPart()
|
blamePart, err := br.NextPart()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -179,13 +181,23 @@ func fillBlameResult(br *git.BlameReader, r *blameResult) error {
|
||||||
if blamePart == nil {
|
if blamePart == nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
r.Parts = append(r.Parts, *blamePart)
|
|
||||||
|
if prev, ok := previousHelper[blamePart.Sha]; ok {
|
||||||
|
if blamePart.PreviousSha == "" {
|
||||||
|
blamePart.PreviousSha = prev.PreviousSha
|
||||||
|
blamePart.PreviousPath = prev.PreviousPath
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
previousHelper[blamePart.Sha] = blamePart
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Parts = append(r.Parts, blamePart)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func processBlameParts(ctx *context.Context, blameParts []git.BlamePart) map[string]*user_model.UserCommit {
|
func processBlameParts(ctx *context.Context, blameParts []*git.BlamePart) map[string]*user_model.UserCommit {
|
||||||
// store commit data by SHA to look up avatar info etc
|
// store commit data by SHA to look up avatar info etc
|
||||||
commitNames := make(map[string]*user_model.UserCommit)
|
commitNames := make(map[string]*user_model.UserCommit)
|
||||||
// and as blameParts can reference the same commits multiple
|
// and as blameParts can reference the same commits multiple
|
||||||
|
@ -227,7 +239,7 @@ func processBlameParts(ctx *context.Context, blameParts []git.BlamePart) map[str
|
||||||
return commitNames
|
return commitNames
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderBlame(ctx *context.Context, blameParts []git.BlamePart, commitNames map[string]*user_model.UserCommit) {
|
func renderBlame(ctx *context.Context, blameParts []*git.BlamePart, commitNames map[string]*user_model.UserCommit) {
|
||||||
repoLink := ctx.Repo.RepoLink
|
repoLink := ctx.Repo.RepoLink
|
||||||
|
|
||||||
language := ""
|
language := ""
|
||||||
|
@ -310,8 +322,7 @@ func renderBlame(ctx *context.Context, blameParts []git.BlamePart, commitNames m
|
||||||
lexerName = lexerNameForLine
|
lexerName = lexerNameForLine
|
||||||
}
|
}
|
||||||
|
|
||||||
br.EscapeStatus, line = charset.EscapeControlHTML(line, ctx.Locale)
|
br.EscapeStatus, br.Code = charset.EscapeControlHTML(line, ctx.Locale)
|
||||||
br.Code = gotemplate.HTML(line)
|
|
||||||
rows = append(rows, br)
|
rows = append(rows, br)
|
||||||
escapeStatus = escapeStatus.Or(br.EscapeStatus)
|
escapeStatus = escapeStatus.Or(br.EscapeStatus)
|
||||||
}
|
}
|
||||||
|
|
|
@ -191,9 +191,9 @@ func CreateBranch(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
err = release_service.CreateNewTag(ctx, ctx.Doer, ctx.Repo.Repository, target, form.NewBranchName, "")
|
err = release_service.CreateNewTag(ctx, ctx.Doer, ctx.Repo.Repository, target, form.NewBranchName, "")
|
||||||
} else if ctx.Repo.IsViewBranch {
|
} else if ctx.Repo.IsViewBranch {
|
||||||
err = repo_service.CreateNewBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.BranchName, form.NewBranchName)
|
err = repo_service.CreateNewBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.Repo.BranchName, form.NewBranchName)
|
||||||
} else {
|
} else {
|
||||||
err = repo_service.CreateNewBranchFromCommit(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.CommitID, form.NewBranchName)
|
err = repo_service.CreateNewBranchFromCommit(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.Repo.CommitID, form.NewBranchName)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if models.IsErrProtectedTagName(err) {
|
if models.IsErrProtectedTagName(err) {
|
||||||
|
|
|
@ -193,16 +193,7 @@ func SoftDeleteContentHistory(ctx *context.Context) {
|
||||||
var comment *issues_model.Comment
|
var comment *issues_model.Comment
|
||||||
var history *issues_model.ContentHistory
|
var history *issues_model.ContentHistory
|
||||||
var err error
|
var err error
|
||||||
if commentID != 0 {
|
|
||||||
if comment, err = issues_model.GetCommentByID(ctx, commentID); err != nil {
|
|
||||||
log.Error("can not get comment for issue content history %v. err=%v", historyID, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if comment.IssueID != issue.ID {
|
|
||||||
ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if history, err = issues_model.GetIssueContentHistoryByID(ctx, historyID); err != nil {
|
if history, err = issues_model.GetIssueContentHistoryByID(ctx, historyID); err != nil {
|
||||||
log.Error("can not get issue content history %v. err=%v", historyID, err)
|
log.Error("can not get issue content history %v. err=%v", historyID, err)
|
||||||
return
|
return
|
||||||
|
@ -211,6 +202,21 @@ func SoftDeleteContentHistory(ctx *context.Context) {
|
||||||
ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
|
ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if commentID != 0 {
|
||||||
|
if history.CommentID != commentID {
|
||||||
|
ctx.NotFound("CompareCommentID", issues_model.ErrCommentNotExist{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if comment, err = issues_model.GetCommentByID(ctx, commentID); err != nil {
|
||||||
|
log.Error("can not get comment for issue content history %v. err=%v", historyID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if comment.IssueID != issue.ID {
|
||||||
|
ctx.NotFound("CompareIssueID", issues_model.ErrCommentNotExist{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
canSoftDelete := canSoftDeleteContentHistory(ctx, issue, comment, history)
|
canSoftDelete := canSoftDeleteContentHistory(ctx, issue, comment, history)
|
||||||
if !canSoftDelete {
|
if !canSoftDelete {
|
||||||
|
|
|
@ -89,8 +89,10 @@ func IssuePinMove(ctx *context.Context) {
|
||||||
log.Error(err.Error())
|
log.Error(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if issue.RepoID != ctx.Repo.Repository.ID {
|
if issue.RepoID != ctx.Repo.Repository.ID {
|
||||||
ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
|
ctx.Status(http.StatusNotFound)
|
||||||
|
log.Error("Issue does not belong to this repository")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
gocontext "context"
|
gocontext "context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html/template"
|
||||||
"image"
|
"image"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -317,19 +318,18 @@ func renderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.Tr
|
||||||
}, rd)
|
}, rd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Render failed for %s in %-v: %v Falling back to rendering source", readmeFile.Name(), ctx.Repo.Repository, err)
|
log.Error("Render failed for %s in %-v: %v Falling back to rendering source", readmeFile.Name(), ctx.Repo.Repository, err)
|
||||||
buf := &bytes.Buffer{}
|
delete(ctx.Data, "IsMarkup")
|
||||||
ctx.Data["EscapeStatus"], _ = charset.EscapeControlStringReader(rd, buf, ctx.Locale)
|
|
||||||
ctx.Data["FileContent"] = buf.String()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ctx.Data["IsPlainText"] = true
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
ctx.Data["EscapeStatus"], err = charset.EscapeControlStringReader(rd, buf, ctx.Locale)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Read failed: %v", err)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ctx.Data["FileContent"] = buf.String()
|
if ctx.Data["IsMarkup"] != true {
|
||||||
|
ctx.Data["IsPlainText"] = true
|
||||||
|
content, err := io.ReadAll(rd)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Read readme content failed: %v", err)
|
||||||
|
}
|
||||||
|
contentEscaped := template.HTMLEscapeString(util.UnsafeBytesToString(content))
|
||||||
|
ctx.Data["EscapeStatus"], ctx.Data["FileContent"] = charset.EscapeControlHTML(template.HTML(contentEscaped), ctx.Locale)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -611,7 +611,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func markupRender(ctx *context.Context, renderCtx *markup.RenderContext, input io.Reader) (escaped *charset.EscapeStatus, output string, err error) {
|
func markupRender(ctx *context.Context, renderCtx *markup.RenderContext, input io.Reader) (escaped *charset.EscapeStatus, output template.HTML, err error) {
|
||||||
markupRd, markupWr := io.Pipe()
|
markupRd, markupWr := io.Pipe()
|
||||||
defer markupWr.Close()
|
defer markupWr.Close()
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
|
@ -619,7 +619,7 @@ func markupRender(ctx *context.Context, renderCtx *markup.RenderContext, input i
|
||||||
sb := &strings.Builder{}
|
sb := &strings.Builder{}
|
||||||
// We allow NBSP here this is rendered
|
// We allow NBSP here this is rendered
|
||||||
escaped, _ = charset.EscapeControlReader(markupRd, sb, ctx.Locale, charset.RuneNBSP)
|
escaped, _ = charset.EscapeControlReader(markupRd, sb, ctx.Locale, charset.RuneNBSP)
|
||||||
output = sb.String()
|
output = template.HTML(sb.String())
|
||||||
close(done)
|
close(done)
|
||||||
}()
|
}()
|
||||||
err = markup.Render(renderCtx, input, markupWr)
|
err = markup.Render(renderCtx, input, markupWr)
|
||||||
|
|
|
@ -798,6 +798,24 @@ func registerRoutes(m *web.Route) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
individualPermsChecker := func(ctx *context.Context) {
|
||||||
|
// org permissions have been checked in context.OrgAssignment(), but individual permissions haven't been checked.
|
||||||
|
if ctx.ContextUser.IsIndividual() {
|
||||||
|
switch {
|
||||||
|
case ctx.ContextUser.Visibility == structs.VisibleTypePrivate:
|
||||||
|
if ctx.Doer == nil || (ctx.ContextUser.ID != ctx.Doer.ID && !ctx.Doer.IsAdmin) {
|
||||||
|
ctx.NotFound("Visit Project", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case ctx.ContextUser.Visibility == structs.VisibleTypeLimited:
|
||||||
|
if ctx.Doer == nil {
|
||||||
|
ctx.NotFound("Visit Project", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ***** START: Organization *****
|
// ***** START: Organization *****
|
||||||
m.Group("/org", func() {
|
m.Group("/org", func() {
|
||||||
m.Group("/{org}", func() {
|
m.Group("/{org}", func() {
|
||||||
|
@ -984,11 +1002,11 @@ func registerRoutes(m *web.Route) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
}, reqUnitAccess(unit.TypeProjects, perm.AccessModeRead, true), individualPermsChecker)
|
||||||
|
|
||||||
m.Group("", func() {
|
m.Group("", func() {
|
||||||
m.Get("/code", user.CodeSearch)
|
m.Get("/code", user.CodeSearch)
|
||||||
}, reqUnitAccess(unit.TypeCode, perm.AccessModeRead, false))
|
}, reqUnitAccess(unit.TypeCode, perm.AccessModeRead, false), individualPermsChecker)
|
||||||
}, ignSignIn, context_service.UserAssignmentWeb(), context.OrgAssignment()) // for "/{username}/-" (packages, projects, code)
|
}, ignSignIn, context_service.UserAssignmentWeb(), context.OrgAssignment()) // for "/{username}/-" (packages, projects, code)
|
||||||
|
|
||||||
m.Group("/{username}/{reponame}", func() {
|
m.Group("/{username}/{reponame}", func() {
|
||||||
|
|
|
@ -422,7 +422,7 @@ func handleSchedules(
|
||||||
OwnerID: input.Repo.OwnerID,
|
OwnerID: input.Repo.OwnerID,
|
||||||
WorkflowID: dwf.EntryName,
|
WorkflowID: dwf.EntryName,
|
||||||
TriggerUserID: input.Doer.ID,
|
TriggerUserID: input.Doer.ID,
|
||||||
Ref: input.Ref,
|
Ref: input.Repo.DefaultBranch,
|
||||||
CommitSHA: commit.ID.String(),
|
CommitSHA: commit.ID.String(),
|
||||||
Event: input.Event,
|
Event: input.Event,
|
||||||
EventPayload: string(p),
|
EventPayload: string(p),
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: Copyright the Forgejo contributors
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package oauth2
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
var HTTPClient *http.Client
|
|
|
@ -63,9 +63,7 @@ func init() {
|
||||||
if setting.OAuth2Client.EnableAutoRegistration {
|
if setting.OAuth2Client.EnableAutoRegistration {
|
||||||
scopes = append(scopes, "user:email")
|
scopes = append(scopes, "user:email")
|
||||||
}
|
}
|
||||||
provider := github.NewCustomisedURL(clientID, secret, callbackURL, custom.AuthURL, custom.TokenURL, custom.ProfileURL, custom.EmailURL, scopes...)
|
return github.NewCustomisedURL(clientID, secret, callbackURL, custom.AuthURL, custom.TokenURL, custom.ProfileURL, custom.EmailURL, scopes...), nil
|
||||||
provider.HTTPClient = HTTPClient
|
|
||||||
return provider, nil
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
RegisterGothProvider(NewCustomProvider(
|
RegisterGothProvider(NewCustomProvider(
|
||||||
|
@ -75,9 +73,7 @@ func init() {
|
||||||
ProfileURL: availableAttribute(gitlab.ProfileURL),
|
ProfileURL: availableAttribute(gitlab.ProfileURL),
|
||||||
}, func(clientID, secret, callbackURL string, custom *CustomURLMapping, scopes []string) (goth.Provider, error) {
|
}, func(clientID, secret, callbackURL string, custom *CustomURLMapping, scopes []string) (goth.Provider, error) {
|
||||||
scopes = append(scopes, "read_user")
|
scopes = append(scopes, "read_user")
|
||||||
provider := gitlab.NewCustomisedURL(clientID, secret, callbackURL, custom.AuthURL, custom.TokenURL, custom.ProfileURL, scopes...)
|
return gitlab.NewCustomisedURL(clientID, secret, callbackURL, custom.AuthURL, custom.TokenURL, custom.ProfileURL, scopes...), nil
|
||||||
provider.HTTPClient = HTTPClient
|
|
||||||
return provider, nil
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
RegisterGothProvider(NewCustomProvider(
|
RegisterGothProvider(NewCustomProvider(
|
||||||
|
@ -87,9 +83,7 @@ func init() {
|
||||||
ProfileURL: requiredAttribute(gitea.ProfileURL),
|
ProfileURL: requiredAttribute(gitea.ProfileURL),
|
||||||
},
|
},
|
||||||
func(clientID, secret, callbackURL string, custom *CustomURLMapping, scopes []string) (goth.Provider, error) {
|
func(clientID, secret, callbackURL string, custom *CustomURLMapping, scopes []string) (goth.Provider, error) {
|
||||||
provider := gitea.NewCustomisedURL(clientID, secret, callbackURL, custom.AuthURL, custom.TokenURL, custom.ProfileURL, scopes...)
|
return gitea.NewCustomisedURL(clientID, secret, callbackURL, custom.AuthURL, custom.TokenURL, custom.ProfileURL, scopes...), nil
|
||||||
provider.HTTPClient = HTTPClient
|
|
||||||
return provider, nil
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
RegisterGothProvider(NewCustomProvider(
|
RegisterGothProvider(NewCustomProvider(
|
||||||
|
@ -99,9 +93,7 @@ func init() {
|
||||||
ProfileURL: requiredAttribute(nextcloud.ProfileURL),
|
ProfileURL: requiredAttribute(nextcloud.ProfileURL),
|
||||||
},
|
},
|
||||||
func(clientID, secret, callbackURL string, custom *CustomURLMapping, scopes []string) (goth.Provider, error) {
|
func(clientID, secret, callbackURL string, custom *CustomURLMapping, scopes []string) (goth.Provider, error) {
|
||||||
provider := nextcloud.NewCustomisedURL(clientID, secret, callbackURL, custom.AuthURL, custom.TokenURL, custom.ProfileURL, scopes...)
|
return nextcloud.NewCustomisedURL(clientID, secret, callbackURL, custom.AuthURL, custom.TokenURL, custom.ProfileURL, scopes...), nil
|
||||||
provider.HTTPClient = HTTPClient
|
|
||||||
return provider, nil
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
RegisterGothProvider(NewCustomProvider(
|
RegisterGothProvider(NewCustomProvider(
|
||||||
|
@ -109,9 +101,7 @@ func init() {
|
||||||
AuthURL: requiredAttribute(mastodon.InstanceURL),
|
AuthURL: requiredAttribute(mastodon.InstanceURL),
|
||||||
},
|
},
|
||||||
func(clientID, secret, callbackURL string, custom *CustomURLMapping, scopes []string) (goth.Provider, error) {
|
func(clientID, secret, callbackURL string, custom *CustomURLMapping, scopes []string) (goth.Provider, error) {
|
||||||
provider := mastodon.NewCustomisedURL(clientID, secret, callbackURL, custom.AuthURL, scopes...)
|
return mastodon.NewCustomisedURL(clientID, secret, callbackURL, custom.AuthURL, scopes...), nil
|
||||||
provider.HTTPClient = HTTPClient
|
|
||||||
return provider, nil
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
RegisterGothProvider(NewCustomProvider(
|
RegisterGothProvider(NewCustomProvider(
|
||||||
|
@ -124,12 +114,10 @@ func init() {
|
||||||
azureScopes[i] = azureadv2.ScopeType(scope)
|
azureScopes[i] = azureadv2.ScopeType(scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
provider := azureadv2.New(clientID, secret, callbackURL, azureadv2.ProviderOptions{
|
return azureadv2.New(clientID, secret, callbackURL, azureadv2.ProviderOptions{
|
||||||
Tenant: azureadv2.TenantType(custom.Tenant),
|
Tenant: azureadv2.TenantType(custom.Tenant),
|
||||||
Scopes: azureScopes,
|
Scopes: azureScopes,
|
||||||
})
|
}), nil
|
||||||
provider.HTTPClient = HTTPClient
|
|
||||||
return provider, nil
|
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,6 @@ func (o *OpenIDProvider) CreateGothProvider(providerName, callbackURL string, so
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Failed to create OpenID Connect Provider with name '%s' with url '%s': %v", providerName, source.OpenIDConnectAutoDiscoveryURL, err)
|
log.Warn("Failed to create OpenID Connect Provider with name '%s' with url '%s': %v", providerName, source.OpenIDConnectAutoDiscoveryURL, err)
|
||||||
}
|
}
|
||||||
provider.HTTPClient = HTTPClient
|
|
||||||
return provider, err
|
return provider, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -84,13 +84,15 @@ func (t *Task) RunWithUser(doer *user_model.User, config Config) {
|
||||||
t.lock.Unlock()
|
t.lock.Unlock()
|
||||||
defer func() {
|
defer func() {
|
||||||
taskStatusTable.Stop(t.Name)
|
taskStatusTable.Stop(t.Name)
|
||||||
if err := recover(); err != nil {
|
|
||||||
// Recover a panic within the
|
|
||||||
combinedErr := fmt.Errorf("%s\n%s", err, log.Stack(2))
|
|
||||||
log.Error("PANIC whilst running task: %s Value: %v", t.Name, combinedErr)
|
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
graceful.GetManager().RunWithShutdownContext(func(baseCtx context.Context) {
|
graceful.GetManager().RunWithShutdownContext(func(baseCtx context.Context) {
|
||||||
|
defer func() {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
// Recover a panic within the execution of the task.
|
||||||
|
combinedErr := fmt.Errorf("%s\n%s", err, log.Stack(2))
|
||||||
|
log.Error("PANIC whilst running task: %s Value: %v", t.Name, combinedErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
// Store the time of this run, before the function is executed, so it
|
// Store the time of this run, before the function is executed, so it
|
||||||
// matches the behavior of what the cron library does.
|
// matches the behavior of what the cron library does.
|
||||||
t.lock.Lock()
|
t.lock.Lock()
|
||||||
|
|
|
@ -285,15 +285,15 @@ type DiffInline struct {
|
||||||
|
|
||||||
// DiffInlineWithUnicodeEscape makes a DiffInline with hidden unicode characters escaped
|
// DiffInlineWithUnicodeEscape makes a DiffInline with hidden unicode characters escaped
|
||||||
func DiffInlineWithUnicodeEscape(s template.HTML, locale translation.Locale) DiffInline {
|
func DiffInlineWithUnicodeEscape(s template.HTML, locale translation.Locale) DiffInline {
|
||||||
status, content := charset.EscapeControlHTML(string(s), locale)
|
status, content := charset.EscapeControlHTML(s, locale)
|
||||||
return DiffInline{EscapeStatus: status, Content: template.HTML(content)}
|
return DiffInline{EscapeStatus: status, Content: content}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DiffInlineWithHighlightCode makes a DiffInline with code highlight and hidden unicode characters escaped
|
// DiffInlineWithHighlightCode makes a DiffInline with code highlight and hidden unicode characters escaped
|
||||||
func DiffInlineWithHighlightCode(fileName, language, code string, locale translation.Locale) DiffInline {
|
func DiffInlineWithHighlightCode(fileName, language, code string, locale translation.Locale) DiffInline {
|
||||||
highlighted, _ := highlight.Code(fileName, language, code)
|
highlighted, _ := highlight.Code(fileName, language, code)
|
||||||
status, content := charset.EscapeControlHTML(highlighted, locale)
|
status, content := charset.EscapeControlHTML(highlighted, locale)
|
||||||
return DiffInline{EscapeStatus: status, Content: template.HTML(content)}
|
return DiffInline{EscapeStatus: status, Content: content}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetComputedInlineDiffFor computes inline diff for the given line.
|
// GetComputedInlineDiffFor computes inline diff for the given line.
|
||||||
|
|
|
@ -93,10 +93,10 @@ func (hcd *highlightCodeDiff) diffWithHighlight(filename, language, codeA, codeB
|
||||||
highlightCodeA, _ := highlight.Code(filename, language, codeA)
|
highlightCodeA, _ := highlight.Code(filename, language, codeA)
|
||||||
highlightCodeB, _ := highlight.Code(filename, language, codeB)
|
highlightCodeB, _ := highlight.Code(filename, language, codeB)
|
||||||
|
|
||||||
highlightCodeA = hcd.convertToPlaceholders(highlightCodeA)
|
convertedCodeA := hcd.convertToPlaceholders(string(highlightCodeA))
|
||||||
highlightCodeB = hcd.convertToPlaceholders(highlightCodeB)
|
convertedCodeB := hcd.convertToPlaceholders(string(highlightCodeB))
|
||||||
|
|
||||||
diffs := diffMatchPatch.DiffMain(highlightCodeA, highlightCodeB, true)
|
diffs := diffMatchPatch.DiffMain(convertedCodeA, convertedCodeB, true)
|
||||||
diffs = diffMatchPatch.DiffCleanupEfficiency(diffs)
|
diffs = diffMatchPatch.DiffCleanupEfficiency(diffs)
|
||||||
|
|
||||||
for i := range diffs {
|
for i := range diffs {
|
||||||
|
|
|
@ -82,10 +82,7 @@ func BuildAllRepositoryFiles(ctx context.Context, ownerID int64) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, pf := range pfs {
|
for _, pf := range pfs {
|
||||||
if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypeFile, pf.ID); err != nil {
|
if err := packages_service.DeletePackageFile(ctx, pf); err != nil {
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := packages_model.DeleteFileByID(ctx, pf.ID); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -157,12 +154,11 @@ func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *package
|
||||||
pf, err := packages_model.GetFileForVersionByName(ctx, repoVersion.ID, IndexFilename, fmt.Sprintf("%s|%s|%s", branch, repository, architecture))
|
pf, err := packages_model.GetFileForVersionByName(ctx, repoVersion.ID, IndexFilename, fmt.Sprintf("%s|%s|%s", branch, repository, architecture))
|
||||||
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
||||||
return err
|
return err
|
||||||
|
} else if pf == nil {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypeFile, pf.ID); err != nil {
|
return packages_service.DeletePackageFile(ctx, pf)
|
||||||
return err
|
|
||||||
}
|
|
||||||
return packages_model.DeleteFileByID(ctx, pf.ID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache data needed for all repository files
|
// Cache data needed for all repository files
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
container_model "code.gitea.io/gitea/models/packages/container"
|
container_model "code.gitea.io/gitea/models/packages/container"
|
||||||
container_module "code.gitea.io/gitea/modules/packages/container"
|
container_module "code.gitea.io/gitea/modules/packages/container"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
packages_service "code.gitea.io/gitea/services/packages"
|
||||||
|
|
||||||
digest "github.com/opencontainers/go-digest"
|
digest "github.com/opencontainers/go-digest"
|
||||||
)
|
)
|
||||||
|
@ -47,10 +48,7 @@ func cleanupExpiredUploadedBlobs(ctx context.Context, olderThan time.Duration) e
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, pf := range pfs {
|
for _, pf := range pfs {
|
||||||
if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypeFile, pf.ID); err != nil {
|
if err := packages_service.DeletePackageFile(ctx, pf); err != nil {
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := packages_model.DeleteFileByID(ctx, pf.ID); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -110,10 +110,7 @@ func BuildAllRepositoryFiles(ctx context.Context, ownerID int64) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, pf := range pfs {
|
for _, pf := range pfs {
|
||||||
if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypeFile, pf.ID); err != nil {
|
if err := packages_service.DeletePackageFile(ctx, pf); err != nil {
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := packages_model.DeleteFileByID(ctx, pf.ID); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -182,12 +179,11 @@ func buildPackagesIndices(ctx context.Context, ownerID int64, repoVersion *packa
|
||||||
pf, err := packages_model.GetFileForVersionByName(ctx, repoVersion.ID, filename, key)
|
pf, err := packages_model.GetFileForVersionByName(ctx, repoVersion.ID, filename, key)
|
||||||
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
||||||
return err
|
return err
|
||||||
|
} else if pf == nil {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypeFile, pf.ID); err != nil {
|
if err := packages_service.DeletePackageFile(ctx, pf); err != nil {
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := packages_model.DeleteFileByID(ctx, pf.ID); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -285,12 +281,11 @@ func buildReleaseFiles(ctx context.Context, ownerID int64, repoVersion *packages
|
||||||
pf, err := packages_model.GetFileForVersionByName(ctx, repoVersion.ID, filename, distribution)
|
pf, err := packages_model.GetFileForVersionByName(ctx, repoVersion.ID, filename, distribution)
|
||||||
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
||||||
return err
|
return err
|
||||||
|
} else if pf == nil {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypeFile, pf.ID); err != nil {
|
if err := packages_service.DeletePackageFile(ctx, pf); err != nil {
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := packages_model.DeleteFileByID(ctx, pf.ID); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -148,10 +148,7 @@ func BuildRepositoryFiles(ctx context.Context, ownerID int64) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, pf := range pfs {
|
for _, pf := range pfs {
|
||||||
if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypeFile, pf.ID); err != nil {
|
if err := packages_service.DeletePackageFile(ctx, pf); err != nil {
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := packages_model.DeleteFileByID(ctx, pf.ID); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/queue"
|
"code.gitea.io/gitea/modules/queue"
|
||||||
repo_module "code.gitea.io/gitea/modules/repository"
|
repo_module "code.gitea.io/gitea/modules/repository"
|
||||||
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
notify_service "code.gitea.io/gitea/services/notify"
|
notify_service "code.gitea.io/gitea/services/notify"
|
||||||
files_service "code.gitea.io/gitea/services/repository/files"
|
files_service "code.gitea.io/gitea/services/repository/files"
|
||||||
|
@ -28,30 +29,13 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// CreateNewBranch creates a new repository branch
|
// CreateNewBranch creates a new repository branch
|
||||||
func CreateNewBranch(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldBranchName, branchName string) (err error) {
|
func CreateNewBranch(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, gitRepo *git.Repository, oldBranchName, branchName string) (err error) {
|
||||||
// Check if branch name can be used
|
branch, err := git_model.GetBranch(ctx, repo.ID, oldBranchName)
|
||||||
if err := checkBranchName(ctx, repo, branchName); err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !git.IsBranchExist(ctx, repo.RepoPath(), oldBranchName) {
|
return CreateNewBranchFromCommit(ctx, doer, repo, gitRepo, branch.CommitID, branchName)
|
||||||
return git_model.ErrBranchNotExist{
|
|
||||||
BranchName: oldBranchName,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := git.Push(ctx, repo.RepoPath(), git.PushOptions{
|
|
||||||
Remote: repo.RepoPath(),
|
|
||||||
Branch: fmt.Sprintf("%s%s:%s%s", git.BranchPrefix, oldBranchName, git.BranchPrefix, branchName),
|
|
||||||
Env: repo_module.PushingEnvironment(doer, repo),
|
|
||||||
}); err != nil {
|
|
||||||
if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return fmt.Errorf("push: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Branch contains the branch information
|
// Branch contains the branch information
|
||||||
|
@ -244,8 +228,54 @@ func checkBranchName(ctx context.Context, repo *repo_model.Repository, name stri
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// syncBranchToDB sync the branch information in the database. It will try to update the branch first,
|
||||||
|
// if updated success with affect records > 0, then all are done. Because that means the branch has been in the database.
|
||||||
|
// If no record is affected, that means the branch does not exist in database. So there are two possibilities.
|
||||||
|
// One is this is a new branch, then we just need to insert the record. Another is the branches haven't been synced,
|
||||||
|
// then we need to sync all the branches into database.
|
||||||
|
func syncBranchToDB(ctx context.Context, repoID, pusherID int64, branchName string, commit *git.Commit) error {
|
||||||
|
cnt, err := git_model.UpdateBranch(ctx, repoID, pusherID, branchName, commit)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("git_model.UpdateBranch %d:%s failed: %v", repoID, branchName, err)
|
||||||
|
}
|
||||||
|
if cnt > 0 { // This means branch does exist, so it's a normal update. It also means the branch has been synced.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// if user haven't visit UI but directly push to a branch after upgrading from 1.20 -> 1.21,
|
||||||
|
// we cannot simply insert the branch but need to check we have branches or not
|
||||||
|
hasBranch, err := db.Exist[git_model.Branch](ctx, git_model.FindBranchOptions{
|
||||||
|
RepoID: repoID,
|
||||||
|
IsDeletedBranch: util.OptionalBoolFalse,
|
||||||
|
}.ToConds())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !hasBranch {
|
||||||
|
if _, err = repo_module.SyncRepoBranches(ctx, repoID, pusherID); err != nil {
|
||||||
|
return fmt.Errorf("repo_module.SyncRepoBranches %d:%s failed: %v", repoID, branchName, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// if database have branches but not this branch, it means this is a new branch
|
||||||
|
return db.Insert(ctx, &git_model.Branch{
|
||||||
|
RepoID: repoID,
|
||||||
|
Name: branchName,
|
||||||
|
CommitID: commit.ID.String(),
|
||||||
|
CommitMessage: commit.Summary(),
|
||||||
|
PusherID: pusherID,
|
||||||
|
CommitTime: timeutil.TimeStamp(commit.Committer.When.Unix()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// CreateNewBranchFromCommit creates a new repository branch
|
// CreateNewBranchFromCommit creates a new repository branch
|
||||||
func CreateNewBranchFromCommit(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, commit, branchName string) (err error) {
|
func CreateNewBranchFromCommit(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, gitRepo *git.Repository, commitID, branchName string) (err error) {
|
||||||
|
err = repo.MustNotBeArchived()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Check if branch name can be used
|
// Check if branch name can be used
|
||||||
if err := checkBranchName(ctx, repo, branchName); err != nil {
|
if err := checkBranchName(ctx, repo, branchName); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -253,7 +283,7 @@ func CreateNewBranchFromCommit(ctx context.Context, doer *user_model.User, repo
|
||||||
|
|
||||||
if err := git.Push(ctx, repo.RepoPath(), git.PushOptions{
|
if err := git.Push(ctx, repo.RepoPath(), git.PushOptions{
|
||||||
Remote: repo.RepoPath(),
|
Remote: repo.RepoPath(),
|
||||||
Branch: fmt.Sprintf("%s:%s%s", commit, git.BranchPrefix, branchName),
|
Branch: fmt.Sprintf("%s:%s%s", commitID, git.BranchPrefix, branchName),
|
||||||
Env: repo_module.PushingEnvironment(doer, repo),
|
Env: repo_module.PushingEnvironment(doer, repo),
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) {
|
if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) {
|
||||||
|
|
|
@ -257,7 +257,7 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
|
||||||
commits.Commits = commits.Commits[:setting.UI.FeedMaxCommitNum]
|
commits.Commits = commits.Commits[:setting.UI.FeedMaxCommitNum]
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = git_model.UpdateBranch(ctx, repo.ID, opts.PusherID, branch, newCommit); err != nil {
|
if err = syncBranchToDB(ctx, repo.ID, opts.PusherID, branch, newCommit); err != nil {
|
||||||
return fmt.Errorf("git_model.UpdateBranch %s:%s failed: %v", repo.FullName(), branch, err)
|
return fmt.Errorf("git_model.UpdateBranch %s:%s failed: %v", repo.FullName(), branch, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
<a href="{{$repo.Link}}/src/commit/{{$result.CommitID | PathEscape}}/{{$result.Filename | PathEscapeSegments}}#L{{.}}"><span>{{.}}</span></a>
|
<a href="{{$repo.Link}}/src/commit/{{$result.CommitID | PathEscape}}/{{$result.Filename | PathEscapeSegments}}#L{{.}}"><span>{{.}}</span></a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</td>
|
</td>
|
||||||
<td class="lines-code chroma"><code class="code-inner">{{.FormattedLines | Safe}}</code></td>
|
<td class="lines-code chroma"><code class="code-inner">{{.FormattedLines}}</code></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -73,8 +73,8 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="required field">
|
<div class="required field">
|
||||||
<label for="repo_name">{{ctx.Locale.Tr "repo.repo_name"}}</label>
|
<label for="repo_name_to_delete">{{ctx.Locale.Tr "repo.repo_name"}}</label>
|
||||||
<input id="repo_name" name="repo_name" required>
|
<input id="repo_name_to_delete" name="repo_name" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text right actions">
|
<div class="text right actions">
|
||||||
|
|
|
@ -53,7 +53,7 @@
|
||||||
<a href="{{$.SourcePath}}/src/commit/{{PathEscape $result.CommitID}}/{{PathEscapeSegments $result.Filename}}#L{{.}}"><span>{{.}}</span></a>
|
<a href="{{$.SourcePath}}/src/commit/{{PathEscape $result.CommitID}}/{{PathEscapeSegments $result.Filename}}#L{{.}}"><span>{{.}}</span></a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</td>
|
</td>
|
||||||
<td class="lines-code chroma"><code class="code-inner">{{.FormattedLines | Safe}}</code></td>
|
<td class="lines-code chroma"><code class="code-inner">{{.FormattedLines}}</code></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -921,8 +921,8 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="required field">
|
<div class="required field">
|
||||||
<label for="repo_name">{{ctx.Locale.Tr "repo.repo_name"}}</label>
|
<label for="repo_name_to_delete">{{ctx.Locale.Tr "repo.repo_name"}}</label>
|
||||||
<input id="repo_name" name="repo_name" required>
|
<input id="repo_name_to_delete" name="repo_name" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text right actions">
|
<div class="text right actions">
|
||||||
|
|
|
@ -69,9 +69,9 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
<div class="file-view{{if .IsMarkup}} markup {{.MarkupType}}{{else if .IsPlainText}} plain-text{{else if .IsTextSource}} code-view{{end}}">
|
<div class="file-view{{if .IsMarkup}} markup {{.MarkupType}}{{else if .IsPlainText}} plain-text{{else if .IsTextSource}} code-view{{end}}">
|
||||||
{{if .IsMarkup}}
|
{{if .IsMarkup}}
|
||||||
{{if .FileContent}}{{.FileContent | Safe}}{{end}}
|
{{if .FileContent}}{{.FileContent}}{{end}}
|
||||||
{{else if .IsPlainText}}
|
{{else if .IsPlainText}}
|
||||||
<pre>{{if .FileContent}}{{.FileContent | Safe}}{{end}}</pre>
|
<pre>{{if .FileContent}}{{.FileContent}}{{end}}</pre>
|
||||||
{{else if not .IsTextSource}}
|
{{else if not .IsTextSource}}
|
||||||
<div class="view-raw">
|
<div class="view-raw">
|
||||||
{{if .IsImageFile}}
|
{{if .IsImageFile}}
|
||||||
|
@ -109,7 +109,7 @@
|
||||||
{{if $.EscapeStatus.Escaped}}
|
{{if $.EscapeStatus.Escaped}}
|
||||||
<td class="lines-escape">{{if (index $.LineEscapeStatus $idx).Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{if (index $.LineEscapeStatus $idx).HasInvisible}}{{ctx.Locale.Tr "repo.invisible_runes_line"}} {{end}}{{if (index $.LineEscapeStatus $idx).HasAmbiguous}}{{ctx.Locale.Tr "repo.ambiguous_runes_line"}}{{end}}"></button>{{end}}</td>
|
<td class="lines-escape">{{if (index $.LineEscapeStatus $idx).Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{if (index $.LineEscapeStatus $idx).HasInvisible}}{{ctx.Locale.Tr "repo.invisible_runes_line"}} {{end}}{{if (index $.LineEscapeStatus $idx).HasAmbiguous}}{{ctx.Locale.Tr "repo.ambiguous_runes_line"}}{{end}}"></button>{{end}}</td>
|
||||||
{{end}}
|
{{end}}
|
||||||
<td rel="L{{$line}}" class="lines-code chroma"><code class="code-inner">{{$code | Safe}}</code></td>
|
<td rel="L{{$line}}" class="lines-code chroma"><code class="code-inner">{{$code}}</code></td>
|
||||||
</tr>
|
</tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
3
templates/swagger/v1_json.tmpl
generated
3
templates/swagger/v1_json.tmpl
generated
|
@ -18596,8 +18596,7 @@
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"username",
|
"username",
|
||||||
"email",
|
"email"
|
||||||
"password"
|
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"created_at": {
|
"created_at": {
|
||||||
|
|
|
@ -78,7 +78,7 @@
|
||||||
<input readonly="" value="{{$.TokenToSign}}">
|
<input readonly="" value="{{$.TokenToSign}}">
|
||||||
<div class="help">
|
<div class="help">
|
||||||
<p>{{ctx.Locale.Tr "settings.ssh_token_help"}}</p>
|
<p>{{ctx.Locale.Tr "settings.ssh_token_help"}}</p>
|
||||||
<p><code>{{printf "echo -n '%s' | ssh-keygen -Y sign -n gitea -f /path_to_your_privkey" $.TokenToSign}}</code></p>
|
<p><code>{{printf "echo -n '%s' | ssh-keygen -Y sign -n gitea -f /path_to_PrivateKey_or_RelatedPublicKey" $.TokenToSign}}</code></p>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -70,15 +70,16 @@ func testAPIGetContentsList(t *testing.T, u *url.URL) {
|
||||||
session = loginUser(t, user4.Name)
|
session = loginUser(t, user4.Name)
|
||||||
token4 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
|
token4 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
|
||||||
|
|
||||||
// Make a new branch in repo1
|
|
||||||
newBranch := "test_branch"
|
|
||||||
err := repo_service.CreateNewBranch(git.DefaultContext, user2, repo1, repo1.DefaultBranch, newBranch)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
// Get the commit ID of the default branch
|
// Get the commit ID of the default branch
|
||||||
gitRepo, err := git.OpenRepository(git.DefaultContext, repo1.RepoPath())
|
gitRepo, err := git.OpenRepository(git.DefaultContext, repo1.RepoPath())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
defer gitRepo.Close()
|
defer gitRepo.Close()
|
||||||
|
|
||||||
|
// Make a new branch in repo1
|
||||||
|
newBranch := "test_branch"
|
||||||
|
err = repo_service.CreateNewBranch(git.DefaultContext, user2, repo1, gitRepo, repo1.DefaultBranch, newBranch)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
commitID, _ := gitRepo.GetBranchCommitID(repo1.DefaultBranch)
|
commitID, _ := gitRepo.GetBranchCommitID(repo1.DefaultBranch)
|
||||||
// Make a new tag in repo1
|
// Make a new tag in repo1
|
||||||
newTag := "test_tag"
|
newTag := "test_tag"
|
||||||
|
|
|
@ -72,15 +72,16 @@ func testAPIGetContents(t *testing.T, u *url.URL) {
|
||||||
session = loginUser(t, user4.Name)
|
session = loginUser(t, user4.Name)
|
||||||
token4 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
|
token4 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
|
||||||
|
|
||||||
// Make a new branch in repo1
|
|
||||||
newBranch := "test_branch"
|
|
||||||
err := repo_service.CreateNewBranch(git.DefaultContext, user2, repo1, repo1.DefaultBranch, newBranch)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
// Get the commit ID of the default branch
|
// Get the commit ID of the default branch
|
||||||
gitRepo, err := git.OpenRepository(git.DefaultContext, repo1.RepoPath())
|
gitRepo, err := git.OpenRepository(git.DefaultContext, repo1.RepoPath())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
defer gitRepo.Close()
|
defer gitRepo.Close()
|
||||||
|
|
||||||
|
// Make a new branch in repo1
|
||||||
|
newBranch := "test_branch"
|
||||||
|
err = repo_service.CreateNewBranch(git.DefaultContext, user2, repo1, gitRepo, repo1.DefaultBranch, newBranch)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
commitID, err := gitRepo.GetBranchCommitID(repo1.DefaultBranch)
|
commitID, err := gitRepo.GetBranchCommitID(repo1.DefaultBranch)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
// Make a new tag in repo1
|
// Make a new tag in repo1
|
||||||
|
|
|
@ -4,72 +4,55 @@
|
||||||
package integration
|
package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/translation"
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
"code.gitea.io/gitea/tests"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
gitea_context "code.gitea.io/gitea/modules/context"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestViewBranches(t *testing.T) {
|
func TestBranchActions(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
|
||||||
|
|
||||||
req := NewRequest(t, "GET", "/user2/repo1/branches")
|
|
||||||
resp := MakeRequest(t, req, http.StatusOK)
|
|
||||||
|
|
||||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
|
||||||
_, exists := htmlDoc.doc.Find(".delete-branch-button").Attr("data-url")
|
|
||||||
assert.False(t, exists, "The template has changed")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeleteBranch(t *testing.T) {
|
|
||||||
defer tests.PrepareTestEnv(t)()
|
|
||||||
|
|
||||||
deleteBranch(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUndoDeleteBranch(t *testing.T) {
|
|
||||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||||
deleteBranch(t)
|
session := loginUser(t, "user2")
|
||||||
htmlDoc, name := branchAction(t, ".restore-branch-button")
|
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
assert.Contains(t,
|
branch3 := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{ID: 3, RepoID: repo1.ID})
|
||||||
htmlDoc.doc.Find(".ui.positive.message").Text(),
|
branchesLink := repo1.FullName() + "/branches"
|
||||||
translation.NewLocale("en-US").Tr("repo.branch.restore_success", name),
|
|
||||||
)
|
t.Run("View", func(t *testing.T) {
|
||||||
|
req := NewRequest(t, "GET", branchesLink)
|
||||||
|
MakeRequest(t, req, http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Delete branch", func(t *testing.T) {
|
||||||
|
link := fmt.Sprintf("/%s/branches/delete?name=%s", repo1.FullName(), branch3.Name)
|
||||||
|
req := NewRequestWithValues(t, "POST", link, map[string]string{
|
||||||
|
"_csrf": GetCSRF(t, session, branchesLink),
|
||||||
|
})
|
||||||
|
session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
flashCookie := session.GetCookie(gitea_context.CookieNameFlash)
|
||||||
|
assert.NotNil(t, flashCookie)
|
||||||
|
assert.Contains(t, flashCookie.Value, "success%3DBranch%2B%2522branch2%2522%2Bhas%2Bbeen%2Bdeleted.")
|
||||||
|
|
||||||
|
assert.True(t, unittest.AssertExistsAndLoadBean(t, &git_model.Branch{ID: 3, RepoID: repo1.ID}).IsDeleted)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Restore branch", func(t *testing.T) {
|
||||||
|
link := fmt.Sprintf("/%s/branches/restore?branch_id=%d&name=%s", repo1.FullName(), branch3.ID, branch3.Name)
|
||||||
|
req := NewRequestWithValues(t, "POST", link, map[string]string{
|
||||||
|
"_csrf": GetCSRF(t, session, branchesLink),
|
||||||
|
})
|
||||||
|
session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
flashCookie := session.GetCookie(gitea_context.CookieNameFlash)
|
||||||
|
assert.NotNil(t, flashCookie)
|
||||||
|
assert.Contains(t, flashCookie.Value, "success%3DBranch%2B%2522branch2%2522%2Bhas%2Bbeen%2Brestored")
|
||||||
|
|
||||||
|
assert.False(t, unittest.AssertExistsAndLoadBean(t, &git_model.Branch{ID: 3, RepoID: repo1.ID}).IsDeleted)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteBranch(t *testing.T) {
|
|
||||||
htmlDoc, name := branchAction(t, ".delete-branch-button")
|
|
||||||
assert.Contains(t,
|
|
||||||
htmlDoc.doc.Find(".ui.positive.message").Text(),
|
|
||||||
translation.NewLocale("en-US").Tr("repo.branch.deletion_success", name),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func branchAction(t *testing.T, button string) (*HTMLDoc, string) {
|
|
||||||
session := loginUser(t, "user2")
|
|
||||||
req := NewRequest(t, "GET", "/user2/repo1/branches")
|
|
||||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
|
||||||
|
|
||||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
|
||||||
link, exists := htmlDoc.doc.Find(button).Attr("data-url")
|
|
||||||
if !assert.True(t, exists, "The template has changed") {
|
|
||||||
t.Skip()
|
|
||||||
}
|
|
||||||
|
|
||||||
req = NewRequestWithValues(t, "POST", link, map[string]string{
|
|
||||||
"_csrf": htmlDoc.GetCSRF(),
|
|
||||||
})
|
|
||||||
session.MakeRequest(t, req, http.StatusOK)
|
|
||||||
|
|
||||||
url, err := url.Parse(link)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
req = NewRequest(t, "GET", "/user2/repo1/branches")
|
|
||||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
|
||||||
|
|
||||||
return NewHTMLParser(t, resp.Body), url.Query().Get("name")
|
|
||||||
}
|
|
||||||
|
|
139
tests/integration/forgejo_git_test.go
Normal file
139
tests/integration/forgejo_git_test.go
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
// Copyright Earl Warren <contact@earl-warren.org>
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
"code.gitea.io/gitea/tests"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestActionsUserGit(t *testing.T) {
|
||||||
|
onGiteaRun(t, testActionsUserGit)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewActionsUserTestContext(t *testing.T, username, reponame string) APITestContext {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame})
|
||||||
|
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: username})
|
||||||
|
|
||||||
|
task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 47})
|
||||||
|
task.RepoID = repo.ID
|
||||||
|
task.OwnerID = repoOwner.ID
|
||||||
|
task.GenerateToken()
|
||||||
|
|
||||||
|
actions_model.UpdateTask(db.DefaultContext, task)
|
||||||
|
return APITestContext{
|
||||||
|
Session: emptyTestSession(t),
|
||||||
|
Token: task.Token,
|
||||||
|
Username: username,
|
||||||
|
Reponame: reponame,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testActionsUserGit(t *testing.T, u *url.URL) {
|
||||||
|
username := "user2"
|
||||||
|
reponame := "repo1"
|
||||||
|
httpContext := NewAPITestContext(t, username, reponame, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||||
|
|
||||||
|
for _, testCase := range []struct {
|
||||||
|
name string
|
||||||
|
head string
|
||||||
|
ctx APITestContext
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "UserTypeIndividual",
|
||||||
|
head: "individualhead",
|
||||||
|
ctx: httpContext,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ActionsUser",
|
||||||
|
head: "actionsuserhead",
|
||||||
|
ctx: NewActionsUserTestContext(t, username, reponame),
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run("CreatePR "+testCase.name, func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
dstPath := t.TempDir()
|
||||||
|
u.Path = httpContext.GitPath()
|
||||||
|
u.User = url.UserPassword(httpContext.Username, userPassword)
|
||||||
|
t.Run("Clone", doGitClone(dstPath, u))
|
||||||
|
t.Run("PopulateBranch", doActionsUserPopulateBranch(dstPath, &httpContext, "master", testCase.head))
|
||||||
|
t.Run("CreatePR", doActionsUserPR(httpContext, testCase.ctx, "master", testCase.head))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func doActionsUserPopulateBranch(dstPath string, ctx *APITestContext, baseBranch, headBranch string) func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
t.Run("CreateHeadBranch", doGitCreateBranch(dstPath, headBranch))
|
||||||
|
|
||||||
|
t.Run("AddCommit", func(t *testing.T) {
|
||||||
|
err := os.WriteFile(path.Join(dstPath, "test_file"), []byte("## test content"), 0o666)
|
||||||
|
if !assert.NoError(t, err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = git.AddChanges(dstPath, true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = git.CommitChanges(dstPath, git.CommitChangesOptions{
|
||||||
|
Committer: &git.Signature{
|
||||||
|
Email: "user2@example.com",
|
||||||
|
Name: "user2",
|
||||||
|
When: time.Now(),
|
||||||
|
},
|
||||||
|
Author: &git.Signature{
|
||||||
|
Email: "user2@example.com",
|
||||||
|
Name: "user2",
|
||||||
|
When: time.Now(),
|
||||||
|
},
|
||||||
|
Message: "Testing commit 1",
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Push", func(t *testing.T) {
|
||||||
|
err := git.NewCommand(git.DefaultContext, "push", "origin").AddDynamicArguments("HEAD:refs/heads/" + headBranch).Run(&git.RunOpts{Dir: dstPath})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func doActionsUserPR(ctx, doerCtx APITestContext, baseBranch, headBranch string) func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
var pr api.PullRequest
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Create a test pullrequest
|
||||||
|
t.Run("CreatePullRequest", func(t *testing.T) {
|
||||||
|
pr, err = doAPICreatePullRequest(doerCtx, ctx.Username, ctx.Reponame, baseBranch, headBranch)(t)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
doerCtx.ExpectedCode = http.StatusCreated
|
||||||
|
t.Run("AutoMergePR", doAPIAutoMergePullRequest(doerCtx, ctx.Username, ctx.Reponame, pr.Index))
|
||||||
|
// Ensure the PR page works
|
||||||
|
t.Run("EnsureCanSeePull", doEnsureCanSeePull(ctx, pr))
|
||||||
|
}
|
||||||
|
}
|
23
tests/integration/project_test.go
Normal file
23
tests/integration/project_test.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/tests"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPrivateRepoProject(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
// not logged in user
|
||||||
|
req := NewRequest(t, "GET", "/user31/-/projects")
|
||||||
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
|
||||||
|
sess := loginUser(t, "user1")
|
||||||
|
req = NewRequest(t, "GET", "/user31/-/projects")
|
||||||
|
sess.MakeRequest(t, req, http.StatusOK)
|
||||||
|
}
|
|
@ -10,6 +10,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/test"
|
"code.gitea.io/gitea/modules/test"
|
||||||
"code.gitea.io/gitea/modules/translation"
|
"code.gitea.io/gitea/modules/translation"
|
||||||
|
@ -47,12 +49,14 @@ func testCreateBranches(t *testing.T, giteaURL *url.URL) {
|
||||||
CreateRelease string
|
CreateRelease string
|
||||||
FlashMessage string
|
FlashMessage string
|
||||||
ExpectedStatus int
|
ExpectedStatus int
|
||||||
|
CheckBranch bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
OldRefSubURL: "branch/master",
|
OldRefSubURL: "branch/master",
|
||||||
NewBranch: "feature/test1",
|
NewBranch: "feature/test1",
|
||||||
ExpectedStatus: http.StatusSeeOther,
|
ExpectedStatus: http.StatusSeeOther,
|
||||||
FlashMessage: translation.NewLocale("en-US").Tr("repo.branch.create_success", "feature/test1"),
|
FlashMessage: translation.NewLocale("en-US").Tr("repo.branch.create_success", "feature/test1"),
|
||||||
|
CheckBranch: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
OldRefSubURL: "branch/master",
|
OldRefSubURL: "branch/master",
|
||||||
|
@ -65,6 +69,7 @@ func testCreateBranches(t *testing.T, giteaURL *url.URL) {
|
||||||
NewBranch: "feature=test1",
|
NewBranch: "feature=test1",
|
||||||
ExpectedStatus: http.StatusSeeOther,
|
ExpectedStatus: http.StatusSeeOther,
|
||||||
FlashMessage: translation.NewLocale("en-US").Tr("repo.branch.create_success", "feature=test1"),
|
FlashMessage: translation.NewLocale("en-US").Tr("repo.branch.create_success", "feature=test1"),
|
||||||
|
CheckBranch: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
OldRefSubURL: "branch/master",
|
OldRefSubURL: "branch/master",
|
||||||
|
@ -94,6 +99,7 @@ func testCreateBranches(t *testing.T, giteaURL *url.URL) {
|
||||||
NewBranch: "feature/test3",
|
NewBranch: "feature/test3",
|
||||||
ExpectedStatus: http.StatusSeeOther,
|
ExpectedStatus: http.StatusSeeOther,
|
||||||
FlashMessage: translation.NewLocale("en-US").Tr("repo.branch.create_success", "feature/test3"),
|
FlashMessage: translation.NewLocale("en-US").Tr("repo.branch.create_success", "feature/test3"),
|
||||||
|
CheckBranch: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
OldRefSubURL: "branch/master",
|
OldRefSubURL: "branch/master",
|
||||||
|
@ -108,10 +114,15 @@ func testCreateBranches(t *testing.T, giteaURL *url.URL) {
|
||||||
CreateRelease: "v1.0.1",
|
CreateRelease: "v1.0.1",
|
||||||
ExpectedStatus: http.StatusSeeOther,
|
ExpectedStatus: http.StatusSeeOther,
|
||||||
FlashMessage: translation.NewLocale("en-US").Tr("repo.branch.create_success", "feature/test4"),
|
FlashMessage: translation.NewLocale("en-US").Tr("repo.branch.create_success", "feature/test4"),
|
||||||
|
CheckBranch: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
session := loginUser(t, "user2")
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
session := loginUser(t, "user2")
|
if test.CheckBranch {
|
||||||
|
unittest.AssertNotExistsBean(t, &git_model.Branch{RepoID: 1, Name: test.NewBranch})
|
||||||
|
}
|
||||||
if test.CreateRelease != "" {
|
if test.CreateRelease != "" {
|
||||||
createNewRelease(t, session, "/user2/repo1", test.CreateRelease, test.CreateRelease, false, false)
|
createNewRelease(t, session, "/user2/repo1", test.CreateRelease, test.CreateRelease, false, false)
|
||||||
}
|
}
|
||||||
|
@ -125,6 +136,9 @@ func testCreateBranches(t *testing.T, giteaURL *url.URL) {
|
||||||
test.FlashMessage,
|
test.FlashMessage,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if test.CheckBranch {
|
||||||
|
unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: 1, Name: test.NewBranch})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import $ from 'jquery';
|
||||||
|
|
||||||
const {pageData} = window.config;
|
const {pageData} = window.config;
|
||||||
|
|
||||||
const initInputCitationValue = async ($citationCopyApa, $citationCopyBibtex) => {
|
async function initInputCitationValue($citationCopyApa, $citationCopyBibtex) {
|
||||||
const [{Cite, plugins}] = await Promise.all([
|
const [{Cite, plugins}] = await Promise.all([
|
||||||
import(/* webpackChunkName: "citation-js-core" */'@citation-js/core'),
|
import(/* webpackChunkName: "citation-js-core" */'@citation-js/core'),
|
||||||
import(/* webpackChunkName: "citation-js-formats" */'@citation-js/plugin-software-formats'),
|
import(/* webpackChunkName: "citation-js-formats" */'@citation-js/plugin-software-formats'),
|
||||||
|
@ -19,9 +19,9 @@ const initInputCitationValue = async ($citationCopyApa, $citationCopyBibtex) =>
|
||||||
const bibtexOutput = citationFormatter.format('bibtex', {lang});
|
const bibtexOutput = citationFormatter.format('bibtex', {lang});
|
||||||
$citationCopyBibtex.attr('data-text', bibtexOutput);
|
$citationCopyBibtex.attr('data-text', bibtexOutput);
|
||||||
$citationCopyApa.attr('data-text', apaOutput);
|
$citationCopyApa.attr('data-text', apaOutput);
|
||||||
};
|
}
|
||||||
|
|
||||||
export function initCitationFileCopyContent() {
|
export async function initCitationFileCopyContent() {
|
||||||
const defaultCitationFormat = 'apa'; // apa or bibtex
|
const defaultCitationFormat = 'apa'; // apa or bibtex
|
||||||
|
|
||||||
if (!pageData.citationFileContent) return;
|
if (!pageData.citationFileContent) return;
|
||||||
|
@ -39,7 +39,14 @@ export function initCitationFileCopyContent() {
|
||||||
$citationCopyBibtex.toggleClass('primary', isBibtex);
|
$citationCopyBibtex.toggleClass('primary', isBibtex);
|
||||||
$citationCopyApa.toggleClass('primary', !isBibtex);
|
$citationCopyApa.toggleClass('primary', !isBibtex);
|
||||||
};
|
};
|
||||||
initInputCitationValue($citationCopyApa, $citationCopyBibtex).then(updateUi);
|
|
||||||
|
try {
|
||||||
|
await initInputCitationValue($citationCopyApa, $citationCopyBibtex);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`initCitationFileCopyContent error: ${e}`, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateUi();
|
||||||
|
|
||||||
$citationCopyApa.on('click', () => {
|
$citationCopyApa.on('click', () => {
|
||||||
localStorage.setItem('citation-copy-format', 'apa');
|
localStorage.setItem('citation-copy-format', 'apa');
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {initCompColorPicker} from './comp/ColorPicker.js';
|
||||||
import {showGlobalErrorMessage} from '../bootstrap.js';
|
import {showGlobalErrorMessage} from '../bootstrap.js';
|
||||||
import {handleGlobalEnterQuickSubmit} from './comp/QuickSubmit.js';
|
import {handleGlobalEnterQuickSubmit} from './comp/QuickSubmit.js';
|
||||||
import {svg} from '../svg.js';
|
import {svg} from '../svg.js';
|
||||||
import {hideElem, showElem, toggleElem} from '../utils/dom.js';
|
import {hideElem, showElem, toggleElem, initSubmitEventPolyfill, submitEventSubmitter} from '../utils/dom.js';
|
||||||
import {htmlEscape} from 'escape-goat';
|
import {htmlEscape} from 'escape-goat';
|
||||||
import {showTemporaryTooltip} from '../modules/tippy.js';
|
import {showTemporaryTooltip} from '../modules/tippy.js';
|
||||||
import {confirmModal} from './comp/ConfirmModal.js';
|
import {confirmModal} from './comp/ConfirmModal.js';
|
||||||
|
@ -122,7 +122,8 @@ async function formFetchAction(e) {
|
||||||
const formMethod = formEl.getAttribute('method') || 'get';
|
const formMethod = formEl.getAttribute('method') || 'get';
|
||||||
const formActionUrl = formEl.getAttribute('action');
|
const formActionUrl = formEl.getAttribute('action');
|
||||||
const formData = new FormData(formEl);
|
const formData = new FormData(formEl);
|
||||||
const [submitterName, submitterValue] = [e.submitter?.getAttribute('name'), e.submitter?.getAttribute('value')];
|
const formSubmitter = submitEventSubmitter(e);
|
||||||
|
const [submitterName, submitterValue] = [formSubmitter?.getAttribute('name'), formSubmitter?.getAttribute('value')];
|
||||||
if (submitterName) {
|
if (submitterName) {
|
||||||
formData.append(submitterName, submitterValue || '');
|
formData.append(submitterName, submitterValue || '');
|
||||||
}
|
}
|
||||||
|
@ -193,6 +194,7 @@ export function initGlobalCommon() {
|
||||||
|
|
||||||
$('.tabular.menu .item').tab();
|
$('.tabular.menu .item').tab();
|
||||||
|
|
||||||
|
initSubmitEventPolyfill();
|
||||||
document.addEventListener('submit', formFetchAction);
|
document.addEventListener('submit', formFetchAction);
|
||||||
document.addEventListener('click', linkAction);
|
document.addEventListener('click', linkAction);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import {isElemHidden, onInputDebounce, toggleElem} from '../utils/dom.js';
|
import {isElemHidden, onInputDebounce, submitEventSubmitter, toggleElem} from '../utils/dom.js';
|
||||||
import {GET} from '../modules/fetch.js';
|
import {GET} from '../modules/fetch.js';
|
||||||
|
|
||||||
const {appSubUrl} = window.config;
|
const {appSubUrl} = window.config;
|
||||||
|
@ -40,7 +40,7 @@ export function initCommonIssueListQuickGoto() {
|
||||||
$form.on('submit', (e) => {
|
$form.on('submit', (e) => {
|
||||||
// if there is no goto button, or the form is submitted by non-quick-goto elements, submit the form directly
|
// if there is no goto button, or the form is submitted by non-quick-goto elements, submit the form directly
|
||||||
let doQuickGoto = !isElemHidden($goto);
|
let doQuickGoto = !isElemHidden($goto);
|
||||||
const submitter = e.originalEvent.submitter;
|
const submitter = submitEventSubmitter(e.originalEvent);
|
||||||
if (submitter !== $form[0] && submitter !== $input[0] && submitter !== $goto[0]) doQuickGoto = false;
|
if (submitter !== $form[0] && submitter !== $input[0] && submitter !== $goto[0]) doQuickGoto = false;
|
||||||
if (!doQuickGoto) return;
|
if (!doQuickGoto) return;
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {validateTextareaNonEmpty} from './comp/ComboMarkdownEditor.js';
|
||||||
import {initViewedCheckboxListenerFor, countAndUpdateViewedFiles, initExpandAndCollapseFilesButton} from './pull-view-file.js';
|
import {initViewedCheckboxListenerFor, countAndUpdateViewedFiles, initExpandAndCollapseFilesButton} from './pull-view-file.js';
|
||||||
import {initImageDiff} from './imagediff.js';
|
import {initImageDiff} from './imagediff.js';
|
||||||
import {showErrorToast} from '../modules/toast.js';
|
import {showErrorToast} from '../modules/toast.js';
|
||||||
|
import {submitEventSubmitter} from '../utils/dom.js';
|
||||||
|
|
||||||
const {csrfToken, pageData, i18n} = window.config;
|
const {csrfToken, pageData, i18n} = window.config;
|
||||||
|
|
||||||
|
@ -57,7 +58,7 @@ function initRepoDiffConversationForm() {
|
||||||
const formData = new FormData($form[0]);
|
const formData = new FormData($form[0]);
|
||||||
|
|
||||||
// if the form is submitted by a button, append the button's name and value to the form data
|
// if the form is submitted by a button, append the button's name and value to the form data
|
||||||
const submitter = e.originalEvent?.submitter;
|
const submitter = submitEventSubmitter(e.originalEvent);
|
||||||
const isSubmittedByButton = (submitter?.nodeName === 'BUTTON') || (submitter?.nodeName === 'INPUT' && submitter.type === 'submit');
|
const isSubmittedByButton = (submitter?.nodeName === 'BUTTON') || (submitter?.nodeName === 'INPUT' && submitter.type === 'submit');
|
||||||
if (isSubmittedByButton && submitter.name) {
|
if (isSubmittedByButton && submitter.name) {
|
||||||
formData.append(submitter.name, submitter.value);
|
formData.append(submitter.name, submitter.value);
|
||||||
|
|
|
@ -106,7 +106,7 @@ function switchTitleToTooltip(target) {
|
||||||
/**
|
/**
|
||||||
* Creating tooltip tippy instance is expensive, so we only create it when the user hovers over the element
|
* Creating tooltip tippy instance is expensive, so we only create it when the user hovers over the element
|
||||||
* According to https://www.w3.org/TR/DOM-Level-3-Events/#events-mouseevent-event-order , mouseover event is fired before mouseenter event
|
* According to https://www.w3.org/TR/DOM-Level-3-Events/#events-mouseevent-event-order , mouseover event is fired before mouseenter event
|
||||||
* Some old browsers like Pale Moon doesn't support "mouseenter(capture)"
|
* Some browsers like PaleMoon don't support "addEventListener('mouseenter', capture)"
|
||||||
* The tippy by default uses "mouseenter" event to show, so we use "mouseover" event to switch to tippy
|
* The tippy by default uses "mouseenter" event to show, so we use "mouseover" event to switch to tippy
|
||||||
* @param e {Event}
|
* @param e {Event}
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -194,3 +194,24 @@ export function loadElem(el, src) {
|
||||||
el.src = src;
|
el.src = src;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// some browsers like PaleMoon don't have "SubmitEvent" support, so polyfill it by a tricky method: use the last clicked button as submitter
|
||||||
|
// it can't use other transparent polyfill patches because PaleMoon also doesn't support "addEventListener(capture)"
|
||||||
|
const needSubmitEventPolyfill = typeof SubmitEvent === 'undefined';
|
||||||
|
|
||||||
|
export function submitEventSubmitter(e) {
|
||||||
|
return needSubmitEventPolyfill ? (e.target._submitter || null) : e.submitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitEventPolyfillListener(e) {
|
||||||
|
const form = e.target.closest('form');
|
||||||
|
if (!form) return;
|
||||||
|
form._submitter = e.target.closest('button:not([type]), button[type="submit"], input[type="submit"]');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initSubmitEventPolyfill() {
|
||||||
|
if (!needSubmitEventPolyfill) return;
|
||||||
|
console.warn(`This browser doesn't have "SubmitEvent" support, use a tricky method to polyfill`);
|
||||||
|
document.body.addEventListener('click', submitEventPolyfillListener);
|
||||||
|
document.body.addEventListener('focus', submitEventPolyfillListener);
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import '@webcomponents/custom-elements'; // polyfill for some browsers like Pale Moon
|
import '@webcomponents/custom-elements'; // polyfill for some browsers like PaleMoon
|
||||||
import './polyfill.js';
|
import './polyfill.js';
|
||||||
|
|
||||||
import '@github/relative-time-element';
|
import '@github/relative-time-element';
|
||||||
|
|
Loading…
Reference in a new issue