Compare commits
39 commits
209d9f3507
...
26a7becd56
Author | SHA1 | Date | |
---|---|---|---|
26a7becd56 | |||
6f5e340458 | |||
|
b63df8b5c4 | ||
|
11af4c9aad | ||
|
052c83393f | ||
|
e25033ef8a | ||
|
f34f2c3141 | ||
|
4df75c254f | ||
|
1689b3da55 | ||
|
9c0380fe84 | ||
|
193e04c43b | ||
|
6b5ef0fad7 | ||
|
d5845521a8 | ||
|
a6c2201dd4 | ||
|
833cf722ab | ||
|
0b1175f21b | ||
|
4d2b4008d3 | ||
|
1d228e6ee9 | ||
|
53e4f672a3 | ||
|
bb84b7565f | ||
|
66016b3fe3 | ||
|
d7aa9fc964 | ||
|
c407810217 | ||
|
560ff3ea36 | ||
|
4da20765e8 | ||
|
03b397a408 | ||
|
c1efe5b104 | ||
|
a98cb4d806 | ||
|
ef46b01168 | ||
|
06c45d3b6e | ||
|
5abca17b64 | ||
|
19a49e763a | ||
|
2f6d011503 | ||
|
ec4b6d7d04 | ||
|
7f7e1ccab8 | ||
|
18b4554009 | ||
|
d3b8870700 | ||
|
ebf80c3d90 | ||
|
fa25b9eec6 |
111 changed files with 1042 additions and 249 deletions
|
@ -58,3 +58,22 @@ jobs:
|
||||||
gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
|
gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
gpg-passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
gpg-passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||||
verbose: ${{ secrets.VERBOSE }}
|
verbose: ${{ secrets.VERBOSE }}
|
||||||
|
|
||||||
|
|
||||||
|
- name: set up go for the DNS update below
|
||||||
|
uses: https://code.forgejo.org/actions/setup-go@v4
|
||||||
|
if: secrets.ROLE == 'forgejo-experimental'
|
||||||
|
with:
|
||||||
|
go-version: ">=1.21"
|
||||||
|
check-latest: true
|
||||||
|
- name: update the _release.experimental DNS record
|
||||||
|
if: secrets.ROLE == 'forgejo-experimental'
|
||||||
|
uses: https://code.forgejo.org/actions/ovh-dns-update@v1
|
||||||
|
with:
|
||||||
|
subdomain: _release.experimental
|
||||||
|
domain: forgejo.com # there is a CNAME from .org to .com (for security reasons)
|
||||||
|
record-id: 5283602601
|
||||||
|
value: v=${{ github.ref_name }}
|
||||||
|
ovh-app-key: ${{ secrets.OVH_APP_KEY }}
|
||||||
|
ovh-app-secret: ${{ secrets.OVH_APP_SECRET }}
|
||||||
|
ovh-consumer-key: ${{ secrets.OVH_CON_KEY }}
|
||||||
|
|
15
.woodpecker.yml
Normal file
15
.woodpecker.yml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
pipeline:
|
||||||
|
nulo-container:
|
||||||
|
image: docker.io/woodpeckerci/plugin-docker-buildx
|
||||||
|
settings:
|
||||||
|
repo: gitea.nulo.in/Nulo/forgejo
|
||||||
|
tag: v1.20.4-0
|
||||||
|
registry: https://gitea.nulo.in
|
||||||
|
username: Nulo
|
||||||
|
password:
|
||||||
|
from_secret: registry_secret
|
||||||
|
secrets: [REGISTRY_SECRET]
|
||||||
|
when:
|
||||||
|
branch: "nulo/release/v1.20"
|
||||||
|
event: "push"
|
||||||
|
|
28
CHANGELOG.md
28
CHANGELOG.md
|
@ -4,6 +4,34 @@ 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.20.4](https://github.com/go-gitea/gitea/releases/tag/1.20.4) - 2023-09-08
|
||||||
|
|
||||||
|
* SECURITY
|
||||||
|
* Check blocklist for emails when adding them to account (#26812) (#26831)
|
||||||
|
* ENHANCEMENTS
|
||||||
|
* Add `branch_filter` to hooks API endpoints (#26599) (#26632)
|
||||||
|
* Fix incorrect "tabindex" attributes (#26733) (#26734)
|
||||||
|
* Use line-height: normal by default (#26635) (#26708)
|
||||||
|
* Fix unable to display individual-level project (#26198) (#26636)
|
||||||
|
* BUGFIXES
|
||||||
|
* Fix wrong review requested number (#26784) (#26880)
|
||||||
|
* Avoid double-unescaping of form value (#26853) (#26863)
|
||||||
|
* Redirect from `{repo}/issues/new` to `{repo}/issues/new/choose` when blank issues are disabled (#26813) (#26847)
|
||||||
|
* Sync tags when adopting repos (#26816) (#26834)
|
||||||
|
* Fix verifyCommits error when push a new branch (#26664) (#26810)
|
||||||
|
* Include the GITHUB_TOKEN/GITEA_TOKEN secret for fork pull requests (#26759) (#26806)
|
||||||
|
* Fix some slice append usages (#26778) (#26798)
|
||||||
|
* Add fix incorrect can_create_org_repo for org owner team (#26683) (#26791)
|
||||||
|
* Fix bug for ctx usage (#26763)
|
||||||
|
* Make issue template field template access correct template data (#26698) (#26709)
|
||||||
|
* Use correct minio error (#26634) (#26639)
|
||||||
|
* Ignore the trailing slashes when comparing oauth2 redirect_uri (#26597) (#26618)
|
||||||
|
* Set errwriter for urfave/cli v1 (#26616)
|
||||||
|
* Fix reopen logic for agit flow pull request (#26399) (#26613)
|
||||||
|
* Fix context filter has no effect in dashboard (#26695) (#26811)
|
||||||
|
* Fix being unable to use a repo that prohibits accepting PRs as a PR source. (#26785) (#26790)
|
||||||
|
* Fix Page Not Found error (#26768)
|
||||||
|
|
||||||
## [1.20.3](https://github.com/go-gitea/gitea/releases/tag/v1.20.3) - 2023-08-20
|
## [1.20.3](https://github.com/go-gitea/gitea/releases/tag/v1.20.3) - 2023-08-20
|
||||||
|
|
||||||
* BREAKING
|
* BREAKING
|
||||||
|
|
2
Makefile
2
Makefile
|
@ -89,7 +89,7 @@ endif
|
||||||
VERSION = ${GITEA_VERSION}
|
VERSION = ${GITEA_VERSION}
|
||||||
|
|
||||||
# SemVer
|
# SemVer
|
||||||
FORGEJO_VERSION := 5.0.2+0-gitea-1.20.3
|
FORGEJO_VERSION := 5.0.3+0-gitea-1.20.4
|
||||||
|
|
||||||
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)"
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
;; Do not copy the whole file as-is, as it contains some invalid sections for illustrative purposes.
|
;; Do not copy the whole file as-is, as it contains some invalid sections for illustrative purposes.
|
||||||
;; If you don't know what a setting is you should not set it.
|
;; If you don't know what a setting is you should not set it.
|
||||||
;;
|
;;
|
||||||
;; see https://docs.gitea.io/en-us/config-cheat-sheet/ for additional documentation.
|
;; see https://docs.gitea.com/administration/config-cheat-sheet for additional documentation.
|
||||||
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
@ -1804,8 +1804,9 @@ LEVEL = Info
|
||||||
;; Currently, only `minio` is supported.
|
;; Currently, only `minio` is supported.
|
||||||
;SERVE_DIRECT = false
|
;SERVE_DIRECT = false
|
||||||
;;
|
;;
|
||||||
;; Path for attachments. Defaults to `data/attachments` only available when STORAGE_TYPE is `local`
|
;; Path for attachments. Defaults to `attachments`. Only available when STORAGE_TYPE is `local`
|
||||||
;PATH = data/attachments
|
;; Relative paths will be resolved to `${AppDataPath}/${attachment.PATH}`
|
||||||
|
;PATH = attachments
|
||||||
;;
|
;;
|
||||||
;; Minio endpoint to connect only available when STORAGE_TYPE is `minio`
|
;; Minio endpoint to connect only available when STORAGE_TYPE is `minio`
|
||||||
;MINIO_ENDPOINT = localhost:9000
|
;MINIO_ENDPOINT = localhost:9000
|
||||||
|
|
|
@ -818,7 +818,7 @@ Default templates for project boards:
|
||||||
- `MAX_FILES`: **5**: Maximum number of attachments that can be uploaded at once.
|
- `MAX_FILES`: **5**: Maximum number of attachments that can be uploaded at once.
|
||||||
- `STORAGE_TYPE`: **local**: Storage type for attachments, `local` for local disk or `minio` for s3 compatible object storage service, default is `local` or other name defined with `[storage.xxx]`
|
- `STORAGE_TYPE`: **local**: Storage type for attachments, `local` for local disk or `minio` for s3 compatible object storage service, default is `local` or other name defined with `[storage.xxx]`
|
||||||
- `SERVE_DIRECT`: **false**: Allows the storage driver to redirect to authenticated URLs to serve files directly. Currently, only Minio/S3 is supported via signed URLs, local does nothing.
|
- `SERVE_DIRECT`: **false**: Allows the storage driver to redirect to authenticated URLs to serve files directly. Currently, only Minio/S3 is supported via signed URLs, local does nothing.
|
||||||
- `PATH`: **data/attachments**: Path to store attachments only available when STORAGE_TYPE is `local`
|
- `PATH`: **attachments**: Path to store attachments only available when STORAGE_TYPE is `local`, relative paths will be resolved to `${AppDataPath}/${attachment.PATH}`.
|
||||||
- `MINIO_ENDPOINT`: **localhost:9000**: Minio endpoint to connect only available when STORAGE_TYPE is `minio`
|
- `MINIO_ENDPOINT`: **localhost:9000**: Minio endpoint to connect only available when STORAGE_TYPE is `minio`
|
||||||
- `MINIO_ACCESS_KEY_ID`: Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`
|
- `MINIO_ACCESS_KEY_ID`: Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`
|
||||||
- `MINIO_SECRET_ACCESS_KEY`: Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio`
|
- `MINIO_SECRET_ACCESS_KEY`: Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio`
|
||||||
|
|
|
@ -230,7 +230,7 @@ menu:
|
||||||
- `MAX_SIZE`: 附件最大限制,单位 MB,比如: `4`。
|
- `MAX_SIZE`: 附件最大限制,单位 MB,比如: `4`。
|
||||||
- `MAX_FILES`: 一次最多上传的附件数量,比如: `5`。
|
- `MAX_FILES`: 一次最多上传的附件数量,比如: `5`。
|
||||||
- `STORAGE_TYPE`: **local**: 附件存储类型,`local` 将存储到本地文件夹, `minio` 将存储到 s3 兼容的对象存储服务中。
|
- `STORAGE_TYPE`: **local**: 附件存储类型,`local` 将存储到本地文件夹, `minio` 将存储到 s3 兼容的对象存储服务中。
|
||||||
- `PATH`: **data/attachments**: 附件存储路径,仅当 `STORAGE_TYPE` 为 `local` 时有效。
|
- `PATH`: **attachments**: 存储附件的路径,仅当 STORAGE_TYPE 为 `local` 时可用。如果是相对路径,将会被解析为 `${AppDataPath}/${attachment.PATH}`.
|
||||||
- `MINIO_ENDPOINT`: **localhost:9000**: Minio 终端,仅当 `STORAGE_TYPE` 是 `minio` 时有效。
|
- `MINIO_ENDPOINT`: **localhost:9000**: Minio 终端,仅当 `STORAGE_TYPE` 是 `minio` 时有效。
|
||||||
- `MINIO_ACCESS_KEY_ID`: Minio accessKeyID ,仅当 `STORAGE_TYPE` 是 `minio` 时有效。
|
- `MINIO_ACCESS_KEY_ID`: Minio accessKeyID ,仅当 `STORAGE_TYPE` 是 `minio` 时有效。
|
||||||
- `MINIO_SECRET_ACCESS_KEY`: Minio secretAccessKey,仅当 `STORAGE_TYPE` 是 `minio` 时有效。
|
- `MINIO_SECRET_ACCESS_KEY`: Minio secretAccessKey,仅当 `STORAGE_TYPE` 是 `minio` 时有效。
|
||||||
|
|
|
@ -24,7 +24,7 @@ it is just a matter of:
|
||||||
- add some configuration to your `app.ini` file
|
- add some configuration to your `app.ini` file
|
||||||
- restart your Gitea instance
|
- restart your Gitea instance
|
||||||
|
|
||||||
This supports rendering of whole files. If you want to render code blocks in markdown you would need to do something with javascript. See some examples on the [Customizing Gitea](../customizing-gitea) page.
|
This supports rendering of whole files. If you want to render code blocks in markdown you would need to do something with javascript. See some examples on the [Customizing Gitea](administration/customizing-gitea.md) page.
|
||||||
|
|
||||||
## Installing external binaries
|
## Installing external binaries
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ KEY_FILE = key.pem
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that if your certificate is signed by a third party certificate authority (i.e. not self-signed), then cert.pem should contain the certificate chain. The server certificate must be the first entry in cert.pem, followed by the intermediaries in order (if any). The root certificate does not have to be included because the connecting client must already have it in order to estalbish the trust relationship.
|
Note that if your certificate is signed by a third party certificate authority (i.e. not self-signed), then cert.pem should contain the certificate chain. The server certificate must be the first entry in cert.pem, followed by the intermediaries in order (if any). The root certificate does not have to be included because the connecting client must already have it in order to estalbish the trust relationship.
|
||||||
To learn more about the config values, please checkout the [Config Cheat Sheet](../config-cheat-sheet#server-server).
|
To learn more about the config values, please checkout the [Config Cheat Sheet](administration/config-cheat-sheet.md#server-server).
|
||||||
|
|
||||||
For the `CERT_FILE` or `KEY_FILE` field, the file path is relative to the `GITEA_CUSTOM` environment variable when it is a relative path. It can be an absolute path as well.
|
For the `CERT_FILE` or `KEY_FILE` field, the file path is relative to the `GITEA_CUSTOM` environment variable when it is a relative path. It can be an absolute path as well.
|
||||||
|
|
||||||
|
@ -85,11 +85,11 @@ ACME_DIRECTORY=https
|
||||||
ACME_EMAIL=email@example.com
|
ACME_EMAIL=email@example.com
|
||||||
```
|
```
|
||||||
|
|
||||||
To learn more about the config values, please checkout the [Config Cheat Sheet](../config-cheat-sheet#server-server).
|
To learn more about the config values, please checkout the [Config Cheat Sheet](administration/config-cheat-sheet.md#server-server).
|
||||||
|
|
||||||
## Using a reverse proxy
|
## Using a reverse proxy
|
||||||
|
|
||||||
Setup up your reverse proxy as shown in the [reverse proxy guide](../reverse-proxies).
|
Setup up your reverse proxy as shown in the [reverse proxy guide](administration/reverse-proxies.md).
|
||||||
|
|
||||||
After that, enable HTTPS by following one of these guides:
|
After that, enable HTTPS by following one of these guides:
|
||||||
|
|
||||||
|
|
|
@ -82,11 +82,11 @@ ACME_DIRECTORY=https
|
||||||
ACME_EMAIL=email@example.com
|
ACME_EMAIL=email@example.com
|
||||||
```
|
```
|
||||||
|
|
||||||
要了解关于配置, 请访问 [配置备忘单](../config-cheat-sheet#server-server)获取更多信息
|
要了解关于配置, 请访问 [配置备忘单](administration/config-cheat-sheet.md#server-server)获取更多信息
|
||||||
|
|
||||||
## 使用反向代理服务器
|
## 使用反向代理服务器
|
||||||
|
|
||||||
按照 [reverse proxy guide](../reverse-proxies) 的规则设置你的反向代理服务器
|
按照 [reverse proxy guide](administration/reverse-proxies.md) 的规则设置你的反向代理服务器
|
||||||
|
|
||||||
然后,按照下面的向导启用 HTTPS:
|
然后,按照下面的向导启用 HTTPS:
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,8 @@ menu:
|
||||||
- [Paid Commercial Support](https://about.gitea.com/)
|
- [Paid Commercial Support](https://about.gitea.com/)
|
||||||
- [Discord](https://discord.gg/Gitea)
|
- [Discord](https://discord.gg/Gitea)
|
||||||
- [Discourse Forum](https://discourse.gitea.io/)
|
- [Discourse Forum](https://discourse.gitea.io/)
|
||||||
|
- [Matrix](https://matrix.to/#/#gitea-space:matrix.org)
|
||||||
|
- NOTE: Most of the Matrix channels are bridged with their counterpart in Discord and may experience some degree of flakiness with the bridge process.
|
||||||
|
|
||||||
**NOTE:** When asking for support, it may be a good idea to have the following available so that the person helping has all the info they need:
|
**NOTE:** When asking for support, it may be a good idea to have the following available so that the person helping has all the info they need:
|
||||||
|
|
||||||
|
|
|
@ -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 工作路径
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,7 @@ git checkout v@version@
|
||||||
|
|
||||||
- `go` @minGoVersion@ 或以上版本, 详见[这里](https://golang.google.cn/doc/install)
|
- `go` @minGoVersion@ 或以上版本, 详见[这里](https://golang.google.cn/doc/install)
|
||||||
- `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)
|
||||||
可以用来使编译过程更方便。
|
可以用来使编译过程更方便。
|
||||||
|
|
|
@ -81,7 +81,7 @@ docker run --entrypoint="" --rm -it gitea/act_runner:latest act_runner generate-
|
||||||
When you are using the docker image, you can specify the configuration file by using the `CONFIG_FILE` environment variable. Make sure that the file is mounted into the container as a volume:
|
When you are using the docker image, you can specify the configuration file by using the `CONFIG_FILE` environment variable. Make sure that the file is mounted into the container as a volume:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -v $(pwd)/config.yaml:/config.yaml -e CONFIG_FILE=/config.yaml ...
|
docker run -v $PWD/config.yaml:/config.yaml -e CONFIG_FILE=/config.yaml ...
|
||||||
```
|
```
|
||||||
|
|
||||||
You may notice the commands above are both incomplete, because it is not the time to run the act runner yet.
|
You may notice the commands above are both incomplete, because it is not the time to run the act runner yet.
|
||||||
|
@ -157,8 +157,8 @@ If you are using the docker image, behaviour will be slightly different. Registr
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run \
|
docker run \
|
||||||
-v $(pwd)/config.yaml:/config.yaml \
|
-v $PWD/config.yaml:/config.yaml \
|
||||||
-v $(pwd)/data:/data \
|
-v $PWD/data:/data \
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
-e CONFIG_FILE=/config.yaml \
|
-e CONFIG_FILE=/config.yaml \
|
||||||
-e GITEA_INSTANCE_URL=<instance_url> \
|
-e GITEA_INSTANCE_URL=<instance_url> \
|
||||||
|
|
|
@ -157,12 +157,13 @@ Uses the following fields:
|
||||||
|
|
||||||
- User Attribute in Group (optional)
|
- User Attribute in Group (optional)
|
||||||
|
|
||||||
- Which user LDAP attribute is listed in the group.
|
- The user attribute that is used to reference a user in the group object.
|
||||||
- Example: `uid`
|
- Example: `uid` if the group objects contains a `member: bender` and the user object contains a `uid: bender`.
|
||||||
|
- Example: `dn` if the group object contains a `member: uid=bender,ou=users,dc=planetexpress,dc=com`.
|
||||||
|
|
||||||
- Group Attribute for User (optional)
|
- Group Attribute for User (optional)
|
||||||
- Which group LDAP attribute contains an array above user attribute names.
|
- The attribute of the group object that lists/contains the group members.
|
||||||
- Example: `memberUid`
|
- Example: `memberUid` or `member`
|
||||||
|
|
||||||
## PAM (Pluggable Authentication Module)
|
## PAM (Pluggable Authentication Module)
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ menu:
|
||||||
|
|
||||||
标签具有必填的名称和颜色,可选的描述,以及必须是独占的或非独占的(见下面的“作用域标签”)。
|
标签具有必填的名称和颜色,可选的描述,以及必须是独占的或非独占的(见下面的“作用域标签”)。
|
||||||
|
|
||||||
当您创建一个仓库时,可以通过使用 `工单标签(Issue Labels)` 选项来选择标签集。该选项列出了一些在您的实例上 [全局配置的可用标签集](../administration/customizing-gitea/#labels)。在创建仓库时,这些标签也将被创建。
|
当您创建一个仓库时,可以通过使用 `工单标签(Issue Labels)` 选项来选择标签集。该选项列出了一些在您的实例上 [全局配置的可用标签集](administration/customizing-gitea.md#labels)。在创建仓库时,这些标签也将被创建。
|
||||||
|
|
||||||
## 作用域标签
|
## 作用域标签
|
||||||
|
|
||||||
|
|
|
@ -94,7 +94,7 @@ Sometimes a commit or pull request may fix or bring back a problem documented
|
||||||
in a particular issue. Gitea supports closing and reopening the referenced
|
in a particular issue. Gitea supports closing and reopening the referenced
|
||||||
issues by preceding the reference with a particular _keyword_. Common keywords
|
issues by preceding the reference with a particular _keyword_. Common keywords
|
||||||
include "closes", "fixes", "reopens", etc. This list can be
|
include "closes", "fixes", "reopens", etc. This list can be
|
||||||
[customized]({{< ref "doc/administration/config-cheat-sheet.en-us.md" >}}) by the
|
[customized](administration/config-cheat-sheet.md) by the
|
||||||
site administrator.
|
site administrator.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
|
@ -66,7 +66,7 @@ menu:
|
||||||
|
|
||||||
## 可操作的引用在合并请求和提交消息中
|
## 可操作的引用在合并请求和提交消息中
|
||||||
|
|
||||||
有时,一个提交或合并请求可能会修复或重新出现在某个特定工单中。Gitea 支持在引用之前加上特定的“关键字”来关闭和重新打开被引用的工单。常见的关键字包括“closes”、“fixes”、“reopens”等。这个列表可以由站点管理员进行 [自定义]({{< ref "doc/administration/config-cheat-sheet.zh-cn.md" >}})。
|
有时,一个提交或合并请求可能会修复或重新出现在某个特定工单中。Gitea 支持在引用之前加上特定的“关键字”来关闭和重新打开被引用的工单。常见的关键字包括“closes”、“fixes”、“reopens”等。这个列表可以由站点管理员进行 [自定义](administration/config-cheat-sheet.md)。
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
|
|
||||||
|
|
35
docs/content/usage/multi-factor-authentication.en-us.md
Normal file
35
docs/content/usage/multi-factor-authentication.en-us.md
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
---
|
||||||
|
date: "2023-08-22T14:21:00+08:00"
|
||||||
|
title: "Multi-factor Authentication (MFA)"
|
||||||
|
slug: "multi-factor-authentication"
|
||||||
|
weight: 15
|
||||||
|
toc: false
|
||||||
|
draft: false
|
||||||
|
menu:
|
||||||
|
sidebar:
|
||||||
|
parent: "usage"
|
||||||
|
name: "Multi-factor Authentication (MFA)"
|
||||||
|
weight: 15
|
||||||
|
identifier: "multi-factor-authentication"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Multi-factor Authentication (MFA)
|
||||||
|
|
||||||
|
Multi-factor Authentication (also referred to as MFA or 2FA) enhances security by requiring a time-sensitive set of credentials in addition to a password.
|
||||||
|
If a password were later to be compromised, logging into Gitea will not be possible without the additional credentials and the account would remain secure.
|
||||||
|
Gitea supports both TOTP (Time-based One-Time Password) tokens and FIDO-based hardware keys using the Webauthn API.
|
||||||
|
|
||||||
|
MFA can be configured within the "Security" tab of the user settings page.
|
||||||
|
|
||||||
|
## MFA Considerations
|
||||||
|
|
||||||
|
Enabling MFA on a user does affect how the Git HTTP protocol can be used with the Git CLI.
|
||||||
|
This interface does not support MFA, and trying to use a password normally will no longer be possible whilst MFA is enabled.
|
||||||
|
If SSH is not an option for Git operations, an access token can be generated within the "Applications" tab of the user settings page.
|
||||||
|
This access token can be used as if it were a password in order to allow the Git CLI to function over HTTP.
|
||||||
|
|
||||||
|
> **Warning** - By its very nature, an access token sidesteps the security benefits of MFA.
|
||||||
|
> It must be kept secure and should only be used as a last resort.
|
||||||
|
|
||||||
|
The Gitea API supports providing the relevant TOTP password in the `X-Gitea-OTP` header, as described in [API Usage](development/api-usage.md).
|
||||||
|
This should be used instead of an access token where possible.
|
|
@ -66,4 +66,4 @@ The first value of the list will be used in helpers.
|
||||||
|
|
||||||
## Pull Request Templates
|
## Pull Request Templates
|
||||||
|
|
||||||
You can find more information about pull request templates at the page [Issue and Pull Request templates](issue-pull-request-templates).
|
You can find more information about pull request templates at the page [Issue and Pull Request templates](usage/issue-pull-request-templates.md).
|
||||||
|
|
|
@ -30,4 +30,4 @@ WORK_IN_PROGRESS_PREFIXES=WIP:,[WIP]
|
||||||
|
|
||||||
## 合并请求模板
|
## 合并请求模板
|
||||||
|
|
||||||
有关合并请求模板的更多信息请您移步 : [工单与合并请求模板](issue-pull-request-templates)
|
有关合并请求模板的更多信息请您移步 : [工单与合并请求模板](usage/issue-pull-request-templates.md)
|
||||||
|
|
|
@ -31,4 +31,4 @@ WORK_IN_PROGRESS_PREFIXES=WIP:,[WIP]
|
||||||
|
|
||||||
## 合併請求範本
|
## 合併請求範本
|
||||||
|
|
||||||
您可以在[問題與合併請求範本](issue-pull-request-templates)找到更多關於合併請求範本的資訊。
|
您可以在[問題與合併請求範本](usage/issue-pull-request-templates.md)找到更多關於合併請求範本的資訊。
|
||||||
|
|
|
@ -53,6 +53,15 @@ func (app *OAuth2Application) TableName() string {
|
||||||
|
|
||||||
// ContainsRedirectURI checks if redirectURI is allowed for app
|
// ContainsRedirectURI checks if redirectURI is allowed for app
|
||||||
func (app *OAuth2Application) ContainsRedirectURI(redirectURI string) bool {
|
func (app *OAuth2Application) ContainsRedirectURI(redirectURI string) bool {
|
||||||
|
contains := func(s string) bool {
|
||||||
|
s = strings.TrimSuffix(strings.ToLower(s), "/")
|
||||||
|
for _, u := range app.RedirectURIs {
|
||||||
|
if strings.TrimSuffix(strings.ToLower(u), "/") == s {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
if !app.ConfidentialClient {
|
if !app.ConfidentialClient {
|
||||||
uri, err := url.Parse(redirectURI)
|
uri, err := url.Parse(redirectURI)
|
||||||
// ignore port for http loopback uris following https://datatracker.ietf.org/doc/html/rfc8252#section-7.3
|
// ignore port for http loopback uris following https://datatracker.ietf.org/doc/html/rfc8252#section-7.3
|
||||||
|
@ -61,13 +70,13 @@ func (app *OAuth2Application) ContainsRedirectURI(redirectURI string) bool {
|
||||||
if ip != nil && ip.IsLoopback() {
|
if ip != nil && ip.IsLoopback() {
|
||||||
// strip port
|
// strip port
|
||||||
uri.Host = uri.Hostname()
|
uri.Host = uri.Hostname()
|
||||||
if util.SliceContainsString(app.RedirectURIs, uri.String(), true) {
|
if contains(uri.String()) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return util.SliceContainsString(app.RedirectURIs, redirectURI, true)
|
return contains(redirectURI)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Base32 characters, but lowercased.
|
// Base32 characters, but lowercased.
|
||||||
|
|
|
@ -63,6 +63,18 @@ func TestOAuth2Application_ContainsRedirectURI_WithPort(t *testing.T) {
|
||||||
assert.False(t, app.ContainsRedirectURI(":"))
|
assert.False(t, app.ContainsRedirectURI(":"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOAuth2Application_ContainsRedirect_Slash(t *testing.T) {
|
||||||
|
app := &auth_model.OAuth2Application{RedirectURIs: []string{"http://127.0.0.1"}}
|
||||||
|
assert.True(t, app.ContainsRedirectURI("http://127.0.0.1"))
|
||||||
|
assert.True(t, app.ContainsRedirectURI("http://127.0.0.1/"))
|
||||||
|
assert.False(t, app.ContainsRedirectURI("http://127.0.0.1/other"))
|
||||||
|
|
||||||
|
app = &auth_model.OAuth2Application{RedirectURIs: []string{"http://127.0.0.1/"}}
|
||||||
|
assert.True(t, app.ContainsRedirectURI("http://127.0.0.1"))
|
||||||
|
assert.True(t, app.ContainsRedirectURI("http://127.0.0.1/"))
|
||||||
|
assert.False(t, app.ContainsRedirectURI("http://127.0.0.1/other"))
|
||||||
|
}
|
||||||
|
|
||||||
func TestOAuth2Application_ValidateClientSecret(t *testing.T) {
|
func TestOAuth2Application_ValidateClientSecret(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
app := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1})
|
app := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1})
|
||||||
|
|
|
@ -11,10 +11,13 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
|
||||||
"xorm.io/xorm"
|
"xorm.io/xorm"
|
||||||
|
"xorm.io/xorm/contexts"
|
||||||
"xorm.io/xorm/names"
|
"xorm.io/xorm/names"
|
||||||
"xorm.io/xorm/schemas"
|
"xorm.io/xorm/schemas"
|
||||||
|
|
||||||
|
@ -147,6 +150,13 @@ func InitEngine(ctx context.Context) error {
|
||||||
xormEngine.SetConnMaxLifetime(setting.Database.ConnMaxLifetime)
|
xormEngine.SetConnMaxLifetime(setting.Database.ConnMaxLifetime)
|
||||||
xormEngine.SetDefaultContext(ctx)
|
xormEngine.SetDefaultContext(ctx)
|
||||||
|
|
||||||
|
if setting.Database.SlowQueryTreshold > 0 {
|
||||||
|
xormEngine.AddHook(&SlowQueryHook{
|
||||||
|
Treshold: setting.Database.SlowQueryTreshold,
|
||||||
|
Logger: log.GetLogger("xorm"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
SetDefaultEngine(ctx, xormEngine)
|
SetDefaultEngine(ctx, xormEngine)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -300,3 +310,21 @@ func SetLogSQL(ctx context.Context, on bool) {
|
||||||
sess.Engine().ShowSQL(on)
|
sess.Engine().ShowSQL(on)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SlowQueryHook struct {
|
||||||
|
Treshold time.Duration
|
||||||
|
Logger log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ contexts.Hook = &SlowQueryHook{}
|
||||||
|
|
||||||
|
func (SlowQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
|
||||||
|
return c.Ctx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SlowQueryHook) AfterProcess(c *contexts.ContextHook) error {
|
||||||
|
if c.ExecuteTime >= h.Treshold {
|
||||||
|
h.Logger.Log(8, log.WARN, "[Slow SQL Query] %s %v - %v", c.SQL, c.Args, c.ExecuteTime)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -6,15 +6,19 @@ package db_test
|
||||||
import (
|
import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
issues_model "code.gitea.io/gitea/models/issues"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
|
|
||||||
_ "code.gitea.io/gitea/cmd" // for TestPrimaryKeys
|
_ "code.gitea.io/gitea/cmd" // for TestPrimaryKeys
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"xorm.io/xorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDumpDatabase(t *testing.T) {
|
func TestDumpDatabase(t *testing.T) {
|
||||||
|
@ -85,3 +89,37 @@ func TestPrimaryKeys(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSlowQuery(t *testing.T) {
|
||||||
|
lc, cleanup := test.NewLogChecker("slow-query")
|
||||||
|
lc.StopMark("[Slow SQL Query]")
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
e := db.GetEngine(db.DefaultContext)
|
||||||
|
engine, ok := e.(*xorm.Engine)
|
||||||
|
assert.True(t, ok)
|
||||||
|
|
||||||
|
// It's not possible to clean this up with XORM, but it's luckily not harmful
|
||||||
|
// to leave around.
|
||||||
|
engine.AddHook(&db.SlowQueryHook{
|
||||||
|
Treshold: time.Second * 10,
|
||||||
|
Logger: log.GetLogger("slow-query"),
|
||||||
|
})
|
||||||
|
|
||||||
|
// NOOP query.
|
||||||
|
e.Exec("SELECT 1 WHERE false;")
|
||||||
|
|
||||||
|
_, stopped := lc.Check(100 * time.Millisecond)
|
||||||
|
assert.False(t, stopped)
|
||||||
|
|
||||||
|
engine.AddHook(&db.SlowQueryHook{
|
||||||
|
Treshold: 0, // Every query should be logged.
|
||||||
|
Logger: log.GetLogger("slow-query"),
|
||||||
|
})
|
||||||
|
|
||||||
|
// NOOP query.
|
||||||
|
e.Exec("SELECT 1 WHERE false;")
|
||||||
|
|
||||||
|
_, stopped = lc.Check(100 * time.Millisecond)
|
||||||
|
assert.True(t, stopped)
|
||||||
|
}
|
||||||
|
|
|
@ -277,3 +277,11 @@
|
||||||
lower_email: user2-2@example.com
|
lower_email: user2-2@example.com
|
||||||
is_activated: false
|
is_activated: false
|
||||||
is_primary: false
|
is_primary: false
|
||||||
|
|
||||||
|
-
|
||||||
|
id: 36
|
||||||
|
uid: 36
|
||||||
|
email: abcde@gitea.com
|
||||||
|
lower_email: abcde@gitea.com
|
||||||
|
is_activated: true
|
||||||
|
is_primary: false
|
||||||
|
|
|
@ -1 +1,23 @@
|
||||||
[] # empty
|
-
|
||||||
|
id: 5
|
||||||
|
owner_id: 36
|
||||||
|
key_id: B15431642629B826
|
||||||
|
primary_key_id:
|
||||||
|
content: xsDNBGTrY3UBDAC2HLBqmMplAV15qSnC7g1c4dV406f5EHNhFr95Nup2My6b2eafTlvedv77s8PT/I7F3fy4apOZs5A7w2SsPlLMcQ3ev4uGOsxRtkq5RLy1Yb6SNueX0Da2UVKR5KTC5Q6BWaqxwS0IjKOLZ/xz0Pbe/ClV3bZSKBEY2omkVo3Z0HZ771vB2clPRvGJ/IdeKOsZ3ZytSFXfyiJBdARmeSPmydXLil8+Ibq5iLAeow5PK8hK1TCOnKHzLWNqcNq70tyjoHvcGi70iGjoVEEUgPCLLuU8WmzTJwlvA3BuDzjtaO7TLo/jdE6iqkHtMSS8x+43sAH6hcFRCWAVh/0Uq7n36uGDfNxGnX3YrmX3LR9x5IsBES1rGGWbpxio4o5GIf/Xd+JgDd9rzJCqRuZ3/sW/TxK38htWaVNZV0kMkHUCTc1ctzWpCm635hbFCHBhPYIp+/z206khkAKDbz/CNuU91Wazsh7KO07wrwDtxfDDbInJ8TfHE2TGjzjQzgChfmcAEQEAAQ==
|
||||||
|
verified: true
|
||||||
|
can_sign: true
|
||||||
|
can_encrypt_comms: true
|
||||||
|
can_encrypt_storage: true
|
||||||
|
can_certify: true
|
||||||
|
|
||||||
|
-
|
||||||
|
id: 6
|
||||||
|
owner_id: 36
|
||||||
|
key_id: EE3AF48454AFD619
|
||||||
|
primary_key_id: B15431642629B826
|
||||||
|
content: zsDNBGTrY3UBDADsHrzuOicQaPdUQm0+0UNrs92cESm/j/4yBBUk+sfLZAo6J99c4eh4nAQzzZ7al080rYKB0G+7xoRz1eHcQH6zrVcqB8KYtf/sdY47WaMiMyxM+kTSvzp7tsv7QuSQZ0neUEXRyYMz5ttBfIjWUd+3NDItuHyB+MtNWlS3zXgaUbe5VifqKaNmzN0Ye4yXTKcpypE3AOqPVz+iIFv3c6TmsqLHJaR4VoicCleAqLyF/28WsJO7M9dDW+EM3MZVnsVpycTURyHAJGfSk10waQZAaRwmarCN/q0KEJ+aEAK/SRliUneBZoMO5hY5iBeG432tofwaQqAahPv9uXIb1n2JEMKwnMlMA9UGD1AcDbywfj1m/ZGBBw95i4Ekkfn43RvV3THr7uJU/dRqqP+iic4MwpUrOxqELW/kmeHXlBcNbZZhEEvwRoW7U2/9eeuog4nRleRJ0pi/xOP9wmxkKjaIPIK3phdBtEpVk4w/UTAWNdyIIrFggukeAnZFyGJwlm8AEQEAAQ==
|
||||||
|
verified: true
|
||||||
|
can_sign: true
|
||||||
|
can_encrypt_comms: true
|
||||||
|
can_encrypt_storage: true
|
||||||
|
can_certify: true
|
||||||
|
|
|
@ -1301,7 +1301,7 @@
|
||||||
lower_name: limited_org36
|
lower_name: limited_org36
|
||||||
name: limited_org36
|
name: limited_org36
|
||||||
full_name: Limited Org 36
|
full_name: Limited Org 36
|
||||||
email: limited_org36@example.com
|
email: abcde@gitea.com
|
||||||
keep_email_private: false
|
keep_email_private: false
|
||||||
email_notifications_preference: enabled
|
email_notifications_preference: enabled
|
||||||
passwd: ZogKvWdyEx:password
|
passwd: ZogKvWdyEx:password
|
||||||
|
@ -1320,7 +1320,7 @@
|
||||||
allow_create_organization: true
|
allow_create_organization: true
|
||||||
prohibit_login: false
|
prohibit_login: false
|
||||||
avatar: avatar22
|
avatar: avatar22
|
||||||
avatar_email: limited_org36@example.com
|
avatar_email: abcde@gitea.com
|
||||||
use_custom_avatar: false
|
use_custom_avatar: false
|
||||||
num_followers: 0
|
num_followers: 0
|
||||||
num_following: 0
|
num_following: 0
|
||||||
|
|
|
@ -345,12 +345,21 @@ func applyMentionedCondition(sess *xorm.Session, mentionedID int64) *xorm.Sessio
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyReviewRequestedCondition(sess *xorm.Session, reviewRequestedID int64) *xorm.Session {
|
func applyReviewRequestedCondition(sess *xorm.Session, reviewRequestedID int64) *xorm.Session {
|
||||||
return sess.Join("INNER", []string{"review", "r"}, "issue.id = r.issue_id").
|
existInTeamQuery := builder.Select("team_user.team_id").
|
||||||
And("issue.poster_id <> ?", reviewRequestedID).
|
From("team_user").
|
||||||
And("r.type = ?", ReviewTypeRequest).
|
Where(builder.Eq{"team_user.uid": reviewRequestedID})
|
||||||
And("r.reviewer_id = ? and r.id in (select max(id) from review where issue_id = r.issue_id and reviewer_id = r.reviewer_id and type in (?, ?, ?))"+
|
|
||||||
" or r.reviewer_team_id in (select team_id from team_user where uid = ?)",
|
subQuery := builder.Select("review.issue_id").
|
||||||
reviewRequestedID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest, reviewRequestedID)
|
From("review").
|
||||||
|
Where(builder.And(
|
||||||
|
builder.In("review.type", []ReviewType{ReviewTypeRequest, ReviewTypeReject, ReviewTypeApprove}),
|
||||||
|
builder.Or(
|
||||||
|
builder.Eq{"review.reviewer_id": reviewRequestedID},
|
||||||
|
builder.In("review.reviewer_team_id", existInTeamQuery),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
return sess.Where("issue.poster_id <> ?", reviewRequestedID).
|
||||||
|
And(builder.In("issue.id", subQuery))
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyReviewedCondition(sess *xorm.Session, reviewedID int64) *xorm.Session {
|
func applyReviewedCondition(sess *xorm.Session, reviewedID int64) *xorm.Session {
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
"code.gitea.io/gitea/modules/validation"
|
||||||
|
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
|
@ -161,7 +162,17 @@ func ValidateEmail(email string) error {
|
||||||
return ErrEmailInvalid{email}
|
return ErrEmailInvalid{email}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: add an email allow/block list
|
// if there is no allow list, then check email against block list
|
||||||
|
if len(setting.Service.EmailDomainAllowList) == 0 &&
|
||||||
|
validation.IsEmailDomainListed(setting.Service.EmailDomainBlockList, email) {
|
||||||
|
return ErrEmailInvalid{email}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there is an allow list, then check email against allow list
|
||||||
|
if len(setting.Service.EmailDomainAllowList) > 0 &&
|
||||||
|
!validation.IsEmailDomainListed(setting.Service.EmailDomainAllowList, email) {
|
||||||
|
return ErrEmailInvalid{email}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,29 +4,18 @@
|
||||||
package context
|
package context
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/url"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetQueryBeforeSince return parsed time (unix format) from URL query's before and since
|
// GetQueryBeforeSince return parsed time (unix format) from URL query's before and since
|
||||||
func GetQueryBeforeSince(ctx *Base) (before, since int64, err error) {
|
func GetQueryBeforeSince(ctx *Base) (before, since int64, err error) {
|
||||||
qCreatedBefore, err := prepareQueryArg(ctx, "before")
|
before, err = parseFormTime(ctx, "before")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, err
|
return 0, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
qCreatedSince, err := prepareQueryArg(ctx, "since")
|
since, err = parseFormTime(ctx, "since")
|
||||||
if err != nil {
|
|
||||||
return 0, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
before, err = parseTime(qCreatedBefore)
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
since, err = parseTime(qCreatedSince)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, err
|
return 0, 0, err
|
||||||
}
|
}
|
||||||
|
@ -34,7 +23,8 @@ func GetQueryBeforeSince(ctx *Base) (before, since int64, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseTime parse time and return unix timestamp
|
// parseTime parse time and return unix timestamp
|
||||||
func parseTime(value string) (int64, error) {
|
func parseFormTime(ctx *Base, name string) (int64, error) {
|
||||||
|
value := strings.TrimSpace(ctx.FormString(name))
|
||||||
if len(value) != 0 {
|
if len(value) != 0 {
|
||||||
t, err := time.Parse(time.RFC3339, value)
|
t, err := time.Parse(time.RFC3339, value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -46,10 +36,3 @@ func parseTime(value string) (int64, error) {
|
||||||
}
|
}
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// prepareQueryArg unescape and trim a query arg
|
|
||||||
func prepareQueryArg(ctx *Base, name string) (value string, err error) {
|
|
||||||
value, err = url.PathUnescape(ctx.FormString(name))
|
|
||||||
value = strings.TrimSpace(value)
|
|
||||||
return value, err
|
|
||||||
}
|
|
||||||
|
|
61
modules/doctor/fix8312.go
Normal file
61
modules/doctor/fix8312.go
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package doctor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models"
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
org_model "code.gitea.io/gitea/models/organization"
|
||||||
|
"code.gitea.io/gitea/models/perm"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
|
||||||
|
"xorm.io/builder"
|
||||||
|
)
|
||||||
|
|
||||||
|
func fixOwnerTeamCreateOrgRepo(ctx context.Context, logger log.Logger, autofix bool) error {
|
||||||
|
count := 0
|
||||||
|
|
||||||
|
err := db.Iterate(
|
||||||
|
ctx,
|
||||||
|
builder.Eq{"authorize": perm.AccessModeOwner, "can_create_org_repo": false},
|
||||||
|
func(ctx context.Context, team *org_model.Team) error {
|
||||||
|
team.CanCreateOrgRepo = true
|
||||||
|
count++
|
||||||
|
|
||||||
|
if !autofix {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return models.UpdateTeam(team, false, false)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
logger.Critical("Unable to iterate across repounits to fix incorrect can_create_org_repo: Error %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !autofix {
|
||||||
|
if count == 0 {
|
||||||
|
logger.Info("Found no team with incorrect can_create_org_repo")
|
||||||
|
} else {
|
||||||
|
logger.Warn("Found %d teams with incorrect can_create_org_repo", count)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
logger.Info("Fixed %d teams with incorrect can_create_org_repo", count)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(&Check{
|
||||||
|
Title: "Check for incorrect can_create_org_repo for org owner teams",
|
||||||
|
Name: "fix-owner-team-create-org-repo",
|
||||||
|
IsDefault: false,
|
||||||
|
Run: fixOwnerTeamCreateOrgRepo,
|
||||||
|
Priority: 7,
|
||||||
|
})
|
||||||
|
}
|
|
@ -46,6 +46,7 @@ var (
|
||||||
ConnMaxLifetime time.Duration
|
ConnMaxLifetime time.Duration
|
||||||
IterateBufferSize int
|
IterateBufferSize int
|
||||||
AutoMigration bool
|
AutoMigration bool
|
||||||
|
SlowQueryTreshold time.Duration
|
||||||
}{
|
}{
|
||||||
Timeout: 500,
|
Timeout: 500,
|
||||||
IterateBufferSize: 50,
|
IterateBufferSize: 50,
|
||||||
|
@ -92,6 +93,7 @@ func loadDBSetting(rootCfg ConfigProvider) {
|
||||||
Database.DBConnectRetries = sec.Key("DB_RETRIES").MustInt(10)
|
Database.DBConnectRetries = sec.Key("DB_RETRIES").MustInt(10)
|
||||||
Database.DBConnectBackoff = sec.Key("DB_RETRY_BACKOFF").MustDuration(3 * time.Second)
|
Database.DBConnectBackoff = sec.Key("DB_RETRY_BACKOFF").MustDuration(3 * time.Second)
|
||||||
Database.AutoMigration = sec.Key("AUTO_MIGRATION").MustBool(true)
|
Database.AutoMigration = sec.Key("AUTO_MIGRATION").MustBool(true)
|
||||||
|
Database.SlowQueryTreshold = sec.Key("SLOW_QUERY_TRESHOLD").MustDuration(5 * time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DBConnStr returns database connection string
|
// DBConnStr returns database connection string
|
||||||
|
|
263
modules/setting/forgejo_storage_test.go
Normal file
263
modules/setting/forgejo_storage_test.go
Normal file
|
@ -0,0 +1,263 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
//
|
||||||
|
// Tests verifying the Forgejo documentation on storage settings is correct
|
||||||
|
//
|
||||||
|
// https://forgejo.org/docs/v1.20/admin/storage/
|
||||||
|
//
|
||||||
|
|
||||||
|
package setting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestForgejoDocs_StorageTypes(t *testing.T) {
|
||||||
|
iniStr := `
|
||||||
|
[server]
|
||||||
|
APP_DATA_PATH = /
|
||||||
|
`
|
||||||
|
testStorageTypesDefaultAndSpecificStorage(t, iniStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testStorageGetPath(storage *Storage) string {
|
||||||
|
if storage.Type == MinioStorageType {
|
||||||
|
return storage.MinioConfig.BasePath
|
||||||
|
}
|
||||||
|
return storage.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
var testSectionToBasePath = map[string]string{
|
||||||
|
"attachment": "attachments",
|
||||||
|
"lfs": "lfs",
|
||||||
|
"avatar": "avatars",
|
||||||
|
"repo-avatar": "repo-avatars",
|
||||||
|
"repo-archive": "repo-archive",
|
||||||
|
"packages": "packages",
|
||||||
|
"storage.actions_log": "actions_log",
|
||||||
|
"actions.artifacts": "actions_artifacts",
|
||||||
|
}
|
||||||
|
|
||||||
|
type testSectionToPathFun func(StorageType, string) string
|
||||||
|
|
||||||
|
func testBuildPath(t StorageType, path string) string {
|
||||||
|
if t == LocalStorageType {
|
||||||
|
return "/" + path
|
||||||
|
}
|
||||||
|
return path + "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSectionToPath(t StorageType, section string) string {
|
||||||
|
return testBuildPath(t, testSectionToBasePath[section])
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSpecificPath(t StorageType, section string) string {
|
||||||
|
if t == LocalStorageType {
|
||||||
|
return "/specific_local_path"
|
||||||
|
}
|
||||||
|
return "specific_s3_base_path/"
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDefaultDir(t StorageType) string {
|
||||||
|
if t == LocalStorageType {
|
||||||
|
return "default_local_path"
|
||||||
|
}
|
||||||
|
return "default_s3_base_path"
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDefaultPath(t StorageType) string {
|
||||||
|
return testBuildPath(t, testDefaultDir(t))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSectionToDefaultPath(t StorageType, section string) string {
|
||||||
|
return testBuildPath(t, filepath.Join(testDefaultDir(t), testSectionToPath(t, section)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLegacyPath(t StorageType, section string) string {
|
||||||
|
return testBuildPath(t, fmt.Sprintf("legacy_%s_path", section))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testStorageTypeToSetting(t StorageType) string {
|
||||||
|
if t == LocalStorageType {
|
||||||
|
return "PATH"
|
||||||
|
}
|
||||||
|
return "MINIO_BASE_PATH"
|
||||||
|
}
|
||||||
|
|
||||||
|
var testSectionToLegacy = map[string]string{
|
||||||
|
"lfs": fmt.Sprintf(`
|
||||||
|
[server]
|
||||||
|
APP_DATA_PATH = /
|
||||||
|
LFS_CONTENT_PATH = %s
|
||||||
|
`, testLegacyPath(LocalStorageType, "lfs")),
|
||||||
|
"avatar": fmt.Sprintf(`
|
||||||
|
[picture]
|
||||||
|
AVATAR_UPLOAD_PATH = %s
|
||||||
|
`, testLegacyPath(LocalStorageType, "avatar")),
|
||||||
|
"repo-avatar": fmt.Sprintf(`
|
||||||
|
[picture]
|
||||||
|
REPOSITORY_AVATAR_UPLOAD_PATH = %s
|
||||||
|
`, testLegacyPath(LocalStorageType, "repo-avatar")),
|
||||||
|
}
|
||||||
|
|
||||||
|
func testStorageTypesDefaultAndSpecificStorage(t *testing.T, iniStr string) {
|
||||||
|
storageType := MinioStorageType
|
||||||
|
t.Run(string(storageType), func(t *testing.T) {
|
||||||
|
t.Run("override type minio", func(t *testing.T) {
|
||||||
|
storageSection := `
|
||||||
|
[storage]
|
||||||
|
STORAGE_TYPE = minio
|
||||||
|
`
|
||||||
|
testStorageTypesSpecificStorages(t, iniStr+storageSection, storageType, testSectionToPath, testSectionToPath)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
storageType = LocalStorageType
|
||||||
|
|
||||||
|
t.Run(string(storageType), func(t *testing.T) {
|
||||||
|
storageSection := ""
|
||||||
|
testStorageTypesSpecificStorages(t, iniStr+storageSection, storageType, testSectionToPath, testSectionToPath)
|
||||||
|
|
||||||
|
t.Run("override type local", func(t *testing.T) {
|
||||||
|
storageSection := `
|
||||||
|
[storage]
|
||||||
|
STORAGE_TYPE = local
|
||||||
|
`
|
||||||
|
testStorageTypesSpecificStorages(t, iniStr+storageSection, storageType, testSectionToPath, testSectionToPath)
|
||||||
|
|
||||||
|
storageSection = fmt.Sprintf(`
|
||||||
|
[storage]
|
||||||
|
STORAGE_TYPE = local
|
||||||
|
PATH = %s
|
||||||
|
`, testDefaultPath(LocalStorageType))
|
||||||
|
testStorageTypesSpecificStorageSections(t, iniStr+storageSection, storageType, testSectionToDefaultPath, testSectionToPath)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testStorageTypesSpecificStorageSections(t *testing.T, iniStr string, defaultStorageType StorageType, defaultStorageTypePath, testSectionToPath testSectionToPathFun) {
|
||||||
|
testSectionsMap := map[string]**Storage{
|
||||||
|
"attachment": &Attachment.Storage,
|
||||||
|
"lfs": &LFS.Storage,
|
||||||
|
"avatar": &Avatar.Storage,
|
||||||
|
"repo-avatar": &RepoAvatar.Storage,
|
||||||
|
"repo-archive": &RepoArchive.Storage,
|
||||||
|
"packages": &Packages.Storage,
|
||||||
|
// there are inconsistencies in how actions storage is determined in v1.20
|
||||||
|
// it is still alpha and undocumented and is ignored for now
|
||||||
|
//"storage.actions_log": &Actions.LogStorage,
|
||||||
|
//"actions.artifacts": &Actions.ArtifactStorage,
|
||||||
|
}
|
||||||
|
|
||||||
|
for sectionName, storage := range testSectionsMap {
|
||||||
|
t.Run(sectionName, func(t *testing.T) {
|
||||||
|
testStorageTypesSpecificStorage(t, iniStr, defaultStorageType, defaultStorageTypePath, testSectionToPath, sectionName, storage)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testStorageTypesSpecificStorages(t *testing.T, iniStr string, defaultStorageType StorageType, defaultStorageTypePath, testSectionToPath testSectionToPathFun) {
|
||||||
|
testSectionsMap := map[string]**Storage{
|
||||||
|
"attachment": &Attachment.Storage,
|
||||||
|
"lfs": &LFS.Storage,
|
||||||
|
"avatar": &Avatar.Storage,
|
||||||
|
"repo-avatar": &RepoAvatar.Storage,
|
||||||
|
"repo-archive": &RepoArchive.Storage,
|
||||||
|
"packages": &Packages.Storage,
|
||||||
|
"storage.actions_log": &Actions.LogStorage,
|
||||||
|
"actions.artifacts": &Actions.ArtifactStorage,
|
||||||
|
}
|
||||||
|
|
||||||
|
for sectionName, storage := range testSectionsMap {
|
||||||
|
t.Run(sectionName, func(t *testing.T) {
|
||||||
|
if legacy, ok := testSectionToLegacy[sectionName]; ok {
|
||||||
|
if defaultStorageType == LocalStorageType {
|
||||||
|
t.Run("legacy local", func(t *testing.T) {
|
||||||
|
testStorageTypesSpecificStorage(t, iniStr+legacy, LocalStorageType, testLegacyPath, testSectionToPath, sectionName, storage)
|
||||||
|
testStorageTypesSpecificStorageTypeOverride(t, iniStr+legacy, LocalStorageType, testLegacyPath, testSectionToPath, sectionName, storage)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
t.Run("legacy minio", func(t *testing.T) {
|
||||||
|
testStorageTypesSpecificStorage(t, iniStr+legacy, MinioStorageType, defaultStorageTypePath, testSectionToPath, sectionName, storage)
|
||||||
|
testStorageTypesSpecificStorageTypeOverride(t, iniStr+legacy, LocalStorageType, testLegacyPath, testSectionToPath, sectionName, storage)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, specificStorageType := range storageTypes {
|
||||||
|
testStorageTypesSpecificStorageTypeOverride(t, iniStr, specificStorageType, defaultStorageTypePath, testSectionToPath, sectionName, storage)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testStorageTypesSpecificStorage(t *testing.T, iniStr string, defaultStorageType StorageType, defaultStorageTypePath, testSectionToPath testSectionToPathFun, sectionName string, storage **Storage) {
|
||||||
|
var section string
|
||||||
|
|
||||||
|
//
|
||||||
|
// Specific section is absent
|
||||||
|
//
|
||||||
|
testStoragePathMatch(t, iniStr, defaultStorageType, defaultStorageTypePath, sectionName, storage)
|
||||||
|
|
||||||
|
//
|
||||||
|
// Specific section is empty
|
||||||
|
//
|
||||||
|
section = fmt.Sprintf(`
|
||||||
|
[%s]
|
||||||
|
`,
|
||||||
|
sectionName)
|
||||||
|
testStoragePathMatch(t, iniStr+section, defaultStorageType, defaultStorageTypePath, sectionName, storage)
|
||||||
|
|
||||||
|
//
|
||||||
|
// Specific section with a path override
|
||||||
|
//
|
||||||
|
section = fmt.Sprintf(`
|
||||||
|
[%s]
|
||||||
|
%s = %s
|
||||||
|
`,
|
||||||
|
sectionName,
|
||||||
|
testStorageTypeToSetting(defaultStorageType),
|
||||||
|
testSpecificPath(defaultStorageType, ""))
|
||||||
|
testStoragePathMatch(t, iniStr+section, defaultStorageType, testSpecificPath, sectionName, storage)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testStorageTypesSpecificStorageTypeOverride(t *testing.T, iniStr string, overrideStorageType StorageType, defaultStorageTypePath, testSectionToPath testSectionToPathFun, sectionName string, storage **Storage) {
|
||||||
|
var section string
|
||||||
|
t.Run("specific-"+string(overrideStorageType), func(t *testing.T) {
|
||||||
|
//
|
||||||
|
// Specific section with a path and storage type override
|
||||||
|
//
|
||||||
|
section = fmt.Sprintf(`
|
||||||
|
[%s]
|
||||||
|
STORAGE_TYPE = %s
|
||||||
|
%s = %s
|
||||||
|
`,
|
||||||
|
sectionName,
|
||||||
|
overrideStorageType,
|
||||||
|
testStorageTypeToSetting(overrideStorageType),
|
||||||
|
testSpecificPath(overrideStorageType, ""))
|
||||||
|
testStoragePathMatch(t, iniStr+section, overrideStorageType, testSpecificPath, sectionName, storage)
|
||||||
|
|
||||||
|
//
|
||||||
|
// Specific section with type override
|
||||||
|
//
|
||||||
|
section = fmt.Sprintf(`
|
||||||
|
[%s]
|
||||||
|
STORAGE_TYPE = %s
|
||||||
|
`,
|
||||||
|
sectionName,
|
||||||
|
overrideStorageType)
|
||||||
|
testStoragePathMatch(t, iniStr+section, overrideStorageType, defaultStorageTypePath, sectionName, storage)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testStoragePathMatch(t *testing.T, iniStr string, storageType StorageType, testSectionToPath testSectionToPathFun, section string, storage **Storage) {
|
||||||
|
cfg, err := NewConfigProviderFromData(iniStr)
|
||||||
|
assert.NoError(t, err, iniStr)
|
||||||
|
assert.NoError(t, loadCommonSettingsFrom(cfg), iniStr)
|
||||||
|
assert.EqualValues(t, testSectionToPath(storageType, section), testStorageGetPath(*storage), iniStr)
|
||||||
|
assert.EqualValues(t, storageType, (*storage).Type, iniStr)
|
||||||
|
}
|
|
@ -221,7 +221,7 @@ func loadServiceFrom(rootCfg ConfigProvider) {
|
||||||
Service.UserDeleteWithCommentsMaxTime = sec.Key("USER_DELETE_WITH_COMMENTS_MAX_TIME").MustDuration(0)
|
Service.UserDeleteWithCommentsMaxTime = sec.Key("USER_DELETE_WITH_COMMENTS_MAX_TIME").MustDuration(0)
|
||||||
sec.Key("VALID_SITE_URL_SCHEMES").MustString("http,https")
|
sec.Key("VALID_SITE_URL_SCHEMES").MustString("http,https")
|
||||||
Service.ValidSiteURLSchemes = sec.Key("VALID_SITE_URL_SCHEMES").Strings(",")
|
Service.ValidSiteURLSchemes = sec.Key("VALID_SITE_URL_SCHEMES").Strings(",")
|
||||||
schemes := make([]string, len(Service.ValidSiteURLSchemes))
|
schemes := make([]string, 0, len(Service.ValidSiteURLSchemes))
|
||||||
for _, scheme := range Service.ValidSiteURLSchemes {
|
for _, scheme := range Service.ValidSiteURLSchemes {
|
||||||
scheme = strings.ToLower(strings.TrimSpace(scheme))
|
scheme = strings.ToLower(strings.TrimSpace(scheme))
|
||||||
if scheme != "" {
|
if scheme != "" {
|
||||||
|
|
|
@ -91,8 +91,8 @@ func NewMinioStorage(ctx context.Context, cfg *setting.Storage) (ObjectStorage,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check to see if we already own this bucket
|
// Check to see if we already own this bucket
|
||||||
exists, errBucketExists := minioClient.BucketExists(ctx, config.Bucket)
|
exists, err := minioClient.BucketExists(ctx, config.Bucket)
|
||||||
if errBucketExists != nil {
|
if err != nil {
|
||||||
return nil, convertMinioErr(err)
|
return nil, convertMinioErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ var ErrInvalidReceiveHook = errors.New("Invalid JSON payload received over webho
|
||||||
type Hook struct {
|
type Hook struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
BranchFilter string `json:"branch_filter"`
|
||||||
URL string `json:"-"`
|
URL string `json:"-"`
|
||||||
Config map[string]string `json:"config"`
|
Config map[string]string `json:"config"`
|
||||||
Events []string `json:"events"`
|
Events []string `json:"events"`
|
||||||
|
|
|
@ -10,6 +10,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
|
||||||
|
"github.com/gobwas/glob"
|
||||||
)
|
)
|
||||||
|
|
||||||
var externalTrackerRegex = regexp.MustCompile(`({?)(?:user|repo|index)+?(}?)`)
|
var externalTrackerRegex = regexp.MustCompile(`({?)(?:user|repo|index)+?(}?)`)
|
||||||
|
@ -48,6 +50,29 @@ func IsValidSiteURL(uri string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsEmailDomainListed checks whether the domain of an email address
|
||||||
|
// matches a list of domains
|
||||||
|
func IsEmailDomainListed(globs []glob.Glob, email string) bool {
|
||||||
|
if len(globs) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
n := strings.LastIndex(email, "@")
|
||||||
|
if n <= 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
domain := strings.ToLower(email[n+1:])
|
||||||
|
|
||||||
|
for _, g := range globs {
|
||||||
|
if g.Match(domain) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// IsAPIURL checks if URL is current Gitea instance API URL
|
// IsAPIURL checks if URL is current Gitea instance API URL
|
||||||
func IsAPIURL(uri string) bool {
|
func IsAPIURL(uri string) bool {
|
||||||
return strings.HasPrefix(strings.ToLower(uri), strings.ToLower(setting.AppURL+"api"))
|
return strings.HasPrefix(strings.ToLower(uri), strings.ToLower(setting.AppURL+"api"))
|
||||||
|
|
|
@ -53,8 +53,12 @@ func pickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv
|
||||||
|
|
||||||
func getSecretsOfTask(ctx context.Context, task *actions_model.ActionTask) map[string]string {
|
func getSecretsOfTask(ctx context.Context, task *actions_model.ActionTask) map[string]string {
|
||||||
secrets := map[string]string{}
|
secrets := map[string]string{}
|
||||||
|
|
||||||
|
secrets["GITHUB_TOKEN"] = task.Token
|
||||||
|
secrets["GITEA_TOKEN"] = task.Token
|
||||||
|
|
||||||
if task.Job.Run.IsForkPullRequest {
|
if task.Job.Run.IsForkPullRequest {
|
||||||
// ignore secrets for fork pull request
|
// ignore secrets for fork pull request, except GITHUB_TOKEN and GITEA_TOKEN which are automatically generated.
|
||||||
return secrets
|
return secrets
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,13 +82,6 @@ func getSecretsOfTask(ctx context.Context, task *actions_model.ActionTask) map[s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := secrets["GITHUB_TOKEN"]; !ok {
|
|
||||||
secrets["GITHUB_TOKEN"] = task.Token
|
|
||||||
}
|
|
||||||
if _, ok := secrets["GITEA_TOKEN"]; !ok {
|
|
||||||
secrets["GITEA_TOKEN"] = task.Token
|
|
||||||
}
|
|
||||||
|
|
||||||
return secrets
|
return secrets
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -326,11 +326,9 @@ func CreatePullRequest(ctx *context.APIContext) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
labelIDs = make([]int64, len(form.Labels))
|
labelIDs = make([]int64, 0, len(labels))
|
||||||
orgLabelIDs := make([]int64, len(form.Labels))
|
for _, label := range labels {
|
||||||
|
labelIDs = append(labelIDs, label.ID)
|
||||||
for i := range labels {
|
|
||||||
labelIDs[i] = labels[i].ID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.Repo.Owner.IsOrganization() {
|
if ctx.Repo.Owner.IsOrganization() {
|
||||||
|
@ -340,12 +338,12 @@ func CreatePullRequest(ctx *context.APIContext) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range orgLabels {
|
orgLabelIDs := make([]int64, 0, len(orgLabels))
|
||||||
orgLabelIDs[i] = orgLabels[i].ID
|
for _, orgLabel := range orgLabels {
|
||||||
|
orgLabelIDs = append(orgLabelIDs, orgLabel.ID)
|
||||||
}
|
}
|
||||||
|
labelIDs = append(labelIDs, orgLabelIDs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
labelIDs = append(labelIDs, orgLabelIDs...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if form.Milestone > 0 {
|
if form.Milestone > 0 {
|
||||||
|
|
|
@ -28,23 +28,31 @@ func verifyCommits(oldCommitID, newCommitID string, repo *git.Repository, env []
|
||||||
_ = stdoutWriter.Close()
|
_ = stdoutWriter.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
var command *git.Command
|
||||||
|
if oldCommitID == git.EmptySHA {
|
||||||
|
// When creating a new branch, the oldCommitID is empty, by using "newCommitID --not --all":
|
||||||
|
// List commits that are reachable by following the newCommitID, exclude "all" existing heads/tags commits
|
||||||
|
// So, it only lists the new commits received, doesn't list the commits already present in the receiving repository
|
||||||
|
command = git.NewCommand(repo.Ctx, "rev-list").AddDynamicArguments(newCommitID).AddArguments("--not", "--all")
|
||||||
|
} else {
|
||||||
|
command = git.NewCommand(repo.Ctx, "rev-list").AddDynamicArguments(oldCommitID + "..." + newCommitID)
|
||||||
|
}
|
||||||
// This is safe as force pushes are already forbidden
|
// This is safe as force pushes are already forbidden
|
||||||
err = git.NewCommand(repo.Ctx, "rev-list").AddDynamicArguments(oldCommitID + "..." + newCommitID).
|
err = command.Run(&git.RunOpts{
|
||||||
Run(&git.RunOpts{
|
Env: env,
|
||||||
Env: env,
|
Dir: repo.Path,
|
||||||
Dir: repo.Path,
|
Stdout: stdoutWriter,
|
||||||
Stdout: stdoutWriter,
|
PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error {
|
||||||
PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error {
|
_ = stdoutWriter.Close()
|
||||||
_ = stdoutWriter.Close()
|
err := readAndVerifyCommitsFromShaReader(stdoutReader, repo, env)
|
||||||
err := readAndVerifyCommitsFromShaReader(stdoutReader, repo, env)
|
if err != nil {
|
||||||
if err != nil {
|
log.Error("%v", err)
|
||||||
log.Error("%v", err)
|
cancel()
|
||||||
cancel()
|
}
|
||||||
}
|
_ = stdoutReader.Close()
|
||||||
_ = stdoutReader.Close()
|
return err
|
||||||
return err
|
},
|
||||||
},
|
})
|
||||||
})
|
|
||||||
if err != nil && !isErrUnverifiedCommit(err) {
|
if err != nil && !isErrUnverifiedCommit(err) {
|
||||||
log.Error("Unable to check commits from %s to %s in %s: %v", oldCommitID, newCommitID, repo.Path, err)
|
log.Error("Unable to check commits from %s to %s in %s: %v", oldCommitID, newCommitID, repo.Path, err)
|
||||||
}
|
}
|
||||||
|
|
43
routers/private/hook_verification_test.go
Normal file
43
routers/private/hook_verification_test.go
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package private
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testReposDir = "tests/repos/"
|
||||||
|
|
||||||
|
func TestVerifyCommits(t *testing.T) {
|
||||||
|
unittest.PrepareTestEnv(t)
|
||||||
|
|
||||||
|
gitRepo, err := git.OpenRepository(context.Background(), testReposDir+"repo1_hook_verification")
|
||||||
|
defer gitRepo.Close()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
base, head string
|
||||||
|
verified bool
|
||||||
|
}{
|
||||||
|
{"72920278f2f999e3005801e5d5b8ab8139d3641c", "d766f2917716d45be24bfa968b8409544941be32", true},
|
||||||
|
{git.EmptySHA, "93eac826f6188f34646cea81bf426aa5ba7d3bfe", true}, // New branch with verified commit
|
||||||
|
{"9779d17a04f1e2640583d35703c62460b2d86e0a", "72920278f2f999e3005801e5d5b8ab8139d3641c", false},
|
||||||
|
{git.EmptySHA, "9ce3f779ae33f31fce17fac3c512047b75d7498b", false}, // New branch with unverified commit
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
err = verifyCommits(tc.base, tc.head, gitRepo, nil)
|
||||||
|
if tc.verified {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
} else {
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
routers/private/main_test.go
Normal file
17
routers/private/main_test.go
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package private
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
unittest.MainTest(m, &unittest.TestOptions{
|
||||||
|
GiteaRootPath: filepath.Join("..", ".."),
|
||||||
|
})
|
||||||
|
}
|
1
routers/private/tests/repos/repo1_hook_verification/HEAD
Normal file
1
routers/private/tests/repos/repo1_hook_verification/HEAD
Normal file
|
@ -0,0 +1 @@
|
||||||
|
ref: refs/heads/main
|
|
@ -0,0 +1,6 @@
|
||||||
|
[core]
|
||||||
|
repositoryformatversion = 0
|
||||||
|
filemode = false
|
||||||
|
bare = true
|
||||||
|
symlinks = false
|
||||||
|
ignorecase = true
|
|
@ -0,0 +1 @@
|
||||||
|
d766f2917716d45be24bfa968b8409544941be32 refs/heads/main
|
|
@ -0,0 +1 @@
|
||||||
|
0000000000000000000000000000000000000000 d766f2917716d45be24bfa968b8409544941be32 Gitea <gitea@fake.local> 1693148474 +0800 push
|
|
@ -0,0 +1 @@
|
||||||
|
0000000000000000000000000000000000000000 d766f2917716d45be24bfa968b8409544941be32 Gitea <gitea@fake.local> 1693148474 +0800 push
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,2 @@
|
||||||
|
x•ŽK
|
||||||
|
1]çÙÒéüAÄS¸ï$Í"32ooð®ŠWð òÞ{›!žæ`–˜JC%¡.˜$Ár]sѱe$ïmòâMƒ·)£÷±(O`ªbtlÐE[:;4–àHÐ1_û<5F>”rayýáþl“é’÷~“ÊEL@cå€Xv…Mþã":µMÛƒG«_}À?Ý
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,2 @@
|
||||||
|
x•<>1
|
||||||
|
!ES{ŠéAwGGa 9EúQgW·Èí#¹AªÞû©ÕZ§/£‹€³Œ–p±ì(¤(<28>ó®óBhÈÛ¼&ᙟãÝ:pLY`ûÍãU†ð-µzŸÁ°ô†\µ×ZM:<3A>†ü¡¨Êå€óxJ/ûG}:µ3
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,3 @@
|
||||||
|
x•ŽA
|
||||||
|
Â0E]ç³$™L“ˆx•L2µ]´•<C2B4>
|
||||||
|
ÞÞê
\}ø¼ÿøe[–¹:{êM’°õZ5bŠ8$¡–Ävž°fÉRÍ37];Ôˆìbt¡Ò úå3‡$‰,tXœ¨G“÷>m
²”ªpýÅý1wÍ—²-7p<37>½£Ä„p¶ÉZs´Ç±®L̾¾´Íã¤åµLæëe@ó
|
|
@ -0,0 +1,3 @@
|
||||||
|
x•’Ë®«FE3æ+zn%44æ!%Qxƒ<78>Û€s˜AÓ`8Øæëã{£Ì2IM¶j•ª´¥Údèûf²Ìý2<C3BD>”‚"‡$§e‰¶
|
||||||
|
-(â Ä!´ÝJ"åaŲ@•BaîùHo3 ŸVØòå<$<24>/)å$JøJD’B¡•H¤§˜ü{¾#ÈRRðûOù«nfšÿF†þOÀ‰
|
||||||
|
âq[°<>2„̇~ŒÍô¬Ô÷zjjðë<C3B0>ÒLÛÅÀ·}prm¬Fqhþä`@Ø«¦ªš®ª¥Õ˜fî?3Ç[7Š³…ê¨Ð) ^™þuÿÖ¿,µ<>Æl7©zÝÿr|&«Ou4<75>Ø9Ó:µÎQjôû·êÕ1x±õå6ÍQ‡÷ƒÀ%Áåtû‰sò¸íV‰|( V¿<56>,aL,ù«G~²Ç<16>¹‹<C2B9>‹r¥ùûî@·`·Àþ$[! XËŠep©Œæ[8 oýä(›« k£Z´Î³yóeйÙÆÄ«Y²¿kÖd€¯6•3¾;3ÜÔ RÔiÞ‹dYÓDk91V]/Cê#º¾&ÿêpo´Fáb¯‹¶}§¹ô¦òuW&]+m xaqdÜIõX¯þ3
Žƒ3¶×ÆK’ÚÓI#Æi_ärgðñÁ<C3B1>ôôõÄ©7<C2A9>=ú`@[õŠ&AóṲ̂ÞLÖo–‹3~MÆóõü8MGtö²ï>ÄôŒx›¼vQ²(…<>aÅÄWŸo"¡Ës±r‰z”°eÓÅ}å†QDñóÖ¨fK)ó˜mÆr>>•ª†¿‚†ÝÌš$ÇF8³x™
Ä×^J<>k{mczþI*²^ÆMb‡þ m¸6Š”M~h¹pÕÍ
{¡¡±0€ö•]€?nUwgþÉ ‰<C2A0>ÿJ ³Ð±©Þ<ó7Û2
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1 @@
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,3 @@
|
||||||
|
x•ŽA
|
||||||
|
Â0E]ç³$™L“ˆx•L2µ]´•<C2B4>
|
||||||
|
ÞÞê
\}ø¼ÿøe[–¹:{êM’°õZ5bŠ8$¡–Ävž°fÉRÍ37];Ôˆìbt¡Ò úå3‡$‰,tXœ¨G“÷>m
²”ªpýÅý1wÍ—²-7p<37>½£Ä„p¶ÉZs´Ç±®L̾¾´Íã¤åµLæëe@ó
|
|
@ -0,0 +1 @@
|
||||||
|
d766f2917716d45be24bfa968b8409544941be32
|
|
@ -0,0 +1,127 @@
|
||||||
|
# GPG key for abcde@gitea.com
|
||||||
|
|
||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
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
|
||||||
|
4akc
|
||||||
|
=OYIo
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||||
|
|
||||||
|
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
|
||||||
|
ZZtqph2TItCeV04HoaKHHc254akc
|
||||||
|
=PPG4
|
||||||
|
-----END PGP PRIVATE KEY BLOCK-----
|
|
@ -459,7 +459,7 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo {
|
||||||
rootRepo.ID != ci.HeadRepo.ID &&
|
rootRepo.ID != ci.HeadRepo.ID &&
|
||||||
rootRepo.ID != baseRepo.ID {
|
rootRepo.ID != baseRepo.ID {
|
||||||
canRead := access_model.CheckRepoUnitUser(ctx, rootRepo, ctx.Doer, unit.TypeCode)
|
canRead := access_model.CheckRepoUnitUser(ctx, rootRepo, ctx.Doer, unit.TypeCode)
|
||||||
if canRead && rootRepo.AllowsPulls() {
|
if canRead {
|
||||||
ctx.Data["RootRepo"] = rootRepo
|
ctx.Data["RootRepo"] = rootRepo
|
||||||
if !fileOnly {
|
if !fileOnly {
|
||||||
branches, tags, err := getBranchesAndTagsForRepo(ctx, rootRepo)
|
branches, tags, err := getBranchesAndTagsForRepo(ctx, rootRepo)
|
||||||
|
|
|
@ -514,6 +514,12 @@ func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *repo_model.R
|
||||||
}
|
}
|
||||||
|
|
||||||
func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
|
func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
|
||||||
|
// Distinguish whether the owner of the repository
|
||||||
|
// is an individual or an organization
|
||||||
|
repoOwnerType := project_model.TypeIndividual
|
||||||
|
if repo.Owner.IsOrganization() {
|
||||||
|
repoOwnerType = project_model.TypeOrganization
|
||||||
|
}
|
||||||
var err error
|
var err error
|
||||||
projects, _, err := project_model.FindProjects(ctx, project_model.SearchOptions{
|
projects, _, err := project_model.FindProjects(ctx, project_model.SearchOptions{
|
||||||
RepoID: repo.ID,
|
RepoID: repo.ID,
|
||||||
|
@ -529,7 +535,7 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
|
||||||
OwnerID: repo.OwnerID,
|
OwnerID: repo.OwnerID,
|
||||||
Page: -1,
|
Page: -1,
|
||||||
IsClosed: util.OptionalBoolFalse,
|
IsClosed: util.OptionalBoolFalse,
|
||||||
Type: project_model.TypeOrganization,
|
Type: repoOwnerType,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetProjects", err)
|
ctx.ServerError("GetProjects", err)
|
||||||
|
@ -552,7 +558,7 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
|
||||||
OwnerID: repo.OwnerID,
|
OwnerID: repo.OwnerID,
|
||||||
Page: -1,
|
Page: -1,
|
||||||
IsClosed: util.OptionalBoolTrue,
|
IsClosed: util.OptionalBoolTrue,
|
||||||
Type: project_model.TypeOrganization,
|
Type: repoOwnerType,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetProjects", err)
|
ctx.ServerError("GetProjects", err)
|
||||||
|
@ -871,9 +877,17 @@ func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles
|
||||||
|
|
||||||
// NewIssue render creating issue page
|
// NewIssue render creating issue page
|
||||||
func NewIssue(ctx *context.Context) {
|
func NewIssue(ctx *context.Context) {
|
||||||
|
issueConfig, _ := issue_service.GetTemplateConfigFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
|
||||||
|
hasTemplates := issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo)
|
||||||
|
if !issueConfig.BlankIssuesEnabled && hasTemplates {
|
||||||
|
// The "issues/new" and "issues/new/choose" share the same query parameters "project" and "milestone", if blank issues are disabled, just redirect to the "issues/choose" page with these parameters.
|
||||||
|
ctx.Redirect(fmt.Sprintf("%s/issues/new/choose?%s", ctx.Repo.Repository.Link(), ctx.Req.URL.RawQuery), http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
ctx.Data["Title"] = ctx.Tr("repo.issues.new")
|
ctx.Data["Title"] = ctx.Tr("repo.issues.new")
|
||||||
ctx.Data["PageIsIssueList"] = true
|
ctx.Data["PageIsIssueList"] = true
|
||||||
ctx.Data["NewIssueChooseTemplate"] = issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo)
|
ctx.Data["NewIssueChooseTemplate"] = hasTemplates
|
||||||
ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
|
ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
|
||||||
title := ctx.FormString("title")
|
title := ctx.FormString("title")
|
||||||
ctx.Data["TitleQuery"] = title
|
ctx.Data["TitleQuery"] = title
|
||||||
|
@ -2834,53 +2848,55 @@ func NewComment(ctx *context.Context) {
|
||||||
|
|
||||||
// check whether the ref of PR <refs/pulls/pr_index/head> in base repo is consistent with the head commit of head branch in the head repo
|
// check whether the ref of PR <refs/pulls/pr_index/head> in base repo is consistent with the head commit of head branch in the head repo
|
||||||
// get head commit of PR
|
// get head commit of PR
|
||||||
prHeadRef := pull.GetGitRefName()
|
if pull.Flow == issues_model.PullRequestFlowGithub {
|
||||||
if err := pull.LoadBaseRepo(ctx); err != nil {
|
prHeadRef := pull.GetGitRefName()
|
||||||
ctx.ServerError("Unable to load base repo", err)
|
if err := pull.LoadBaseRepo(ctx); err != nil {
|
||||||
return
|
ctx.ServerError("Unable to load base repo", err)
|
||||||
}
|
|
||||||
prHeadCommitID, err := git.GetFullCommitID(ctx, pull.BaseRepo.RepoPath(), prHeadRef)
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("Get head commit Id of pr fail", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// get head commit of branch in the head repo
|
|
||||||
if err := pull.LoadHeadRepo(ctx); err != nil {
|
|
||||||
ctx.ServerError("Unable to load head repo", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if ok := git.IsBranchExist(ctx, pull.HeadRepo.RepoPath(), pull.BaseBranch); !ok {
|
|
||||||
// todo localize
|
|
||||||
ctx.Flash.Error("The origin branch is delete, cannot reopen.")
|
|
||||||
ctx.Redirect(fmt.Sprintf("%s/pulls/%d", ctx.Repo.RepoLink, pull.Index))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
headBranchRef := pull.GetGitHeadBranchRefName()
|
|
||||||
headBranchCommitID, err := git.GetFullCommitID(ctx, pull.HeadRepo.RepoPath(), headBranchRef)
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("Get head commit Id of head branch fail", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = pull.LoadIssue(ctx)
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("load the issue of pull request error", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if prHeadCommitID != headBranchCommitID {
|
|
||||||
// force push to base repo
|
|
||||||
err := git.Push(ctx, pull.HeadRepo.RepoPath(), git.PushOptions{
|
|
||||||
Remote: pull.BaseRepo.RepoPath(),
|
|
||||||
Branch: pull.HeadBranch + ":" + prHeadRef,
|
|
||||||
Force: true,
|
|
||||||
Env: repo_module.InternalPushingEnvironment(pull.Issue.Poster, pull.BaseRepo),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("force push error", err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
prHeadCommitID, err := git.GetFullCommitID(ctx, pull.BaseRepo.RepoPath(), prHeadRef)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("Get head commit Id of pr fail", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get head commit of branch in the head repo
|
||||||
|
if err := pull.LoadHeadRepo(ctx); err != nil {
|
||||||
|
ctx.ServerError("Unable to load head repo", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ok := git.IsBranchExist(ctx, pull.HeadRepo.RepoPath(), pull.BaseBranch); !ok {
|
||||||
|
// todo localize
|
||||||
|
ctx.Flash.Error("The origin branch is delete, cannot reopen.")
|
||||||
|
ctx.Redirect(fmt.Sprintf("%s/pulls/%d", ctx.Repo.RepoLink, pull.Index))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
headBranchRef := pull.GetGitHeadBranchRefName()
|
||||||
|
headBranchCommitID, err := git.GetFullCommitID(ctx, pull.HeadRepo.RepoPath(), headBranchRef)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("Get head commit Id of head branch fail", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = pull.LoadIssue(ctx)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("load the issue of pull request error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if prHeadCommitID != headBranchCommitID {
|
||||||
|
// force push to base repo
|
||||||
|
err := git.Push(ctx, pull.HeadRepo.RepoPath(), git.PushOptions{
|
||||||
|
Remote: pull.BaseRepo.RepoPath(),
|
||||||
|
Branch: pull.HeadBranch + ":" + prHeadRef,
|
||||||
|
Force: true,
|
||||||
|
Env: repo_module.InternalPushingEnvironment(pull.Issue.Poster, pull.BaseRepo),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("force push error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -156,7 +156,7 @@ func Milestones(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
repoOpts := repo_model.SearchRepoOptions{
|
repoOpts := repo_model.SearchRepoOptions{
|
||||||
Actor: ctxUser,
|
Actor: ctx.Doer,
|
||||||
OwnerID: ctxUser.ID,
|
OwnerID: ctxUser.ID,
|
||||||
Private: true,
|
Private: true,
|
||||||
AllPublic: false, // Include also all public repositories of users and public organisations
|
AllPublic: false, // Include also all public repositories of users and public organisations
|
||||||
|
@ -437,7 +437,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
||||||
// - Team has read permission to repository.
|
// - Team has read permission to repository.
|
||||||
repoOpts := &repo_model.SearchRepoOptions{
|
repoOpts := &repo_model.SearchRepoOptions{
|
||||||
Actor: ctx.Doer,
|
Actor: ctx.Doer,
|
||||||
OwnerID: ctx.Doer.ID,
|
OwnerID: ctxUser.ID,
|
||||||
Private: true,
|
Private: true,
|
||||||
AllPublic: false,
|
AllPublic: false,
|
||||||
AllLimited: false,
|
AllLimited: false,
|
||||||
|
|
|
@ -114,12 +114,13 @@ share the following fields:
|
||||||
* Example: (|(cn=gitea_users)(cn=admins))
|
* Example: (|(cn=gitea_users)(cn=admins))
|
||||||
|
|
||||||
* User Attribute in Group (optional)
|
* User Attribute in Group (optional)
|
||||||
* Which user LDAP attribute is listed in the group.
|
* The user attribute that is used to reference a user in the group object.
|
||||||
* Example: uid
|
* Example: uid if the group objects contains a member: bender and the user object contains a uid: bender.
|
||||||
|
* Example: dn if the group object contains a member: uid=bender,ou=users,dc=planetexpress,dc=com.
|
||||||
|
|
||||||
* Group Attribute for User (optional)
|
* Group Attribute for User (optional)
|
||||||
* Which group LDAP attribute contains an array above user attribute names.
|
* The attribute of the group object that lists/contains the group members.
|
||||||
* Example: memberUid
|
* Example: memberUid or member
|
||||||
|
|
||||||
* Team group map (optional)
|
* Team group map (optional)
|
||||||
* Automatically add users to Organization teams, depending on LDAP group memberships.
|
* Automatically add users to Organization teams, depending on LDAP group memberships.
|
||||||
|
|
|
@ -13,10 +13,10 @@ import (
|
||||||
"code.gitea.io/gitea/modules/context"
|
"code.gitea.io/gitea/modules/context"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/structs"
|
"code.gitea.io/gitea/modules/structs"
|
||||||
|
"code.gitea.io/gitea/modules/validation"
|
||||||
"code.gitea.io/gitea/modules/web/middleware"
|
"code.gitea.io/gitea/modules/web/middleware"
|
||||||
|
|
||||||
"gitea.com/go-chi/binding"
|
"gitea.com/go-chi/binding"
|
||||||
"github.com/gobwas/glob"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// InstallForm form for installation page
|
// InstallForm form for installation page
|
||||||
|
@ -103,29 +103,6 @@ func (f *RegisterForm) Validate(req *http.Request, errs binding.Errors) binding.
|
||||||
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsEmailDomainListed checks whether the domain of an email address
|
|
||||||
// matches a list of domains
|
|
||||||
func IsEmailDomainListed(globs []glob.Glob, email string) bool {
|
|
||||||
if len(globs) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
n := strings.LastIndex(email, "@")
|
|
||||||
if n <= 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
domain := strings.ToLower(email[n+1:])
|
|
||||||
|
|
||||||
for _, g := range globs {
|
|
||||||
if g.Match(domain) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsEmailDomainAllowed validates that the email address
|
// IsEmailDomainAllowed validates that the email address
|
||||||
// provided by the user matches what has been configured .
|
// provided by the user matches what has been configured .
|
||||||
// The email is marked as allowed if it matches any of the
|
// The email is marked as allowed if it matches any of the
|
||||||
|
@ -133,10 +110,10 @@ func IsEmailDomainListed(globs []glob.Glob, email string) bool {
|
||||||
// domains in the blocklist, if any such list is not empty.
|
// domains in the blocklist, if any such list is not empty.
|
||||||
func (f *RegisterForm) IsEmailDomainAllowed() bool {
|
func (f *RegisterForm) IsEmailDomainAllowed() bool {
|
||||||
if len(setting.Service.EmailDomainAllowList) == 0 {
|
if len(setting.Service.EmailDomainAllowList) == 0 {
|
||||||
return !IsEmailDomainListed(setting.Service.EmailDomainBlockList, f.Email)
|
return !validation.IsEmailDomainListed(setting.Service.EmailDomainBlockList, f.Email)
|
||||||
}
|
}
|
||||||
|
|
||||||
return IsEmailDomainListed(setting.Service.EmailDomainAllowList, f.Email)
|
return validation.IsEmailDomainListed(setting.Service.EmailDomainAllowList, f.Email)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MustChangePasswordForm form for updating your password after account creation
|
// MustChangePasswordForm form for updating your password after account creation
|
||||||
|
|
|
@ -334,9 +334,9 @@ func DismissApprovalReviews(ctx context.Context, doer *user_model.User, pull *is
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return db.WithTx(ctx, func(subCtx context.Context) error {
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
for _, review := range reviews {
|
for _, review := range reviews {
|
||||||
if err := issues_model.DismissReview(subCtx, review, true); err != nil {
|
if err := issues_model.DismissReview(ctx, review, true); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -186,6 +186,10 @@ func adoptRepository(ctx context.Context, repoPath string, u *user_model.User, r
|
||||||
return fmt.Errorf("updateRepository: %w", err)
|
return fmt.Errorf("updateRepository: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = repo_module.SyncReleasesWithTags(repo, gitRepo); err != nil {
|
||||||
|
return fmt.Errorf("SyncReleasesWithTags: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -114,12 +114,12 @@ func (t *TemporaryUploadRepository) LsFiles(filenames ...string) ([]string, erro
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
filelist := make([]string, len(filenames))
|
fileList := make([]string, 0, len(filenames))
|
||||||
for _, line := range bytes.Split(stdOut.Bytes(), []byte{'\000'}) {
|
for _, line := range bytes.Split(stdOut.Bytes(), []byte{'\000'}) {
|
||||||
filelist = append(filelist, string(line))
|
fileList = append(fileList, string(line))
|
||||||
}
|
}
|
||||||
|
|
||||||
return filelist, nil
|
return fileList, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveFilesFromIndex removes the given files from the index
|
// RemoveFilesFromIndex removes the given files from the index
|
||||||
|
|
|
@ -260,5 +260,6 @@ func ToHook(repoLink string, w *webhook_model.Webhook) (*api.Hook, error) {
|
||||||
AuthorizationHeader: authorizationHeader,
|
AuthorizationHeader: authorizationHeader,
|
||||||
Updated: w.UpdatedUnix.AsTime(),
|
Updated: w.UpdatedUnix.AsTime(),
|
||||||
Created: w.CreatedUnix.AsTime(),
|
Created: w.CreatedUnix.AsTime(),
|
||||||
|
BranchFilter: w.BranchFilter,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,15 +19,15 @@
|
||||||
<span class="inline required field"><label for="visibility">{{.locale.Tr "org.settings.visibility"}}</label></span>
|
<span class="inline required field"><label for="visibility">{{.locale.Tr "org.settings.visibility"}}</label></span>
|
||||||
<div class="inline-grouped-list">
|
<div class="inline-grouped-list">
|
||||||
<div class="ui radio checkbox">
|
<div class="ui radio checkbox">
|
||||||
<input class="enable-system-radio" tabindex="0" name="visibility" type="radio" value="0" {{if .DefaultOrgVisibilityMode.IsPublic}}checked{{end}}>
|
<input class="enable-system-radio" name="visibility" type="radio" value="0" {{if .DefaultOrgVisibilityMode.IsPublic}}checked{{end}}>
|
||||||
<label>{{.locale.Tr "org.settings.visibility.public"}}</label>
|
<label>{{.locale.Tr "org.settings.visibility.public"}}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui radio checkbox">
|
<div class="ui radio checkbox">
|
||||||
<input class="enable-system-radio" tabindex="0" name="visibility" type="radio" value="1" {{if .DefaultOrgVisibilityMode.IsLimited}}checked{{end}}>
|
<input class="enable-system-radio" name="visibility" type="radio" value="1" {{if .DefaultOrgVisibilityMode.IsLimited}}checked{{end}}>
|
||||||
<label>{{.locale.Tr "org.settings.visibility.limited"}}</label>
|
<label>{{.locale.Tr "org.settings.visibility.limited"}}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui radio checkbox">
|
<div class="ui radio checkbox">
|
||||||
<input class="enable-system-radio" tabindex="0" name="visibility" type="radio" value="2" {{if .DefaultOrgVisibilityMode.IsPrivate}}checked{{end}}>
|
<input class="enable-system-radio" name="visibility" type="radio" value="2" {{if .DefaultOrgVisibilityMode.IsPrivate}}checked{{end}}>
|
||||||
<label>{{.locale.Tr "org.settings.visibility.private"}}</label>
|
<label>{{.locale.Tr "org.settings.visibility.private"}}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -36,19 +36,19 @@
|
||||||
<label for="visibility">{{.locale.Tr "org.settings.visibility"}}</label>
|
<label for="visibility">{{.locale.Tr "org.settings.visibility"}}</label>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="ui radio checkbox">
|
<div class="ui radio checkbox">
|
||||||
<input class="enable-system-radio" tabindex="0" name="visibility" type="radio" value="0" {{if eq .CurrentVisibility 0}}checked{{end}}>
|
<input class="enable-system-radio" name="visibility" type="radio" value="0" {{if eq .CurrentVisibility 0}}checked{{end}}>
|
||||||
<label>{{.locale.Tr "org.settings.visibility.public"}}</label>
|
<label>{{.locale.Tr "org.settings.visibility.public"}}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="ui radio checkbox">
|
<div class="ui radio checkbox">
|
||||||
<input class="enable-system-radio" tabindex="0" name="visibility" type="radio" value="1" {{if eq .CurrentVisibility 1}}checked{{end}}>
|
<input class="enable-system-radio" name="visibility" type="radio" value="1" {{if eq .CurrentVisibility 1}}checked{{end}}>
|
||||||
<label>{{.locale.Tr "org.settings.visibility.limited"}}</label>
|
<label>{{.locale.Tr "org.settings.visibility.limited"}}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="ui radio checkbox">
|
<div class="ui radio checkbox">
|
||||||
<input class="enable-system-radio" tabindex="0" name="visibility" type="radio" value="2" {{if eq .CurrentVisibility 2}}checked{{end}}>
|
<input class="enable-system-radio" name="visibility" type="radio" value="2" {{if eq .CurrentVisibility 2}}checked{{end}}>
|
||||||
<label>{{.locale.Tr "org.settings.visibility.private"}}</label>
|
<label>{{.locale.Tr "org.settings.visibility.private"}}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -86,10 +86,10 @@
|
||||||
</div>
|
</div>
|
||||||
{{if and $.CanWriteProjects (ne .ID 0)}}
|
{{if and $.CanWriteProjects (ne .ID 0)}}
|
||||||
<div class="ui dropdown jump item">
|
<div class="ui dropdown jump item">
|
||||||
<div class="not-mobile gt-px-3" tabindex="-1">
|
<div class="not-mobile gt-px-3">
|
||||||
{{svg "octicon-kebab-horizontal"}}
|
{{svg "octicon-kebab-horizontal"}}
|
||||||
</div>
|
</div>
|
||||||
<div class="menu user-menu" tabindex="-1">
|
<div class="menu user-menu">
|
||||||
<a class="item show-modal button" data-modal="#edit-project-board-modal-{{.ID}}">
|
<a class="item show-modal button" data-modal="#edit-project-board-modal-{{.ID}}">
|
||||||
{{svg "octicon-pencil"}}
|
{{svg "octicon-pencil"}}
|
||||||
{{$.locale.Tr "repo.projects.column.edit"}}
|
{{$.locale.Tr "repo.projects.column.edit"}}
|
||||||
|
|
|
@ -77,33 +77,33 @@
|
||||||
<div class="inline field">
|
<div class="inline field">
|
||||||
<label>{{.locale.Tr "repo.template.items"}}</label>
|
<label>{{.locale.Tr "repo.template.items"}}</label>
|
||||||
<div class="ui checkbox">
|
<div class="ui checkbox">
|
||||||
<input name="git_content" type="checkbox" tabindex="0" {{if .git_content}}checked{{end}}>
|
<input name="git_content" type="checkbox" {{if .git_content}}checked{{end}}>
|
||||||
<label>{{.locale.Tr "repo.template.git_content"}}</label>
|
<label>{{.locale.Tr "repo.template.git_content"}}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui checkbox" {{if not .SignedUser.CanEditGitHook}}data-tooltip-content="{{.locale.Tr "repo.template.git_hooks_tooltip"}}"{{end}}>
|
<div class="ui checkbox" {{if not .SignedUser.CanEditGitHook}}data-tooltip-content="{{.locale.Tr "repo.template.git_hooks_tooltip"}}"{{end}}>
|
||||||
<input name="git_hooks" type="checkbox" tabindex="0" {{if .git_hooks}}checked{{end}}>
|
<input name="git_hooks" type="checkbox" {{if .git_hooks}}checked{{end}}>
|
||||||
<label>{{.locale.Tr "repo.template.git_hooks"}}</label>
|
<label>{{.locale.Tr "repo.template.git_hooks"}}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="inline field">
|
<div class="inline field">
|
||||||
<label></label>
|
<label></label>
|
||||||
<div class="ui checkbox">
|
<div class="ui checkbox">
|
||||||
<input name="webhooks" type="checkbox" tabindex="0" {{if .webhooks}}checked{{end}}>
|
<input name="webhooks" type="checkbox" {{if .webhooks}}checked{{end}}>
|
||||||
<label>{{.locale.Tr "repo.template.webhooks"}}</label>
|
<label>{{.locale.Tr "repo.template.webhooks"}}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui checkbox">
|
<div class="ui checkbox">
|
||||||
<input name="topics" type="checkbox" tabindex="0" {{if .topics}}checked{{end}}>
|
<input name="topics" type="checkbox" {{if .topics}}checked{{end}}>
|
||||||
<label>{{.locale.Tr "repo.template.topics"}}</label>
|
<label>{{.locale.Tr "repo.template.topics"}}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="inline field">
|
<div class="inline field">
|
||||||
<label></label>
|
<label></label>
|
||||||
<div class="ui checkbox">
|
<div class="ui checkbox">
|
||||||
<input name="avatar" type="checkbox" tabindex="0" {{if .avatar}}checked{{end}}>
|
<input name="avatar" type="checkbox" {{if .avatar}}checked{{end}}>
|
||||||
<label>{{.locale.Tr "repo.template.avatar"}}</label>
|
<label>{{.locale.Tr "repo.template.avatar"}}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui checkbox">
|
<div class="ui checkbox">
|
||||||
<input name="labels" type="checkbox" tabindex="0" {{if .labels}}checked{{end}}>
|
<input name="labels" type="checkbox" {{if .labels}}checked{{end}}>
|
||||||
<label>{{.locale.Tr "repo.template.issue_labels"}}</label>
|
<label>{{.locale.Tr "repo.template.issue_labels"}}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -169,7 +169,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="inline field">
|
<div class="inline field">
|
||||||
<div class="ui checkbox" id="auto-init">
|
<div class="ui checkbox" id="auto-init">
|
||||||
<input name="auto_init" type="checkbox" tabindex="0" {{if .auto_init}}checked{{end}}>
|
<input name="auto_init" type="checkbox" {{if .auto_init}}checked{{end}}>
|
||||||
<label>{{.locale.Tr "repo.auto_init"}}</label>
|
<label>{{.locale.Tr "repo.auto_init"}}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -204,7 +204,7 @@
|
||||||
<div class="inline field">
|
<div class="inline field">
|
||||||
<label>{{.locale.Tr "repo.template"}}</label>
|
<label>{{.locale.Tr "repo.template"}}</label>
|
||||||
<div class="ui checkbox">
|
<div class="ui checkbox">
|
||||||
<input name="template" type="checkbox" tabindex="0">
|
<input name="template" type="checkbox">
|
||||||
<label>{{.locale.Tr "repo.template_helper"}}</label>
|
<label>{{.locale.Tr "repo.template_helper"}}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -77,7 +77,7 @@
|
||||||
<div class="item" data-url="{{$.OwnForkRepo.Link}}/compare/{{PathEscapeSegments .}}{{$.CompareSeparator}}{{PathEscape $.HeadUser.Name}}/{{PathEscape $.HeadRepo.Name}}:{{PathEscapeSegments $.HeadBranch}}">{{$OwnForkCompareName}}:{{.}}</div>
|
<div class="item" data-url="{{$.OwnForkRepo.Link}}/compare/{{PathEscapeSegments .}}{{$.CompareSeparator}}{{PathEscape $.HeadUser.Name}}/{{PathEscape $.HeadRepo.Name}}:{{PathEscapeSegments $.HeadBranch}}">{{$OwnForkCompareName}}:{{.}}</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if .RootRepo}}
|
{{if and .RootRepo .RootRepo.AllowsPulls}}
|
||||||
{{range .RootRepoBranches}}
|
{{range .RootRepoBranches}}
|
||||||
<div class="item" data-url="{{$.RootRepo.Link}}/compare/{{PathEscapeSegments .}}{{$.CompareSeparator}}{{PathEscape $.HeadUser.Name}}/{{PathEscape $.HeadRepo.Name}}:{{PathEscapeSegments $.HeadBranch}}">{{$RootRepoCompareName}}:{{.}}</div>
|
<div class="item" data-url="{{$.RootRepo.Link}}/compare/{{PathEscapeSegments .}}{{$.CompareSeparator}}{{PathEscape $.HeadUser.Name}}/{{PathEscape $.HeadRepo.Name}}:{{PathEscapeSegments $.HeadBranch}}">{{$RootRepoCompareName}}:{{.}}</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="inline field">
|
<div class="inline field">
|
||||||
<div class="ui checkbox">
|
<div class="ui checkbox">
|
||||||
<input name="signoff" type="checkbox" tabindex="0">
|
<input name="signoff" type="checkbox">
|
||||||
<label>{{.locale.Tr "repo.editor.signoff_desc"}}</label>
|
<label>{{.locale.Tr "repo.editor.signoff_desc"}}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
<div class="field">
|
<div class="field">
|
||||||
{{template "repo/issue/fields/header" .}}
|
{{template "repo/issue/fields/header" .}}
|
||||||
{{$field := .}}
|
|
||||||
{{range $i, $opt := .item.Attributes.options}}
|
{{range $i, $opt := .item.Attributes.options}}
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="ui checkbox">
|
<div class="ui checkbox">
|
||||||
<input type="checkbox" name="form-field-{{$field.ID}}-{{$i}}" {{if $opt.required}}readonly checked{{end}}>
|
<input type="checkbox" name="form-field-{{$.item.ID}}-{{$i}}" {{if $opt.required}}readonly checked{{end}}>
|
||||||
<label>{{$opt.label}}</label>
|
<label>{{$opt.label}}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
{{avatar $.Context .SignedUser 40}}
|
{{avatar $.Context .SignedUser 40}}
|
||||||
<div class="ui segment content gt-my-0">
|
<div class="ui segment content gt-my-0">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<input name="title" id="issue_title" placeholder="{{.locale.Tr "repo.milestones.title"}}" value="{{if .TitleQuery}}{{.TitleQuery}}{{else if .IssueTemplateTitle}}{{.IssueTemplateTitle}}{{else}}{{.title}}{{end}}" tabindex="3" autofocus required maxlength="255" autocomplete="off">
|
<input name="title" id="issue_title" placeholder="{{.locale.Tr "repo.milestones.title"}}" value="{{if .TitleQuery}}{{.TitleQuery}}{{else if .IssueTemplateTitle}}{{.IssueTemplateTitle}}{{else}}{{.title}}{{end}}" autofocus required maxlength="255" autocomplete="off">
|
||||||
{{if .PageIsComparePull}}
|
{{if .PageIsComparePull}}
|
||||||
<div class="title_wip_desc" data-wip-prefixes="{{JsonUtils.EncodeToString .PullRequestWorkInProgressPrefixes}}">{{.locale.Tr "repo.pulls.title_wip_desc" (index .PullRequestWorkInProgressPrefixes 0| Escape) | Safe}}</div>
|
<div class="title_wip_desc" data-wip-prefixes="{{JsonUtils.EncodeToString .PullRequestWorkInProgressPrefixes}}">{{.locale.Tr "repo.pulls.title_wip_desc" (index .PullRequestWorkInProgressPrefixes 0| Escape) | Safe}}</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@ -35,7 +35,7 @@
|
||||||
{{template "repo/issue/comment_tab" .}}
|
{{template "repo/issue/comment_tab" .}}
|
||||||
{{end}}
|
{{end}}
|
||||||
<div class="text right">
|
<div class="text right">
|
||||||
<button class="ui green button loading-button" tabindex="6">
|
<button class="ui green button loading-button">
|
||||||
{{if .PageIsComparePull}}
|
{{if .PageIsComparePull}}
|
||||||
{{.locale.Tr "repo.pulls.create"}}
|
{{.locale.Tr "repo.pulls.create"}}
|
||||||
{{else}}
|
{{else}}
|
||||||
|
|
|
@ -609,7 +609,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="ui fluid dropdown selection" tabindex="0">
|
<div class="ui fluid dropdown selection">
|
||||||
|
|
||||||
<select name="reason">
|
<select name="reason">
|
||||||
<option value=""> </option>
|
<option value=""> </option>
|
||||||
|
|
|
@ -90,10 +90,10 @@
|
||||||
</div>
|
</div>
|
||||||
{{if and $.CanWriteProjects (not $.Repository.IsArchived) (ne .ID 0)}}
|
{{if and $.CanWriteProjects (not $.Repository.IsArchived) (ne .ID 0)}}
|
||||||
<div class="ui dropdown jump item">
|
<div class="ui dropdown jump item">
|
||||||
<div class="not-mobile gt-px-3" tabindex="-1">
|
<div class="not-mobile gt-px-3">
|
||||||
{{svg "octicon-kebab-horizontal"}}
|
{{svg "octicon-kebab-horizontal"}}
|
||||||
</div>
|
</div>
|
||||||
<div class="menu user-menu" tabindex="-1">
|
<div class="menu user-menu">
|
||||||
<a class="item show-modal button" data-modal="#edit-project-board-modal-{{.ID}}">
|
<a class="item show-modal button" data-modal="#edit-project-board-modal-{{.ID}}">
|
||||||
{{svg "octicon-pencil"}}
|
{{svg "octicon-pencil"}}
|
||||||
{{$.locale.Tr "repo.projects.column.edit"}}
|
{{$.locale.Tr "repo.projects.column.edit"}}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue