Skip to content

Commit 46d19c4

Browse files
axifivelunny
authored andcommitted
Fix topics addition (Another solution) (#4031) (#4258)
* Added topics validation, fixed repo topics duplication (#4031) Signed-off-by: Alexey Terentyev <[email protected]> * Added tests Signed-off-by: Alexey Terentyev <[email protected]> * Fixed fmt Signed-off-by: Alexey Terentyev <[email protected]> * Added comments to exported functions Signed-off-by: Alexey Terentyev <[email protected]> * Deleted RemoveDuplicateTopics function Signed-off-by: Alexey Terentyev <[email protected]> * Fixed messages Signed-off-by: Alexey Terentyev <[email protected]> * Added migration Signed-off-by: Alexey Terentyev <[email protected]> * fmt migration file Signed-off-by: Alexey Terentyev <[email protected]> * fixed lint Signed-off-by: Alexey Terentyev <[email protected]> * Added Copyright Signed-off-by: Alexey Terentyev <[email protected]> * Added query solution for duplicates Signed-off-by: Alexey Terentyev <[email protected]> * Fixed migration query Signed-off-by: Alexey Terentyev <[email protected]> * Changed RegExp. Fixed migration Signed-off-by: Alexey Terentyev <[email protected]> * fmt migration file Signed-off-by: Alexey Terentyev <[email protected]> * Fixed test for changed regexp Signed-off-by: Alexey Terentyev <[email protected]> * Removed validation log messages Signed-off-by: Alexey Terentyev <[email protected]> * Renamed migration file Signed-off-by: Alexey Terentyev <[email protected]> * Renamed validate function Signed-off-by: Alexey Terentyev <[email protected]>
1 parent 9ae7664 commit 46d19c4

File tree

8 files changed

+229
-4
lines changed

8 files changed

+229
-4
lines changed

models/migrations/migrations.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,8 @@ var migrations = []Migration{
188188
NewMigration("add login source id column for public_key table", addLoginSourceIDToPublicKeyTable),
189189
// v67 -> v68
190190
NewMigration("remove stale watches", removeStaleWatches),
191+
// v68 -> V69
192+
NewMigration("Reformat and remove incorrect topics", reformatAndRemoveIncorrectTopics),
191193
}
192194

193195
// Migrate database to current version

models/migrations/v68.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// Copyright 2018 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+
"strings"
9+
10+
"code.gitea.io/gitea/models"
11+
"code.gitea.io/gitea/modules/log"
12+
13+
"github.com/go-xorm/xorm"
14+
)
15+
16+
func reformatAndRemoveIncorrectTopics(x *xorm.Engine) (err error) {
17+
log.Info("This migration could take up to minutes, please be patient.")
18+
type Topic struct {
19+
ID int64
20+
Name string `xorm:"unique"`
21+
}
22+
23+
sess := x.NewSession()
24+
defer sess.Close()
25+
26+
const batchSize = 100
27+
touchedRepo := make(map[int64]struct{})
28+
topics := make([]*Topic, 0, batchSize)
29+
delTopicIDs := make([]int64, 0, batchSize)
30+
ids := make([]int64, 0, 30)
31+
32+
if err := sess.Begin(); err != nil {
33+
return err
34+
}
35+
log.Info("Validating existed topics...")
36+
for start := 0; ; start += batchSize {
37+
topics = topics[:0]
38+
if err := sess.Asc("id").Limit(batchSize, start).Find(&topics); err != nil {
39+
return err
40+
}
41+
if len(topics) == 0 {
42+
break
43+
}
44+
for _, topic := range topics {
45+
if models.ValidateTopic(topic.Name) {
46+
continue
47+
}
48+
topic.Name = strings.Replace(strings.TrimSpace(strings.ToLower(topic.Name)), " ", "-", -1)
49+
50+
if err := sess.Table("repo_topic").Cols("repo_id").
51+
Where("topic_id = ?", topic.ID).Find(&ids); err != nil {
52+
return err
53+
}
54+
for _, id := range ids {
55+
touchedRepo[id] = struct{}{}
56+
}
57+
58+
if models.ValidateTopic(topic.Name) {
59+
log.Info("Updating topic: id = %v, name = %v", topic.ID, topic.Name)
60+
if _, err := sess.Table("topic").ID(topic.ID).
61+
Update(&Topic{Name: topic.Name}); err != nil {
62+
return err
63+
}
64+
} else {
65+
delTopicIDs = append(delTopicIDs, topic.ID)
66+
}
67+
}
68+
}
69+
70+
log.Info("Deleting incorrect topics...")
71+
for start := 0; ; start += batchSize {
72+
if (start + batchSize) < len(delTopicIDs) {
73+
ids = delTopicIDs[start:(start + batchSize)]
74+
} else {
75+
ids = delTopicIDs[start:]
76+
}
77+
78+
log.Info("Deleting 'repo_topic' rows for topics with ids = %v", ids)
79+
if _, err := sess.In("topic_id", ids).Delete(&models.RepoTopic{}); err != nil {
80+
return err
81+
}
82+
83+
log.Info("Deleting topics with id = %v", ids)
84+
if _, err := sess.In("id", ids).Delete(&Topic{}); err != nil {
85+
return err
86+
}
87+
88+
if len(ids) < batchSize {
89+
break
90+
}
91+
}
92+
93+
repoTopics := make([]*models.RepoTopic, 0, batchSize)
94+
delRepoTopics := make([]*models.RepoTopic, 0, batchSize)
95+
tmpRepoTopics := make([]*models.RepoTopic, 0, 30)
96+
97+
log.Info("Checking the number of topics in the repositories...")
98+
for start := 0; ; start += batchSize {
99+
repoTopics = repoTopics[:0]
100+
if err := sess.Cols("repo_id").Asc("repo_id").Limit(batchSize, start).
101+
GroupBy("repo_id").Having("COUNT(*) > 25").Find(&repoTopics); err != nil {
102+
return err
103+
}
104+
if len(repoTopics) == 0 {
105+
break
106+
}
107+
108+
log.Info("Number of repositories with more than 25 topics: %v", len(repoTopics))
109+
for _, repoTopic := range repoTopics {
110+
touchedRepo[repoTopic.RepoID] = struct{}{}
111+
112+
tmpRepoTopics = tmpRepoTopics[:0]
113+
if err := sess.Where("repo_id = ?", repoTopic.RepoID).Find(&tmpRepoTopics); err != nil {
114+
return err
115+
}
116+
117+
log.Info("Repository with id = %v has %v topics", repoTopic.RepoID, len(tmpRepoTopics))
118+
119+
for i := len(tmpRepoTopics) - 1; i > 24; i-- {
120+
delRepoTopics = append(delRepoTopics, tmpRepoTopics[i])
121+
}
122+
}
123+
}
124+
125+
log.Info("Deleting superfluous topics for repositories (more than 25 topics)...")
126+
for _, repoTopic := range delRepoTopics {
127+
log.Info("Deleting 'repo_topic' rows for 'repository' with id = %v. Topic id = %v",
128+
repoTopic.RepoID, repoTopic.TopicID)
129+
130+
if _, err := sess.Where("repo_id = ? AND topic_id = ?", repoTopic.RepoID,
131+
repoTopic.TopicID).Delete(&models.RepoTopic{}); err != nil {
132+
return err
133+
}
134+
if _, err := sess.Exec(
135+
"UPDATE topic SET repo_count = (SELECT repo_count FROM topic WHERE id = ?) - 1 WHERE id = ?",
136+
repoTopic.TopicID, repoTopic.TopicID); err != nil {
137+
return err
138+
}
139+
}
140+
141+
topicNames := make([]string, 0, 30)
142+
log.Info("Updating repositories 'topics' fields...")
143+
for repoID := range touchedRepo {
144+
if err := sess.Table("topic").Cols("name").
145+
Join("INNER", "repo_topic", "topic.id = repo_topic.topic_id").
146+
Where("repo_topic.repo_id = ?", repoID).Find(&topicNames); err != nil {
147+
return err
148+
}
149+
log.Info("Updating 'topics' field for repository with id = %v", repoID)
150+
if _, err := sess.ID(repoID).Cols("topics").
151+
Update(&models.Repository{Topics: topicNames}); err != nil {
152+
return err
153+
}
154+
}
155+
if err := sess.Commit(); err != nil {
156+
return err
157+
}
158+
159+
return nil
160+
}

models/topic.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package models
66

77
import (
88
"fmt"
9+
"regexp"
910
"strings"
1011

1112
"code.gitea.io/gitea/modules/util"
@@ -20,6 +21,8 @@ func init() {
2021
)
2122
}
2223

24+
var topicPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`)
25+
2326
// Topic represents a topic of repositories
2427
type Topic struct {
2528
ID int64
@@ -51,6 +54,11 @@ func (err ErrTopicNotExist) Error() string {
5154
return fmt.Sprintf("topic is not exist [name: %s]", err.Name)
5255
}
5356

57+
// ValidateTopic checks topics by length and match pattern rules
58+
func ValidateTopic(topic string) bool {
59+
return len(topic) <= 35 && topicPattern.MatchString(topic)
60+
}
61+
5462
// GetTopicByName retrieves topic by name
5563
func GetTopicByName(name string) (*Topic, error) {
5664
var topic Topic
@@ -182,6 +190,13 @@ func SaveTopics(repoID int64, topicNames ...string) error {
182190
}
183191
}
184192

193+
topicNames = topicNames[:0]
194+
if err := sess.Table("topic").Cols("name").
195+
Join("INNER", "repo_topic", "topic.id = repo_topic.topic_id").
196+
Where("repo_topic.repo_id = ?", repoID).Find(&topicNames); err != nil {
197+
return err
198+
}
199+
185200
if _, err := sess.ID(repoID).Cols("topics").Update(&Repository{
186201
Topics: topicNames,
187202
}); err != nil {

models/topic_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,16 @@ func TestAddTopic(t *testing.T) {
5555
assert.NoError(t, err)
5656
assert.EqualValues(t, 2, len(topics))
5757
}
58+
59+
func TestTopicValidator(t *testing.T) {
60+
assert.True(t, ValidateTopic("12345"))
61+
assert.True(t, ValidateTopic("2-test"))
62+
assert.True(t, ValidateTopic("test-3"))
63+
assert.True(t, ValidateTopic("first"))
64+
assert.True(t, ValidateTopic("second-test-topic"))
65+
assert.True(t, ValidateTopic("third-project-topic-with-max-length"))
66+
67+
assert.False(t, ValidateTopic("$fourth-test,topic"))
68+
assert.False(t, ValidateTopic("-fifth-test-topic"))
69+
assert.False(t, ValidateTopic("sixth-go-project-topic-with-excess-length"))
70+
}

options/locale/locale_en-US.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1167,6 +1167,8 @@ branch.protected_deletion_failed = Branch '%s' is protected. It cannot be delete
11671167

11681168
topic.manage_topics = Manage Topics
11691169
topic.done = Done
1170+
topic.count_prompt = You can't select more than 25 topics
1171+
topic.format_prompt = Topics must start with a letter or number, can include hyphens(-) and must be no more than 35 characters long
11701172
11711173
[org]
11721174
org_name_holder = Organization Name

public/js/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2336,8 +2336,10 @@ function initTopicbar() {
23362336
}).done(function() {
23372337
editDiv.hide();
23382338
viewDiv.show();
2339+
}).fail(function(xhr) {
2340+
alert(xhr.responseJSON.message)
23392341
})
2340-
})
2342+
});
23412343

23422344
$('#topic_edit .dropdown').dropdown({
23432345
allowAdditions: true,

routers/repo/topic.go

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import (
1212
"code.gitea.io/gitea/modules/log"
1313
)
1414

15-
// TopicPost response for creating repository
16-
func TopicPost(ctx *context.Context) {
15+
// TopicsPost response for creating repository
16+
func TopicsPost(ctx *context.Context) {
1717
if ctx.User == nil {
1818
ctx.JSON(403, map[string]interface{}{
1919
"message": "Only owners could change the topics.",
@@ -27,6 +27,37 @@ func TopicPost(ctx *context.Context) {
2727
topics = strings.Split(topicsStr, ",")
2828
}
2929

30+
invalidTopics := make([]string, 0)
31+
i := 0
32+
for _, topic := range topics {
33+
topic = strings.TrimSpace(strings.ToLower(topic))
34+
// ignore empty string
35+
if len(topic) > 0 {
36+
topics[i] = topic
37+
i++
38+
}
39+
if !models.ValidateTopic(topic) {
40+
invalidTopics = append(invalidTopics, topic)
41+
}
42+
}
43+
topics = topics[:i]
44+
45+
if len(topics) > 25 {
46+
ctx.JSON(422, map[string]interface{}{
47+
"invalidTopics": topics[:0],
48+
"message": ctx.Tr("repo.topic.count_prompt"),
49+
})
50+
return
51+
}
52+
53+
if len(invalidTopics) > 0 {
54+
ctx.JSON(422, map[string]interface{}{
55+
"invalidTopics": invalidTopics,
56+
"message": ctx.Tr("repo.topic.format_prompt"),
57+
})
58+
return
59+
}
60+
3061
err := models.SaveTopics(ctx.Repo.Repository.ID, topics...)
3162
if err != nil {
3263
log.Error(2, "SaveTopics failed: %v", err)

routers/routes/routes.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -626,7 +626,7 @@ func RegisterRoutes(m *macaron.Macaron) {
626626
}, context.RepoAssignment(), context.UnitTypes(), context.LoadRepoUnits(), context.CheckUnit(models.UnitTypeReleases))
627627

628628
m.Group("/:username/:reponame", func() {
629-
m.Post("/topics", repo.TopicPost)
629+
m.Post("/topics", repo.TopicsPost)
630630
}, context.RepoAssignment(), reqRepoAdmin)
631631

632632
m.Group("/:username/:reponame", func() {

0 commit comments

Comments
 (0)