Compare commits

...

11 Commits

Author SHA1 Message Date
silverwind
79e7a6ec1e
Add CSS rules for basic colored labels (#23774)
Before:

<img width="164" alt="Screenshot 2023-03-28 at 23 35 46"
src="https://user-images.githubusercontent.com/115237/228372437-663111b9-7285-4fa2-9125-fb5e1cad21d7.png">

After:
<img width="166" alt="Screenshot 2023-03-28 at 23 35 54"
src="https://user-images.githubusercontent.com/115237/228372441-49430517-6b2d-4389-b11c-c30a724f6de7.png">

Also I removed the `!important` on the primary label as it's very likely
unnecessary with the amount of specificity the selector already has.
2023-03-28 22:58:31 -04:00
techknowlogick
92c160d8e7
Add meilisearch support (#23136)
Add meilisearch support

Fixes #20665
2023-03-28 22:23:23 -04:00
Jason Song
265675a31c
Add missing translation for actions.runners.reset_registration_token_success (#23732)
Used at

4011821c94/routers/web/shared/actions/runners.go (L157)
2023-03-29 09:20:48 +08:00
GiteaBot
9c0dc6c73a [skip ci] Updated translations via Crowdin 2023-03-29 00:40:29 +00:00
JakobDev
f384b13f1c
Implement Issue Config (#20956)
Closes #20955

This PR adds the possibility to disable blank Issues, when the Repo has
templates. This can be done by creating the file
`.gitea/issue_config.yaml` with the content `blank_issues_enabled` in
the Repo.
2023-03-28 14:22:07 -04:00
Mai-Lapyst
5cd1d6c93b
Set repository link based on the url in package.json for npm packages (#20379)
automatically set repository link for package based on the repository
url present inside package.json

closes #20146
2023-03-28 13:55:03 -04:00
qwerty287
3cab9c6b0c
Add API to manage issue dependencies (#17935)
Adds API endpoints to manage issue/PR dependencies
* `GET /repos/{owner}/{repo}/issues/{index}/blocks` List issues that are
blocked by this issue
* `POST /repos/{owner}/{repo}/issues/{index}/blocks` Block the issue
given in the body by the issue in path
* `DELETE /repos/{owner}/{repo}/issues/{index}/blocks` Unblock the issue
given in the body by the issue in path
* `GET /repos/{owner}/{repo}/issues/{index}/dependencies` List an
issue's dependencies
* `POST /repos/{owner}/{repo}/issues/{index}/dependencies` Create a new
issue dependencies
* `DELETE /repos/{owner}/{repo}/issues/{index}/dependencies` Remove an
issue dependency

Closes https://github.com/go-gitea/gitea/issues/15393
Closes #22115

Co-authored-by: Andrew Thornton <art27@cantab.net>
2023-03-28 13:23:25 -04:00
Balki
85e8c837b8
Add creation time in tag list page (#23693)
Fixes #21699
2023-03-28 12:51:13 -04:00
wxiaoguang
5727056ea1
Make minio package support legacy MD5 checksum (#23768)
A feedback from discord:
https://discord.com/channels/322538954119184384/561007778139734027/1090185427115319386

Some storages like:

 * https://developers.cloudflare.com/r2/api/s3/api/
 * https://www.backblaze.com/b2/docs/s3_compatible_api.html

They do not support "x-amz-checksum-algorithm" header

But minio recently uses that header with CRC32C by default. So we have
to tell minio to use legacy MD5 checksum.

I guess this needs to be backported because IIRC we 1.19 and 1.20 are
using similar minio package.


The minio package code for SendContentMD5 looks like this:

<details>

<img width="755" alt="image"
src="https://user-images.githubusercontent.com/2114189/228186768-4f2f6f67-62b9-4aee-9251-5af714ad9674.png">

</details>
2023-03-28 11:10:24 -04:00
Yarden Shoham
6a0ef71984
Yarden Shoham has a new email address (#23767)
Got a domain
2023-03-28 18:24:08 +08:00
Hester Gong
f401337f64
fix br display for packages curls (#23737)
Before:
<img width="1403" alt="截屏2023-03-27 15 48 23"
src="https://user-images.githubusercontent.com/17645053/227875392-399debf7-db75-4d9a-9436-409f75447c65.png">
This happens because the `<br>` matches this
[rule](e6e602fd8d/web_src/css/markup/content.css (L428)),
which is not necessary here (This is introduced by #22861, did a quick
check, and this is the only place used `<br>` inside `<code>` from the
PR):
```css
.markup code br,
.markup tt br {
  display: none;
}
```

After:
<img width="1398" alt="截屏2023-03-27 15 46 50"
src="https://user-images.githubusercontent.com/17645053/227875244-b7fba432-b32c-42f7-9517-4e05bb2e64ea.png">

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2023-03-28 01:18:01 -04:00
40 changed files with 2134 additions and 89 deletions

View File

@ -44,7 +44,7 @@ Jimmy Praet <jimmy.praet@telenet.be> (@jpraet)
Leon Hofmeister <dev.lh@web.de> (@delvh) Leon Hofmeister <dev.lh@web.de> (@delvh)
Wim <wim@42.be> (@42wim) Wim <wim@42.be> (@42wim)
Jason Song <i@wolfogre.com> (@wolfogre) Jason Song <i@wolfogre.com> (@wolfogre)
Yarden Shoham <hrsi88@gmail.com> (@yardenshoham) Yarden Shoham <git@yardenshoham.com> (@yardenshoham)
Yu Tian <zettat123@gmail.com> (@Zettat123) Yu Tian <zettat123@gmail.com> (@Zettat123)
Eddie Yang <576951401@qq.com> (@yp05327) Eddie Yang <576951401@qq.com> (@yp05327)
Dong Ge <gedong_1994@163.com> (@sillyguodong) Dong Ge <gedong_1994@163.com> (@sillyguodong)

File diff suppressed because one or more lines are too long

View File

@ -72,12 +72,21 @@ var CmdMigrateStorage = cli.Command{
cli.StringFlag{ cli.StringFlag{
Name: "minio-base-path", Name: "minio-base-path",
Value: "", Value: "",
Usage: "Minio storage basepath on the bucket", Usage: "Minio storage base path on the bucket",
}, },
cli.BoolFlag{ cli.BoolFlag{
Name: "minio-use-ssl", Name: "minio-use-ssl",
Usage: "Enable SSL for minio", Usage: "Enable SSL for minio",
}, },
cli.BoolFlag{
Name: "minio-insecure-skip-verify",
Usage: "Skip SSL verification",
},
cli.StringFlag{
Name: "minio-checksum-algorithm",
Value: "",
Usage: "Minio checksum algorithm (default/md5)",
},
}, },
} }
@ -168,13 +177,15 @@ func runMigrateStorage(ctx *cli.Context) error {
dstStorage, err = storage.NewMinioStorage( dstStorage, err = storage.NewMinioStorage(
stdCtx, stdCtx,
storage.MinioStorageConfig{ storage.MinioStorageConfig{
Endpoint: ctx.String("minio-endpoint"), Endpoint: ctx.String("minio-endpoint"),
AccessKeyID: ctx.String("minio-access-key-id"), AccessKeyID: ctx.String("minio-access-key-id"),
SecretAccessKey: ctx.String("minio-secret-access-key"), SecretAccessKey: ctx.String("minio-secret-access-key"),
Bucket: ctx.String("minio-bucket"), Bucket: ctx.String("minio-bucket"),
Location: ctx.String("minio-location"), Location: ctx.String("minio-location"),
BasePath: ctx.String("minio-base-path"), BasePath: ctx.String("minio-base-path"),
UseSSL: ctx.Bool("minio-use-ssl"), UseSSL: ctx.Bool("minio-use-ssl"),
InsecureSkipVerify: ctx.Bool("minio-insecure-skip-verify"),
ChecksumAlgorithm: ctx.String("minio-checksum-algorithm"),
}) })
default: default:
return fmt.Errorf("unsupported storage type: %s", ctx.String("storage")) return fmt.Errorf("unsupported storage type: %s", ctx.String("storage"))

View File

@ -1357,13 +1357,13 @@ ROUTER = console
;; Issue Indexer settings ;; Issue Indexer settings
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; ;;
;; Issue indexer type, currently support: bleve, db or elasticsearch, default is bleve ;; Issue indexer type, currently support: bleve, db, elasticsearch or meilisearch default is bleve
;ISSUE_INDEXER_TYPE = bleve ;ISSUE_INDEXER_TYPE = bleve
;; ;;
;; Issue indexer storage path, available when ISSUE_INDEXER_TYPE is bleve ;; Issue indexer storage path, available when ISSUE_INDEXER_TYPE is bleve
;ISSUE_INDEXER_PATH = indexers/issues.bleve ; Relative paths will be made absolute against _`AppWorkPath`_. ;ISSUE_INDEXER_PATH = indexers/issues.bleve ; Relative paths will be made absolute against _`AppWorkPath`_.
;; ;;
;; Issue indexer connection string, available when ISSUE_INDEXER_TYPE is elasticsearch ;; Issue indexer connection string, available when ISSUE_INDEXER_TYPE is elasticsearch or meilisearch
;ISSUE_INDEXER_CONN_STR = http://elastic:changeme@localhost:9200 ;ISSUE_INDEXER_CONN_STR = http://elastic:changeme@localhost:9200
;; ;;
;; Issue indexer name, available when ISSUE_INDEXER_TYPE is elasticsearch ;; Issue indexer name, available when ISSUE_INDEXER_TYPE is elasticsearch
@ -1886,6 +1886,9 @@ ROUTER = console
;; ;;
;; Minio skip SSL verification available when STORAGE_TYPE is `minio` ;; Minio skip SSL verification available when STORAGE_TYPE is `minio`
;MINIO_INSECURE_SKIP_VERIFY = false ;MINIO_INSECURE_SKIP_VERIFY = false
;;
;; Minio checksum algorithm: default (for MinIO or AWS S3) or md5 (for Cloudflare or Backblaze)
;MINIO_CHECKSUM_ALGORITHM = default
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@ -456,8 +456,8 @@ relation to port exhaustion.
## Indexer (`indexer`) ## Indexer (`indexer`)
- `ISSUE_INDEXER_TYPE`: **bleve**: Issue indexer type, currently supported: `bleve`, `db` or `elasticsearch`. - `ISSUE_INDEXER_TYPE`: **bleve**: Issue indexer type, currently supported: `bleve`, `db`, `elasticsearch` or `meilisearch`.
- `ISSUE_INDEXER_CONN_STR`: ****: Issue indexer connection string, available when ISSUE_INDEXER_TYPE is elasticsearch. i.e. http://elastic:changeme@localhost:9200 - `ISSUE_INDEXER_CONN_STR`: ****: Issue indexer connection string, available when ISSUE_INDEXER_TYPE is elasticsearch, or meilisearch. i.e. http://elastic:changeme@localhost:9200
- `ISSUE_INDEXER_NAME`: **gitea_issues**: Issue indexer name, available when ISSUE_INDEXER_TYPE is elasticsearch - `ISSUE_INDEXER_NAME`: **gitea_issues**: Issue indexer name, available when ISSUE_INDEXER_TYPE is elasticsearch
- `ISSUE_INDEXER_PATH`: **indexers/issues.bleve**: Index file used for issue search; available when ISSUE_INDEXER_TYPE is bleve and elasticsearch. Relative paths will be made absolute against _`AppWorkPath`_. - `ISSUE_INDEXER_PATH`: **indexers/issues.bleve**: Index file used for issue search; available when ISSUE_INDEXER_TYPE is bleve and elasticsearch. Relative paths will be made absolute against _`AppWorkPath`_.
- The next 4 configuration values are deprecated and should be set in `queue.issue_indexer` however are kept for backwards compatibility: - The next 4 configuration values are deprecated and should be set in `queue.issue_indexer` however are kept for backwards compatibility:
@ -855,6 +855,7 @@ Default templates for project boards:
- `MINIO_BASE_PATH`: **attachments/**: Minio base path on the bucket only available when STORAGE_TYPE is `minio` - `MINIO_BASE_PATH`: **attachments/**: Minio base path on the bucket only available when STORAGE_TYPE is `minio`
- `MINIO_USE_SSL`: **false**: Minio enabled ssl only available when STORAGE_TYPE is `minio` - `MINIO_USE_SSL`: **false**: Minio enabled ssl only available when STORAGE_TYPE is `minio`
- `MINIO_INSECURE_SKIP_VERIFY`: **false**: Minio skip SSL verification available when STORAGE_TYPE is `minio` - `MINIO_INSECURE_SKIP_VERIFY`: **false**: Minio skip SSL verification available when STORAGE_TYPE is `minio`
- `MINIO_CHECKSUM_ALGORITHM`: **default**: Minio checksum algorithm: `default` (for MinIO or AWS S3) or `md5` (for Cloudflare or Backblaze)
## Log (`log`) ## Log (`log`)

View File

@ -50,6 +50,17 @@ Possible file names for issue templates:
- `.github/issue_template.yaml` - `.github/issue_template.yaml`
- `.github/issue_template.yml` - `.github/issue_template.yml`
Possible file names for issue config:
- `.gitea/ISSUE_TEMPLATE/config.yaml`
- `.gitea/ISSUE_TEMPLATE/config.yml`
- `.gitea/issue_template/config.yaml`
- `.gitea/issue_template/config.yml`
- `.github/ISSUE_TEMPLATE/config.yaml`
- `.github/ISSUE_TEMPLATE/config.yml`
- `.github/issue_template/config.yaml`
- `.github/issue_template/config.yml`
Possible file names for PR templates: Possible file names for PR templates:
- `PULL_REQUEST_TEMPLATE.md` - `PULL_REQUEST_TEMPLATE.md`
@ -267,3 +278,30 @@ For each value in the options array, you can set the following keys.
|----------|------------------------------------------------------------------------------------------------------------------------------------------|----------|---------|---------|---------| |----------|------------------------------------------------------------------------------------------------------------------------------------------|----------|---------|---------|---------|
| label | The identifier for the option, which is displayed in the form. Markdown is supported for bold or italic text formatting, and hyperlinks. | Required | String | - | - | | label | The identifier for the option, which is displayed in the form. Markdown is supported for bold or italic text formatting, and hyperlinks. | Required | String | - | - |
| required | Prevents form submission until element is completed. | Optional | Boolean | false | - | | required | Prevents form submission until element is completed. | Optional | Boolean | false | - |
## Syntax for issue config
This is a example for a issue config file
```yaml
blank_issues_enabled: true
contact_links:
- name: Gitea
url: https://gitea.io
about: Visit the Gitea Website
```
### Possible Options
| Key | Description | Type | Default |
|----------------------|-------------------------------------------------------------------------------------------------------|--------------------|----------------|
| blank_issues_enabled | If set to false, the User is forced to use a Template | Boolean | true |
| contact_links | Custom Links to show in the Choose Box | Contact Link Array | Empty Array |
### Contact Link
| Key | Description | Type | Required |
|----------------------|-------------------------------------------------------------------------------------------------------|---------|----------|
| name | the name of your link | String | true |
| url | The URL of your Link | String | true |
| about | A short description of your Link | String | true |

5
go.mod
View File

@ -73,6 +73,7 @@ require (
github.com/markbates/goth v1.76.0 github.com/markbates/goth v1.76.0
github.com/mattn/go-isatty v0.0.17 github.com/mattn/go-isatty v0.0.17
github.com/mattn/go-sqlite3 v1.14.16 github.com/mattn/go-sqlite3 v1.14.16
github.com/meilisearch/meilisearch-go v0.23.0
github.com/mholt/archiver/v3 v3.5.1 github.com/mholt/archiver/v3 v3.5.1
github.com/microcosm-cc/bluemonday v1.0.22 github.com/microcosm-cc/bluemonday v1.0.22
github.com/minio/minio-go/v7 v7.0.49 github.com/minio/minio-go/v7 v7.0.49
@ -218,7 +219,7 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mholt/acmez v1.1.0 // indirect github.com/mholt/acmez v1.0.4 // indirect
github.com/miekg/dns v1.1.50 // indirect github.com/miekg/dns v1.1.50 // indirect
github.com/minio/md5-simd v1.1.2 // indirect github.com/minio/md5-simd v1.1.2 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect
@ -259,6 +260,8 @@ require (
github.com/toqueteos/webbrowser v1.2.0 // indirect github.com/toqueteos/webbrowser v1.2.0 // indirect
github.com/ulikunitz/xz v0.5.11 // indirect github.com/ulikunitz/xz v0.5.11 // indirect
github.com/unknwon/com v1.0.1 // indirect github.com/unknwon/com v1.0.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.44.0 // indirect
github.com/valyala/fastjson v1.6.4 // indirect github.com/valyala/fastjson v1.6.4 // indirect
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect

26
go.sum
View File

@ -127,6 +127,7 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
@ -151,6 +152,7 @@ github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZw
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@ -496,6 +498,7 @@ github.com/gogs/cron v0.0.0-20171120032916-9f6c956d3e14/go.mod h1:jPoNZLWDAqA5N3
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 h1:UjoPNDAQ5JPCjlxoJd6K8ALZqSDDhk2ymieAZOVaDg0= github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 h1:UjoPNDAQ5JPCjlxoJd6K8ALZqSDDhk2ymieAZOVaDg0=
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85/go.mod h1:fR6z1Ie6rtF7kl/vBYMfgD5/G5B1blui7z426/sj2DU= github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85/go.mod h1:fR6z1Ie6rtF7kl/vBYMfgD5/G5B1blui7z426/sj2DU=
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
@ -771,6 +774,9 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw= github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw=
github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4= github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
@ -862,8 +868,10 @@ github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwp
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mholt/acmez v1.1.0 h1:IQ9CGHKOHokorxnffsqDvmmE30mDenO1lptYZ1AYkHY= github.com/meilisearch/meilisearch-go v0.23.0 h1:CuqB+/NyEJKXF2SovTetAZW7lX+nSH+QTqbgSH6bv+Q=
github.com/mholt/acmez v1.1.0/go.mod h1:zwo5+fbLLTowAX8o8ETfQzbDtwGEXnPhkmGdKIP+bgs= github.com/meilisearch/meilisearch-go v0.23.0/go.mod h1:sAPJgywANHUCFUo/spCQ8SoP6sJhmfIKFWIXu7Dd5GQ=
github.com/mholt/acmez v1.0.4 h1:N3cE4Pek+dSolbsofIkAYz6H1d3pE+2G0os7QHslf80=
github.com/mholt/acmez v1.0.4/go.mod h1:qFGLZ4u+ehWINeJZjzPlsnjJBCPAADWTcIqE/7DAYQY=
github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo= github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4= github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
github.com/microcosm-cc/bluemonday v1.0.22 h1:p2tT7RNzRdCi0qmwxG+HbqD6ILkmwter1ZwVZn1oTxA= github.com/microcosm-cc/bluemonday v1.0.22 h1:p2tT7RNzRdCi0qmwxG+HbqD6ILkmwter1ZwVZn1oTxA=
@ -1181,9 +1189,15 @@ github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtX
github.com/urfave/cli v1.22.12 h1:igJgVw1JdKH+trcLWLeLwZjU9fEfPesQ+9/e4MQ44S8= github.com/urfave/cli v1.22.12 h1:igJgVw1JdKH+trcLWLeLwZjU9fEfPesQ+9/e4MQ44S8=
github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8= github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8=
github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.37.1-0.20220607072126-8a320890c08d/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=
github.com/valyala/fasthttp v1.44.0 h1:R+gLUhldIsfg1HokMuQjdQ5bh9nuXHPIfvkYUu9eR5Q=
github.com/valyala/fasthttp v1.44.0/go.mod h1:f6VbjjoI3z1NDOZOv17o6RvtRSWxC77seBFc2uWtgiY=
github.com/valyala/fastjson v1.6.3/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/valyala/fastjson v1.6.3/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
@ -1249,18 +1263,22 @@ go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@ -1383,8 +1401,10 @@ golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
@ -1492,6 +1512,7 @@ golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -1591,6 +1612,7 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=

View File

@ -134,7 +134,7 @@ func CreateIssueDependency(user *user_model.User, issue, dep *Issue) error {
} }
defer committer.Close() defer committer.Close()
// Check if it aleready exists // Check if it already exists
exists, err := issueDepExists(ctx, issue.ID, dep.ID) exists, err := issueDepExists(ctx, issue.ID, dep.ID)
if err != nil { if err != nil {
return err return err

View File

@ -189,7 +189,7 @@ func (issue *Issue) IsOverdue() bool {
// LoadRepo loads issue's repository // LoadRepo loads issue's repository
func (issue *Issue) LoadRepo(ctx context.Context) (err error) { func (issue *Issue) LoadRepo(ctx context.Context) (err error) {
if issue.Repo == nil { if issue.Repo == nil && issue.RepoID != 0 {
issue.Repo, err = repo_model.GetRepositoryByID(ctx, issue.RepoID) issue.Repo, err = repo_model.GetRepositoryByID(ctx, issue.RepoID)
if err != nil { if err != nil {
return fmt.Errorf("getRepositoryByID [%d]: %w", issue.RepoID, err) return fmt.Errorf("getRepositoryByID [%d]: %w", issue.RepoID, err)
@ -223,7 +223,7 @@ func (issue *Issue) GetPullRequest() (pr *PullRequest, err error) {
// LoadLabels loads labels // LoadLabels loads labels
func (issue *Issue) LoadLabels(ctx context.Context) (err error) { func (issue *Issue) LoadLabels(ctx context.Context) (err error) {
if issue.Labels == nil { if issue.Labels == nil && issue.ID != 0 {
issue.Labels, err = GetLabelsByIssueID(ctx, issue.ID) issue.Labels, err = GetLabelsByIssueID(ctx, issue.ID)
if err != nil { if err != nil {
return fmt.Errorf("getLabelsByIssueID [%d]: %w", issue.ID, err) return fmt.Errorf("getLabelsByIssueID [%d]: %w", issue.ID, err)
@ -234,7 +234,7 @@ func (issue *Issue) LoadLabels(ctx context.Context) (err error) {
// LoadPoster loads poster // LoadPoster loads poster
func (issue *Issue) LoadPoster(ctx context.Context) (err error) { func (issue *Issue) LoadPoster(ctx context.Context) (err error) {
if issue.Poster == nil { if issue.Poster == nil && issue.PosterID != 0 {
issue.Poster, err = user_model.GetPossibleUserByID(ctx, issue.PosterID) issue.Poster, err = user_model.GetPossibleUserByID(ctx, issue.PosterID)
if err != nil { if err != nil {
issue.PosterID = -1 issue.PosterID = -1
@ -252,7 +252,7 @@ func (issue *Issue) LoadPoster(ctx context.Context) (err error) {
// LoadPullRequest loads pull request info // LoadPullRequest loads pull request info
func (issue *Issue) LoadPullRequest(ctx context.Context) (err error) { func (issue *Issue) LoadPullRequest(ctx context.Context) (err error) {
if issue.IsPull { if issue.IsPull {
if issue.PullRequest == nil { if issue.PullRequest == nil && issue.ID != 0 {
issue.PullRequest, err = GetPullRequestByIssueID(ctx, issue.ID) issue.PullRequest, err = GetPullRequestByIssueID(ctx, issue.ID)
if err != nil { if err != nil {
if IsErrPullRequestNotExist(err) { if IsErrPullRequestNotExist(err) {
@ -261,7 +261,9 @@ func (issue *Issue) LoadPullRequest(ctx context.Context) (err error) {
return fmt.Errorf("getPullRequestByIssueID [%d]: %w", issue.ID, err) return fmt.Errorf("getPullRequestByIssueID [%d]: %w", issue.ID, err)
} }
} }
issue.PullRequest.Issue = issue if issue.PullRequest != nil {
issue.PullRequest.Issue = issue
}
} }
return nil return nil
} }
@ -2128,15 +2130,18 @@ func (issue *Issue) GetParticipantIDsByIssue(ctx context.Context) ([]int64, erro
} }
// BlockedByDependencies finds all Dependencies an issue is blocked by // BlockedByDependencies finds all Dependencies an issue is blocked by
func (issue *Issue) BlockedByDependencies(ctx context.Context) (issueDeps []*DependencyInfo, err error) { func (issue *Issue) BlockedByDependencies(ctx context.Context, opts db.ListOptions) (issueDeps []*DependencyInfo, err error) {
err = db.GetEngine(ctx). sess := db.GetEngine(ctx).
Table("issue"). Table("issue").
Join("INNER", "repository", "repository.id = issue.repo_id"). Join("INNER", "repository", "repository.id = issue.repo_id").
Join("INNER", "issue_dependency", "issue_dependency.dependency_id = issue.id"). Join("INNER", "issue_dependency", "issue_dependency.dependency_id = issue.id").
Where("issue_id = ?", issue.ID). Where("issue_id = ?", issue.ID).
// sort by repo id then created date, with the issues of the same repo at the beginning of the list // sort by repo id then created date, with the issues of the same repo at the beginning of the list
OrderBy("CASE WHEN issue.repo_id = ? THEN 0 ELSE issue.repo_id END, issue.created_unix DESC", issue.RepoID). OrderBy("CASE WHEN issue.repo_id = ? THEN 0 ELSE issue.repo_id END, issue.created_unix DESC", issue.RepoID)
Find(&issueDeps) if opts.Page != 0 {
sess = db.SetSessionPagination(sess, &opts)
}
err = sess.Find(&issueDeps)
for _, depInfo := range issueDeps { for _, depInfo := range issueDeps {
depInfo.Issue.Repo = &depInfo.Repository depInfo.Issue.Repo = &depInfo.Repository

View File

@ -658,6 +658,49 @@ func GetRepositoryByName(ownerID int64, name string) (*Repository, error) {
return repo, err return repo, err
} }
// getRepositoryURLPathSegments returns segments (owner, reponame) extracted from a url
func getRepositoryURLPathSegments(repoURL string) []string {
if strings.HasPrefix(repoURL, setting.AppURL) {
return strings.Split(strings.TrimPrefix(repoURL, setting.AppURL), "/")
}
sshURLVariants := [4]string{
setting.SSH.Domain + ":",
setting.SSH.User + "@" + setting.SSH.Domain + ":",
"git+ssh://" + setting.SSH.Domain + "/",
"git+ssh://" + setting.SSH.User + "@" + setting.SSH.Domain + "/",
}
for _, sshURL := range sshURLVariants {
if strings.HasPrefix(repoURL, sshURL) {
return strings.Split(strings.TrimPrefix(repoURL, sshURL), "/")
}
}
return nil
}
// GetRepositoryByURL returns the repository by given url
func GetRepositoryByURL(ctx context.Context, repoURL string) (*Repository, error) {
// possible urls for git:
// https://my.domain/sub-path/<owner>/<repo>.git
// https://my.domain/sub-path/<owner>/<repo>
// git+ssh://user@my.domain/<owner>/<repo>.git
// git+ssh://user@my.domain/<owner>/<repo>
// user@my.domain:<owner>/<repo>.git
// user@my.domain:<owner>/<repo>
pathSegments := getRepositoryURLPathSegments(repoURL)
if len(pathSegments) != 2 {
return nil, fmt.Errorf("unknown or malformed repository URL")
}
ownerName := pathSegments[0]
repoName := strings.TrimSuffix(pathSegments[1], ".git")
return GetRepositoryByOwnerAndName(ctx, ownerName, repoName)
}
// GetRepositoryByID returns the repository by given id if exists. // GetRepositoryByID returns the repository by given id if exists.
func GetRepositoryByID(ctx context.Context, id int64) (*Repository, error) { func GetRepositoryByID(ctx context.Context, id int64) (*Repository, error) {
repo := new(Repository) repo := new(Repository)

View File

@ -124,3 +124,65 @@ func TestMetas(t *testing.T) {
assert.Equal(t, "user3", metas["org"]) assert.Equal(t, "user3", metas["org"])
assert.Equal(t, ",owners,team1,", metas["teams"]) assert.Equal(t, ",owners,team1,", metas["teams"])
} }
func TestGetRepositoryByURL(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
t.Run("InvalidPath", func(t *testing.T) {
repo, err := repo_model.GetRepositoryByURL(db.DefaultContext, "something")
assert.Nil(t, repo)
assert.Error(t, err)
})
t.Run("ValidHttpURL", func(t *testing.T) {
test := func(t *testing.T, url string) {
repo, err := repo_model.GetRepositoryByURL(db.DefaultContext, url)
assert.NotNil(t, repo)
assert.NoError(t, err)
assert.Equal(t, repo.ID, int64(2))
assert.Equal(t, repo.OwnerID, int64(2))
}
test(t, "https://try.gitea.io/user2/repo2")
test(t, "https://try.gitea.io/user2/repo2.git")
})
t.Run("ValidGitSshURL", func(t *testing.T) {
test := func(t *testing.T, url string) {
repo, err := repo_model.GetRepositoryByURL(db.DefaultContext, url)
assert.NotNil(t, repo)
assert.NoError(t, err)
assert.Equal(t, repo.ID, int64(2))
assert.Equal(t, repo.OwnerID, int64(2))
}
test(t, "git+ssh://sshuser@try.gitea.io/user2/repo2")
test(t, "git+ssh://sshuser@try.gitea.io/user2/repo2.git")
test(t, "git+ssh://try.gitea.io/user2/repo2")
test(t, "git+ssh://try.gitea.io/user2/repo2.git")
})
t.Run("ValidImplicitSshURL", func(t *testing.T) {
test := func(t *testing.T, url string) {
repo, err := repo_model.GetRepositoryByURL(db.DefaultContext, url)
assert.NotNil(t, repo)
assert.NoError(t, err)
assert.Equal(t, repo.ID, int64(2))
assert.Equal(t, repo.OwnerID, int64(2))
}
test(t, "sshuser@try.gitea.io:user2/repo2")
test(t, "sshuser@try.gitea.io:user2/repo2.git")
test(t, "try.gitea.io:user2/repo2")
test(t, "try.gitea.io:user2/repo2.git")
})
}

View File

@ -8,6 +8,7 @@ import (
"context" "context"
"fmt" "fmt"
"html" "html"
"io"
"net/http" "net/http"
"net/url" "net/url"
"path" "path"
@ -33,6 +34,7 @@ import (
asymkey_service "code.gitea.io/gitea/services/asymkey" asymkey_service "code.gitea.io/gitea/services/asymkey"
"github.com/editorconfig/editorconfig-core-go/v2" "github.com/editorconfig/editorconfig-core-go/v2"
"gopkg.in/yaml.v3"
) )
// IssueTemplateDirCandidates issue templates directory // IssueTemplateDirCandidates issue templates directory
@ -47,6 +49,13 @@ var IssueTemplateDirCandidates = []string{
".gitlab/issue_template", ".gitlab/issue_template",
} }
var IssueConfigCandidates = []string{
".gitea/ISSUE_TEMPLATE/config",
".gitea/issue_template/config",
".github/ISSUE_TEMPLATE/config",
".github/issue_template/config",
}
// PullRequest contains information to make a pull request // PullRequest contains information to make a pull request
type PullRequest struct { type PullRequest struct {
BaseRepo *repo_model.Repository BaseRepo *repo_model.Repository
@ -1088,3 +1097,108 @@ func (ctx *Context) IssueTemplatesErrorsFromDefaultBranch() ([]*api.IssueTemplat
} }
return issueTemplates, invalidFiles return issueTemplates, invalidFiles
} }
func GetDefaultIssueConfig() api.IssueConfig {
return api.IssueConfig{
BlankIssuesEnabled: true,
ContactLinks: make([]api.IssueConfigContactLink, 0),
}
}
// GetIssueConfig loads the given issue config file.
// It never returns a nil config.
func (r *Repository) GetIssueConfig(path string, commit *git.Commit) (api.IssueConfig, error) {
if r.GitRepo == nil {
return GetDefaultIssueConfig(), nil
}
var err error
treeEntry, err := commit.GetTreeEntryByPath(path)
if err != nil {
return GetDefaultIssueConfig(), err
}
reader, err := treeEntry.Blob().DataAsync()
if err != nil {
log.Debug("DataAsync: %v", err)
return GetDefaultIssueConfig(), nil
}
defer reader.Close()
configContent, err := io.ReadAll(reader)
if err != nil {
return GetDefaultIssueConfig(), err
}
issueConfig := api.IssueConfig{}
if err := yaml.Unmarshal(configContent, &issueConfig); err != nil {
return GetDefaultIssueConfig(), err
}
for pos, link := range issueConfig.ContactLinks {
if link.Name == "" {
return GetDefaultIssueConfig(), fmt.Errorf("contact_link at position %d is missing name key", pos+1)
}
if link.URL == "" {
return GetDefaultIssueConfig(), fmt.Errorf("contact_link at position %d is missing url key", pos+1)
}
if link.About == "" {
return GetDefaultIssueConfig(), fmt.Errorf("contact_link at position %d is missing about key", pos+1)
}
_, err = url.ParseRequestURI(link.URL)
if err != nil {
return GetDefaultIssueConfig(), fmt.Errorf("%s is not a valid URL", link.URL)
}
}
return issueConfig, nil
}
// IssueConfigFromDefaultBranch returns the issue config for this repo.
// It never returns a nil config.
func (ctx *Context) IssueConfigFromDefaultBranch() (api.IssueConfig, error) {
if ctx.Repo.Repository.IsEmpty {
return GetDefaultIssueConfig(), nil
}
commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
if err != nil {
return GetDefaultIssueConfig(), err
}
for _, configName := range IssueConfigCandidates {
if _, err := commit.GetTreeEntryByPath(configName + ".yaml"); err == nil {
return ctx.Repo.GetIssueConfig(configName+".yaml", commit)
}
if _, err := commit.GetTreeEntryByPath(configName + ".yml"); err == nil {
return ctx.Repo.GetIssueConfig(configName+".yml", commit)
}
}
return GetDefaultIssueConfig(), nil
}
// IsIssueConfig returns if the given path is a issue config file.
func (r *Repository) IsIssueConfig(path string) bool {
for _, configName := range IssueConfigCandidates {
if path == configName+".yaml" || path == configName+".yml" {
return true
}
}
return false
}
func (ctx *Context) HasIssueTemplatesOrContactLinks() bool {
if len(ctx.IssueTemplatesFromDefaultBranch()) > 0 {
return true
}
issueConfig, _ := ctx.IssueConfigFromDefaultBranch()
return len(issueConfig.ContactLinks) > 0
}

View File

@ -107,7 +107,7 @@ func InitIssueIndexer(syncReindex bool) {
// Create the Queue // Create the Queue
switch setting.Indexer.IssueType { switch setting.Indexer.IssueType {
case "bleve", "elasticsearch": case "bleve", "elasticsearch", "meilisearch":
handler := func(data ...queue.Data) []queue.Data { handler := func(data ...queue.Data) []queue.Data {
indexer := holder.get() indexer := holder.get()
if indexer == nil { if indexer == nil {
@ -220,6 +220,21 @@ func InitIssueIndexer(syncReindex bool) {
issueIndexer := &DBIndexer{} issueIndexer := &DBIndexer{}
holder.set(issueIndexer) holder.set(issueIndexer)
graceful.GetManager().RunAtTerminate(finished) graceful.GetManager().RunAtTerminate(finished)
case "meilisearch":
graceful.GetManager().RunWithShutdownFns(func(_, atTerminate func(func())) {
pprof.SetGoroutineLabels(ctx)
issueIndexer, err := NewMeilisearchIndexer(setting.Indexer.IssueConnStr, setting.Indexer.IssueConnAuth, setting.Indexer.IssueIndexerName)
if err != nil {
log.Fatal("Unable to initialize Meilisearch Issue Indexer at connection: %s Error: %v", setting.Indexer.IssueConnStr, err)
}
exist, err := issueIndexer.Init()
if err != nil {
log.Fatal("Unable to issueIndexer.Init with connection %s Error: %v", setting.Indexer.IssueConnStr, err)
}
populate = !exist
holder.set(issueIndexer)
atTerminate(finished)
})
default: default:
holder.cancel() holder.cancel()
log.Fatal("Unknown issue indexer type: %s", setting.Indexer.IssueType) log.Fatal("Unknown issue indexer type: %s", setting.Indexer.IssueType)

View File

@ -0,0 +1,183 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package issues
import (
"context"
"strconv"
"sync"
"time"
"github.com/meilisearch/meilisearch-go"
)
var _ Indexer = &MeilisearchIndexer{}
// MeilisearchIndexer implements Indexer interface
type MeilisearchIndexer struct {
client *meilisearch.Client
indexerName string
available bool
availabilityCallback func(bool)
stopTimer chan struct{}
lock sync.RWMutex
}
// MeilisearchIndexer creates a new meilisearch indexer
func NewMeilisearchIndexer(url, apiKey, indexerName string) (*MeilisearchIndexer, error) {
client := meilisearch.NewClient(meilisearch.ClientConfig{
Host: url,
APIKey: apiKey,
})
indexer := &MeilisearchIndexer{
client: client,
indexerName: indexerName,
available: true,
stopTimer: make(chan struct{}),
}
ticker := time.NewTicker(10 * time.Second)
go func() {
for {
select {
case <-ticker.C:
indexer.checkAvailability()
case <-indexer.stopTimer:
ticker.Stop()
return
}
}
}()
return indexer, nil
}
// Init will initialize the indexer
func (b *MeilisearchIndexer) Init() (bool, error) {
_, err := b.client.GetIndex(b.indexerName)
if err == nil {
return true, nil
}
_, err = b.client.CreateIndex(&meilisearch.IndexConfig{
Uid: b.indexerName,
PrimaryKey: "id",
})
if err != nil {
return false, b.checkError(err)
}
_, err = b.client.Index(b.indexerName).UpdateFilterableAttributes(&[]string{"repo_id"})
return false, b.checkError(err)
}
// SetAvailabilityChangeCallback sets callback that will be triggered when availability changes
func (b *MeilisearchIndexer) SetAvailabilityChangeCallback(callback func(bool)) {
b.lock.Lock()
defer b.lock.Unlock()
b.availabilityCallback = callback
}
// Ping checks if meilisearch is available
func (b *MeilisearchIndexer) Ping() bool {
b.lock.RLock()
defer b.lock.RUnlock()
return b.available
}
// Index will save the index data
func (b *MeilisearchIndexer) Index(issues []*IndexerData) error {
if len(issues) == 0 {
return nil
}
for _, issue := range issues {
_, err := b.client.Index(b.indexerName).AddDocuments(issue)
if err != nil {
return b.checkError(err)
}
}
// TODO: bulk send index data
return nil
}
// Delete deletes indexes by ids
func (b *MeilisearchIndexer) Delete(ids ...int64) error {
if len(ids) == 0 {
return nil
}
for _, id := range ids {
_, err := b.client.Index(b.indexerName).DeleteDocument(strconv.FormatInt(id, 10))
if err != nil {
return b.checkError(err)
}
}
// TODO: bulk send deletes
return nil
}
// Search searches for issues by given conditions.
// Returns the matching issue IDs
func (b *MeilisearchIndexer) Search(ctx context.Context, keyword string, repoIDs []int64, limit, start int) (*SearchResult, error) {
filter := make([][]string, 0, len(repoIDs))
for _, repoID := range repoIDs {
filter = append(filter, []string{"repo_id = " + strconv.FormatInt(repoID, 10)})
}
searchRes, err := b.client.Index(b.indexerName).Search(keyword, &meilisearch.SearchRequest{
Filter: filter,
Limit: int64(limit),
Offset: int64(start),
})
if err != nil {
return nil, b.checkError(err)
}
hits := make([]Match, 0, len(searchRes.Hits))
for _, hit := range searchRes.Hits {
hits = append(hits, Match{
ID: int64(hit.(map[string]interface{})["id"].(float64)),
})
}
return &SearchResult{
Total: searchRes.TotalHits,
Hits: hits,
}, nil
}
// Close implements indexer
func (b *MeilisearchIndexer) Close() {
select {
case <-b.stopTimer:
default:
close(b.stopTimer)
}
}
func (b *MeilisearchIndexer) checkError(err error) error {
return err
}
func (b *MeilisearchIndexer) checkAvailability() {
_, err := b.client.Health()
if err != nil {
b.setAvailability(false)
return
}
b.setAvailability(true)
}
func (b *MeilisearchIndexer) setAvailability(available bool) {
b.lock.Lock()
defer b.lock.Unlock()
if b.available == available {
return
}
b.available = available
if b.availabilityCallback != nil {
// Call the callback from within the lock to ensure that the ordering remains correct
b.availabilityCallback(b.available)
}
}

View File

@ -60,15 +60,22 @@ func (s *ContentStore) Put(pointer Pointer, r io.Reader) error {
return err return err
} }
// This shouldn't happen but it is sensible to test // check again whether there is any error during the Save operation
if written != pointer.Size { // because some errors might be ignored by the Reader's caller
if err := s.Delete(p); err != nil { if wrappedRd.lastError != nil && !errors.Is(wrappedRd.lastError, io.EOF) {
log.Error("Cleaning the LFS OID[%s] failed: %v", pointer.Oid, err) err = wrappedRd.lastError
} } else if written != pointer.Size {
return ErrSizeMismatch err = ErrSizeMismatch
} }
return nil // if the upload failed, try to delete the file
if err != nil {
if errDel := s.Delete(p); errDel != nil {
log.Error("Cleaning the LFS OID[%s] failed: %v", pointer.Oid, errDel)
}
}
return err
} }
// Exists returns true if the object exists in the content store. // Exists returns true if the object exists in the content store.
@ -109,6 +116,17 @@ type hashingReader struct {
expectedSize int64 expectedSize int64
hash hash.Hash hash hash.Hash
expectedHash string expectedHash string
lastError error
}
// recordError records the last error during the Save operation
// Some callers of the Reader doesn't respect the returned "err"
// For example, MinIO's Put will ignore errors if the written size could equal to expected size
// So we must remember the error by ourselves,
// and later check again whether ErrSizeMismatch or ErrHashMismatch occurs during the Save operation
func (r *hashingReader) recordError(err error) error {
r.lastError = err
return err
} }
func (r *hashingReader) Read(b []byte) (int, error) { func (r *hashingReader) Read(b []byte) (int, error) {
@ -118,22 +136,22 @@ func (r *hashingReader) Read(b []byte) (int, error) {
r.currentSize += int64(n) r.currentSize += int64(n)
wn, werr := r.hash.Write(b[:n]) wn, werr := r.hash.Write(b[:n])
if wn != n || werr != nil { if wn != n || werr != nil {
return n, werr return n, r.recordError(werr)
} }
} }
if err != nil && err == io.EOF { if errors.Is(err, io.EOF) || r.currentSize >= r.expectedSize {
if r.currentSize != r.expectedSize { if r.currentSize != r.expectedSize {
return n, ErrSizeMismatch return n, r.recordError(ErrSizeMismatch)
} }
shaStr := hex.EncodeToString(r.hash.Sum(nil)) shaStr := hex.EncodeToString(r.hash.Sum(nil))
if shaStr != r.expectedHash { if shaStr != r.expectedHash {
return n, ErrHashMismatch return n, r.recordError(ErrHashMismatch)
} }
} }
return n, err return n, r.recordError(err)
} }
func newHashingReader(expectedSize int64, expectedHash string, reader io.Reader) *hashingReader { func newHashingReader(expectedSize int64, expectedHash string, reader io.Reader) *hashingReader {

View File

@ -4,6 +4,7 @@
package setting package setting
import ( import (
"net/url"
"path/filepath" "path/filepath"
"strings" "strings"
"time" "time"
@ -18,6 +19,7 @@ var Indexer = struct {
IssueType string IssueType string
IssuePath string IssuePath string
IssueConnStr string IssueConnStr string
IssueConnAuth string
IssueIndexerName string IssueIndexerName string
StartupTimeout time.Duration StartupTimeout time.Duration
@ -34,6 +36,7 @@ var Indexer = struct {
IssueType: "bleve", IssueType: "bleve",
IssuePath: "indexers/issues.bleve", IssuePath: "indexers/issues.bleve",
IssueConnStr: "", IssueConnStr: "",
IssueConnAuth: "",
IssueIndexerName: "gitea_issues", IssueIndexerName: "gitea_issues",
RepoIndexerEnabled: false, RepoIndexerEnabled: false,
@ -53,6 +56,18 @@ func loadIndexerFrom(rootCfg ConfigProvider) {
Indexer.IssuePath = filepath.ToSlash(filepath.Join(AppWorkPath, Indexer.IssuePath)) Indexer.IssuePath = filepath.ToSlash(filepath.Join(AppWorkPath, Indexer.IssuePath))
} }
Indexer.IssueConnStr = sec.Key("ISSUE_INDEXER_CONN_STR").MustString(Indexer.IssueConnStr) Indexer.IssueConnStr = sec.Key("ISSUE_INDEXER_CONN_STR").MustString(Indexer.IssueConnStr)
if Indexer.IssueType == "meilisearch" {
u, err := url.Parse(Indexer.IssueConnStr)
if err != nil {
log.Warn("Failed to parse ISSUE_INDEXER_CONN_STR: %v", err)
u = &url.URL{}
}
Indexer.IssueConnAuth, _ = u.User.Password()
u.User = nil
Indexer.IssueConnStr = u.String()
}
Indexer.IssueIndexerName = sec.Key("ISSUE_INDEXER_NAME").MustString(Indexer.IssueIndexerName) Indexer.IssueIndexerName = sec.Key("ISSUE_INDEXER_NAME").MustString(Indexer.IssueIndexerName)
// The following settings are deprecated and can be overridden by settings in [queue] or [queue.issue_indexer] // The following settings are deprecated and can be overridden by settings in [queue] or [queue.issue_indexer]

View File

@ -42,6 +42,7 @@ func getStorage(rootCfg ConfigProvider, name, typ string, targetSec *ini.Section
sec.Key("MINIO_LOCATION").MustString("us-east-1") sec.Key("MINIO_LOCATION").MustString("us-east-1")
sec.Key("MINIO_USE_SSL").MustBool(false) sec.Key("MINIO_USE_SSL").MustBool(false)
sec.Key("MINIO_INSECURE_SKIP_VERIFY").MustBool(false) sec.Key("MINIO_INSECURE_SKIP_VERIFY").MustBool(false)
sec.Key("MINIO_CHECKSUM_ALGORITHM").MustString("default")
if targetSec == nil { if targetSec == nil {
targetSec, _ = rootCfg.NewSection(name) targetSec, _ = rootCfg.NewSection(name)

View File

@ -6,6 +6,7 @@ package storage
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"fmt"
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
@ -53,10 +54,12 @@ type MinioStorageConfig struct {
BasePath string `ini:"MINIO_BASE_PATH"` BasePath string `ini:"MINIO_BASE_PATH"`
UseSSL bool `ini:"MINIO_USE_SSL"` UseSSL bool `ini:"MINIO_USE_SSL"`
InsecureSkipVerify bool `ini:"MINIO_INSECURE_SKIP_VERIFY"` InsecureSkipVerify bool `ini:"MINIO_INSECURE_SKIP_VERIFY"`
ChecksumAlgorithm string `ini:"MINIO_CHECKSUM_ALGORITHM"`
} }
// MinioStorage returns a minio bucket storage // MinioStorage returns a minio bucket storage
type MinioStorage struct { type MinioStorage struct {
cfg *MinioStorageConfig
ctx context.Context ctx context.Context
client *minio.Client client *minio.Client
bucket string bucket string
@ -91,6 +94,10 @@ func NewMinioStorage(ctx context.Context, cfg interface{}) (ObjectStorage, error
} }
config := configInterface.(MinioStorageConfig) config := configInterface.(MinioStorageConfig)
if config.ChecksumAlgorithm != "" && config.ChecksumAlgorithm != "default" && config.ChecksumAlgorithm != "md5" {
return nil, fmt.Errorf("invalid minio checksum algorithm: %s", config.ChecksumAlgorithm)
}
log.Info("Creating Minio storage at %s:%s with base path %s", config.Endpoint, config.Bucket, config.BasePath) log.Info("Creating Minio storage at %s:%s with base path %s", config.Endpoint, config.Bucket, config.BasePath)
minioClient, err := minio.New(config.Endpoint, &minio.Options{ minioClient, err := minio.New(config.Endpoint, &minio.Options{
@ -113,6 +120,7 @@ func NewMinioStorage(ctx context.Context, cfg interface{}) (ObjectStorage, error
} }
return &MinioStorage{ return &MinioStorage{
cfg: &config,
ctx: ctx, ctx: ctx,
client: minioClient, client: minioClient,
bucket: config.Bucket, bucket: config.Bucket,
@ -124,7 +132,7 @@ func (m *MinioStorage) buildMinioPath(p string) string {
return util.PathJoinRelX(m.basePath, p) return util.PathJoinRelX(m.basePath, p)
} }
// Open open a file // Open opens a file
func (m *MinioStorage) Open(path string) (Object, error) { func (m *MinioStorage) Open(path string) (Object, error) {
opts := minio.GetObjectOptions{} opts := minio.GetObjectOptions{}
object, err := m.client.GetObject(m.ctx, m.bucket, m.buildMinioPath(path), opts) object, err := m.client.GetObject(m.ctx, m.bucket, m.buildMinioPath(path), opts)
@ -134,7 +142,7 @@ func (m *MinioStorage) Open(path string) (Object, error) {
return &minioObject{object}, nil return &minioObject{object}, nil
} }
// Save save a file to minio // Save saves a file to minio
func (m *MinioStorage) Save(path string, r io.Reader, size int64) (int64, error) { func (m *MinioStorage) Save(path string, r io.Reader, size int64) (int64, error) {
uploadInfo, err := m.client.PutObject( uploadInfo, err := m.client.PutObject(
m.ctx, m.ctx,
@ -142,7 +150,14 @@ func (m *MinioStorage) Save(path string, r io.Reader, size int64) (int64, error)
m.buildMinioPath(path), m.buildMinioPath(path),
r, r,
size, size,
minio.PutObjectOptions{ContentType: "application/octet-stream"}, minio.PutObjectOptions{
ContentType: "application/octet-stream",
// some storages like:
// * https://developers.cloudflare.com/r2/api/s3/api/
// * https://www.backblaze.com/b2/docs/s3_compatible_api.html
// do not support "x-amz-checksum-algorithm" header, so use legacy MD5 checksum
SendContentMd5: m.cfg.ChecksumAlgorithm == "md5",
},
) )
if err != nil { if err != nil {
return 0, convertMinioErr(err) return 0, convertMinioErr(err)

View File

@ -190,6 +190,22 @@ func (l *IssueTemplateLabels) UnmarshalYAML(value *yaml.Node) error {
return fmt.Errorf("line %d: cannot unmarshal %s into IssueTemplateLabels", value.Line, value.ShortTag()) return fmt.Errorf("line %d: cannot unmarshal %s into IssueTemplateLabels", value.Line, value.ShortTag())
} }
type IssueConfigContactLink struct {
Name string `json:"name" yaml:"name"`
URL string `json:"url" yaml:"url"`
About string `json:"about" yaml:"about"`
}
type IssueConfig struct {
BlankIssuesEnabled bool `json:"blank_issues_enabled" yaml:"blank_issues_enabled"`
ContactLinks []IssueConfigContactLink `json:"contact_links" yaml:"contact_links"`
}
type IssueConfigValidation struct {
Valid bool `json:"valid"`
Message string `json:"message"`
}
// IssueTemplateType defines issue template type // IssueTemplateType defines issue template type
type IssueTemplateType string type IssueTemplateType string
@ -211,3 +227,11 @@ func (it IssueTemplate) Type() IssueTemplateType {
} }
return "" return ""
} }
// IssueMeta basic issue information
// swagger:model
type IssueMeta struct {
Index int64 `json:"index"`
Owner string `json:"owner"`
Name string `json:"repo"`
}

View File

@ -1272,10 +1272,12 @@ issues.new.no_assignees = No Assignees
issues.new.no_reviewers = No reviewers issues.new.no_reviewers = No reviewers
issues.new.add_reviewer_title = Request review issues.new.add_reviewer_title = Request review
issues.choose.get_started = Get Started issues.choose.get_started = Get Started
issues.choose.open_external_link = Open
issues.choose.blank = Default issues.choose.blank = Default
issues.choose.blank_about = Create an issue from default template. issues.choose.blank_about = Create an issue from default template.
issues.choose.ignore_invalid_templates = Invalid templates have been ignored issues.choose.ignore_invalid_templates = Invalid templates have been ignored
issues.choose.invalid_templates = %v invalid template(s) found issues.choose.invalid_templates = %v invalid template(s) found
issues.choose.invalid_config = The issue config contains errors:
issues.no_ref = No Branch/Tag Specified issues.no_ref = No Branch/Tag Specified
issues.create = Create Issue issues.create = Create Issue
issues.new_label = New Label issues.new_label = New Label
@ -1489,6 +1491,9 @@ issues.due_date_invalid = "The due date is invalid or out of range. Please use t
issues.dependency.title = Dependencies issues.dependency.title = Dependencies
issues.dependency.issue_no_dependencies = No dependencies set. issues.dependency.issue_no_dependencies = No dependencies set.
issues.dependency.pr_no_dependencies = No dependencies set. issues.dependency.pr_no_dependencies = No dependencies set.
issues.dependency.no_permission_1 = "You do not have permission to read %d dependency"
issues.dependency.no_permission_n = "You do not have permission to read %d dependencies"
issues.dependency.no_permission.can_remove = "You do not have permission to read this dependency but can remove this dependency"
issues.dependency.add = Add dependency… issues.dependency.add = Add dependency…
issues.dependency.cancel = Cancel issues.dependency.cancel = Cancel
issues.dependency.remove = Remove issues.dependency.remove = Remove
@ -3360,6 +3365,7 @@ runners.status.idle = Idle
runners.status.active = Active runners.status.active = Active
runners.status.offline = Offline runners.status.offline = Offline
runners.version = Version runners.version = Version
runners.reset_registration_token_success = Runner registration token reset successfully
runs.all_workflows = All Workflows runs.all_workflows = All Workflows
runs.open_tab = %d Open runs.open_tab = %d Open

View File

@ -84,6 +84,7 @@ add=追加
add_all=すべて追加 add_all=すべて追加
remove=除去 remove=除去
remove_all=すべて除去 remove_all=すべて除去
remove_label_str=アイテム「%s」を削除
edit=編集 edit=編集
enabled=有効 enabled=有効
@ -218,6 +219,7 @@ openid_signup_popup=OpenIDベースでのユーザーのセルフ登録を有効
enable_captcha=登録時のCAPTCHAを有効にする enable_captcha=登録時のCAPTCHAを有効にする
enable_captcha_popup=ユーザーのセルフ登録時にCAPTCHAを必須にします。 enable_captcha_popup=ユーザーのセルフ登録時にCAPTCHAを必須にします。
require_sign_in_view=ページ閲覧にサインインが必要 require_sign_in_view=ページ閲覧にサインインが必要
require_sign_in_view_popup=ページアクセスをサインイン済みユーザーに限定します。 訪問者はサインインページと登録ページだけ見ることができます。
admin_setting_desc=管理者アカウントの作成は任意です。 最初に登録したユーザーは自動的に管理者になります。 admin_setting_desc=管理者アカウントの作成は任意です。 最初に登録したユーザーは自動的に管理者になります。
admin_title=管理者アカウントの設定 admin_title=管理者アカウントの設定
admin_name=管理者ユーザー名 admin_name=管理者ユーザー名
@ -247,6 +249,7 @@ no_reply_address=メールを隠すときのドメイン
no_reply_address_helper=メールアドレスを隠しているユーザーに使用するドメイン名。 例えば 'noreply.example.org' と設定した場合、ユーザー名 'joe' はGitに 'joe@noreply.example.org' としてログインすることになります。 no_reply_address_helper=メールアドレスを隠しているユーザーに使用するドメイン名。 例えば 'noreply.example.org' と設定した場合、ユーザー名 'joe' はGitに 'joe@noreply.example.org' としてログインすることになります。
password_algorithm=パスワードハッシュアルゴリズム password_algorithm=パスワードハッシュアルゴリズム
invalid_password_algorithm=無効なパスワードハッシュアルゴリズム invalid_password_algorithm=無効なパスワードハッシュアルゴリズム
password_algorithm_helper=パスワードハッシュアルゴリズムを設定します。 アルゴリズムにより動作要件と強度が異なります。 argon2アルゴリズムはかなり安全ですが、多くのメモリを使用するため小さなシステムには適さない場合があります。
enable_update_checker=アップデートチェッカーを有効にする enable_update_checker=アップデートチェッカーを有効にする
enable_update_checker_helper=gitea.ioに接続して定期的に新しいバージョンのリリースを確認します。 enable_update_checker_helper=gitea.ioに接続して定期的に新しいバージョンのリリースを確認します。
@ -518,8 +521,14 @@ invalid_ssh_key=SSHキーが確認できません: %s
invalid_gpg_key=GPGキーが確認できません: %s invalid_gpg_key=GPGキーが確認できません: %s
invalid_ssh_principal=無効なプリンシパル: %s invalid_ssh_principal=無効なプリンシパル: %s
must_use_public_key=あなたが提供したキーは秘密鍵です。秘密鍵をどこにもアップロードしないでください。代わりに公開鍵を使用してください。 must_use_public_key=あなたが提供したキーは秘密鍵です。秘密鍵をどこにもアップロードしないでください。代わりに公開鍵を使用してください。
unable_verify_ssh_key=SSHキーが確認できません。間違いが無いか、よく確認してください。
auth_failed=認証に失敗しました: %v auth_failed=認証に失敗しました: %v
still_own_repo=あなたのアカウントは1つ以上のリポジトリを所有しています。 先にそれらを削除するか移転してください。
still_has_org=あなたのアカウントは1つ以上の組織に参加しています。 先にそれらから脱退してください。
still_own_packages=あなたのアカウントは1つ以上のパッケージを所有しています。 先にそれらを削除してください。
org_still_own_repo=組織はまだ1つ以上のリポジトリを所有しています。 先にそれらを削除するか移転してください。
org_still_own_packages=組織はまだ1つ以上のパッケージを所有しています。 先にそれらを削除してください。
target_branch_not_exist=ターゲットのブランチが存在していません。 target_branch_not_exist=ターゲットのブランチが存在していません。
@ -984,6 +993,7 @@ migrate.github_token_desc=GitHub APIにはレート制限があります。移
migrate.clone_local_path=、またはローカルサーバー上のパス migrate.clone_local_path=、またはローカルサーバー上のパス
migrate.permission_denied=ローカルリポジトリをインポートする権限がありません。 migrate.permission_denied=ローカルリポジトリをインポートする権限がありません。
migrate.permission_denied_blocked=許可されていないホストからインポートできません。管理者に問い合わせて、ALLOWED_DOMAINS/ALLOW_LOCALNETWORKS/BLOCKED_DOMAINS の設定を確認してください。 migrate.permission_denied_blocked=許可されていないホストからインポートできません。管理者に問い合わせて、ALLOWED_DOMAINS/ALLOW_LOCALNETWORKS/BLOCKED_DOMAINS の設定を確認してください。
migrate.invalid_local_path=ローカルパスが無効です。 存在しないかディレクトリではありません。
migrate.invalid_lfs_endpoint=LFS エンドポイントが無効です。 migrate.invalid_lfs_endpoint=LFS エンドポイントが無効です。
migrate.failed=移行に失敗しました: %v migrate.failed=移行に失敗しました: %v
migrate.migrate_items_options=追加の項目を移行するにはアクセストークンが必要です migrate.migrate_items_options=追加の項目を移行するにはアクセストークンが必要です
@ -1165,6 +1175,7 @@ commits.commits=コミット
commits.no_commits=共通のコミットはありません。 '%s' と '%s' の履歴はすべて異なっています。 commits.no_commits=共通のコミットはありません。 '%s' と '%s' の履歴はすべて異なっています。
commits.nothing_to_compare=二つのブランチは同じ内容です。 commits.nothing_to_compare=二つのブランチは同じ内容です。
commits.search=コミットの検索… commits.search=コミットの検索…
commits.search.tooltip=`キーワード "author:"、"committer:"、"after:"、"before:" を付けて指定できます。 例 "revert author:Alice before:2019-01-13"`
commits.find=検索 commits.find=検索
commits.search_all=すべてのブランチ commits.search_all=すべてのブランチ
commits.author=作成者 commits.author=作成者
@ -1923,6 +1934,7 @@ settings.trust_model.collaborator.long=共同作業者: 共同作業者による
settings.trust_model.collaborator.desc=このリポジトリの共同作業者による正常な署名は、(署名がコミッターのものかどうかにかかわらず)「信頼済み」とみなします。 署名が共同作業者ではないコミッターのものであれば「信頼不可」、それ以外は「不一致」となります。 settings.trust_model.collaborator.desc=このリポジトリの共同作業者による正常な署名は、(署名がコミッターのものかどうかにかかわらず)「信頼済み」とみなします。 署名が共同作業者ではないコミッターのものであれば「信頼不可」、それ以外は「不一致」となります。
settings.trust_model.committer=コミッター settings.trust_model.committer=コミッター
settings.trust_model.committer.long=コミッター: コミッターによる署名を信頼します (これはGitHub方式であり、Giteaの署名が付いたコミットはコミッターがGitea自身であることが強制されます) settings.trust_model.committer.long=コミッター: コミッターによる署名を信頼します (これはGitHub方式であり、Giteaの署名が付いたコミットはコミッターがGitea自身であることが強制されます)
settings.trust_model.committer.desc=正常な署名は、コミッターに一致する場合のみ「信頼済み」とみなし、それ以外は「不一致」となります。 Giteaは署名付きでコミットすることが強制され、本来のコミッターはコミットの最後に Co-authored-by: と Co-committed-by: で追加されます。 Giteaのデフォルト鍵はデータベース内のユーザー1人とマッチしなければなりません。
settings.trust_model.collaboratorcommitter=共同作業者+コミッター settings.trust_model.collaboratorcommitter=共同作業者+コミッター
settings.trust_model.collaboratorcommitter.long=共同作業者+コミッター: コミッターと一致する共同作業者による署名を信頼します settings.trust_model.collaboratorcommitter.long=共同作業者+コミッター: コミッターと一致する共同作業者による署名を信頼します
settings.trust_model.collaboratorcommitter.desc=このリポジトリの共同作業者による正常な署名は、コミッターと一致する場合に「信頼済み」とみなします。 それ以外の正常な署名のうち、コミッターに一致するものは「信頼不可」、他は「不一致」となります。 Giteaが署名付きコミットのコミッターであることが強制され、本来のコミッターはコミットの最後に Co-Authored-By: と Co-Committed-By: で追加されます。 Giteaのデフォルト鍵はデータベース内のユーザー1人とマッチしなければなりません。 settings.trust_model.collaboratorcommitter.desc=このリポジトリの共同作業者による正常な署名は、コミッターと一致する場合に「信頼済み」とみなします。 それ以外の正常な署名のうち、コミッターに一致するものは「信頼不可」、他は「不一致」となります。 Giteaが署名付きコミットのコミッターであることが強制され、本来のコミッターはコミットの最後に Co-Authored-By: と Co-Committed-By: で追加されます。 Giteaのデフォルト鍵はデータベース内のユーザー1人とマッチしなければなりません。
@ -2464,6 +2476,7 @@ teams.remove_all_repos_title=チームリポジトリをすべて除去
teams.remove_all_repos_desc=チームからすべてのリポジトリを除去します。 teams.remove_all_repos_desc=チームからすべてのリポジトリを除去します。
teams.add_all_repos_title=すべてのリポジトリを追加 teams.add_all_repos_title=すべてのリポジトリを追加
teams.add_all_repos_desc=組織のすべてのリポジトリをチームに追加します。 teams.add_all_repos_desc=組織のすべてのリポジトリをチームに追加します。
teams.add_nonexistent_repo=追加しようとしているリポジトリは存在しません。 先にリポジトリを作成してください。
teams.add_duplicate_users=ユーザーは既にチームのメンバーです。 teams.add_duplicate_users=ユーザーは既にチームのメンバーです。
teams.repos.none=このチームがアクセスできるリポジトリはありません。 teams.repos.none=このチームがアクセスできるリポジトリはありません。
teams.members.none=このチームにはメンバーがいません。 teams.members.none=このチームにはメンバーがいません。
@ -2493,6 +2506,7 @@ first_page=最初
last_page=最後 last_page=最後
total=合計: %d total=合計: %d
dashboard.new_version_hint=Gitea %s が入手可能になりました。 現在実行しているのは %s です。 詳細は <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">ブログ</a> を確認してください。
dashboard.statistic=サマリー dashboard.statistic=サマリー
dashboard.operations=メンテナンス操作 dashboard.operations=メンテナンス操作
dashboard.system_status=システム状況 dashboard.system_status=システム状況
@ -2612,6 +2626,7 @@ users.still_own_repo=このユーザーはまだ1つ以上のリポジトリを
users.still_has_org=このユーザーは組織のメンバーになっています。 先に組織からこのユーザーを削除してください。 users.still_has_org=このユーザーは組織のメンバーになっています。 先に組織からこのユーザーを削除してください。
users.purge=ユーザーを抹消 users.purge=ユーザーを抹消
users.purge_help=強制的にユーザーとそのユーザーが所有していたリポジトリ、組織、パッケージを削除します。コメントもすべて削除します。 users.purge_help=強制的にユーザーとそのユーザーが所有していたリポジトリ、組織、パッケージを削除します。コメントもすべて削除します。
users.still_own_packages=このユーザーはまだ1つ以上のパッケージを所有しています。先にそれらのパッケージを削除してください。
users.deletion_success=ユーザーアカウントを削除しました。 users.deletion_success=ユーザーアカウントを削除しました。
users.reset_2fa=2要素認証をリセット users.reset_2fa=2要素認証をリセット
users.list_status_filter.menu_text=フィルター users.list_status_filter.menu_text=フィルター

View File

@ -13,6 +13,9 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages" packages_model "code.gitea.io/gitea/models/packages"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
packages_module "code.gitea.io/gitea/modules/packages" packages_module "code.gitea.io/gitea/modules/packages"
npm_module "code.gitea.io/gitea/modules/packages/npm" npm_module "code.gitea.io/gitea/modules/packages/npm"
@ -166,6 +169,26 @@ func UploadPackage(ctx *context.Context) {
return return
} }
repo, err := repo_model.GetRepositoryByURL(ctx, npmPackage.Metadata.Repository.URL)
if err == nil {
canWrite := repo.OwnerID == ctx.Doer.ID
if !canWrite {
perms, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
canWrite = perms.CanWrite(unit.TypePackages)
}
if !canWrite {
apiError(ctx, http.StatusForbidden, "no permission to upload this package")
return
}
}
buf, err := packages_module.CreateHashedBufferFromReader(bytes.NewReader(npmPackage.Data), 32*1024*1024) buf, err := packages_module.CreateHashedBufferFromReader(bytes.NewReader(npmPackage.Data), 32*1024*1024)
if err != nil { if err != nil {
apiError(ctx, http.StatusInternalServerError, err) apiError(ctx, http.StatusInternalServerError, err)
@ -217,6 +240,13 @@ func UploadPackage(ctx *context.Context) {
} }
} }
if repo != nil {
if err := packages_model.SetRepositoryLink(ctx, pv.PackageID, repo.ID); err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
}
ctx.Status(http.StatusCreated) ctx.Status(http.StatusCreated)
} }

View File

@ -1026,6 +1026,14 @@ func Routes(ctx gocontext.Context) *web.Route {
Patch(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, bind(api.EditAttachmentOptions{}), repo.EditIssueAttachment). Patch(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, bind(api.EditAttachmentOptions{}), repo.EditIssueAttachment).
Delete(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, repo.DeleteIssueAttachment) Delete(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, repo.DeleteIssueAttachment)
}, mustEnableAttachments) }, mustEnableAttachments)
m.Combo("/dependencies").
Get(repo.GetIssueDependencies).
Post(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, bind(api.IssueMeta{}), repo.CreateIssueDependency).
Delete(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, bind(api.IssueMeta{}), repo.RemoveIssueDependency)
m.Combo("/blocks").
Get(repo.GetIssueBlocks).
Post(reqToken(auth_model.AccessTokenScopeRepo), bind(api.IssueMeta{}), repo.CreateIssueBlocking).
Delete(reqToken(auth_model.AccessTokenScopeRepo), bind(api.IssueMeta{}), repo.RemoveIssueBlocking)
}) })
}, mustEnableIssuesOrPulls) }, mustEnableIssuesOrPulls)
m.Group("/labels", func() { m.Group("/labels", func() {
@ -1161,6 +1169,8 @@ func Routes(ctx gocontext.Context) *web.Route {
}, reqAdmin()) }, reqAdmin())
}, reqAnyRepoReader()) }, reqAnyRepoReader())
m.Get("/issue_templates", context.ReferencesGitRepo(), repo.GetIssueTemplates) m.Get("/issue_templates", context.ReferencesGitRepo(), repo.GetIssueTemplates)
m.Get("/issue_config", context.ReferencesGitRepo(), repo.GetIssueConfig)
m.Get("/issue_config/validate", context.ReferencesGitRepo(), repo.ValidateIssueConfig)
m.Get("/languages", reqRepoReader(unit.TypeCode), repo.GetLanguages) m.Get("/languages", reqRepoReader(unit.TypeCode), repo.GetLanguages)
}, repoAssignment()) }, repoAssignment())
}) })

View File

@ -0,0 +1,598 @@
// Copyright 2016 The Gogs Authors. All rights reserved.
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/convert"
)
// GetIssueDependencies list an issue's dependencies
func GetIssueDependencies(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/dependencies issue issueListIssueDependencies
// ---
// summary: List an issue's dependencies, i.e all issues that block this issue.
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/IssueList"
// If this issue's repository does not enable dependencies then there can be no dependencies by default
if !ctx.Repo.Repository.IsDependenciesEnabled(ctx) {
ctx.NotFound()
return
}
issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.NotFound("IsErrIssueNotExist", err)
} else {
ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
}
return
}
// 1. We must be able to read this issue
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
ctx.NotFound()
return
}
page := ctx.FormInt("page")
if page <= 1 {
page = 1
}
limit := ctx.FormInt("limit")
if limit == 0 {
limit = setting.API.DefaultPagingNum
} else if limit > setting.API.MaxResponseItems {
limit = setting.API.MaxResponseItems
}
canWrite := ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull)
blockerIssues := make([]*issues_model.Issue, 0, limit)
// 2. Get the issues this issue depends on, i.e. the `<#b>`: `<issue> <- <#b>`
blockersInfo, err := issue.BlockedByDependencies(ctx, db.ListOptions{
Page: page,
PageSize: limit,
})
if err != nil {
ctx.Error(http.StatusInternalServerError, "BlockedByDependencies", err)
return
}
var lastRepoID int64
var lastPerm access_model.Permission
for _, blocker := range blockersInfo {
// Get the permissions for this repository
perm := lastPerm
if lastRepoID != blocker.Repository.ID {
if blocker.Repository.ID == ctx.Repo.Repository.ID {
perm = ctx.Repo.Permission
} else {
var err error
perm, err = access_model.GetUserRepoPermission(ctx, &blocker.Repository, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
return
}
}
lastRepoID = blocker.Repository.ID
}
// check permission
if !perm.CanReadIssuesOrPulls(blocker.Issue.IsPull) {
if !canWrite {
hiddenBlocker := &issues_model.DependencyInfo{
Issue: issues_model.Issue{
Title: "HIDDEN",
},
}
blocker = hiddenBlocker
} else {
confidentialBlocker := &issues_model.DependencyInfo{
Issue: issues_model.Issue{
RepoID: blocker.Issue.RepoID,
Index: blocker.Index,
Title: blocker.Title,
IsClosed: blocker.IsClosed,
IsPull: blocker.IsPull,
},
Repository: repo_model.Repository{
ID: blocker.Issue.Repo.ID,
Name: blocker.Issue.Repo.Name,
OwnerName: blocker.Issue.Repo.OwnerName,
},
}
confidentialBlocker.Issue.Repo = &confidentialBlocker.Repository
blocker = confidentialBlocker
}
}
blockerIssues = append(blockerIssues, &blocker.Issue)
}
ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, blockerIssues))
}
// CreateIssueDependency create a new issue dependencies
func CreateIssueDependency(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/dependencies issue issueCreateIssueDependencies
// ---
// summary: Make the issue in the url depend on the issue in the form.
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/IssueMeta"
// responses:
// "201":
// "$ref": "#/responses/Issue"
// "404":
// description: the issue does not exist
// We want to make <:index> depend on <Form>, i.e. <:index> is the target
target := getParamsIssue(ctx)
if ctx.Written() {
return
}
// and <Form> represents the dependency
form := web.GetForm(ctx).(*api.IssueMeta)
dependency := getFormIssue(ctx, form)
if ctx.Written() {
return
}
dependencyPerm := getPermissionForRepo(ctx, target.Repo)
if ctx.Written() {
return
}
createIssueDependency(ctx, target, dependency, ctx.Repo.Permission, *dependencyPerm)
if ctx.Written() {
return
}
ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, target))
}
// RemoveIssueDependency remove an issue dependency
func RemoveIssueDependency(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/dependencies issue issueRemoveIssueDependencies
// ---
// summary: Remove an issue dependency
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/IssueMeta"
// responses:
// "200":
// "$ref": "#/responses/Issue"
// We want to make <:index> depend on <Form>, i.e. <:index> is the target
target := getParamsIssue(ctx)
if ctx.Written() {
return
}
// and <Form> represents the dependency
form := web.GetForm(ctx).(*api.IssueMeta)
dependency := getFormIssue(ctx, form)
if ctx.Written() {
return
}
dependencyPerm := getPermissionForRepo(ctx, target.Repo)
if ctx.Written() {
return
}
removeIssueDependency(ctx, target, dependency, ctx.Repo.Permission, *dependencyPerm)
if ctx.Written() {
return
}
ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, target))
}
// GetIssueBlocks list issues that are blocked by this issue
func GetIssueBlocks(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/blocks issue issueListBlocks
// ---
// summary: List issues that are blocked by this issue
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/IssueList"
// We need to list the issues that DEPEND on this issue not the other way round
// Therefore whether dependencies are enabled or not in this repository is potentially irrelevant.
issue := getParamsIssue(ctx)
if ctx.Written() {
return
}
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
ctx.NotFound()
return
}
page := ctx.FormInt("page")
if page <= 1 {
page = 1
}
limit := ctx.FormInt("limit")
if limit <= 1 {
limit = setting.API.DefaultPagingNum
}
skip := (page - 1) * limit
max := page * limit
deps, err := issue.BlockingDependencies(ctx)
if err != nil {
ctx.Error(http.StatusInternalServerError, "BlockingDependencies", err)
return
}
var lastRepoID int64
var lastPerm access_model.Permission
var issues []*issues_model.Issue
for i, depMeta := range deps {
if i < skip || i >= max {
continue
}
// Get the permissions for this repository
perm := lastPerm
if lastRepoID != depMeta.Repository.ID {
if depMeta.Repository.ID == ctx.Repo.Repository.ID {
perm = ctx.Repo.Permission
} else {
var err error
perm, err = access_model.GetUserRepoPermission(ctx, &depMeta.Repository, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
return
}
}
lastRepoID = depMeta.Repository.ID
}
if !perm.CanReadIssuesOrPulls(depMeta.Issue.IsPull) {
continue
}
depMeta.Issue.Repo = &depMeta.Repository
issues = append(issues, &depMeta.Issue)
}
ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, issues))
}
// CreateIssueBlocking block the issue given in the body by the issue in path
func CreateIssueBlocking(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/blocks issue issueCreateIssueBlocking
// ---
// summary: Block the issue given in the body by the issue in path
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/IssueMeta"
// responses:
// "201":
// "$ref": "#/responses/Issue"
// "404":
// description: the issue does not exist
dependency := getParamsIssue(ctx)
if ctx.Written() {
return
}
form := web.GetForm(ctx).(*api.IssueMeta)
target := getFormIssue(ctx, form)
if ctx.Written() {
return
}
targetPerm := getPermissionForRepo(ctx, target.Repo)
if ctx.Written() {
return
}
createIssueDependency(ctx, target, dependency, *targetPerm, ctx.Repo.Permission)
if ctx.Written() {
return
}
ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, dependency))
}
// RemoveIssueBlocking unblock the issue given in the body by the issue in path
func RemoveIssueBlocking(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/blocks issue issueRemoveIssueBlocking
// ---
// summary: Unblock the issue given in the body by the issue in path
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/IssueMeta"
// responses:
// "200":
// "$ref": "#/responses/Issue"
dependency := getParamsIssue(ctx)
if ctx.Written() {
return
}
form := web.GetForm(ctx).(*api.IssueMeta)
target := getFormIssue(ctx, form)
if ctx.Written() {
return
}
targetPerm := getPermissionForRepo(ctx, target.Repo)
if ctx.Written() {
return
}
removeIssueDependency(ctx, target, dependency, *targetPerm, ctx.Repo.Permission)
if ctx.Written() {
return
}
ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, dependency))
}
func getParamsIssue(ctx *context.APIContext) *issues_model.Issue {
issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.NotFound("IsErrIssueNotExist", err)
} else {
ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
}
return nil
}
issue.Repo = ctx.Repo.Repository
return issue
}
func getFormIssue(ctx *context.APIContext, form *api.IssueMeta) *issues_model.Issue {
var repo *repo_model.Repository
if form.Owner != ctx.Repo.Repository.OwnerName || form.Name != ctx.Repo.Repository.Name {
if !setting.Service.AllowCrossRepositoryDependencies {
ctx.JSON(http.StatusBadRequest, "CrossRepositoryDependencies not enabled")
return nil
}
var err error
repo, err = repo_model.GetRepositoryByOwnerAndName(ctx, form.Owner, form.Name)
if err != nil {
if repo_model.IsErrRepoNotExist(err) {
ctx.NotFound("IsErrRepoNotExist", err)
} else {
ctx.Error(http.StatusInternalServerError, "GetRepositoryByOwnerAndName", err)
}
return nil
}
} else {
repo = ctx.Repo.Repository
}
issue, err := issues_model.GetIssueByIndex(repo.ID, form.Index)
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.NotFound("IsErrIssueNotExist", err)
} else {
ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
}
return nil
}
issue.Repo = repo
return issue
}
func getPermissionForRepo(ctx *context.APIContext, repo *repo_model.Repository) *access_model.Permission {
if repo.ID == ctx.Repo.Repository.ID {
return &ctx.Repo.Permission
}
perm, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
return nil
}
return &perm
}
func createIssueDependency(ctx *context.APIContext, target, dependency *issues_model.Issue, targetPerm, dependencyPerm access_model.Permission) {
if target.Repo.IsArchived || !target.Repo.IsDependenciesEnabled(ctx) {
// The target's repository doesn't have dependencies enabled
ctx.NotFound()
return
}
if !targetPerm.CanWriteIssuesOrPulls(target.IsPull) {
// We can't write to the target
ctx.NotFound()
return
}
if !dependencyPerm.CanReadIssuesOrPulls(dependency.IsPull) {
// We can't read the dependency
ctx.NotFound()
return
}
err := issues_model.CreateIssueDependency(ctx.Doer, target, dependency)
if err != nil {
ctx.Error(http.StatusInternalServerError, "CreateIssueDependency", err)
return
}
}
func removeIssueDependency(ctx *context.APIContext, target, dependency *issues_model.Issue, targetPerm, dependencyPerm access_model.Permission) {
if target.Repo.IsArchived || !target.Repo.IsDependenciesEnabled(ctx) {
// The target's repository doesn't have dependencies enabled
ctx.NotFound()
return
}
if !targetPerm.CanWriteIssuesOrPulls(target.IsPull) {
// We can't write to the target
ctx.NotFound()
return
}
if !dependencyPerm.CanReadIssuesOrPulls(dependency.IsPull) {
// We can't read the dependency
ctx.NotFound()
return
}
err := issues_model.RemoveIssueDependency(ctx.Doer, target, dependency, issues_model.DependencyTypeBlockedBy)
if err != nil {
ctx.Error(http.StatusInternalServerError, "CreateIssueDependency", err)
return
}
}

View File

@ -1144,3 +1144,58 @@ func GetIssueTemplates(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, ctx.IssueTemplatesFromDefaultBranch()) ctx.JSON(http.StatusOK, ctx.IssueTemplatesFromDefaultBranch())
} }
// GetIssueConfig returns the issue config for a repo
func GetIssueConfig(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/issue_config repository repoGetIssueConfig
// ---
// summary: Returns the issue config for a repo
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/RepoIssueConfig"
issueConfig, _ := ctx.IssueConfigFromDefaultBranch()
ctx.JSON(http.StatusOK, issueConfig)
}
// ValidateIssueConfig returns validation errors for the issue config
func ValidateIssueConfig(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/issue_config/validate repository repoValidateIssueConfig
// ---
// summary: Returns the validation information for a issue config
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/RepoIssueConfigValidation"
_, err := ctx.IssueConfigFromDefaultBranch()
if err == nil {
ctx.JSON(http.StatusOK, api.IssueConfigValidation{Valid: true, Message: ""})
} else {
ctx.JSON(http.StatusOK, api.IssueConfigValidation{Valid: false, Message: err.Error()})
}
}

View File

@ -41,6 +41,8 @@ type swaggerParameterBodies struct {
CreateIssueCommentOption api.CreateIssueCommentOption CreateIssueCommentOption api.CreateIssueCommentOption
// in:body // in:body
EditIssueCommentOption api.EditIssueCommentOption EditIssueCommentOption api.EditIssueCommentOption
// in:body
IssueMeta api.IssueMeta
// in:body // in:body
IssueLabelsOption api.IssueLabelsOption IssueLabelsOption api.IssueLabelsOption

View File

@ -386,3 +386,17 @@ type swaggerRepoCollaboratorPermission struct {
// in:body // in:body
Body api.RepoCollaboratorPermission `json:"body"` Body api.RepoCollaboratorPermission `json:"body"`
} }
// RepoIssueConfig
// swagger:response RepoIssueConfig
type swaggerRepoIssueConfig struct {
// in:body
Body api.IssueConfig `json:"body"`
}
// RepoIssueConfigValidation
// swagger:response RepoIssueConfigValidation
type swaggerRepoIssueConfigValidation struct {
// in:body
Body api.IssueConfigValidation `json:"body"`
}

View File

@ -435,7 +435,7 @@ func Issues(ctx *context.Context) {
} }
ctx.Data["Title"] = ctx.Tr("repo.issues") ctx.Data["Title"] = ctx.Tr("repo.issues")
ctx.Data["PageIsIssueList"] = true ctx.Data["PageIsIssueList"] = true
ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0 ctx.Data["NewIssueChooseTemplate"] = ctx.HasIssueTemplatesOrContactLinks()
} }
issues(ctx, ctx.FormInt64("milestone"), ctx.FormInt64("project"), util.OptionalBoolOf(isPullList)) issues(ctx, ctx.FormInt64("milestone"), ctx.FormInt64("project"), util.OptionalBoolOf(isPullList))
@ -848,7 +848,7 @@ func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles
func NewIssue(ctx *context.Context) { func NewIssue(ctx *context.Context) {
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"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0 ctx.Data["NewIssueChooseTemplate"] = ctx.HasIssueTemplatesOrContactLinks()
ctx.Data["RequireTribute"] = true ctx.Data["RequireTribute"] = true
ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
title := ctx.FormString("title") title := ctx.FormString("title")
@ -946,12 +946,16 @@ func NewIssueChooseTemplate(ctx *context.Context) {
ctx.Flash.Warning(renderErrorOfTemplates(ctx, errs), true) ctx.Flash.Warning(renderErrorOfTemplates(ctx, errs), true)
} }
if len(issueTemplates) == 0 { if !ctx.HasIssueTemplatesOrContactLinks() {
// The "issues/new" and "issues/new/choose" share the same query parameters "project" and "milestone", if no template here, just redirect to the "issues/new" page with these parameters. // The "issues/new" and "issues/new/choose" share the same query parameters "project" and "milestone", if no template here, just redirect to the "issues/new" page with these parameters.
ctx.Redirect(fmt.Sprintf("%s/issues/new?%s", ctx.Repo.Repository.Link(), ctx.Req.URL.RawQuery), http.StatusSeeOther) ctx.Redirect(fmt.Sprintf("%s/issues/new?%s", ctx.Repo.Repository.Link(), ctx.Req.URL.RawQuery), http.StatusSeeOther)
return return
} }
issueConfig, err := ctx.IssueConfigFromDefaultBranch()
ctx.Data["IssueConfig"] = issueConfig
ctx.Data["IssueConfigError"] = err // ctx.Flash.Err makes problems here
ctx.Data["milestone"] = ctx.FormInt64("milestone") ctx.Data["milestone"] = ctx.FormInt64("milestone")
ctx.Data["project"] = ctx.FormInt64("project") ctx.Data["project"] = ctx.FormInt64("project")
@ -1086,7 +1090,7 @@ func NewIssuePost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CreateIssueForm) form := web.GetForm(ctx).(*forms.CreateIssueForm)
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"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0 ctx.Data["NewIssueChooseTemplate"] = ctx.HasIssueTemplatesOrContactLinks()
ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
upload.AddUploadContext(ctx, "comment") upload.AddUploadContext(ctx, "comment")
@ -1280,7 +1284,7 @@ func ViewIssue(ctx *context.Context) {
return return
} }
ctx.Data["PageIsIssueList"] = true ctx.Data["PageIsIssueList"] = true
ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0 ctx.Data["NewIssueChooseTemplate"] = ctx.HasIssueTemplatesOrContactLinks()
} }
if issue.IsPull && !ctx.Repo.CanRead(unit.TypeIssues) { if issue.IsPull && !ctx.Repo.CanRead(unit.TypeIssues) {
@ -1812,17 +1816,27 @@ func ViewIssue(ctx *context.Context) {
} }
// Get Dependencies // Get Dependencies
ctx.Data["BlockedByDependencies"], err = issue.BlockedByDependencies(ctx) blockedBy, err := issue.BlockedByDependencies(ctx, db.ListOptions{})
if err != nil { if err != nil {
ctx.ServerError("BlockedByDependencies", err) ctx.ServerError("BlockedByDependencies", err)
return return
} }
ctx.Data["BlockingDependencies"], err = issue.BlockingDependencies(ctx) ctx.Data["BlockedByDependencies"], ctx.Data["BlockedByDependenciesNotPermitted"] = checkBlockedByIssues(ctx, blockedBy)
if ctx.Written() {
return
}
blocking, err := issue.BlockingDependencies(ctx)
if err != nil { if err != nil {
ctx.ServerError("BlockingDependencies", err) ctx.ServerError("BlockingDependencies", err)
return return
} }
ctx.Data["BlockingDependencies"], ctx.Data["BlockingByDependenciesNotPermitted"] = checkBlockedByIssues(ctx, blocking)
if ctx.Written() {
return
}
ctx.Data["Participants"] = participants ctx.Data["Participants"] = participants
ctx.Data["NumParticipants"] = len(participants) ctx.Data["NumParticipants"] = len(participants)
ctx.Data["Issue"] = issue ctx.Data["Issue"] = issue
@ -1851,6 +1865,48 @@ func ViewIssue(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplIssueView) ctx.HTML(http.StatusOK, tplIssueView)
} }
func checkBlockedByIssues(ctx *context.Context, blockers []*issues_model.DependencyInfo) (canRead, notPermitted []*issues_model.DependencyInfo) {
var lastRepoID int64
var lastPerm access_model.Permission
for i, blocker := range blockers {
// Get the permissions for this repository
perm := lastPerm
if lastRepoID != blocker.Repository.ID {
if blocker.Repository.ID == ctx.Repo.Repository.ID {
perm = ctx.Repo.Permission
} else {
var err error
perm, err = access_model.GetUserRepoPermission(ctx, &blocker.Repository, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
return
}
}
lastRepoID = blocker.Repository.ID
}
// check permission
if !perm.CanReadIssuesOrPulls(blocker.Issue.IsPull) {
blockers[len(notPermitted)], blockers[i] = blocker, blockers[len(notPermitted)]
notPermitted = blockers[:len(notPermitted)+1]
}
}
blockers = blockers[len(notPermitted):]
sortDependencyInfo(blockers)
sortDependencyInfo(notPermitted)
return blockers, notPermitted
}
func sortDependencyInfo(blockers []*issues_model.DependencyInfo) {
sort.Slice(blockers, func(i, j int) bool {
if blockers[i].RepoID == blockers[j].RepoID {
return blockers[i].Issue.CreatedUnix < blockers[j].Issue.CreatedUnix
}
return blockers[i].RepoID < blockers[j].RepoID
})
}
// GetActionIssue will return the issue which is used in the context. // GetActionIssue will return the issue which is used in the context.
func GetActionIssue(ctx *context.Context) *issues_model.Issue { func GetActionIssue(ctx *context.Context) *issues_model.Issue {
issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))

View File

@ -7,6 +7,7 @@ import (
"net/http" "net/http"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
) )
@ -44,9 +45,25 @@ func AddDependency(ctx *context.Context) {
} }
// Check if both issues are in the same repo if cross repository dependencies is not enabled // Check if both issues are in the same repo if cross repository dependencies is not enabled
if issue.RepoID != dep.RepoID && !setting.Service.AllowCrossRepositoryDependencies { if issue.RepoID != dep.RepoID {
ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_dep_not_same_repo")) if !setting.Service.AllowCrossRepositoryDependencies {
return ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_dep_not_same_repo"))
return
}
if err := dep.LoadRepo(ctx); err != nil {
ctx.ServerError("loadRepo", err)
return
}
// Can ctx.Doer read issues in the dep repo?
depRepoPerm, err := access_model.GetUserRepoPermission(ctx, dep.Repo, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
return
}
if !depRepoPerm.CanReadIssuesOrPulls(dep.IsPull) {
// you can't see this dependency
return
}
} }
// Check if issue and dependency is the same // Check if issue and dependency is the same

View File

@ -348,6 +348,9 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
if ctx.Repo.TreePath == ".editorconfig" { if ctx.Repo.TreePath == ".editorconfig" {
_, editorconfigErr := ctx.Repo.GetEditorconfig(ctx.Repo.Commit) _, editorconfigErr := ctx.Repo.GetEditorconfig(ctx.Repo.Commit)
ctx.Data["FileError"] = editorconfigErr ctx.Data["FileError"] = editorconfigErr
} else if ctx.Repo.IsIssueConfig(ctx.Repo.TreePath) {
_, issueConfigErr := ctx.Repo.GetIssueConfig(ctx.Repo.TreePath, ctx.Repo.Commit)
ctx.Data["FileError"] = issueConfigErr
} }
isDisplayingSource := ctx.FormString("display") == "source" isDisplayingSource := ctx.FormString("display") == "source"

View File

@ -32,21 +32,15 @@ func ToAPIIssue(ctx context.Context, issue *issues_model.Issue) *api.Issue {
if err := issue.LoadRepo(ctx); err != nil { if err := issue.LoadRepo(ctx); err != nil {
return &api.Issue{} return &api.Issue{}
} }
if err := issue.Repo.LoadOwner(ctx); err != nil {
return &api.Issue{}
}
apiIssue := &api.Issue{ apiIssue := &api.Issue{
ID: issue.ID, ID: issue.ID,
URL: issue.APIURL(),
HTMLURL: issue.HTMLURL(),
Index: issue.Index, Index: issue.Index,
Poster: ToUser(ctx, issue.Poster, nil), Poster: ToUser(ctx, issue.Poster, nil),
Title: issue.Title, Title: issue.Title,
Body: issue.Content, Body: issue.Content,
Attachments: ToAttachments(issue.Attachments), Attachments: ToAttachments(issue.Attachments),
Ref: issue.Ref, Ref: issue.Ref,
Labels: ToLabelList(issue.Labels, issue.Repo, issue.Repo.Owner),
State: issue.State(), State: issue.State(),
IsLocked: issue.IsLocked, IsLocked: issue.IsLocked,
Comments: issue.NumComments, Comments: issue.NumComments,
@ -54,11 +48,19 @@ func ToAPIIssue(ctx context.Context, issue *issues_model.Issue) *api.Issue {
Updated: issue.UpdatedUnix.AsTime(), Updated: issue.UpdatedUnix.AsTime(),
} }
apiIssue.Repo = &api.RepositoryMeta{ if issue.Repo != nil {
ID: issue.Repo.ID, if err := issue.Repo.LoadOwner(ctx); err != nil {
Name: issue.Repo.Name, return &api.Issue{}
Owner: issue.Repo.OwnerName, }
FullName: issue.Repo.FullName(), apiIssue.URL = issue.APIURL()
apiIssue.HTMLURL = issue.HTMLURL()
apiIssue.Labels = ToLabelList(issue.Labels, issue.Repo, issue.Repo.Owner)
apiIssue.Repo = &api.RepositoryMeta{
ID: issue.Repo.ID,
Name: issue.Repo.Name,
Owner: issue.Repo.OwnerName,
FullName: issue.Repo.FullName(),
}
} }
if issue.ClosedUnix != 0 { if issue.ClosedUnix != 0 {
@ -85,11 +87,13 @@ func ToAPIIssue(ctx context.Context, issue *issues_model.Issue) *api.Issue {
if err := issue.LoadPullRequest(ctx); err != nil { if err := issue.LoadPullRequest(ctx); err != nil {
return &api.Issue{} return &api.Issue{}
} }
apiIssue.PullRequest = &api.PullRequestMeta{ if issue.PullRequest != nil {
HasMerged: issue.PullRequest.HasMerged, apiIssue.PullRequest = &api.PullRequestMeta{
} HasMerged: issue.PullRequest.HasMerged,
if issue.PullRequest.HasMerged { }
apiIssue.PullRequest.Merged = issue.PullRequest.MergedUnix.AsTimePtr() if issue.PullRequest.HasMerged {
apiIssue.PullRequest.Merged = issue.PullRequest.MergedUnix.AsTimePtr()
}
} }
} }
if issue.DeadlineUnix != 0 { if issue.DeadlineUnix != 0 {

View File

@ -5,9 +5,9 @@
<div class="field"> <div class="field">
<label>{{svg "octicon-terminal"}} {{.locale.Tr "packages.generic.download"}}</label> <label>{{svg "octicon-terminal"}} {{.locale.Tr "packages.generic.download"}}</label>
<div class="markup"><pre class="code-block"><code> <div class="markup"><pre class="code-block"><code>
{{- range .PackageDescriptor.Files -}} {{- range .PackageDescriptor.Files -}}
curl <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/generic/{{$.PackageDescriptor.Package.Name}}/{{$.PackageDescriptor.Version.Version}}/{{.File.Name}}"></gitea-origin-url><br> curl <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/generic/{{$.PackageDescriptor.Package.Name}}/{{$.PackageDescriptor.Version.Version}}/{{.File.Name}}"></gitea-origin-url>
{{- end -}} {{end -}}
</code></pre></div> </code></pre></div>
</div> </div>
<div class="field"> <div class="field">

View File

@ -20,17 +20,40 @@
</div> </div>
</div> </div>
{{end}} {{end}}
<div class="ui attached segment"> {{range .IssueConfig.ContactLinks}}
<div class="ui two column grid"> <div class="ui attached segment">
<div class="column left aligned"> <div class="ui two column grid">
<strong>{{.locale.Tr "repo.issues.choose.blank"}}</strong> <div class="column left aligned">
<br>{{.locale.Tr "repo.issues.choose.blank_about"}} <strong>{{.Name | RenderEmojiPlain}}</strong>
</div> <br>{{.About | RenderEmojiPlain}}
<div class="column right aligned"> </div>
<a href="{{.RepoLink}}/issues/new?{{if .milestone}}&milestone={{.milestone}}{{end}}{{if $.project}}&project={{$.project}}{{end}}" class="ui green button">{{$.locale.Tr "repo.issues.choose.get_started"}}</a> <div class="column right aligned">
<a href="{{.URL}}" class="ui green button">{{svg "octicon-link-external"}} {{$.locale.Tr "repo.issues.choose.open_external_link"}}</a>
</div>
</div> </div>
</div> </div>
</div> {{end}}
{{if .IssueConfig.BlankIssuesEnabled}}
<div class="ui attached segment">
<div class="ui two column grid">
<div class="column left aligned">
<strong>{{.locale.Tr "repo.issues.choose.blank"}}</strong>
<br/>{{.locale.Tr "repo.issues.choose.blank_about"}}
</div>
<div class="column right aligned">
<a href="{{.RepoLink}}/issues/new?{{if .milestone}}&milestone={{.milestone}}{{end}}{{if $.project}}&project={{$.project}}{{end}}" class="ui green button">{{$.locale.Tr "repo.issues.choose.get_started"}}</a>
</div>
</div>
</div>
{{end}}
{{- if .IssueConfigError}}{{/* normal warning flash makes problems here*/}}
<div class="ui warning message">
<div class="text left">
<div>{{.locale.Tr "repo.issues.choose.invalid_config"}}</div>
<diy>{{.IssueConfigError}}</div>
</div>
</div>
{{end}}
</div> </div>
</div> </div>
{{template "base/footer" .}} {{template "base/footer" .}}

View File

@ -420,7 +420,7 @@
<div class="ui divider"></div> <div class="ui divider"></div>
<div class="ui depending"> <div class="ui depending">
{{if (and (not .BlockedByDependencies) (not .BlockingDependencies))}} {{if (and (not .BlockedByDependencies) (not .BlockedByDependenciesNotPermitted) (not .BlockingDependencies) (not .BlockingDependenciesNotPermitted))}}
<span class="text"><strong>{{.locale.Tr "repo.issues.dependency.title"}}</strong></span> <span class="text"><strong>{{.locale.Tr "repo.issues.dependency.title"}}</strong></span>
<br> <br>
<p> <p>
@ -432,7 +432,7 @@
</p> </p>
{{end}} {{end}}
{{if .BlockingDependencies}} {{if or .BlockingDependencies .BlockingDependenciesNotPermitted}}
<span class="text" data-tooltip-content="{{if .Issue.IsPull}}{{.locale.Tr "repo.issues.dependency.pr_close_blocks"}}{{else}}{{.locale.Tr "repo.issues.dependency.issue_close_blocks"}}{{end}}"> <span class="text" data-tooltip-content="{{if .Issue.IsPull}}{{.locale.Tr "repo.issues.dependency.pr_close_blocks"}}{{else}}{{.locale.Tr "repo.issues.dependency.issue_close_blocks"}}{{end}}">
<strong>{{.locale.Tr "repo.issues.dependency.blocks_short"}}</strong> <strong>{{.locale.Tr "repo.issues.dependency.blocks_short"}}</strong>
</span> </span>
@ -456,10 +456,15 @@
</div> </div>
</div> </div>
{{end}} {{end}}
{{if .BlockingDependenciesNotPermitted}}
<div class="item gt-df gt-ac gt-sb">
<span>{{$.locale.TrN (len .BlockingDependenciesNotPermitted) "repo.issues.dependency.no_permission_1" "repo.issues.dependency.no_permission_n" (len .BlockingDependenciesNotPermitted)}}</span>
</div>
{{end}}
</div> </div>
{{end}} {{end}}
{{if .BlockedByDependencies}} {{if or .BlockedByDependencies .BlockedByDependenciesNotPermitted}}
<span class="text" data-tooltip-content="{{if .Issue.IsPull}}{{.locale.Tr "repo.issues.dependency.pr_closing_blockedby"}}{{else}}{{.locale.Tr "repo.issues.dependency.issue_closing_blockedby"}}{{end}}"> <span class="text" data-tooltip-content="{{if .Issue.IsPull}}{{.locale.Tr "repo.issues.dependency.pr_closing_blockedby"}}{{else}}{{.locale.Tr "repo.issues.dependency.issue_closing_blockedby"}}{{end}}">
<strong>{{.locale.Tr "repo.issues.dependency.blocked_by_short"}}</strong> <strong>{{.locale.Tr "repo.issues.dependency.blocked_by_short"}}</strong>
</span> </span>
@ -483,6 +488,34 @@
</div> </div>
</div> </div>
{{end}} {{end}}
{{if $.CanCreateIssueDependencies}}
{{range .BlockedByDependenciesNotPermitted}}
<div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} gt-df gt-ac gt-sb">
<div class="item-left gt-df gt-jc gt-fc gt-f1">
<div>
<span data-tooltip-content="{{$.locale.Tr "repo.issues.dependency.no_permission.can_remove"}}">{{svg "octicon-lock" 16}}</span>
<span class="title" data-tooltip-content="#{{.Issue.Index}} {{.Issue.Title | RenderEmoji $.Context}}">
#{{.Issue.Index}} {{.Issue.Title | RenderEmoji $.Context}}
</span>
</div>
<div class="text small">
{{.Repository.OwnerName}}/{{.Repository.Name}}
</div>
</div>
<div class="item-right gt-df gt-ac">
{{if and $.CanCreateIssueDependencies (not $.Repository.IsArchived)}}
<a class="delete-dependency-button ci muted" data-id="{{.Issue.ID}}" data-type="blocking" data-tooltip-content="{{$.locale.Tr "repo.issues.dependency.remove_info"}}">
{{svg "octicon-trash" 16}}
</a>
{{end}}
</div>
</div>
{{end}}
{{else if .BlockedByDependenciesNotPermitted}}
<div class="item gt-df gt-ac gt-sb">
<span>{{$.locale.TrN (len .BlockedByDependenciesNotPermitted) "repo.issues.dependency.no_permission_1" "repo.issues.dependency.no_permission_n" (len .BlockedByDependenciesNotPermitted)}}</span>
</div>
{{end}}
</div> </div>
{{end}} {{end}}

View File

@ -39,6 +39,9 @@
</h3> </h3>
<div class="download gt-df gt-ac"> <div class="download gt-df gt-ac">
{{if $.Permission.CanRead $.UnitTypeCode}} {{if $.Permission.CanRead $.UnitTypeCode}}
{{if .CreatedUnix}}
<span class="gt-mr-3">{{svg "octicon-clock" 16 "gt-mr-2"}}{{TimeSinceUnix .CreatedUnix $.locale}}</span>
{{end}}
<a class="gt-mr-3 gt-mono muted" href="{{$.RepoLink}}/src/commit/{{.Sha1}}" rel="nofollow">{{svg "octicon-git-commit" 16 "gt-mr-2"}}{{ShortSha .Sha1}}</a> <a class="gt-mr-3 gt-mono muted" href="{{$.RepoLink}}/src/commit/{{.Sha1}}" rel="nofollow">{{svg "octicon-git-commit" 16 "gt-mr-2"}}{{ShortSha .Sha1}}</a>
{{if not $.DisableDownloadSourceArchives}} {{if not $.DisableDownloadSourceArchives}}
<a class="archive-link gt-mr-3 muted" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.zip" rel="nofollow">{{svg "octicon-file-zip" 16 "gt-mr-2"}}ZIP</a> <a class="archive-link gt-mr-3 muted" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.zip" rel="nofollow">{{svg "octicon-file-zip" 16 "gt-mr-2"}}ZIP</a>

View File

@ -5013,6 +5013,72 @@
} }
} }
}, },
"/repos/{owner}/{repo}/issue_config": {
"get": {
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Returns the issue config for a repo",
"operationId": "repoGetIssueConfig",
"parameters": [
{
"type": "string",
"description": "owner of the repo",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repo",
"name": "repo",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"$ref": "#/responses/RepoIssueConfig"
}
}
}
},
"/repos/{owner}/{repo}/issue_config/validate": {
"get": {
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Returns the validation information for a issue config",
"operationId": "repoValidateIssueConfig",
"parameters": [
{
"type": "string",
"description": "owner of the repo",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repo",
"name": "repo",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"$ref": "#/responses/RepoIssueConfigValidation"
}
}
}
},
"/repos/{owner}/{repo}/issue_templates": { "/repos/{owner}/{repo}/issue_templates": {
"get": { "get": {
"produces": [ "produces": [
@ -6256,6 +6322,151 @@
} }
} }
}, },
"/repos/{owner}/{repo}/issues/{index}/blocks": {
"get": {
"produces": [
"application/json"
],
"tags": [
"issue"
],
"summary": "List issues that are blocked by this issue",
"operationId": "issueListBlocks",
"parameters": [
{
"type": "string",
"description": "owner of the repo",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repo",
"name": "repo",
"in": "path",
"required": true
},
{
"type": "string",
"description": "index of the issue",
"name": "index",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "page number of results to return (1-based)",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "page size of results",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"$ref": "#/responses/IssueList"
}
}
},
"post": {
"produces": [
"application/json"
],
"tags": [
"issue"
],
"summary": "Block the issue given in the body by the issue in path",
"operationId": "issueCreateIssueBlocking",
"parameters": [
{
"type": "string",
"description": "owner of the repo",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repo",
"name": "repo",
"in": "path",
"required": true
},
{
"type": "string",
"description": "index of the issue",
"name": "index",
"in": "path",
"required": true
},
{
"name": "body",
"in": "body",
"schema": {
"$ref": "#/definitions/IssueMeta"
}
}
],
"responses": {
"201": {
"$ref": "#/responses/Issue"
},
"404": {
"description": "the issue does not exist"
}
}
},
"delete": {
"produces": [
"application/json"
],
"tags": [
"issue"
],
"summary": "Unblock the issue given in the body by the issue in path",
"operationId": "issueRemoveIssueBlocking",
"parameters": [
{
"type": "string",
"description": "owner of the repo",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repo",
"name": "repo",
"in": "path",
"required": true
},
{
"type": "string",
"description": "index of the issue",
"name": "index",
"in": "path",
"required": true
},
{
"name": "body",
"in": "body",
"schema": {
"$ref": "#/definitions/IssueMeta"
}
}
],
"responses": {
"200": {
"$ref": "#/responses/Issue"
}
}
}
},
"/repos/{owner}/{repo}/issues/{index}/comments": { "/repos/{owner}/{repo}/issues/{index}/comments": {
"get": { "get": {
"produces": [ "produces": [
@ -6538,6 +6749,151 @@
} }
} }
}, },
"/repos/{owner}/{repo}/issues/{index}/dependencies": {
"get": {
"produces": [
"application/json"
],
"tags": [
"issue"
],
"summary": "List an issue's dependencies, i.e all issues that block this issue.",
"operationId": "issueListIssueDependencies",
"parameters": [
{
"type": "string",
"description": "owner of the repo",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repo",
"name": "repo",
"in": "path",
"required": true
},
{
"type": "string",
"description": "index of the issue",
"name": "index",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "page number of results to return (1-based)",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "page size of results",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"$ref": "#/responses/IssueList"
}
}
},
"post": {
"produces": [
"application/json"
],
"tags": [
"issue"
],
"summary": "Make the issue in the url depend on the issue in the form.",
"operationId": "issueCreateIssueDependencies",
"parameters": [
{
"type": "string",
"description": "owner of the repo",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repo",
"name": "repo",
"in": "path",
"required": true
},
{
"type": "string",
"description": "index of the issue",
"name": "index",
"in": "path",
"required": true
},
{
"name": "body",
"in": "body",
"schema": {
"$ref": "#/definitions/IssueMeta"
}
}
],
"responses": {
"201": {
"$ref": "#/responses/Issue"
},
"404": {
"description": "the issue does not exist"
}
}
},
"delete": {
"produces": [
"application/json"
],
"tags": [
"issue"
],
"summary": "Remove an issue dependency",
"operationId": "issueRemoveIssueDependencies",
"parameters": [
{
"type": "string",
"description": "owner of the repo",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repo",
"name": "repo",
"in": "path",
"required": true
},
{
"type": "string",
"description": "index of the issue",
"name": "index",
"in": "path",
"required": true
},
{
"name": "body",
"in": "body",
"schema": {
"$ref": "#/definitions/IssueMeta"
}
}
],
"responses": {
"200": {
"$ref": "#/responses/Issue"
}
}
}
},
"/repos/{owner}/{repo}/issues/{index}/labels": { "/repos/{owner}/{repo}/issues/{index}/labels": {
"get": { "get": {
"produces": [ "produces": [
@ -17875,6 +18231,55 @@
}, },
"x-go-package": "code.gitea.io/gitea/modules/structs" "x-go-package": "code.gitea.io/gitea/modules/structs"
}, },
"IssueConfig": {
"type": "object",
"properties": {
"blank_issues_enabled": {
"type": "boolean",
"x-go-name": "BlankIssuesEnabled"
},
"contact_links": {
"type": "array",
"items": {
"$ref": "#/definitions/IssueConfigContactLink"
},
"x-go-name": "ContactLinks"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"IssueConfigContactLink": {
"type": "object",
"properties": {
"about": {
"type": "string",
"x-go-name": "About"
},
"name": {
"type": "string",
"x-go-name": "Name"
},
"url": {
"type": "string",
"x-go-name": "URL"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"IssueConfigValidation": {
"type": "object",
"properties": {
"message": {
"type": "string",
"x-go-name": "Message"
},
"valid": {
"type": "boolean",
"x-go-name": "Valid"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"IssueDeadline": { "IssueDeadline": {
"description": "IssueDeadline represents an issue deadline", "description": "IssueDeadline represents an issue deadline",
"type": "object", "type": "object",
@ -17932,6 +18337,26 @@
}, },
"x-go-package": "code.gitea.io/gitea/modules/structs" "x-go-package": "code.gitea.io/gitea/modules/structs"
}, },
"IssueMeta": {
"description": "IssueMeta basic issue information",
"type": "object",
"properties": {
"index": {
"type": "integer",
"format": "int64",
"x-go-name": "Index"
},
"owner": {
"type": "string",
"x-go-name": "Owner"
},
"repo": {
"type": "string",
"x-go-name": "Name"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"IssueTemplate": { "IssueTemplate": {
"description": "IssueTemplate represents an issue template for a repository", "description": "IssueTemplate represents an issue template for a repository",
"type": "object", "type": "object",
@ -21134,6 +21559,18 @@
"$ref": "#/definitions/RepoCollaboratorPermission" "$ref": "#/definitions/RepoCollaboratorPermission"
} }
}, },
"RepoIssueConfig": {
"description": "RepoIssueConfig",
"schema": {
"$ref": "#/definitions/IssueConfig"
}
},
"RepoIssueConfigValidation": {
"description": "RepoIssueConfigValidation",
"schema": {
"$ref": "#/definitions/IssueConfigValidation"
}
},
"Repository": { "Repository": {
"description": "Repository", "description": "Repository",
"schema": { "schema": {

View File

@ -0,0 +1,52 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"fmt"
"net/http"
"testing"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func TestAPIReposGetDefaultIssueConfig(t *testing.T) {
defer tests.PrepareTestEnv(t)()
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issue_config", owner.Name, repo.Name)
req := NewRequest(t, "GET", urlStr)
resp := MakeRequest(t, req, http.StatusOK)
var issueConfig api.IssueConfig
DecodeJSON(t, resp, &issueConfig)
assert.True(t, issueConfig.BlankIssuesEnabled)
assert.Equal(t, issueConfig.ContactLinks, make([]api.IssueConfigContactLink, 0))
}
func TestAPIReposValidateDefaultIssueConfig(t *testing.T) {
defer tests.PrepareTestEnv(t)()
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issue_config/validate", owner.Name, repo.Name)
req := NewRequest(t, "GET", urlStr)
resp := MakeRequest(t, req, http.StatusOK)
var issueConfigValidation api.IssueConfigValidation
DecodeJSON(t, resp, &issueConfigValidation)
assert.True(t, issueConfigValidation.Valid)
assert.Equal(t, issueConfigValidation.Message, "")
}

View File

@ -125,6 +125,7 @@ MINIO_SECRET_ACCESS_KEY = 12345678
MINIO_BUCKET = gitea MINIO_BUCKET = gitea
MINIO_LOCATION = us-east-1 MINIO_LOCATION = us-east-1
MINIO_USE_SSL = false MINIO_USE_SSL = false
MINIO_CHECKSUM_ALGORITHM = md5
[packages] [packages]
ENABLED = true ENABLED = true

View File

@ -2399,9 +2399,107 @@ a.ui.label:hover {
.ui.basic.labels .primary.label, .ui.basic.labels .primary.label,
.ui.ui.ui.basic.primary.label { .ui.ui.ui.basic.primary.label {
background: transparent !important; background: transparent;
border-color: var(--color-primary) !important; border-color: var(--color-primary);
color: var(--color-primary) !important; color: var(--color-primary);
}
.ui.basic.labels .secondary.label,
.ui.ui.ui.basic.secondary.label {
background: transparent;
border-color: var(--color-secondary);
color: var(--color-secondary);
}
.ui.basic.labels .orange.label,
.ui.ui.ui.basic.orange.label {
background: transparent;
border-color: var(--color-orange);
color: var(--color-orange);
}
.ui.basic.labels .green.label,
.ui.ui.ui.basic.green.label {
background: transparent;
border-color: var(--color-green);
color: var(--color-green);
}
.ui.basic.labels .olive.label,
.ui.ui.ui.basic.olive.label {
background: transparent;
border-color: var(--color-olive);
color: var(--color-olive);
}
.ui.basic.labels .teal.label,
.ui.ui.ui.basic.teal.label {
background: transparent;
border-color: var(--color-teal);
color: var(--color-teal);
}
.ui.basic.labels .blue.label,
.ui.ui.ui.basic.blue.label {
background: transparent;
border-color: var(--color-blue);
color: var(--color-blue);
}
.ui.basic.labels .violet.label,
.ui.ui.ui.basic.violet.label {
background: transparent;
border-color: var(--color-violet);
color: var(--color-violet);
}
.ui.basic.labels .purple.label,
.ui.ui.ui.basic.purple.label {
background: transparent;
border-color: var(--color-purple);
color: var(--color-purple);
}
.ui.basic.labels .pink.label,
.ui.ui.ui.basic.pink.label {
background: transparent;
border-color: var(--color-pink);
color: var(--color-pink);
}
.ui.basic.labels .red.label,
.ui.ui.ui.basic.red.label {
background: transparent;
border-color: var(--color-red);
color: var(--color-red);
}
.ui.basic.labels .brown.label,
.ui.ui.ui.basic.brown.label {
background: transparent;
border-color: var(--color-brown);
color: var(--color-brown);
}
.ui.basic.labels .yellow.label,
.ui.ui.ui.basic.yellow.label {
background: transparent;
border-color: var(--color-yellow);
color: var(--color-yellow);
}
.ui.basic.labels .grey.label,
.ui.ui.ui.basic.grey.label {
background: transparent;
border-color: var(--color-grey);
color: var(--color-grey);
}
.ui.basic.labels .black.label,
.ui.ui.ui.basic.black.label {
background: transparent;
border-color: var(--color-black);
color: var(--color-black);
} }
.ui.basic.labels .label, .ui.basic.labels .label,