Compare commits

...

24 Commits

Author SHA1 Message Date
Lunny Xiao
4126aad4aa Add 1.20.5 changelog (#27404) 2023-10-03 12:53:35 +00:00
Lunny Xiao
5c96a2be87 Fix bug of review request number (#27406)
Manually backport #27104 without tests because too many conflicted files
to backport it completely.
2023-10-03 08:08:12 +00:00
Giteabot
acedf0f702 Fix git 2.11 error when checking IsEmpty (#27393) (#27396)
Backport #27393 by @wxiaoguang

Fix #27389

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2023-10-02 23:30:04 +08:00
Giteabot
23139aa27b Allow get release download files and lfs files with oauth2 token format (#26430) (#27378)
Backport #26430 by @lunny

Fix #26165
Fix #25257

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2023-10-01 19:54:15 +08:00
Giteabot
b6b71c78c4 Add logs for data broken of comment review (#27326) (#27344)
Backport #27326 by @lunny

Fix #27306

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2023-09-29 13:31:10 +08:00
Giteabot
2138661dae fix orphan check for deleted branch (#27310) (#27320)
Backport #27310 by @earl-warren

- Modify the deleted branch orphan check to check for the new table
instead.
- Regression from 6e19484f4d
- Resolves https://codeberg.org/forgejo/forgejo/issues/1522

(cherry picked from commit c1d888686fe445e4edecb9d835c5b3893b574b75)

Co-authored-by: Earl Warren <109468362+earl-warren@users.noreply.github.com>
Co-authored-by: Gusted <postmaster@gusted.xyz>
2023-09-28 11:16:05 +08:00
Giteabot
4b37eb2c23 Fix PushEvent NullPointerException jenkinsci/github-plugin (#27203) (#27249)
Backport #27203 by @Nabapadma-sarker

Fixes #27202

Co-authored-by: Nabapadma-sarker <nabapadmacse1991@gmail.com>
2023-09-25 07:02:08 +00:00
Giteabot
dd2f007501 Fix z-index on markdown completion (#27237) (#27242)
Backport #27237 by @silverwind

Fixes: https://github.com/go-gitea/gitea/issues/27230

Co-authored-by: silverwind <me@silverwind.io>
2023-09-25 09:10:46 +08:00
Giteabot
dd44c2164e Fix z-index on markdown completion (#27237) (#27238)
Backport #27237 by @silverwind

Fixes: https://github.com/go-gitea/gitea/issues/27230

Co-authored-by: silverwind <me@silverwind.io>
2023-09-24 23:37:58 +00:00
Giteabot
2604571993 Update database-preparation and add note re: MariaDB (#27232) (#27235)
Backport #27232 by @techknowlogick

update DB docs per feedback.
https://gitea.com/gitea/gitea-docusaurus/issues/69

Co-authored-by: techknowlogick <techknowlogick@gitea.com>
2023-09-25 05:27:15 +08:00
KN4CK3R
eae6985b63 Quote table release in sql queries (#27205) (#27219)
Backport of #27205

Fixes #27174

`release` is a reserved keyword in MySql. I can't reproduce the issue on
my setup and we have a test for that code but it seems there can be
setups where it fails.
2023-09-24 01:48:50 +03:00
Giteabot
d8583edfe7 Fix release URL in webhooks (#27182) (#27184)
Backport #27182 by @jolheiser

Resolves #27180

`URL` points to the API URL, `HTMLURL` points to the web page.

Notably, however, for PRs they are the same URL. I switched them to use
HTMLURL to match the rest of the codebase terminology.

Co-authored-by: John Olheiser <john.olheiser@gmail.com>
2023-09-21 23:59:27 +00:00
Giteabot
d99479c810 Fix organization field being null in POST /orgs/{orgid}/teams (#27150) (#27167)
Backport #27150 by @memphis88

Similarly to the fix in https://github.com/go-gitea/gitea/pull/24694,
this addresses the team creation not returning the organization
information in the response.

This fix is connected to the
[issue](https://gitea.com/gitea/terraform-provider-gitea/issues/27)
discovered in the terraform provider.
Moreover, the
[documentation](https://docs.gitea.com/api/1.20/#tag/organization/operation/orgCreateTeam)
suggests that the response body should include the `organization` field
(currently being `null`).

Co-authored-by: Dionysios Kakouris <1369451+memphis88@users.noreply.github.com>
2023-09-21 22:23:33 +02:00
Giteabot
fbe1f35112 Fix organization field being null in POST /orgs/{orgid}/teams (#27150) (#27162)
Backport #27150 by @memphis88

Similarly to the fix in https://github.com/go-gitea/gitea/pull/24694,
this addresses the team creation not returning the organization
information in the response.

This fix is connected to the
[issue](https://gitea.com/gitea/terraform-provider-gitea/issues/27)
discovered in the terraform provider.
Moreover, the
[documentation](https://docs.gitea.com/api/1.20/#tag/organization/operation/orgCreateTeam)
suggests that the response body should include the `organization` field
(currently being `null`).

Co-authored-by: Dionysios Kakouris <1369451+memphis88@users.noreply.github.com>
2023-09-21 12:14:59 +02:00
Giteabot
25233a9bdc Fix successful return value for SyncAndGetUserSpecificDiff (#27152) (#27156)
Backport #27152 by @delvh

A function should not return an error when it is successful.
Otherwise, things like
https://discord.com/channels/322538954119184384/322538954119184384/1153705341620600833
happen…

Co-authored-by: delvh <dev.lh@web.de>
2023-09-21 00:58:13 +02:00
Giteabot
7a99c7b83c Improve actions docs related to pull_request event (#27126) (#27145)
Backport #27126 by @Zettat123

Related to #27039

The `ref` property in Gitea Actions is different from GitHub Actions.
This PR improves the documentation to explain the difference.

Co-authored-by: Zettat123 <zettat123@gmail.com>
2023-09-20 11:49:58 +02:00
Giteabot
1d6e5c8e58 fix pagination for followers and following (#27127) (#27138)
Backport #27127 by @earl-warren

- Use the correct total amount for pagination. Thereby correctly show
the pagination bare when there's more than one page of
followers/followings.

Refs: https://codeberg.org/forgejo/forgejo/pulls/1477

(cherry picked from commit c1a136318be3bf72511bed108f2d67f2cf34e1b8)

Co-authored-by: Earl Warren <109468362+earl-warren@users.noreply.github.com>
Co-authored-by: Gusted <postmaster@gusted.xyz>
2023-09-19 16:03:01 +00:00
Giteabot
882e465c3a services/wiki: Close() after error handling (#27129) (#27137)
Backport #27129 by @earl-warren

Refs: https://codeberg.org/forgejo/forgejo/pulls/1385

Signed-off-by: Lars Lehtonen <lars.lehtonen@gmail.com>
(cherry picked from commit 589e7d346f51de4a0e2c461b220c8cad34133b2f)

Co-authored-by: Earl Warren <109468362+earl-warren@users.noreply.github.com>
Co-authored-by: Lars Lehtonen <lars.lehtonen@gmail.com>
2023-09-19 15:13:23 +00:00
Giteabot
b139234fa8 Fix issue templates when blank isses are disabled (#27061) (#27082)
Backport #27061 by @JakobDev

Fixes #27060

Co-authored-by: JakobDev <jakobdev@gmx.de>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: delvh <dev.lh@web.de>
2023-09-14 17:39:34 +02:00
sebastian-sauer
d8b39324d7 Load reviewer before sending notification (#27063) (#27064)
Fixes #27035
2023-09-13 15:32:58 -05:00
Lunny Xiao
9df573bddc Fix context cache bug & enable context cache for dashabord commits' authors(#26991) (#27017)
backport #26991 

Unfortunately, when a system setting hasn't been stored in the database,
it cannot be cached.
Meanwhile, this PR also uses context cache for push email avatar display
which should avoid to read user table via email address again and again.

According to my local test, this should reduce dashboard elapsed time
from 150ms -> 80ms .
2023-09-13 15:15:00 +08:00
wxiaoguang
b0a405c5fa Use secure cookie for HTTPS sites (#26999) (#27013)
Backport #26999

If the AppURL(ROOT_URL) is an HTTPS URL, then the COOKIE_SECURE's
default value should be true.

And, if a user visits an "http" site with "https" AppURL, they won't be
able to login, and they should have been warned. The only problem is
that the "language" can't be set either in such case, while I think it
is not a serious problem, and it could be fixed easily if needed.
2023-09-11 09:59:00 +00:00
Infinoid
3c53740244 Correct the database.LOG_SQL default value in config cheat sheet (#26997) (#27002)
This is a manual backport of #26997 to v1.20.
2023-09-10 21:43:42 -04:00
Giteabot
da7d7e60d8 Fix INI parsing for value with trailing slash (#26995) (#27001)
Backport #26995 by @wxiaoguang

Fix #26977 (a temp fix)

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2023-09-10 20:52:25 +02:00
50 changed files with 258 additions and 166 deletions

View File

@@ -4,6 +4,33 @@ This changelog goes through all the changes that have been made in each release
without substantial changes to our git log; to see the highlights of what has without substantial changes to our git log; to see the highlights of what has
been added to each release, please refer to the [blog](https://blog.gitea.com). been added to each release, please refer to the [blog](https://blog.gitea.com).
## [1.20.5](https://github.com/go-gitea/gitea/releases/tag/1.20.5) - 2023-10-03
* ENHANCEMENTS
* Fix z-index on markdown completion (#27237) (#27242 & #27238)
* Use secure cookie for HTTPS sites (#26999) (#27013)
* BUGFIXES
* Fix git 2.11 error when checking IsEmpty (#27393) (#27396)
* Allow get release download files and lfs files with oauth2 token format (#26430) (#27378)
* Fix orphan check for deleted branch (#27310) (#27320)
* Quote table `release` in sql queries (#27205) (#27219)
* Fix release URL in webhooks (#27182) (#27184)
* Fix successful return value for `SyncAndGetUserSpecificDiff` (#27152) (#27156)
* fix pagination for followers and following (#27127) (#27138)
* Fix issue templates when blank isses are disabled (#27061) (#27082)
* Fix context cache bug & enable context cache for dashabord commits' authors(#26991) (#27017)
* Fix INI parsing for value with trailing slash (#26995) (#27001)
* Fix PushEvent NullPointerException jenkinsci/github-plugin (#27203) (#27249)
* Fix organization field being null in POST /orgs/{orgid}/teams (#27150) (#27167 & #27162)
* Fix bug of review request number (#27406) (#27104)
* TESTING
* services/wiki: Close() after error handling (#27129) (#27137)
* DOCS
* Improve actions docs related to `pull_request` event (#27126) (#27145)
* MISC
* Add logs for data broken of comment review (#27326) (#27344)
* Load reviewer before sending notification (#27063) (#27064)
## [1.20.4](https://github.com/go-gitea/gitea/releases/tag/v1.20.4) - 2023-09-08 ## [1.20.4](https://github.com/go-gitea/gitea/releases/tag/v1.20.4) - 2023-09-08
* SECURITY * SECURITY

View File

@@ -1724,8 +1724,8 @@ LEVEL = Info
;; Session cookie name ;; Session cookie name
;COOKIE_NAME = i_like_gitea ;COOKIE_NAME = i_like_gitea
;; ;;
;; If you use session in https only, default is false ;; If you use session in https only: true or false. If not set, it defaults to `true` if the ROOT_URL is an HTTPS URL.
;COOKIE_SECURE = false ;COOKIE_SECURE =
;; ;;
;; Session GC time interval in seconds, default is 86400 (1 day) ;; Session GC time interval in seconds, default is 86400 (1 day)
;GC_INTERVAL_TIME = 86400 ;GC_INTERVAL_TIME = 86400

View File

@@ -443,7 +443,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a
- `ITERATE_BUFFER_SIZE`: **50**: Internal buffer size for iterating. - `ITERATE_BUFFER_SIZE`: **50**: Internal buffer size for iterating.
- `CHARSET`: **utf8mb4**: For MySQL only, either "utf8" or "utf8mb4". NOTICE: for "utf8mb4" you must use MySQL InnoDB > 5.6. Gitea is unable to check this. - `CHARSET`: **utf8mb4**: For MySQL only, either "utf8" or "utf8mb4". NOTICE: for "utf8mb4" you must use MySQL InnoDB > 5.6. Gitea is unable to check this.
- `PATH`: **data/gitea.db**: For SQLite3 only, the database file path. - `PATH`: **data/gitea.db**: For SQLite3 only, the database file path.
- `LOG_SQL`: **true**: Log the executed SQL. - `LOG_SQL`: **false**: Log the executed SQL.
- `DB_RETRIES`: **10**: How many ORM init / DB connect attempts allowed. - `DB_RETRIES`: **10**: How many ORM init / DB connect attempts allowed.
- `DB_RETRY_BACKOFF`: **3s**: time.Duration to wait before trying another ORM init / DB connect attempt, if failure occurred. - `DB_RETRY_BACKOFF`: **3s**: time.Duration to wait before trying another ORM init / DB connect attempt, if failure occurred.
- `MAX_OPEN_CONNS` **0**: Database maximum open connections - default is 0, meaning there is no limit. - `MAX_OPEN_CONNS` **0**: Database maximum open connections - default is 0, meaning there is no limit.
@@ -772,7 +772,7 @@ and
- `PROVIDER`: **memory**: Session engine provider \[memory, file, redis, redis-cluster, db, mysql, couchbase, memcache, postgres\]. Setting `db` will reuse the configuration in `[database]` - `PROVIDER`: **memory**: Session engine provider \[memory, file, redis, redis-cluster, db, mysql, couchbase, memcache, postgres\]. Setting `db` will reuse the configuration in `[database]`
- `PROVIDER_CONFIG`: **data/sessions**: For file, the root path; for db, empty (database config will be used); for others, the connection string. Relative paths will be made absolute against _`AppWorkPath`_. - `PROVIDER_CONFIG`: **data/sessions**: For file, the root path; for db, empty (database config will be used); for others, the connection string. Relative paths will be made absolute against _`AppWorkPath`_.
- `COOKIE_SECURE`: **false**: Enable this to force using HTTPS for all session access. - `COOKIE_SECURE`:**_empty_**: `true` or `false`. Enable this to force using HTTPS for all session access. If not set, it defaults to `true` if the ROOT_URL is an HTTPS URL.
- `COOKIE_NAME`: **i\_like\_gitea**: The name of the cookie used for the session ID. - `COOKIE_NAME`: **i\_like\_gitea**: The name of the cookie used for the session ID.
- `GC_INTERVAL_TIME`: **86400**: GC interval in seconds. - `GC_INTERVAL_TIME`: **86400**: GC interval in seconds.
- `SESSION_LIFE_TIME`: **86400**: Session life time in seconds, default is 86400 (1 day) - `SESSION_LIFE_TIME`: **86400**: Session life time in seconds, default is 86400 (1 day)

View File

@@ -98,7 +98,7 @@ menu:
- `SSL_MODE`: MySQL 或 PostgreSQL数据库是否启用SSL模式。 - `SSL_MODE`: MySQL 或 PostgreSQL数据库是否启用SSL模式。
- `CHARSET`: **utf8mb4**: 仅当数据库为 MySQL 时有效, 可以为 "utf8" 或 "utf8mb4"。注意:如果使用 "utf8mb4",你的 MySQL InnoDB 版本必须在 5.6 以上。 - `CHARSET`: **utf8mb4**: 仅当数据库为 MySQL 时有效, 可以为 "utf8" 或 "utf8mb4"。注意:如果使用 "utf8mb4",你的 MySQL InnoDB 版本必须在 5.6 以上。
- `PATH`: SQLite3 数据文件存放路径。 - `PATH`: SQLite3 数据文件存放路径。
- `LOG_SQL`: **true**: 显示生成的SQL默认为真。 - `LOG_SQL`: **false**: 显示生成的SQL默认为真。
- `MAX_IDLE_CONNS` **0**: 最大空闲数据库连接 - `MAX_IDLE_CONNS` **0**: 最大空闲数据库连接
- `CONN_MAX_LIFETIME` **3s**: 数据库连接最大存活时间 - `CONN_MAX_LIFETIME` **3s**: 数据库连接最大存活时间
@@ -200,7 +200,7 @@ menu:
- `PROVIDER`: Session 内容存储方式,可选 `memory`, `file`, `redis``mysql` - `PROVIDER`: Session 内容存储方式,可选 `memory`, `file`, `redis``mysql`
- `PROVIDER_CONFIG`: 如果是文件,那么这里填根目录;其他的要填主机地址和端口。 - `PROVIDER_CONFIG`: 如果是文件,那么这里填根目录;其他的要填主机地址和端口。
- `COOKIE_SECURE`: 强制使用 HTTPS 作为session访问 - `COOKIE_SECURE`: **_empty_**`true``false`。启用此选项以强制在所有会话访问中使用 HTTPS。如果没有设置当 ROOT_URL 是 https 链接的时候默认设置为 true
- `GC_INTERVAL_TIME`: Session失效时间。 - `GC_INTERVAL_TIME`: Session失效时间。
## Picture (`picture`) ## Picture (`picture`)

View File

@@ -17,13 +17,13 @@ menu:
# Database Preparation # Database Preparation
You need a database to use Gitea. Gitea supports PostgreSQL (>=10), MySQL (>=5.7), SQLite, and MSSQL (>=2008R2 SP3). This page will guide into preparing database. Only PostgreSQL and MySQL will be covered here since those database engines are widely-used in production. If you plan to use SQLite, you can ignore this chapter. You need a database to use Gitea. Gitea supports PostgreSQL (>=10), MySQL (>=5.7), MariaDB, SQLite, and MSSQL (>=2008R2 SP3). This page will guide into preparing database. Only PostgreSQL and MySQL will be covered here since those database engines are widely-used in production. If you plan to use SQLite, you can ignore this chapter.
Database instance can be on same machine as Gitea (local database setup), or on different machine (remote database). Database instance can be on same machine as Gitea (local database setup), or on different machine (remote database).
Note: All steps below requires that the database engine of your choice is installed on your system. For remote database setup, install the server application on database instance and client program on your Gitea server. The client program is used to test connection to the database from Gitea server, while Gitea itself use database driver provided by Go to accomplish the same thing. In addition, make sure you use same engine version for both server and client for some engine features to work. For security reason, protect `root` (MySQL) or `postgres` (PostgreSQL) database superuser with secure password. The steps assumes that you run Linux for both database and Gitea servers. Note: All steps below requires that the database engine of your choice is installed on your system. For remote database setup, install the server application on database instance and client program on your Gitea server. The client program is used to test connection to the database from Gitea server, while Gitea itself use database driver provided by Go to accomplish the same thing. In addition, make sure you use same engine version for both server and client for some engine features to work. For security reason, protect `root` (MySQL) or `postgres` (PostgreSQL) database superuser with secure password. The steps assumes that you run Linux for both database and Gitea servers.
## MySQL ## MySQL/MariaDB
1. For remote database setup, you will need to make MySQL listen to your IP address. Edit `bind-address` option on `/etc/mysql/my.cnf` on database instance to: 1. For remote database setup, you will need to make MySQL listen to your IP address. Edit `bind-address` option on `/etc/mysql/my.cnf` on database instance to:
@@ -45,7 +45,7 @@ Note: All steps below requires that the database engine of your choice is instal
```sql ```sql
SET old_passwords=0; SET old_passwords=0;
CREATE USER 'gitea' IDENTIFIED BY 'gitea'; CREATE USER 'gitea'@'%' IDENTIFIED BY 'gitea';
``` ```
For remote database: For remote database:

View File

@@ -180,3 +180,6 @@ For events supported only by GitHub, see GitHub's [documentation](https://docs.g
| pull_request_review_comment | `created`, `edited` | | pull_request_review_comment | `created`, `edited` |
| release | `published`, `edited` | | release | `published`, `edited` |
| registry_package | `published` | | registry_package | `published` |
> For `pull_request` events, in [GitHub Actions](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request), the `ref` is `refs/pull/:prNumber/merge`, which is a reference to the merge commit preview. However, Gitea has no such reference.
> Therefore, the `ref` in Gitea Actions is `refs/pull/:prNumber/head`, which points to the head of pull request rather than the preview of the merge commit.

View File

@@ -180,3 +180,6 @@ defaults:
| pull_request_review_comment | `created`, `edited` | | pull_request_review_comment | `created`, `edited` |
| release | `published`, `edited` | | release | `published`, `edited` |
| registry_package | `published` | | registry_package | `published` |
> 对于 `pull_request` 事件,在 [GitHub Actions](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request) 中 `ref` 是 `refs/pull/:prNumber/merge`,它指向这个拉取请求合并提交的一个预览。但是 Gitea 没有这种 reference。
> 因此Gitea Actions 中 `ref` 是 `refs/pull/:prNumber/head`,它指向这个拉取请求的头分支而不是合并提交的预览。

View File

@@ -342,7 +342,7 @@ func (stats *ActivityStats) FillReleases(repoID int64, fromTime time.Time) error
// Published releases list // Published releases list
sess := releasesForActivityStatement(repoID, fromTime) sess := releasesForActivityStatement(repoID, fromTime)
sess.OrderBy("release.created_unix DESC") sess.OrderBy("`release`.created_unix DESC")
stats.PublishedReleases = make([]*repo_model.Release, 0) stats.PublishedReleases = make([]*repo_model.Release, 0)
if err = sess.Find(&stats.PublishedReleases); err != nil { if err = sess.Find(&stats.PublishedReleases); err != nil {
return err return err
@@ -350,7 +350,7 @@ func (stats *ActivityStats) FillReleases(repoID int64, fromTime time.Time) error
// Published releases authors // Published releases authors
sess = releasesForActivityStatement(repoID, fromTime) sess = releasesForActivityStatement(repoID, fromTime)
if _, err = sess.Select("count(distinct release.publisher_id) as `count`").Table("release").Get(&count); err != nil { if _, err = sess.Select("count(distinct `release`.publisher_id) as `count`").Table("release").Get(&count); err != nil {
return err return err
} }
stats.PublishedReleaseAuthorCount = count stats.PublishedReleaseAuthorCount = count
@@ -359,7 +359,7 @@ func (stats *ActivityStats) FillReleases(repoID int64, fromTime time.Time) error
} }
func releasesForActivityStatement(repoID int64, fromTime time.Time) *xorm.Session { func releasesForActivityStatement(repoID int64, fromTime time.Time) *xorm.Session {
return db.GetEngine(db.DefaultContext).Where("release.repo_id = ?", repoID). return db.GetEngine(db.DefaultContext).Where("`release`.repo_id = ?", repoID).
And("release.is_draft = ?", false). And("`release`.is_draft = ?", false).
And("release.created_unix >= ?", fromTime.Unix()) And("`release`.created_unix >= ?", fromTime.Unix())
} }

View File

@@ -153,7 +153,12 @@ func generateEmailAvatarLink(ctx context.Context, email string, size int, final
return DefaultAvatarLink() return DefaultAvatarLink()
} }
enableFederatedAvatar := system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureEnableFederatedAvatar) disableGravatar := system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar,
setting.GetDefaultDisableGravatar(),
)
enableFederatedAvatar := system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureEnableFederatedAvatar,
setting.GetDefaultEnableFederatedAvatar(disableGravatar))
var err error var err error
if enableFederatedAvatar && system_model.LibravatarService != nil { if enableFederatedAvatar && system_model.LibravatarService != nil {
@@ -174,7 +179,6 @@ func generateEmailAvatarLink(ctx context.Context, email string, size int, final
return urlStr return urlStr
} }
disableGravatar := system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar)
if !disableGravatar { if !disableGravatar {
// copy GravatarSourceURL, because we will modify its Path. // copy GravatarSourceURL, because we will modify its Path.
avatarURLCopy := *system_model.GravatarSourceURL avatarURLCopy := *system_model.GravatarSourceURL

View File

@@ -140,3 +140,16 @@
download_count: 0 download_count: 0
size: 0 size: 0
created_unix: 946684800 created_unix: 946684800
-
id: 12
uuid: a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22
repo_id: 2
issue_id: 0
release_id: 11
uploader_id: 2
comment_id: 0
name: README.md
download_count: 0
size: 0
created_unix: 946684800

View File

@@ -136,3 +136,17 @@
is_prerelease: false is_prerelease: false
is_tag: false is_tag: false
created_unix: 946684803 created_unix: 946684803
- id: 11
repo_id: 2
publisher_id: 2
tag_name: "v1.1"
lower_tag_name: "v1.1"
target: ""
title: "v1.1"
sha1: "205ac761f3326a7ebe416e8673760016450b5cec"
num_commits: 2
is_draft: false
is_prerelease: false
is_tag: false
created_unix: 946684803

View File

@@ -1,6 +1,6 @@
- -
id: 1 id: 1
setting_key: 'disable_gravatar' setting_key: 'picture.disable_gravatar'
setting_value: 'false' setting_value: 'false'
version: 1 version: 1
created: 1653533198 created: 1653533198
@@ -8,7 +8,7 @@
- -
id: 2 id: 2
setting_key: 'enable_federated_avatar' setting_key: 'picture.enable_federated_avatar'
setting_value: 'false' setting_value: 'false'
version: 1 version: 1
created: 1653533198 created: 1653533198

View File

@@ -10,6 +10,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/log"
) )
// CommentList defines a list of comments // CommentList defines a list of comments
@@ -422,37 +423,18 @@ func (comments CommentList) loadReviews(ctx context.Context) error {
reviewIDs := comments.getReviewIDs() reviewIDs := comments.getReviewIDs()
reviews := make(map[int64]*Review, len(reviewIDs)) reviews := make(map[int64]*Review, len(reviewIDs))
left := len(reviewIDs) if err := db.GetEngine(ctx).In("id", reviewIDs).Find(&reviews); err != nil {
for left > 0 { return err
limit := db.DefaultMaxInSize
if left < limit {
limit = left
}
rows, err := db.GetEngine(ctx).
In("id", reviewIDs[:limit]).
Rows(new(Review))
if err != nil {
return err
}
for rows.Next() {
var review Review
err = rows.Scan(&review)
if err != nil {
_ = rows.Close()
return err
}
reviews[review.ID] = &review
}
_ = rows.Close()
left -= limit
reviewIDs = reviewIDs[limit:]
} }
for _, comment := range comments { for _, comment := range comments {
comment.Review = reviews[comment.ReviewID] comment.Review = reviews[comment.ReviewID]
if comment.Review == nil {
if comment.ReviewID > 0 {
log.Error("comment with review id [%d] but has no review record", comment.ReviewID)
}
continue
}
// If the comment dismisses a review, we need to load the reviewer to show whose review has been dismissed. // If the comment dismisses a review, we need to load the reviewer to show whose review has been dismissed.
// Otherwise, the reviewer is the poster of the comment, so we don't need to load it. // Otherwise, the reviewer is the poster of the comment, so we don't need to load it.

View File

@@ -349,14 +349,21 @@ func applyReviewRequestedCondition(sess *xorm.Session, reviewRequestedID int64)
From("team_user"). From("team_user").
Where(builder.Eq{"team_user.uid": reviewRequestedID}) Where(builder.Eq{"team_user.uid": reviewRequestedID})
// if the review is approved or rejected, it should not be shown in the review requested list
maxReview := builder.Select("MAX(r.id)").
From("review as r").
Where(builder.In("r.type", []ReviewType{ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest})).
GroupBy("r.issue_id, r.reviewer_id, r.reviewer_team_id")
subQuery := builder.Select("review.issue_id"). subQuery := builder.Select("review.issue_id").
From("review"). From("review").
Where(builder.And( Where(builder.And(
builder.In("review.type", []ReviewType{ReviewTypeRequest, ReviewTypeReject, ReviewTypeApprove}), builder.Eq{"review.type": ReviewTypeRequest},
builder.Or( builder.Or(
builder.Eq{"review.reviewer_id": reviewRequestedID}, builder.Eq{"review.reviewer_id": reviewRequestedID},
builder.In("review.reviewer_team_id", existInTeamQuery), builder.In("review.reviewer_team_id", existInTeamQuery),
), ),
builder.In("review.id", maxReview),
)) ))
return sess.Where("issue.poster_id <> ?", reviewRequestedID). return sess.Where("issue.poster_id <> ?", reviewRequestedID).
And(builder.In("issue.id", subQuery)) And(builder.In("issue.id", subQuery))

View File

@@ -94,11 +94,14 @@ func GetSetting(ctx context.Context, key string) (*Setting, error) {
const contextCacheKey = "system_setting" const contextCacheKey = "system_setting"
// GetSettingWithCache returns the setting value via the key // GetSettingWithCache returns the setting value via the key
func GetSettingWithCache(ctx context.Context, key string) (string, error) { func GetSettingWithCache(ctx context.Context, key, defaultVal string) (string, error) {
return cache.GetWithContextCache(ctx, contextCacheKey, key, func() (string, error) { return cache.GetWithContextCache(ctx, contextCacheKey, key, func() (string, error) {
return cache.GetString(genSettingCacheKey(key), func() (string, error) { return cache.GetString(genSettingCacheKey(key), func() (string, error) {
res, err := GetSetting(ctx, key) res, err := GetSetting(ctx, key)
if err != nil { if err != nil {
if IsErrSettingIsNotExist(err) {
return defaultVal, nil
}
return "", err return "", err
} }
return res.SettingValue, nil return res.SettingValue, nil
@@ -108,17 +111,21 @@ func GetSettingWithCache(ctx context.Context, key string) (string, error) {
// GetSettingBool return bool value of setting, // GetSettingBool return bool value of setting,
// none existing keys and errors are ignored and result in false // none existing keys and errors are ignored and result in false
func GetSettingBool(ctx context.Context, key string) bool { func GetSettingBool(ctx context.Context, key string, defaultVal bool) (bool, error) {
s, _ := GetSetting(ctx, key) s, err := GetSetting(ctx, key)
if s == nil { switch {
return false case err == nil:
v, _ := strconv.ParseBool(s.SettingValue)
return v, nil
case IsErrSettingIsNotExist(err):
return defaultVal, nil
default:
return false, err
} }
v, _ := strconv.ParseBool(s.SettingValue)
return v
} }
func GetSettingWithCacheBool(ctx context.Context, key string) bool { func GetSettingWithCacheBool(ctx context.Context, key string, defaultVal bool) bool {
s, _ := GetSettingWithCache(ctx, key) s, _ := GetSettingWithCache(ctx, key, strconv.FormatBool(defaultVal))
v, _ := strconv.ParseBool(s) v, _ := strconv.ParseBool(s)
return v return v
} }
@@ -259,52 +266,41 @@ var (
) )
func Init(ctx context.Context) error { func Init(ctx context.Context) error {
var disableGravatar bool disableGravatar, err := GetSettingBool(ctx, KeyPictureDisableGravatar, setting_module.GetDefaultDisableGravatar())
disableGravatarSetting, err := GetSetting(ctx, KeyPictureDisableGravatar) if err != nil {
if IsErrSettingIsNotExist(err) {
disableGravatar = setting_module.GetDefaultDisableGravatar()
disableGravatarSetting = &Setting{SettingValue: strconv.FormatBool(disableGravatar)}
} else if err != nil {
return err return err
} else {
disableGravatar = disableGravatarSetting.GetValueBool()
} }
var enableFederatedAvatar bool enableFederatedAvatar, err := GetSettingBool(ctx, KeyPictureEnableFederatedAvatar, setting_module.GetDefaultEnableFederatedAvatar(disableGravatar))
enableFederatedAvatarSetting, err := GetSetting(ctx, KeyPictureEnableFederatedAvatar) if err != nil {
if IsErrSettingIsNotExist(err) {
enableFederatedAvatar = setting_module.GetDefaultEnableFederatedAvatar(disableGravatar)
enableFederatedAvatarSetting = &Setting{SettingValue: strconv.FormatBool(enableFederatedAvatar)}
} else if err != nil {
return err return err
} else {
enableFederatedAvatar = disableGravatarSetting.GetValueBool()
} }
if setting_module.OfflineMode { if setting_module.OfflineMode {
disableGravatar = true if !disableGravatar {
enableFederatedAvatar = false
if !GetSettingBool(ctx, KeyPictureDisableGravatar) {
if err := SetSettingNoVersion(ctx, KeyPictureDisableGravatar, "true"); err != nil { if err := SetSettingNoVersion(ctx, KeyPictureDisableGravatar, "true"); err != nil {
return fmt.Errorf("Failed to set setting %q: %w", KeyPictureDisableGravatar, err) return fmt.Errorf("failed to set setting %q: %w", KeyPictureDisableGravatar, err)
} }
} }
if GetSettingBool(ctx, KeyPictureEnableFederatedAvatar) { disableGravatar = true
if enableFederatedAvatar {
if err := SetSettingNoVersion(ctx, KeyPictureEnableFederatedAvatar, "false"); err != nil { if err := SetSettingNoVersion(ctx, KeyPictureEnableFederatedAvatar, "false"); err != nil {
return fmt.Errorf("Failed to set setting %q: %w", KeyPictureEnableFederatedAvatar, err) return fmt.Errorf("failed to set setting %q: %w", KeyPictureEnableFederatedAvatar, err)
} }
} }
enableFederatedAvatar = false
} }
if enableFederatedAvatar || !disableGravatar { if enableFederatedAvatar || !disableGravatar {
var err error var err error
GravatarSourceURL, err = url.Parse(setting_module.GravatarSource) GravatarSourceURL, err = url.Parse(setting_module.GravatarSource)
if err != nil { if err != nil {
return fmt.Errorf("Failed to parse Gravatar URL(%s): %w", setting_module.GravatarSource, err) return fmt.Errorf("failed to parse Gravatar URL(%s): %w", setting_module.GravatarSource, err)
} }
} }
if GravatarSourceURL != nil && enableFederatedAvatarSetting.GetValueBool() { if GravatarSourceURL != nil && enableFederatedAvatar {
LibravatarService = libravatar.New() LibravatarService = libravatar.New()
if GravatarSourceURL.Scheme == "https" { if GravatarSourceURL.Scheme == "https" {
LibravatarService.SetUseHTTPS(true) LibravatarService.SetUseHTTPS(true)

View File

@@ -67,7 +67,9 @@ func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string {
useLocalAvatar := false useLocalAvatar := false
autoGenerateAvatar := false autoGenerateAvatar := false
disableGravatar := system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar) disableGravatar := system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar,
setting.GetDefaultDisableGravatar(),
)
switch { switch {
case u.UseCustomAvatar: case u.UseCustomAvatar:

View File

@@ -101,7 +101,7 @@ func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) er
}, },
// find releases without existing repository // find releases without existing repository
genericOrphanCheck("Orphaned Releases without existing repository", genericOrphanCheck("Orphaned Releases without existing repository",
"release", "repository", "release.repo_id=repository.id"), "release", "repository", "`release`.repo_id=repository.id"),
// find pulls without existing issues // find pulls without existing issues
genericOrphanCheck("Orphaned PullRequests without existing issue", genericOrphanCheck("Orphaned PullRequests without existing issue",
"pull_request", "issue", "pull_request.issue_id=issue.id"), "pull_request", "issue", "pull_request.issue_id=issue.id"),
@@ -168,9 +168,9 @@ func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) er
// find protected branches without existing repository // find protected branches without existing repository
genericOrphanCheck("Protected Branches without existing repository", genericOrphanCheck("Protected Branches without existing repository",
"protected_branch", "repository", "protected_branch.repo_id=repository.id"), "protected_branch", "repository", "protected_branch.repo_id=repository.id"),
// find deleted branches without existing repository // find branches without existing repository
genericOrphanCheck("Deleted Branches without existing repository", genericOrphanCheck("Branches without existing repository",
"deleted_branch", "repository", "deleted_branch.repo_id=repository.id"), "branch", "repository", "branch.repo_id=repository.id"),
// find LFS locks without existing repository // find LFS locks without existing repository
genericOrphanCheck("LFS locks without existing repository", genericOrphanCheck("LFS locks without existing repository",
"lfs_lock", "repository", "lfs_lock.repo_id=repository.id"), "lfs_lock", "repository", "lfs_lock.repo_id=repository.id"),

View File

@@ -86,7 +86,8 @@ func (repo *Repository) IsEmpty() (bool, error) {
Stdout: &output, Stdout: &output,
Stderr: &errbuf, Stderr: &errbuf,
}); err != nil { }); err != nil {
if err.Error() == "exit status 1" && errbuf.String() == "" { if (err.Error() == "exit status 1" && strings.TrimSpace(errbuf.String()) == "") || err.Error() == "exit status 129" {
// git 2.11 exits with 129 if the repo is empty
return true, nil return true, nil
} }
return true, fmt.Errorf("check empty: %w - %s", err, errbuf.String()) return true, fmt.Errorf("check empty: %w - %s", err, errbuf.String())

View File

@@ -177,6 +177,9 @@ func (m *mailNotifier) NotifyPullRequestPushCommits(ctx context.Context, doer *u
} }
func (m *mailNotifier) NotifyPullReviewDismiss(ctx context.Context, doer *user_model.User, review *issues_model.Review, comment *issues_model.Comment) { func (m *mailNotifier) NotifyPullReviewDismiss(ctx context.Context, doer *user_model.User, review *issues_model.Review, comment *issues_model.Comment) {
if err := comment.Review.LoadReviewer(ctx); err != nil {
log.Error("Error in PullReviewDismiss while loading reviewer for issue[%d], review[%d] and reviewer[%d]: %v", review.Issue.ID, comment.Review.ID, comment.Review.ReviewerID, err)
}
if err := mailer.MailParticipantsComment(ctx, comment, activities_model.ActionPullReviewDismissed, review.Issue, nil); err != nil { if err := mailer.MailParticipantsComment(ctx, comment, activities_model.ActionPullReviewDismissed, review.Issue, nil); err != nil {
log.Error("MailParticipantsComment: %v", err) log.Error("MailParticipantsComment: %v", err)
} }

View File

@@ -11,6 +11,7 @@ import (
"code.gitea.io/gitea/models/avatars" "code.gitea.io/gitea/models/avatars"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
@@ -34,42 +35,36 @@ type PushCommits struct {
HeadCommit *PushCommit HeadCommit *PushCommit
CompareURL string CompareURL string
Len int Len int
avatars map[string]string
emailUsers map[string]*user_model.User
} }
// NewPushCommits creates a new PushCommits object. // NewPushCommits creates a new PushCommits object.
func NewPushCommits() *PushCommits { func NewPushCommits() *PushCommits {
return &PushCommits{ return &PushCommits{}
avatars: make(map[string]string),
emailUsers: make(map[string]*user_model.User),
}
} }
// toAPIPayloadCommit converts a single PushCommit to an api.PayloadCommit object. // toAPIPayloadCommit converts a single PushCommit to an api.PayloadCommit object.
func (pc *PushCommits) toAPIPayloadCommit(ctx context.Context, repoPath, repoLink string, commit *PushCommit) (*api.PayloadCommit, error) { func (pc *PushCommits) toAPIPayloadCommit(ctx context.Context, emailUsers map[string]*user_model.User, repoPath, repoLink string, commit *PushCommit) (*api.PayloadCommit, error) {
var err error var err error
authorUsername := "" authorUsername := ""
author, ok := pc.emailUsers[commit.AuthorEmail] author, ok := emailUsers[commit.AuthorEmail]
if !ok { if !ok {
author, err = user_model.GetUserByEmail(ctx, commit.AuthorEmail) author, err = user_model.GetUserByEmail(ctx, commit.AuthorEmail)
if err == nil { if err == nil {
authorUsername = author.Name authorUsername = author.Name
pc.emailUsers[commit.AuthorEmail] = author emailUsers[commit.AuthorEmail] = author
} }
} else { } else {
authorUsername = author.Name authorUsername = author.Name
} }
committerUsername := "" committerUsername := ""
committer, ok := pc.emailUsers[commit.CommitterEmail] committer, ok := emailUsers[commit.CommitterEmail]
if !ok { if !ok {
committer, err = user_model.GetUserByEmail(ctx, commit.CommitterEmail) committer, err = user_model.GetUserByEmail(ctx, commit.CommitterEmail)
if err == nil { if err == nil {
// TODO: check errors other than email not found. // TODO: check errors other than email not found.
committerUsername = committer.Name committerUsername = committer.Name
pc.emailUsers[commit.CommitterEmail] = committer emailUsers[commit.CommitterEmail] = committer
} }
} else { } else {
committerUsername = committer.Name committerUsername = committer.Name
@@ -107,11 +102,10 @@ func (pc *PushCommits) ToAPIPayloadCommits(ctx context.Context, repoPath, repoLi
commits := make([]*api.PayloadCommit, len(pc.Commits)) commits := make([]*api.PayloadCommit, len(pc.Commits))
var headCommit *api.PayloadCommit var headCommit *api.PayloadCommit
if pc.emailUsers == nil { emailUsers := make(map[string]*user_model.User)
pc.emailUsers = make(map[string]*user_model.User)
}
for i, commit := range pc.Commits { for i, commit := range pc.Commits {
apiCommit, err := pc.toAPIPayloadCommit(ctx, repoPath, repoLink, commit) apiCommit, err := pc.toAPIPayloadCommit(ctx, emailUsers, repoPath, repoLink, commit)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -123,7 +117,7 @@ func (pc *PushCommits) ToAPIPayloadCommits(ctx context.Context, repoPath, repoLi
} }
if pc.HeadCommit != nil && headCommit == nil { if pc.HeadCommit != nil && headCommit == nil {
var err error var err error
headCommit, err = pc.toAPIPayloadCommit(ctx, repoPath, repoLink, pc.HeadCommit) headCommit, err = pc.toAPIPayloadCommit(ctx, emailUsers, repoPath, repoLink, pc.HeadCommit)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -134,35 +128,21 @@ func (pc *PushCommits) ToAPIPayloadCommits(ctx context.Context, repoPath, repoLi
// AvatarLink tries to match user in database with e-mail // AvatarLink tries to match user in database with e-mail
// in order to show custom avatar, and falls back to general avatar link. // in order to show custom avatar, and falls back to general avatar link.
func (pc *PushCommits) AvatarLink(ctx context.Context, email string) string { func (pc *PushCommits) AvatarLink(ctx context.Context, email string) string {
if pc.avatars == nil {
pc.avatars = make(map[string]string)
}
avatar, ok := pc.avatars[email]
if ok {
return avatar
}
size := avatars.DefaultAvatarPixelSize * setting.Avatar.RenderedSizeFactor size := avatars.DefaultAvatarPixelSize * setting.Avatar.RenderedSizeFactor
u, ok := pc.emailUsers[email] v, _ := cache.GetWithContextCache(ctx, "push_commits", email, func() (string, error) {
if !ok { u, err := user_model.GetUserByEmail(ctx, email)
var err error
u, err = user_model.GetUserByEmail(ctx, email)
if err != nil { if err != nil {
pc.avatars[email] = avatars.GenerateEmailAvatarFastLink(ctx, email, size)
if !user_model.IsErrUserNotExist(err) { if !user_model.IsErrUserNotExist(err) {
log.Error("GetUserByEmail: %v", err) log.Error("GetUserByEmail: %v", err)
return "" return "", err
} }
} else { return avatars.GenerateEmailAvatarFastLink(ctx, email, size), nil
pc.emailUsers[email] = u
} }
} return u.AvatarLinkWithSize(ctx, size), nil
if u != nil { })
pc.avatars[email] = u.AvatarLinkWithSize(ctx, size)
}
return pc.avatars[email] return v
} }
// CommitToPushCommit transforms a git.Commit to PushCommit type. // CommitToPushCommit transforms a git.Commit to PushCommit type.
@@ -189,7 +169,5 @@ func GitToPushCommits(gitCommits []*git.Commit) *PushCommits {
HeadCommit: nil, HeadCommit: nil,
CompareURL: "", CompareURL: "",
Len: len(commits), Len: len(commits),
avatars: make(map[string]string),
emailUsers: make(map[string]*user_model.User),
} }
} }

View File

@@ -103,11 +103,9 @@ func TestPushCommits_ToAPIPayloadCommits(t *testing.T) {
assert.EqualValues(t, []string{"readme.md"}, headCommit.Modified) assert.EqualValues(t, []string{"readme.md"}, headCommit.Modified)
} }
func enableGravatar(t *testing.T) { func initGravatarSource(t *testing.T) {
err := system_model.SetSettingNoVersion(db.DefaultContext, system_model.KeyPictureDisableGravatar, "false")
assert.NoError(t, err)
setting.GravatarSource = "https://secure.gravatar.com/avatar" setting.GravatarSource = "https://secure.gravatar.com/avatar"
err = system_model.Init(db.DefaultContext) err := system_model.Init(db.DefaultContext)
assert.NoError(t, err) assert.NoError(t, err)
} }
@@ -134,7 +132,7 @@ func TestPushCommits_AvatarLink(t *testing.T) {
}, },
} }
enableGravatar(t) initGravatarSource(t)
assert.Equal(t, assert.Equal(t,
"https://secure.gravatar.com/avatar/ab53a2911ddf9b4817ac01ddcd3d975f?d=identicon&s="+strconv.Itoa(28*setting.Avatar.RenderedSizeFactor), "https://secure.gravatar.com/avatar/ab53a2911ddf9b4817ac01ddcd3d975f?d=identicon&s="+strconv.Itoa(28*setting.Avatar.RenderedSizeFactor),

View File

@@ -174,9 +174,16 @@ func (s *iniConfigSection) ChildSections() (sections []ConfigSection) {
return sections return sections
} }
func configProviderLoadOptions() ini.LoadOptions {
return ini.LoadOptions{
KeyValueDelimiterOnWrite: " = ",
IgnoreContinuation: true,
}
}
// NewConfigProviderFromData this function is mainly for testing purpose // NewConfigProviderFromData this function is mainly for testing purpose
func NewConfigProviderFromData(configContent string) (ConfigProvider, error) { func NewConfigProviderFromData(configContent string) (ConfigProvider, error) {
cfg, err := ini.Load(strings.NewReader(configContent)) cfg, err := ini.LoadSources(configProviderLoadOptions(), strings.NewReader(configContent))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -190,7 +197,7 @@ func NewConfigProviderFromData(configContent string) (ConfigProvider, error) {
// NewConfigProviderFromFile load configuration from file. // NewConfigProviderFromFile load configuration from file.
// NOTE: do not print any log except error. // NOTE: do not print any log except error.
func NewConfigProviderFromFile(file string, extraConfigs ...string) (ConfigProvider, error) { func NewConfigProviderFromFile(file string, extraConfigs ...string) (ConfigProvider, error) {
cfg := ini.Empty(ini.LoadOptions{KeyValueDelimiterOnWrite: " = "}) cfg := ini.Empty(configProviderLoadOptions())
loadedFromEmpty := true loadedFromEmpty := true
if file != "" { if file != "" {
@@ -339,6 +346,7 @@ func NewConfigProviderForLocale(source any, others ...any) (ConfigProvider, erro
iniFile, err := ini.LoadSources(ini.LoadOptions{ iniFile, err := ini.LoadSources(ini.LoadOptions{
IgnoreInlineComment: true, IgnoreInlineComment: true,
UnescapeValueCommentSymbols: true, UnescapeValueCommentSymbols: true,
IgnoreContinuation: true,
}, source, others...) }, source, others...)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to load locale ini: %w", err) return nil, fmt.Errorf("unable to load locale ini: %w", err)

View File

@@ -30,6 +30,16 @@ key = 123
secSub := cfg.Section("foo.bar.xxx") secSub := cfg.Section("foo.bar.xxx")
assert.Equal(t, "123", secSub.Key("key").String()) assert.Equal(t, "123", secSub.Key("key").String())
}) })
t.Run("TrailingSlash", func(t *testing.T) {
cfg, _ := NewConfigProviderFromData(`
[foo]
key = E:\
xxx = yyy
`)
sec := cfg.Section("foo")
assert.Equal(t, "E:\\", sec.Key("key").String())
assert.Equal(t, "yyy", sec.Key("xxx").String())
})
} }
func TestConfigProviderHelper(t *testing.T) { func TestConfigProviderHelper(t *testing.T) {

View File

@@ -50,7 +50,7 @@ func loadSessionFrom(rootCfg ConfigProvider) {
} }
SessionConfig.CookieName = sec.Key("COOKIE_NAME").MustString("i_like_gitea") SessionConfig.CookieName = sec.Key("COOKIE_NAME").MustString("i_like_gitea")
SessionConfig.CookiePath = AppSubURL + "/" // there was a bug, old code only set CookePath=AppSubURL, no trailing slash SessionConfig.CookiePath = AppSubURL + "/" // there was a bug, old code only set CookePath=AppSubURL, no trailing slash
SessionConfig.Secure = sec.Key("COOKIE_SECURE").MustBool(false) SessionConfig.Secure = sec.Key("COOKIE_SECURE").MustBool(strings.HasPrefix(strings.ToLower(AppURL), "https://"))
SessionConfig.Gclifetime = sec.Key("GC_INTERVAL_TIME").MustInt64(86400) SessionConfig.Gclifetime = sec.Key("GC_INTERVAL_TIME").MustInt64(86400)
SessionConfig.Maxlifetime = sec.Key("SESSION_LIFE_TIME").MustInt64(86400) SessionConfig.Maxlifetime = sec.Key("SESSION_LIFE_TIME").MustInt64(86400)
SessionConfig.Domain = sec.Key("DOMAIN").String() SessionConfig.Domain = sec.Key("DOMAIN").String()

View File

@@ -63,6 +63,7 @@ type Repository struct {
Language string `json:"language"` Language string `json:"language"`
LanguagesURL string `json:"languages_url"` LanguagesURL string `json:"languages_url"`
HTMLURL string `json:"html_url"` HTMLURL string `json:"html_url"`
URL string `json:"url"`
Link string `json:"link"` Link string `json:"link"`
SSHURL string `json:"ssh_url"` SSHURL string `json:"ssh_url"`
CloneURL string `json:"clone_url"` CloneURL string `json:"clone_url"`

View File

@@ -104,7 +104,7 @@ func NewFuncMap() template.FuncMap {
return setting.AssetVersion return setting.AssetVersion
}, },
"DisableGravatar": func(ctx context.Context) bool { "DisableGravatar": func(ctx context.Context) bool {
return system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar) return system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar, setting.GetDefaultDisableGravatar())
}, },
"DefaultShowFullName": func() bool { "DefaultShowFullName": func() bool {
return setting.UI.DefaultShowFullName return setting.UI.DefaultShowFullName

View File

@@ -241,7 +241,7 @@ func CreateTeam(ctx *context.APIContext) {
return return
} }
apiTeam, err := convert.ToTeam(ctx, team) apiTeam, err := convert.ToTeam(ctx, team, true)
if err != nil { if err != nil {
ctx.InternalServerError(err) ctx.InternalServerError(err)
return return

View File

@@ -785,7 +785,7 @@ func CompareDiff(ctx *context.Context) {
ctx.Data["IsRepoToolbarCommits"] = true ctx.Data["IsRepoToolbarCommits"] = true
ctx.Data["IsDiffCompare"] = true ctx.Data["IsDiffCompare"] = true
templateErrs := setTemplateIfExists(ctx, pullRequestTemplateKey, pullRequestTemplateCandidates) _, templateErrs := setTemplateIfExists(ctx, pullRequestTemplateKey, pullRequestTemplateCandidates)
if len(templateErrs) > 0 { if len(templateErrs) > 0 {
ctx.Flash.Warning(renderErrorOfTemplates(ctx, templateErrs), true) ctx.Flash.Warning(renderErrorOfTemplates(ctx, templateErrs), true)

View File

@@ -804,10 +804,11 @@ func RetrieveRepoMetas(ctx *context.Context, repo *repo_model.Repository, isPull
return labels return labels
} }
func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles []string) map[string]error { // Tries to load and set an issue template. The first return value indicates if a template was loaded.
func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles []string) (bool, map[string]error) {
commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
if err != nil { if err != nil {
return nil return false, nil
} }
templateCandidates := make([]string, 0, 1+len(possibleFiles)) templateCandidates := make([]string, 0, 1+len(possibleFiles))
@@ -870,20 +871,15 @@ func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles
ctx.Data["label_ids"] = strings.Join(labelIDs, ",") ctx.Data["label_ids"] = strings.Join(labelIDs, ",")
ctx.Data["Reference"] = template.Ref ctx.Data["Reference"] = template.Ref
ctx.Data["RefEndName"] = git.RefName(template.Ref).ShortName() ctx.Data["RefEndName"] = git.RefName(template.Ref).ShortName()
return templateErrs return true, templateErrs
} }
return templateErrs return false, templateErrs
} }
// NewIssue render creating issue page // NewIssue render creating issue page
func NewIssue(ctx *context.Context) { func NewIssue(ctx *context.Context) {
issueConfig, _ := issue_service.GetTemplateConfigFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo) issueConfig, _ := issue_service.GetTemplateConfigFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
hasTemplates := issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo) hasTemplates := issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo)
if !issueConfig.BlankIssuesEnabled && hasTemplates {
// The "issues/new" and "issues/new/choose" share the same query parameters "project" and "milestone", if blank issues are disabled, just redirect to the "issues/choose" page with these parameters.
ctx.Redirect(fmt.Sprintf("%s/issues/new/choose?%s", ctx.Repo.Repository.Link(), ctx.Req.URL.RawQuery), http.StatusSeeOther)
return
}
ctx.Data["Title"] = ctx.Tr("repo.issues.new") ctx.Data["Title"] = ctx.Tr("repo.issues.new")
ctx.Data["PageIsIssueList"] = true ctx.Data["PageIsIssueList"] = true
@@ -930,7 +926,8 @@ func NewIssue(ctx *context.Context) {
RetrieveRepoMetas(ctx, ctx.Repo.Repository, false) RetrieveRepoMetas(ctx, ctx.Repo.Repository, false)
_, templateErrs := issue_service.GetTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo) _, templateErrs := issue_service.GetTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
if errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates); len(errs) > 0 { templateLoaded, errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates)
if len(errs) > 0 {
for k, v := range errs { for k, v := range errs {
templateErrs[k] = v templateErrs[k] = v
} }
@@ -945,6 +942,12 @@ func NewIssue(ctx *context.Context) {
ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWrite(unit.TypeIssues) ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWrite(unit.TypeIssues)
if !issueConfig.BlankIssuesEnabled && hasTemplates && !templateLoaded {
// The "issues/new" and "issues/new/choose" share the same query parameters "project" and "milestone", if blank issues are disabled, just redirect to the "issues/choose" page with these parameters.
ctx.Redirect(fmt.Sprintf("%s/issues/new/choose?%s", ctx.Repo.Repository.Link(), ctx.Req.URL.RawQuery), http.StatusSeeOther)
return
}
ctx.HTML(http.StatusOK, tplIssueNew) ctx.HTML(http.StatusOK, tplIssueNew)
} }

View File

@@ -223,10 +223,10 @@ func Profile(ctx *context.Context) {
switch tab { switch tab {
case "followers": case "followers":
ctx.Data["Cards"] = followers ctx.Data["Cards"] = followers
total = int(count) total = int(numFollowers)
case "following": case "following":
ctx.Data["Cards"] = following ctx.Data["Cards"] = following
total = int(count) total = int(numFollowing)
case "activity": case "activity":
date := ctx.FormString("date") date := ctx.FormString("date")
items, count, err := activities_model.GetFeeds(ctx, activities_model.GetFeedsOptions{ items, count, err := activities_model.GetFeeds(ctx, activities_model.GetFeedsOptions{

View File

@@ -863,9 +863,6 @@ func registerRoutes(m *web.Route) {
}, reqUnitAccess(unit.TypeCode, perm.AccessModeRead, false)) }, reqUnitAccess(unit.TypeCode, perm.AccessModeRead, false))
}, ignSignIn, context_service.UserAssignmentWeb(), context.OrgAssignment()) // for "/{username}/-" (packages, projects, code) }, ignSignIn, context_service.UserAssignmentWeb(), context.OrgAssignment()) // for "/{username}/-" (packages, projects, code)
// ***** Release Attachment Download without Signin
m.Get("/{username}/{reponame}/releases/download/{vTag}/{fileName}", ignSignIn, context.RepoAssignment, repo.MustBeNotEmpty, repo.RedirectDownload)
m.Group("/{username}/{reponame}", func() { m.Group("/{username}/{reponame}", func() {
m.Group("/settings", func() { m.Group("/settings", func() {
m.Group("", func() { m.Group("", func() {
@@ -1118,8 +1115,9 @@ func registerRoutes(m *web.Route) {
m.Get(".rss", feedEnabled, repo.ReleasesFeedRSS) m.Get(".rss", feedEnabled, repo.ReleasesFeedRSS)
m.Get(".atom", feedEnabled, repo.ReleasesFeedAtom) m.Get(".atom", feedEnabled, repo.ReleasesFeedAtom)
}, ctxDataSet("EnableFeed", setting.Other.EnableFeed), }, ctxDataSet("EnableFeed", setting.Other.EnableFeed),
repo.MustBeNotEmpty, reqRepoReleaseReader, context.RepoRefByType(context.RepoRefTag, true)) repo.MustBeNotEmpty, context.RepoRefByType(context.RepoRefTag, true))
m.Get("/releases/attachments/{uuid}", repo.MustBeNotEmpty, reqRepoReleaseReader, repo.GetAttachment) m.Get("/releases/attachments/{uuid}", repo.MustBeNotEmpty, repo.GetAttachment)
m.Get("/releases/download/{vTag}/{fileName}", repo.MustBeNotEmpty, repo.RedirectDownload)
m.Group("/releases", func() { m.Group("/releases", func() {
m.Get("/new", repo.NewRelease) m.Get("/new", repo.NewRelease)
m.Post("/new", web.Bind(forms.NewReleaseForm{}), repo.NewReleasePost) m.Post("/new", web.Bind(forms.NewReleaseForm{}), repo.NewReleasePost)

View File

@@ -126,7 +126,9 @@ func (o *OAuth2) userIDFromToken(tokenSHA string, store DataStore) int64 {
// If verification is successful returns an existing user object. // If verification is successful returns an existing user object.
// Returns nil if verification fails. // Returns nil if verification fails.
func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) { func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) {
if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isAuthenticatedTokenRequest(req) { // These paths are not API paths, but we still want to check for tokens because they maybe in the API returned URLs
if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isAuthenticatedTokenRequest(req) &&
!gitRawReleasePathRe.MatchString(req.URL.Path) {
return nil, nil return nil, nil
} }

View File

@@ -181,6 +181,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
Parent: parent, Parent: parent,
Mirror: repo.IsMirror, Mirror: repo.IsMirror,
HTMLURL: repo.HTMLURL(), HTMLURL: repo.HTMLURL(),
URL: repoAPIURL,
SSHURL: cloneLink.SSH, SSHURL: cloneLink.SSH,
CloneURL: cloneLink.HTTPS, CloneURL: cloneLink.HTTPS,
OriginalURL: repo.SanitizedOriginalURL(), OriginalURL: repo.SanitizedOriginalURL(),

View File

@@ -1312,7 +1312,7 @@ outer:
} }
} }
return diff, err return diff, nil
} }
// CommentAsDiff returns c.Patch as *Diff // CommentAsDiff returns c.Patch as *Diff

View File

@@ -170,7 +170,7 @@ func (d *DingtalkPayload) Repository(p *api.RepositoryPayload) (api.Payloader, e
func (d *DingtalkPayload) Release(p *api.ReleasePayload) (api.Payloader, error) { func (d *DingtalkPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
text, _ := getReleasePayloadInfo(p, noneLinkFormatter, true) text, _ := getReleasePayloadInfo(p, noneLinkFormatter, true)
return createDingtalkPayload(text, text, "view release", p.Release.URL), nil return createDingtalkPayload(text, text, "view release", p.Release.HTMLURL), nil
} }
func createDingtalkPayload(title, text, singleTitle, singleURL string) *DingtalkPayload { func createDingtalkPayload(title, text, singleTitle, singleURL string) *DingtalkPayload {

View File

@@ -238,7 +238,7 @@ func TestDingTalkPayload(t *testing.T) {
assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.(*DingtalkPayload).ActionCard.Text) assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.(*DingtalkPayload).ActionCard.Text)
assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.(*DingtalkPayload).ActionCard.Title) assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.(*DingtalkPayload).ActionCard.Title)
assert.Equal(t, "view release", pl.(*DingtalkPayload).ActionCard.SingleTitle) assert.Equal(t, "view release", pl.(*DingtalkPayload).ActionCard.SingleTitle)
assert.Equal(t, "http://localhost:3000/api/v1/repos/test/repo/releases/2", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL)) assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
}) })
} }

View File

@@ -253,7 +253,7 @@ func (d *DiscordPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
func (d *DiscordPayload) Release(p *api.ReleasePayload) (api.Payloader, error) { func (d *DiscordPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
text, color := getReleasePayloadInfo(p, noneLinkFormatter, false) text, color := getReleasePayloadInfo(p, noneLinkFormatter, false)
return d.createPayload(p.Sender, text, p.Release.Note, p.Release.URL, color), nil return d.createPayload(p.Sender, text, p.Release.Note, p.Release.HTMLURL, color), nil
} }
// GetDiscordPayload converts a discord webhook into a DiscordPayload // GetDiscordPayload converts a discord webhook into a DiscordPayload

View File

@@ -270,7 +270,7 @@ func TestDiscordPayload(t *testing.T) {
assert.Len(t, pl.(*DiscordPayload).Embeds, 1) assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
assert.Equal(t, "[test/repo] Release created: v1.0", pl.(*DiscordPayload).Embeds[0].Title) assert.Equal(t, "[test/repo] Release created: v1.0", pl.(*DiscordPayload).Embeds[0].Title)
assert.Equal(t, "Note of first stable release", pl.(*DiscordPayload).Embeds[0].Description) assert.Equal(t, "Note of first stable release", pl.(*DiscordPayload).Embeds[0].Description)
assert.Equal(t, "http://localhost:3000/api/v1/repos/test/repo/releases/2", pl.(*DiscordPayload).Embeds[0].URL) assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", pl.(*DiscordPayload).Embeds[0].URL)
assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name) assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL) assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL) assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)

View File

@@ -228,7 +228,7 @@ func pullReleaseTestPayload() *api.ReleasePayload {
Target: "master", Target: "master",
Title: "First stable release", Title: "First stable release",
Note: "Note of first stable release", Note: "Note of first stable release",
URL: "http://localhost:3000/api/v1/repos/test/repo/releases/2", HTMLURL: "http://localhost:3000/test/repo/releases/tag/v1.0",
}, },
} }
} }

View File

@@ -177,7 +177,7 @@ func (m *MatrixPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, e
func (m *MatrixPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) { func (m *MatrixPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
senderLink := MatrixLinkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName) senderLink := MatrixLinkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)
title := fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title) title := fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title)
titleLink := MatrixLinkFormatter(p.PullRequest.URL, title) titleLink := MatrixLinkFormatter(p.PullRequest.HTMLURL, title)
repoLink := MatrixLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName) repoLink := MatrixLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
var text string var text string

View File

@@ -290,7 +290,7 @@ func (m *MSTeamsPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
p.Sender, p.Sender,
title, title,
"", "",
p.Release.URL, p.Release.HTMLURL,
color, color,
&MSTeamsFact{"Tag:", p.Release.TagName}, &MSTeamsFact{"Tag:", p.Release.TagName},
), nil ), nil

View File

@@ -429,7 +429,7 @@ func TestMSTeamsPayload(t *testing.T) {
} }
assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
assert.Equal(t, "http://localhost:3000/api/v1/repos/test/repo/releases/2", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
}) })
} }

View File

@@ -223,7 +223,7 @@ func (s *SlackPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, er
attachments = append(attachments, SlackAttachment{ attachments = append(attachments, SlackAttachment{
Color: fmt.Sprintf("%x", color), Color: fmt.Sprintf("%x", color),
Title: issueTitle, Title: issueTitle,
TitleLink: p.PullRequest.URL, TitleLink: p.PullRequest.HTMLURL,
Text: attachmentText, Text: attachmentText,
}) })
} }

View File

@@ -249,8 +249,8 @@ func TestPrepareWikiFileName(t *testing.T) {
unittest.PrepareTestEnv(t) unittest.PrepareTestEnv(t)
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
gitRepo, err := git.OpenRepository(git.DefaultContext, repo.WikiPath()) gitRepo, err := git.OpenRepository(git.DefaultContext, repo.WikiPath())
defer gitRepo.Close()
assert.NoError(t, err) assert.NoError(t, err)
defer gitRepo.Close()
tests := []struct { tests := []struct {
name string name string
@@ -301,8 +301,8 @@ func TestPrepareWikiFileName_FirstPage(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
gitRepo, err := git.OpenRepository(git.DefaultContext, tmpDir) gitRepo, err := git.OpenRepository(git.DefaultContext, tmpDir)
defer gitRepo.Close()
assert.NoError(t, err) assert.NoError(t, err)
defer gitRepo.Close()
existence, newWikiPath, err := prepareGitPath(gitRepo, "Home") existence, newWikiPath, err := prepareGitPath(gitRepo, "Home")
assert.False(t, existence) assert.False(t, existence)

View File

@@ -21038,6 +21038,10 @@
"format": "date-time", "format": "date-time",
"x-go-name": "Updated" "x-go-name": "Updated"
}, },
"url": {
"type": "string",
"x-go-name": "URL"
},
"watchers_count": { "watchers_count": {
"type": "integer", "type": "integer",
"format": "int64", "format": "int64",

View File

@@ -0,0 +1 @@
1032bbf17fbc0d9c95bb5418dabe8f8c99278700

View File

@@ -239,3 +239,20 @@ func TestViewTagsList(t *testing.T) {
assert.EqualValues(t, []string{"v1.0", "delete-tag", "v1.1"}, tagNames) assert.EqualValues(t, []string{"v1.0", "delete-tag", "v1.1"}, tagNames)
} }
func TestDownloadReleaseAttachment(t *testing.T) {
defer tests.PrepareTestEnv(t)()
tests.PrepareAttachmentsStorage(t)
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
url := repo.Link() + "/releases/download/v1.1/README.md"
req := NewRequest(t, "GET", url)
MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "GET", url)
session := loginUser(t, "user2")
session.MakeRequest(t, req, http.StatusOK)
}

View File

@@ -176,6 +176,20 @@ func InitTest(requireGitea bool) {
routers.InitWebInstalled(graceful.GetManager().HammerContext()) routers.InitWebInstalled(graceful.GetManager().HammerContext())
} }
func PrepareAttachmentsStorage(t testing.TB) {
// prepare attachments directory and files
assert.NoError(t, storage.Clean(storage.Attachments))
s, err := storage.NewStorage(setting.LocalStorageType, &setting.Storage{
Path: filepath.Join(filepath.Dir(setting.AppPath), "tests", "testdata", "data", "attachments"),
})
assert.NoError(t, err)
assert.NoError(t, s.IterateObjects("", func(p string, obj storage.Object) error {
_, err = storage.Copy(storage.Attachments, p, s, p)
return err
}))
}
func PrepareTestEnv(t testing.TB, skip ...int) func() { func PrepareTestEnv(t testing.TB, skip ...int) func() {
t.Helper() t.Helper()
ourSkip := 2 ourSkip := 2

View File

@@ -0,0 +1 @@
# This is a release README

View File

@@ -86,6 +86,7 @@ text-expander .suggestions {
border-radius: 5px; border-radius: 5px;
border: 1px solid var(--color-secondary); border: 1px solid var(--color-secondary);
box-shadow: 0 .5rem 1rem var(--color-shadow); box-shadow: 0 .5rem 1rem var(--color-shadow);
z-index: 100; /* needs to be > 20 to be on top of dropzone's .dz-details */
} }
text-expander .suggestions li { text-expander .suggestions li {