Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 3cfaecc

Browse files
authoredJun 17, 2021
Merge branch 'main' into feature-jwt-asymmetric
2 parents 0ac1771 + fdf9ab1 commit 3cfaecc

File tree

26 files changed

+326
-72
lines changed

26 files changed

+326
-72
lines changed
 

‎custom/conf/app.example.ini

+5-5
Original file line numberDiff line numberDiff line change
@@ -1155,20 +1155,20 @@ PATH =
11551155
;STARTUP_TIMEOUT = 30s
11561156
;;
11571157
;; Issue indexer queue, currently support: channel, levelqueue or redis, default is levelqueue (deprecated - use [queue.issue_indexer])
1158-
;ISSUE_INDEXER_QUEUE_TYPE = levelqueue
1158+
;ISSUE_INDEXER_QUEUE_TYPE = levelqueue; **DEPRECATED** use settings in `[queue.issue_indexer]`.
11591159
;;
11601160
;; When ISSUE_INDEXER_QUEUE_TYPE is levelqueue, this will be the path where the queue will be saved.
11611161
;; This can be overridden by `ISSUE_INDEXER_QUEUE_CONN_STR`.
11621162
;; default is queues/common
1163-
;ISSUE_INDEXER_QUEUE_DIR = queues/common
1163+
;ISSUE_INDEXER_QUEUE_DIR = queues/common; **DEPRECATED** use settings in `[queue.issue_indexer]`.
11641164
;;
11651165
;; When `ISSUE_INDEXER_QUEUE_TYPE` is `redis`, this will store the redis connection string.
11661166
;; When `ISSUE_INDEXER_QUEUE_TYPE` is `levelqueue`, this is a directory or additional options of
11671167
;; the form `leveldb://path/to/db?option=value&....`, and overrides `ISSUE_INDEXER_QUEUE_DIR`.
1168-
;ISSUE_INDEXER_QUEUE_CONN_STR = "addrs=127.0.0.1:6379 db=0"
1168+
;ISSUE_INDEXER_QUEUE_CONN_STR = "addrs=127.0.0.1:6379 db=0"; **DEPRECATED** use settings in `[queue.issue_indexer]`.
11691169
;;
11701170
;; Batch queue number, default is 20
1171-
;ISSUE_INDEXER_QUEUE_BATCH_NUMBER = 20
1171+
;ISSUE_INDEXER_QUEUE_BATCH_NUMBER = 20; **DEPRECATED** use settings in `[queue.issue_indexer]`.
11721172

11731173
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
11741174
;; Repository Indexer settings
@@ -1197,7 +1197,7 @@ PATH =
11971197
;REPO_INDEXER_EXCLUDE =
11981198
;;
11991199
;;
1200-
;UPDATE_BUFFER_LEN = 20
1200+
;UPDATE_BUFFER_LEN = 20; **DEPRECATED** use settings in `[queue.issue_indexer]`.
12011201
;MAX_FILE_SIZE = 1048576
12021202

12031203
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

‎docs/content/doc/advanced/config-cheat-sheet.en-us.md

+5-5
Original file line numberDiff line numberDiff line change
@@ -356,10 +356,10 @@ relation to port exhaustion.
356356
- `ISSUE_INDEXER_NAME`: **gitea_issues**: Issue indexer name, available when ISSUE_INDEXER_TYPE is elasticsearch
357357
- `ISSUE_INDEXER_PATH`: **indexers/issues.bleve**: Index file used for issue search; available when ISSUE_INDEXER_TYPE is bleve and elasticsearch.
358358
- The next 4 configuration values are deprecated and should be set in `queue.issue_indexer` however are kept for backwards compatibility:
359-
- `ISSUE_INDEXER_QUEUE_TYPE`: **levelqueue**: Issue indexer queue, currently supports:`channel`, `levelqueue`, `redis`.
360-
- `ISSUE_INDEXER_QUEUE_DIR`: **queues/common**: When `ISSUE_INDEXER_QUEUE_TYPE` is `levelqueue`, this will be the path where the queue will be saved. (Previously this was `indexers/issues.queue`.)
361-
- `ISSUE_INDEXER_QUEUE_CONN_STR`: **addrs=127.0.0.1:6379 db=0**: When `ISSUE_INDEXER_QUEUE_TYPE` is `redis`, this will store the redis connection string. When `ISSUE_INDEXER_QUEUE_TYPE` is `levelqueue`, this is a directory or additional options of the form `leveldb://path/to/db?option=value&....`, and overrides `ISSUE_INDEXER_QUEUE_DIR`.
362-
- `ISSUE_INDEXER_QUEUE_BATCH_NUMBER`: **20**: Batch queue number.
359+
- `ISSUE_INDEXER_QUEUE_TYPE`: **levelqueue**: Issue indexer queue, currently supports:`channel`, `levelqueue`, `redis`. **DEPRECATED** use settings in `[queue.issue_indexer]`.
360+
- `ISSUE_INDEXER_QUEUE_DIR`: **queues/common**: When `ISSUE_INDEXER_QUEUE_TYPE` is `levelqueue`, this will be the path where the queue will be saved. **DEPRECATED** use settings in `[queue.issue_indexer]`.
361+
- `ISSUE_INDEXER_QUEUE_CONN_STR`: **addrs=127.0.0.1:6379 db=0**: When `ISSUE_INDEXER_QUEUE_TYPE` is `redis`, this will store the redis connection string. When `ISSUE_INDEXER_QUEUE_TYPE` is `levelqueue`, this is a directory or additional options of the form `leveldb://path/to/db?option=value&....`, and overrides `ISSUE_INDEXER_QUEUE_DIR`. **DEPRECATED** use settings in `[queue.issue_indexer]`.
362+
- `ISSUE_INDEXER_QUEUE_BATCH_NUMBER`: **20**: Batch queue number. **DEPRECATED** use settings in `[queue.issue_indexer]`.
363363

364364
- `REPO_INDEXER_ENABLED`: **false**: Enables code search (uses a lot of disk space, about 6 times more than the repository size).
365365
- `REPO_INDEXER_TYPE`: **bleve**: Code search engine type, could be `bleve` or `elasticsearch`.
@@ -370,7 +370,7 @@ relation to port exhaustion.
370370
- `REPO_INDEXER_INCLUDE`: **empty**: A comma separated list of glob patterns (see https://github.com/gobwas/glob) to **include** in the index. Use `**.txt` to match any files with .txt extension. An empty list means include all files.
371371
- `REPO_INDEXER_EXCLUDE`: **empty**: A comma separated list of glob patterns (see https://github.com/gobwas/glob) to **exclude** from the index. Files that match this list will not be indexed, even if they match in `REPO_INDEXER_INCLUDE`.
372372
- `REPO_INDEXER_EXCLUDE_VENDORED`: **true**: Exclude vendored files from index.
373-
- `UPDATE_BUFFER_LEN`: **20**: Buffer length of index request.
373+
- `UPDATE_BUFFER_LEN`: **20**: Buffer length of index request. **DEPRECATED** use settings in `[queue.issue_indexer]`.
374374
- `MAX_FILE_SIZE`: **1048576**: Maximum size in bytes of files to be indexed.
375375
- `STARTUP_TIMEOUT`: **30s**: If the indexer takes longer than this timeout to start - fail. (This timeout will be added to the hammer time above for child processes - as bleve will not start until the previous parent is shutdown.) Set to zero to never timeout.
376376

‎integrations/api_issue_test.go

+26-6
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,10 @@ func TestAPIListIssues(t *testing.T) {
2525

2626
session := loginUser(t, owner.Name)
2727
token := getTokenForLoggedInUser(t, session)
28-
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues?state=all&token=%s",
29-
owner.Name, repo.Name, token)
30-
resp := session.MakeRequest(t, req, http.StatusOK)
28+
link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner.Name, repo.Name))
29+
30+
link.RawQuery = url.Values{"token": {token}, "state": {"all"}}.Encode()
31+
resp := session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
3132
var apiIssues []*api.Issue
3233
DecodeJSON(t, resp, &apiIssues)
3334
assert.Len(t, apiIssues, models.GetCount(t, &models.Issue{RepoID: repo.ID}))
@@ -36,15 +37,34 @@ func TestAPIListIssues(t *testing.T) {
3637
}
3738

3839
// test milestone filter
39-
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues?state=all&type=all&milestones=ignore,milestone1,3,4&token=%s",
40-
owner.Name, repo.Name, token)
41-
resp = session.MakeRequest(t, req, http.StatusOK)
40+
link.RawQuery = url.Values{"token": {token}, "state": {"all"}, "type": {"all"}, "milestones": {"ignore,milestone1,3,4"}}.Encode()
41+
resp = session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
4242
DecodeJSON(t, resp, &apiIssues)
4343
if assert.Len(t, apiIssues, 2) {
4444
assert.EqualValues(t, 3, apiIssues[0].Milestone.ID)
4545
assert.EqualValues(t, 1, apiIssues[1].Milestone.ID)
4646
}
4747

48+
link.RawQuery = url.Values{"token": {token}, "state": {"all"}, "created_by": {"user2"}}.Encode()
49+
resp = session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
50+
DecodeJSON(t, resp, &apiIssues)
51+
if assert.Len(t, apiIssues, 1) {
52+
assert.EqualValues(t, 5, apiIssues[0].ID)
53+
}
54+
55+
link.RawQuery = url.Values{"token": {token}, "state": {"all"}, "assigned_by": {"user1"}}.Encode()
56+
resp = session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
57+
DecodeJSON(t, resp, &apiIssues)
58+
if assert.Len(t, apiIssues, 1) {
59+
assert.EqualValues(t, 1, apiIssues[0].ID)
60+
}
61+
62+
link.RawQuery = url.Values{"token": {token}, "state": {"all"}, "mentioned_by": {"user4"}}.Encode()
63+
resp = session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
64+
DecodeJSON(t, resp, &apiIssues)
65+
if assert.Len(t, apiIssues, 1) {
66+
assert.EqualValues(t, 1, apiIssues[0].ID)
67+
}
4868
}
4969

5070
func TestAPICreateIssue(t *testing.T) {

‎models/fixtures/issue_user.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,4 @@
1717
uid: 4
1818
issue_id: 1
1919
is_read: false
20-
is_mentioned: false
20+
is_mentioned: true

‎models/migrations/migrations.go

+2
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,8 @@ var migrations = []Migration{
317317
NewMigration("Add issue resource index table", addIssueResourceIndexTable),
318318
// v183 -> v184
319319
NewMigration("Create PushMirror table", createPushMirrorTable),
320+
// v184 -> v185
321+
NewMigration("Rename Task errors to message", renameTaskErrorsToMessage),
320322
}
321323

322324
// GetCurrentDBVersion returns the current db version

‎models/migrations/v184.go

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright 2021 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package migrations
6+
7+
import (
8+
"fmt"
9+
10+
"code.gitea.io/gitea/modules/setting"
11+
12+
"xorm.io/xorm"
13+
)
14+
15+
func renameTaskErrorsToMessage(x *xorm.Engine) error {
16+
type Task struct {
17+
Errors string `xorm:"TEXT"` // if task failed, saved the error reason
18+
Type int
19+
Status int `xorm:"index"`
20+
}
21+
22+
sess := x.NewSession()
23+
defer sess.Close()
24+
if err := sess.Begin(); err != nil {
25+
return err
26+
}
27+
28+
if err := sess.Sync2(new(Task)); err != nil {
29+
return fmt.Errorf("error on Sync2: %v", err)
30+
}
31+
32+
switch {
33+
case setting.Database.UseMySQL:
34+
if _, err := sess.Exec("ALTER TABLE `task` CHANGE errors message text"); err != nil {
35+
return err
36+
}
37+
case setting.Database.UseMSSQL:
38+
if _, err := sess.Exec("sp_rename 'task.errors', 'message', 'COLUMN'"); err != nil {
39+
return err
40+
}
41+
default:
42+
if _, err := sess.Exec("ALTER TABLE `task` RENAME COLUMN errors TO message"); err != nil {
43+
return err
44+
}
45+
}
46+
return sess.Commit()
47+
}

‎models/task.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,16 @@ type Task struct {
3232
StartTime timeutil.TimeStamp
3333
EndTime timeutil.TimeStamp
3434
PayloadContent string `xorm:"TEXT"`
35-
Errors string `xorm:"TEXT"` // if task failed, saved the error reason
35+
Message string `xorm:"TEXT"` // if task failed, saved the error reason
3636
Created timeutil.TimeStamp `xorm:"created"`
3737
}
3838

39+
// TranslatableMessage represents JSON struct that can be translated with a Locale
40+
type TranslatableMessage struct {
41+
Format string
42+
Args []interface{} `json:"omitempty"`
43+
}
44+
3945
// LoadRepo loads repository of the task
4046
func (task *Task) LoadRepo() error {
4147
return task.loadRepo(x)

‎modules/git/repo_language_stats_nogogit.go

+3-7
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,7 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
2525
defer cancel()
2626

2727
writeID := func(id string) error {
28-
_, err := batchStdinWriter.Write([]byte(id))
29-
if err != nil {
30-
return err
31-
}
32-
_, err = batchStdinWriter.Write([]byte{'\n'})
28+
_, err := batchStdinWriter.Write([]byte(id + "\n"))
3329
return err
3430
}
3531

@@ -85,10 +81,10 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
8581
}
8682

8783
sizeToRead := size
88-
discard := int64(0)
84+
discard := int64(1)
8985
if size > fileSizeLimit {
9086
sizeToRead = fileSizeLimit
91-
discard = size - fileSizeLimit
87+
discard = size - fileSizeLimit + 1
9288
}
9389

9490
_, err = contentBuf.ReadFrom(io.LimitReader(batchReader, sizeToRead))

‎modules/migrations/base/messenger.go

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright 2021 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package base
6+
7+
// Messenger is a formatting function similar to i18n.Tr
8+
type Messenger func(key string, args ...interface{})
9+
10+
// NilMessenger represents an empty formatting function
11+
func NilMessenger(string, ...interface{}) {}

‎modules/migrations/dump.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -555,7 +555,7 @@ func DumpRepository(ctx context.Context, baseDir, ownerName string, opts base.Mi
555555
return err
556556
}
557557

558-
if err := migrateRepository(downloader, uploader, opts); err != nil {
558+
if err := migrateRepository(downloader, uploader, opts, nil); err != nil {
559559
if err1 := uploader.Rollback(); err1 != nil {
560560
log.Error("rollback failed: %v", err1)
561561
}
@@ -620,7 +620,7 @@ func RestoreRepository(ctx context.Context, baseDir string, ownerName, repoName
620620
}
621621
updateOptionsUnits(&migrateOpts, units)
622622

623-
if err = migrateRepository(downloader, uploader, migrateOpts); err != nil {
623+
if err = migrateRepository(downloader, uploader, migrateOpts, nil); err != nil {
624624
if err1 := uploader.Rollback(); err1 != nil {
625625
log.Error("rollback failed: %v", err1)
626626
}

‎modules/migrations/gitea_uploader_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func TestGiteaUploadRepo(t *testing.T) {
4747
PullRequests: true,
4848
Private: true,
4949
Mirror: false,
50-
})
50+
}, nil)
5151
assert.NoError(t, err)
5252

5353
repo := models.AssertExistsAndLoadBean(t, &models.Repository{OwnerID: user.ID, Name: repoName}).(*models.Repository)

‎modules/migrations/migrate.go

+14-3
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ func IsMigrateURLAllowed(remoteURL string, doer *models.User) error {
9999
}
100100

101101
// MigrateRepository migrate repository according MigrateOptions
102-
func MigrateRepository(ctx context.Context, doer *models.User, ownerName string, opts base.MigrateOptions) (*models.Repository, error) {
102+
func MigrateRepository(ctx context.Context, doer *models.User, ownerName string, opts base.MigrateOptions, messenger base.Messenger) (*models.Repository, error) {
103103
err := IsMigrateURLAllowed(opts.CloneAddr, doer)
104104
if err != nil {
105105
return nil, err
@@ -118,7 +118,7 @@ func MigrateRepository(ctx context.Context, doer *models.User, ownerName string,
118118
var uploader = NewGiteaLocalUploader(ctx, doer, ownerName, opts.RepoName)
119119
uploader.gitServiceType = opts.GitServiceType
120120

121-
if err := migrateRepository(downloader, uploader, opts); err != nil {
121+
if err := migrateRepository(downloader, uploader, opts, messenger); err != nil {
122122
if err1 := uploader.Rollback(); err1 != nil {
123123
log.Error("rollback failed: %v", err1)
124124
}
@@ -167,7 +167,11 @@ func newDownloader(ctx context.Context, ownerName string, opts base.MigrateOptio
167167
// migrateRepository will download information and then upload it to Uploader, this is a simple
168168
// process for small repository. For a big repository, save all the data to disk
169169
// before upload is better
170-
func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts base.MigrateOptions) error {
170+
func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts base.MigrateOptions, messenger base.Messenger) error {
171+
if messenger == nil {
172+
messenger = base.NilMessenger
173+
}
174+
171175
repo, err := downloader.GetRepoInfo()
172176
if err != nil {
173177
if !base.IsErrNotSupported(err) {
@@ -185,12 +189,14 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts
185189
}
186190

187191
log.Trace("migrating git data from %s", repo.CloneURL)
192+
messenger("repo.migrate.migrating_git")
188193
if err = uploader.CreateRepo(repo, opts); err != nil {
189194
return err
190195
}
191196
defer uploader.Close()
192197

193198
log.Trace("migrating topics")
199+
messenger("repo.migrate.migrating_topics")
194200
topics, err := downloader.GetTopics()
195201
if err != nil {
196202
if !base.IsErrNotSupported(err) {
@@ -206,6 +212,7 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts
206212

207213
if opts.Milestones {
208214
log.Trace("migrating milestones")
215+
messenger("repo.migrate.migrating_milestones")
209216
milestones, err := downloader.GetMilestones()
210217
if err != nil {
211218
if !base.IsErrNotSupported(err) {
@@ -229,6 +236,7 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts
229236

230237
if opts.Labels {
231238
log.Trace("migrating labels")
239+
messenger("repo.migrate.migrating_labels")
232240
labels, err := downloader.GetLabels()
233241
if err != nil {
234242
if !base.IsErrNotSupported(err) {
@@ -252,6 +260,7 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts
252260

253261
if opts.Releases {
254262
log.Trace("migrating releases")
263+
messenger("repo.migrate.migrating_releases")
255264
releases, err := downloader.GetReleases()
256265
if err != nil {
257266
if !base.IsErrNotSupported(err) {
@@ -285,6 +294,7 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts
285294

286295
if opts.Issues {
287296
log.Trace("migrating issues and comments")
297+
messenger("repo.migrate.migrating_issues")
288298
var issueBatchSize = uploader.MaxBatchInsertSize("issue")
289299

290300
for i := 1; ; i++ {
@@ -339,6 +349,7 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts
339349

340350
if opts.PullRequests {
341351
log.Trace("migrating pull requests and comments")
352+
messenger("repo.migrate.migrating_pulls")
342353
var prBatchSize = uploader.MaxBatchInsertSize("pullrequest")
343354
for i := 1; ; i++ {
344355
prs, isEnd, err := downloader.GetPullRequests(i, prBatchSize)

‎modules/setting/indexer.go

+15-17
Original file line numberDiff line numberDiff line change
@@ -28,31 +28,28 @@ var (
2828
IssuePath string
2929
IssueConnStr string
3030
IssueIndexerName string
31-
IssueQueueType string
32-
IssueQueueDir string
33-
IssueQueueConnStr string
34-
IssueQueueBatchNumber int
31+
IssueQueueType string // DEPRECATED - replaced by queue.issue_indexer
32+
IssueQueueDir string // DEPRECATED - replaced by queue.issue_indexer
33+
IssueQueueConnStr string // DEPRECATED - replaced by queue.issue_indexer
34+
IssueQueueBatchNumber int // DEPRECATED - replaced by queue.issue_indexer
3535
StartupTimeout time.Duration
3636

3737
RepoIndexerEnabled bool
3838
RepoType string
3939
RepoPath string
4040
RepoConnStr string
4141
RepoIndexerName string
42-
UpdateQueueLength int
42+
UpdateQueueLength int // DEPRECATED - replaced by queue.issue_indexer
4343
MaxIndexerFileSize int64
4444
IncludePatterns []glob.Glob
4545
ExcludePatterns []glob.Glob
4646
ExcludeVendored bool
4747
}{
48-
IssueType: "bleve",
49-
IssuePath: "indexers/issues.bleve",
50-
IssueConnStr: "",
51-
IssueIndexerName: "gitea_issues",
52-
IssueQueueType: LevelQueueType,
53-
IssueQueueDir: "queues/common",
54-
IssueQueueConnStr: "",
55-
IssueQueueBatchNumber: 20,
48+
IssueType: "bleve",
49+
IssuePath: "indexers/issues.bleve",
50+
IssueConnStr: "",
51+
IssueIndexerName: "gitea_issues",
52+
IssueQueueType: LevelQueueType,
5653

5754
RepoIndexerEnabled: false,
5855
RepoType: "bleve",
@@ -74,10 +71,12 @@ func newIndexerService() {
7471
Indexer.IssueConnStr = sec.Key("ISSUE_INDEXER_CONN_STR").MustString(Indexer.IssueConnStr)
7572
Indexer.IssueIndexerName = sec.Key("ISSUE_INDEXER_NAME").MustString(Indexer.IssueIndexerName)
7673

77-
Indexer.IssueQueueType = sec.Key("ISSUE_INDEXER_QUEUE_TYPE").MustString(LevelQueueType)
78-
Indexer.IssueQueueDir = filepath.ToSlash(sec.Key("ISSUE_INDEXER_QUEUE_DIR").MustString(filepath.ToSlash(filepath.Join(AppDataPath, "queues/common"))))
74+
// The following settings are deprecated and can be overridden by settings in [queue] or [queue.issue_indexer]
75+
Indexer.IssueQueueType = sec.Key("ISSUE_INDEXER_QUEUE_TYPE").MustString("")
76+
Indexer.IssueQueueDir = filepath.ToSlash(sec.Key("ISSUE_INDEXER_QUEUE_DIR").MustString(""))
7977
Indexer.IssueQueueConnStr = sec.Key("ISSUE_INDEXER_QUEUE_CONN_STR").MustString("")
80-
Indexer.IssueQueueBatchNumber = sec.Key("ISSUE_INDEXER_QUEUE_BATCH_NUMBER").MustInt(20)
78+
Indexer.IssueQueueBatchNumber = sec.Key("ISSUE_INDEXER_QUEUE_BATCH_NUMBER").MustInt(0)
79+
Indexer.UpdateQueueLength = sec.Key("UPDATE_BUFFER_LEN").MustInt(0)
8180

8281
Indexer.RepoIndexerEnabled = sec.Key("REPO_INDEXER_ENABLED").MustBool(false)
8382
Indexer.RepoType = sec.Key("REPO_INDEXER_TYPE").MustString("bleve")
@@ -91,7 +90,6 @@ func newIndexerService() {
9190
Indexer.IncludePatterns = IndexerGlobFromString(sec.Key("REPO_INDEXER_INCLUDE").MustString(""))
9291
Indexer.ExcludePatterns = IndexerGlobFromString(sec.Key("REPO_INDEXER_EXCLUDE").MustString(""))
9392
Indexer.ExcludeVendored = sec.Key("REPO_INDEXER_EXCLUDE_VENDORED").MustBool(true)
94-
Indexer.UpdateQueueLength = sec.Key("UPDATE_BUFFER_LEN").MustInt(20)
9593
Indexer.MaxIndexerFileSize = sec.Key("MAX_FILE_SIZE").MustInt64(1024 * 1024)
9694
Indexer.StartupTimeout = sec.Key("STARTUP_TIMEOUT").MustDuration(30 * time.Second)
9795
}

‎modules/setting/queue.go

+9-6
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ func GetQueueSettings(name string) QueueSettings {
6565
q.SetName = q.QueueName + Queue.SetName
6666
}
6767
if !filepath.IsAbs(q.DataDir) {
68-
q.DataDir = filepath.Join(AppDataPath, q.DataDir)
68+
q.DataDir = filepath.ToSlash(filepath.Join(AppDataPath, q.DataDir))
6969
}
7070
_, _ = sec.NewKey("DATADIR", q.DataDir)
7171

@@ -98,6 +98,7 @@ func NewQueueService() {
9898
Queue.QueueLength = sec.Key("LENGTH").MustInt(20)
9999
Queue.BatchLength = sec.Key("BATCH_LENGTH").MustInt(20)
100100
Queue.ConnectionString = sec.Key("CONN_STR").MustString("")
101+
defaultType := sec.Key("TYPE").String()
101102
Queue.Type = sec.Key("TYPE").MustString("persistable-channel")
102103
Queue.Network, Queue.Addresses, Queue.Password, Queue.DBIndex, _ = ParseQueueConnStr(Queue.ConnectionString)
103104
Queue.WrapIfNecessary = sec.Key("WRAP_IF_NECESSARY").MustBool(true)
@@ -117,29 +118,31 @@ func NewQueueService() {
117118
for _, key := range section.Keys() {
118119
sectionMap[key.Name()] = true
119120
}
120-
if _, ok := sectionMap["TYPE"]; !ok {
121+
if _, ok := sectionMap["TYPE"]; !ok && defaultType == "" {
121122
switch Indexer.IssueQueueType {
122123
case LevelQueueType:
123124
_, _ = section.NewKey("TYPE", "level")
124125
case ChannelQueueType:
125126
_, _ = section.NewKey("TYPE", "persistable-channel")
126127
case RedisQueueType:
127128
_, _ = section.NewKey("TYPE", "redis")
129+
case "":
130+
_, _ = section.NewKey("TYPE", "level")
128131
default:
129132
log.Fatal("Unsupported indexer queue type: %v",
130133
Indexer.IssueQueueType)
131134
}
132135
}
133-
if _, ok := sectionMap["LENGTH"]; !ok {
136+
if _, ok := sectionMap["LENGTH"]; !ok && Indexer.UpdateQueueLength != 0 {
134137
_, _ = section.NewKey("LENGTH", fmt.Sprintf("%d", Indexer.UpdateQueueLength))
135138
}
136-
if _, ok := sectionMap["BATCH_LENGTH"]; !ok {
139+
if _, ok := sectionMap["BATCH_LENGTH"]; !ok && Indexer.IssueQueueBatchNumber != 0 {
137140
_, _ = section.NewKey("BATCH_LENGTH", fmt.Sprintf("%d", Indexer.IssueQueueBatchNumber))
138141
}
139-
if _, ok := sectionMap["DATADIR"]; !ok {
142+
if _, ok := sectionMap["DATADIR"]; !ok && Indexer.IssueQueueDir != "" {
140143
_, _ = section.NewKey("DATADIR", Indexer.IssueQueueDir)
141144
}
142-
if _, ok := sectionMap["CONN_STR"]; !ok {
145+
if _, ok := sectionMap["CONN_STR"]; !ok && Indexer.IssueQueueConnStr != "" {
143146
_, _ = section.NewKey("CONN_STR", Indexer.IssueQueueConnStr)
144147
}
145148

‎modules/setting/setting.go

+6
Original file line numberDiff line numberDiff line change
@@ -1180,3 +1180,9 @@ func NewServices() {
11801180
newProject()
11811181
newMimeTypeMap()
11821182
}
1183+
1184+
// NewServicesForInstall initializes the services for install
1185+
func NewServicesForInstall() {
1186+
newService()
1187+
newMailService()
1188+
}

‎modules/task/migrate.go

+12-2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"code.gitea.io/gitea/modules/structs"
2121
"code.gitea.io/gitea/modules/timeutil"
2222
"code.gitea.io/gitea/modules/util"
23+
jsoniter "github.com/json-iterator/go"
2324
)
2425

2526
func handleCreateError(owner *models.User, err error) error {
@@ -56,7 +57,7 @@ func runMigrateTask(t *models.Task) (err error) {
5657

5758
t.EndTime = timeutil.TimeStampNow()
5859
t.Status = structs.TaskStatusFailed
59-
t.Errors = err.Error()
60+
t.Message = err.Error()
6061
t.RepoID = 0
6162
if err := t.UpdateCols("status", "errors", "repo_id", "end_time"); err != nil {
6263
log.Error("Task UpdateCols failed: %v", err)
@@ -106,7 +107,16 @@ func runMigrateTask(t *models.Task) (err error) {
106107
return
107108
}
108109

109-
repo, err = migrations.MigrateRepository(ctx, t.Doer, t.Owner.Name, *opts)
110+
repo, err = migrations.MigrateRepository(ctx, t.Doer, t.Owner.Name, *opts, func(format string, args ...interface{}) {
111+
message := models.TranslatableMessage{
112+
Format: format,
113+
Args: args,
114+
}
115+
json := jsoniter.ConfigCompatibleWithStandardLibrary
116+
bs, _ := json.Marshal(message)
117+
t.Message = string(bs)
118+
_ = t.UpdateCols("message")
119+
})
110120
if err == nil {
111121
log.Trace("Repository migrated [%d]: %s/%s", repo.ID, t.Owner.Name, repo.Name)
112122
return

‎options/locale/locale_en-US.ini

+8
Original file line numberDiff line numberDiff line change
@@ -824,11 +824,19 @@ migrated_from_fake = Migrated From %[1]s
824824
migrate.migrate = Migrate From %s
825825
migrate.migrating = Migrating from <b>%s</b> ...
826826
migrate.migrating_failed = Migrating from <b>%s</b> failed.
827+
migrate.migrating_failed.error = Error: %s
827828
migrate.github.description = Migrating data from Github.com or Github Enterprise.
828829
migrate.git.description = Migrating or Mirroring git data from Git services
829830
migrate.gitlab.description = Migrating data from GitLab.com or Self-Hosted gitlab server.
830831
migrate.gitea.description = Migrating data from Gitea.com or Self-Hosted Gitea server.
831832
migrate.gogs.description = Migrating data from notabug.org or other Self-Hosted Gogs server.
833+
migrate.migrating_git = Migrating Git Data
834+
migrate.migrating_topics = Migrating Topics
835+
migrate.migrating_milestones = Migrating Milestones
836+
migrate.migrating_labels = Migrating Labels
837+
migrate.migrating_releases = Migrating Releases
838+
migrate.migrating_issues = Migrating Issues
839+
migrate.migrating_pulls = Migrating Pull Requests
832840

833841
mirror_from = mirror of
834842
forked_from = forked from

‎options/locale/locale_zh-TW.ini

+15-1
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,10 @@ loading=載入中…
9191
step1=第一步:
9292
step2=第二步:
9393

94+
error=錯誤
9495
error404=您正嘗試訪問的頁面 <strong>不存在</strong> 或 <strong>您尚未被授權</strong> 查看該頁面。
9596

97+
never=從來沒有
9698

9799
[error]
98100
occurred=發生錯誤
@@ -723,6 +725,7 @@ mirror_prune_desc=刪除過時的遠端追蹤參考
723725
mirror_interval=鏡像間隔(有效時間單位為 'h''m''s')。設為 0 以停用自動同步。
724726
mirror_interval_invalid=鏡像週期無效
725727
mirror_address=從 URL Clone
728+
mirror_address_desc=在授權資訊中填入必要的資料。
726729
mirror_address_url_invalid=提供的網址無效。請檢查您輸入的網址是否正確。
727730
mirror_address_protocol_invalid=提供的網址無效。只能從 http(s):// 或是 git:// 位址鏡像儲存庫。
728731
mirror_lfs=Large File Storage (LFS)
@@ -785,6 +788,7 @@ form.reach_limit_of_creation_n=您已經達到了您儲存庫的數量上限 (%d
785788
form.name_reserved=儲存庫名稱 '%s' 是預留的。
786789
form.name_pattern_not_allowed=儲存庫名稱無法使用 "%s"
787790

791+
need_auth=授權
788792
migrate_options=遷移選項
789793
migrate_service=遷移服務
790794
migrate_options_mirror_helper=將此儲存庫設定為<span class="text blue">鏡像儲存庫</span>
@@ -1545,7 +1549,16 @@ settings.hooks=Webhook
15451549
settings.githooks=Git Hook
15461550
settings.basic_settings=基本設定
15471551
settings.mirror_settings=鏡像設定
1548-
settings.sync_mirror=現在同步
1552+
settings.mirror_settings.docs=設定您的專案自動向其它儲存庫推送、拉取變更,分支、標籤和提交會自動同步。<a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io/en-us/repo-mirror/">如何鏡像儲存庫?</a>
1553+
settings.mirror_settings.mirrored_repository=已鏡像的儲存庫
1554+
settings.mirror_settings.direction=方向
1555+
settings.mirror_settings.direction.pull=拉取
1556+
settings.mirror_settings.direction.push=推送
1557+
settings.mirror_settings.last_update=最近更新時間
1558+
settings.mirror_settings.push_mirror.none=未設定推送鏡像
1559+
settings.mirror_settings.push_mirror.remote_url=Git 遠端儲存庫 URL
1560+
settings.mirror_settings.push_mirror.add=新增推送鏡像
1561+
settings.sync_mirror=立即同步
15491562
settings.mirror_sync_in_progress=鏡像同步正在進行中。 請稍後再回來看看。
15501563
settings.email_notifications.enable=啟用郵件通知
15511564
settings.email_notifications.onmention=只在被提到時傳送郵件通知
@@ -1610,6 +1623,7 @@ settings.transfer_form_title=輸入儲存庫名稱以確認:
16101623
settings.transfer_in_progress=目前正在進行轉移。如果您想要將此儲存庫轉移給其它使用者,請取消它。
16111624
settings.transfer_notices_1=- 如果將此儲存庫轉移給個別使用者,您將會失去此儲存庫的存取權。
16121625
settings.transfer_notices_2=- 如果將此儲存庫轉移到您(共同)擁有的組織,您將能繼續保有此儲存庫的存取權。
1626+
settings.transfer_notices_3=- 如果此儲存庫為私有儲存庫且將轉移給個別使用者,此動作確保該使用者至少擁有讀取權限(必要時將會修改權限)。
16131627
settings.transfer_owner=新擁有者
16141628
settings.transfer_perform=進行轉移
16151629
settings.transfer_started=此儲存庫已被標記為待轉移且正在等待「%s」的確認

‎routers/api/v1/repo/issue.go

+75-8
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,30 @@ func ListIssues(ctx *context.APIContext) {
266266
// in: query
267267
// description: comma separated list of milestone names or ids. It uses names and fall back to ids. Fetch only issues that have any of this milestones. Non existent milestones are discarded
268268
// type: string
269+
// - name: since
270+
// in: query
271+
// description: Only show notifications updated after the given time. This is a timestamp in RFC 3339 format
272+
// type: string
273+
// format: date-time
274+
// required: false
275+
// - name: before
276+
// in: query
277+
// description: Only show notifications updated before the given time. This is a timestamp in RFC 3339 format
278+
// type: string
279+
// format: date-time
280+
// required: false
281+
// - name: created_by
282+
// in: query
283+
// description: filter (issues / pulls) created to
284+
// type: string
285+
// - name: assigned_by
286+
// in: query
287+
// description: filter (issues / pulls) assigned to
288+
// type: string
289+
// - name: mentioned_by
290+
// in: query
291+
// description: filter (issues / pulls) mentioning to
292+
// type: string
269293
// - name: page
270294
// in: query
271295
// description: page number of results to return (1-based)
@@ -277,6 +301,11 @@ func ListIssues(ctx *context.APIContext) {
277301
// responses:
278302
// "200":
279303
// "$ref": "#/responses/IssueList"
304+
before, since, err := utils.GetQueryBeforeSince(ctx)
305+
if err != nil {
306+
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
307+
return
308+
}
280309

281310
var isClosed util.OptionalBool
282311
switch ctx.Query("state") {
@@ -297,7 +326,6 @@ func ListIssues(ctx *context.APIContext) {
297326
}
298327
var issueIDs []int64
299328
var labelIDs []int64
300-
var err error
301329
if len(keyword) > 0 {
302330
issueIDs, err = issue_indexer.SearchIssuesByKeyword([]int64{ctx.Repo.Repository.ID}, keyword)
303331
if err != nil {
@@ -356,17 +384,36 @@ func ListIssues(ctx *context.APIContext) {
356384
isPull = util.OptionalBoolNone
357385
}
358386

387+
// FIXME: we should be more efficient here
388+
createdByID := getUserIDForFilter(ctx, "created_by")
389+
if ctx.Written() {
390+
return
391+
}
392+
assignedByID := getUserIDForFilter(ctx, "assigned_by")
393+
if ctx.Written() {
394+
return
395+
}
396+
mentionedByID := getUserIDForFilter(ctx, "mentioned_by")
397+
if ctx.Written() {
398+
return
399+
}
400+
359401
// Only fetch the issues if we either don't have a keyword or the search returned issues
360402
// This would otherwise return all issues if no issues were found by the search.
361403
if len(keyword) == 0 || len(issueIDs) > 0 || len(labelIDs) > 0 {
362404
issuesOpt := &models.IssuesOptions{
363-
ListOptions: listOptions,
364-
RepoIDs: []int64{ctx.Repo.Repository.ID},
365-
IsClosed: isClosed,
366-
IssueIDs: issueIDs,
367-
LabelIDs: labelIDs,
368-
MilestoneIDs: mileIDs,
369-
IsPull: isPull,
405+
ListOptions: listOptions,
406+
RepoIDs: []int64{ctx.Repo.Repository.ID},
407+
IsClosed: isClosed,
408+
IssueIDs: issueIDs,
409+
LabelIDs: labelIDs,
410+
MilestoneIDs: mileIDs,
411+
IsPull: isPull,
412+
UpdatedBeforeUnix: before,
413+
UpdatedAfterUnix: since,
414+
PosterID: createdByID,
415+
AssigneeID: assignedByID,
416+
MentionedID: mentionedByID,
370417
}
371418

372419
if issues, err = models.Issues(issuesOpt); err != nil {
@@ -389,6 +436,26 @@ func ListIssues(ctx *context.APIContext) {
389436
ctx.JSON(http.StatusOK, convert.ToAPIIssueList(issues))
390437
}
391438

439+
func getUserIDForFilter(ctx *context.APIContext, queryName string) int64 {
440+
userName := ctx.Query(queryName)
441+
if len(userName) == 0 {
442+
return 0
443+
}
444+
445+
user, err := models.GetUserByName(userName)
446+
if models.IsErrUserNotExist(err) {
447+
ctx.NotFound(err)
448+
return 0
449+
}
450+
451+
if err != nil {
452+
ctx.InternalServerError(err)
453+
return 0
454+
}
455+
456+
return user.ID
457+
}
458+
392459
// GetIssue get an issue of a repository
393460
func GetIssue(ctx *context.APIContext) {
394461
// swagger:operation GET /repos/{owner}/{repo}/issues/{index} issue issueGetIssue

‎routers/api/v1/repo/migrate.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ func Migrate(ctx *context.APIContext) {
199199
}
200200
}()
201201

202-
if _, err = migrations.MigrateRepository(graceful.GetManager().HammerContext(), ctx.User, repoOwner.Name, opts); err != nil {
202+
if _, err = migrations.MigrateRepository(graceful.GetManager().HammerContext(), ctx.User, repoOwner.Name, opts, nil); err != nil {
203203
handleMigrateError(ctx, repoOwner, remoteAddr, err)
204204
return
205205
}

‎routers/install/setting.go

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ func PreloadSettings(ctx context.Context) bool {
2828
log.Info("SQLite3 Supported")
2929
}
3030
setting.InitDBConfig()
31+
setting.NewServicesForInstall()
3132
svg.Init()
3233
}
3334

‎routers/web/user/task.go

+17-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
"code.gitea.io/gitea/models"
1111
"code.gitea.io/gitea/modules/context"
12+
jsoniter "github.com/json-iterator/go"
1213
)
1314

1415
// TaskStatus returns task's status
@@ -21,9 +22,24 @@ func TaskStatus(ctx *context.Context) {
2122
return
2223
}
2324

25+
message := task.Message
26+
27+
if task.Message != "" && task.Message[0] == '{' {
28+
// assume message is actually a translatable string
29+
json := jsoniter.ConfigCompatibleWithStandardLibrary
30+
var translatableMessage models.TranslatableMessage
31+
if err := json.Unmarshal([]byte(message), &translatableMessage); err != nil {
32+
translatableMessage = models.TranslatableMessage{
33+
Format: "migrate.migrating_failed.error",
34+
Args: []interface{}{task.Message},
35+
}
36+
}
37+
message = ctx.Tr(translatableMessage.Format, translatableMessage.Args...)
38+
}
39+
2440
ctx.JSON(http.StatusOK, map[string]interface{}{
2541
"status": task.Status,
26-
"err": task.Errors,
42+
"message": message,
2743
"repo-id": task.RepoID,
2844
"repo-name": opts.RepoName,
2945
"start": task.StartTime,

‎templates/repo/diff/comment_form.tmpl

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<input type="hidden" name="diff_start_cid">
1010
<input type="hidden" name="diff_end_cid">
1111
<input type="hidden" name="diff_base_cid">
12-
<div class="ui top tabular menu" {{if not $.hidden}}onload="assingMenuAttributes(this)" {{end}}data-write="write" data-preview="preview">
12+
<div class="ui top tabular menu" {{if not $.hidden}}onload="assignMenuAttributes(this)" {{end}}data-write="write" data-preview="preview">
1313
<a class="active item" data-tab="write">{{$.root.i18n.Tr "write"}}</a>
1414
<a class="item" data-tab="preview" data-url="{{$.root.Repository.APIURL}}/markdown" data-context="{{$.root.RepoLink}}">{{$.root.i18n.Tr "preview"}}</a>
1515
</div>

‎templates/repo/migrate/migrating.tmpl

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
<div class="sixteen wide center aligned centered column">
2323
<div id="repo_migrating_progress">
2424
<p>{{.i18n.Tr "repo.migrate.migrating" .CloneAddr | Safe}}</p>
25+
<p id="repo_migrating_progress_message"></p>
2526
</div>
2627
<div id="repo_migrating_failed" hidden>
2728
<p>{{.i18n.Tr "repo.migrate.migrating_failed" .CloneAddr | Safe}}</p>

‎templates/swagger/v1_json.tmpl

+32
Original file line numberDiff line numberDiff line change
@@ -4234,6 +4234,38 @@
42344234
"name": "milestones",
42354235
"in": "query"
42364236
},
4237+
{
4238+
"type": "string",
4239+
"format": "date-time",
4240+
"description": "Only show notifications updated after the given time. This is a timestamp in RFC 3339 format",
4241+
"name": "since",
4242+
"in": "query"
4243+
},
4244+
{
4245+
"type": "string",
4246+
"format": "date-time",
4247+
"description": "Only show notifications updated before the given time. This is a timestamp in RFC 3339 format",
4248+
"name": "before",
4249+
"in": "query"
4250+
},
4251+
{
4252+
"type": "string",
4253+
"description": "filter (issues / pulls) created to",
4254+
"name": "created_by",
4255+
"in": "query"
4256+
},
4257+
{
4258+
"type": "string",
4259+
"description": "filter (issues / pulls) assigned to",
4260+
"name": "assigned_by",
4261+
"in": "query"
4262+
},
4263+
{
4264+
"type": "string",
4265+
"description": "filter (issues / pulls) mentioning to",
4266+
"name": "mentioned_by",
4267+
"in": "query"
4268+
},
42374269
{
42384270
"type": "integer",
42394271
"description": "page number of results to return (1-based)",

‎web_src/js/index.js

+9-4
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ function initRepoStatusChecker() {
202202
const migrating = $('#repo_migrating');
203203
$('#repo_migrating_failed').hide();
204204
$('#repo_migrating_failed_image').hide();
205+
$('#repo_migrating_progress_message').hide();
205206
if (migrating) {
206207
const task = migrating.attr('task');
207208
if (typeof task === 'undefined') {
@@ -223,9 +224,13 @@ function initRepoStatusChecker() {
223224
$('#repo_migrating').hide();
224225
$('#repo_migrating_failed').show();
225226
$('#repo_migrating_failed_image').show();
226-
$('#repo_migrating_failed_error').text(xhr.responseJSON.err);
227+
$('#repo_migrating_failed_error').text(xhr.responseJSON.message);
227228
return;
228229
}
230+
if (xhr.responseJSON.message) {
231+
$('#repo_migrating_progress_message').show();
232+
$('#repo_migrating_progress_message').text(xhr.responseJSON.message);
233+
}
229234
setTimeout(() => {
230235
initRepoStatusChecker();
231236
}, 2000);
@@ -1377,7 +1382,7 @@ function initPullRequestReview() {
13771382
}
13781383
$textarea.focus();
13791384
$simplemde.codemirror.focus();
1380-
assingMenuAttributes(form.find('.menu'));
1385+
assignMenuAttributes(form.find('.menu'));
13811386
});
13821387

13831388
const $reviewBox = $('.review-box');
@@ -1435,7 +1440,7 @@ function initPullRequestReview() {
14351440
const data = await $.get($(this).data('new-comment-url'));
14361441
td.html(data);
14371442
commentCloud = td.find('.comment-code-cloud');
1438-
assingMenuAttributes(commentCloud.find('.menu'));
1443+
assignMenuAttributes(commentCloud.find('.menu'));
14391444
td.find("input[name='line']").val(idx);
14401445
td.find("input[name='side']").val(side === 'left' ? 'previous' : 'proposed');
14411446
td.find("input[name='path']").val(path);
@@ -1448,7 +1453,7 @@ function initPullRequestReview() {
14481453
});
14491454
}
14501455

1451-
function assingMenuAttributes(menu) {
1456+
function assignMenuAttributes(menu) {
14521457
const id = Math.floor(Math.random() * Math.floor(1000000));
14531458
menu.attr('data-write', menu.attr('data-write') + id);
14541459
menu.attr('data-preview', menu.attr('data-preview') + id);

0 commit comments

Comments
 (0)
Please sign in to comment.