Skip to content

Commit 6ec5a0c

Browse files
Add PUT ​/repos​/{owner}​/{repo}​/topics and remove GET ​/repos​/{owner}​/{repo}​/topics
1 parent f0f49bd commit 6ec5a0c

File tree

7 files changed

+131
-73
lines changed

7 files changed

+131
-73
lines changed

models/topic.go

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,33 @@ func (err ErrTopicNotExist) Error() string {
5454
return fmt.Sprintf("topic is not exist [name: %s]", err.Name)
5555
}
5656

57-
// ValidateTopic checks topics by length and match pattern rules
57+
// ValidateTopic checks a topic by length and match pattern rules
5858
func ValidateTopic(topic string) bool {
5959
return len(topic) <= 35 && topicPattern.MatchString(topic)
6060
}
6161

62+
// SanitizeAndValidateTopics sanitizes and checks an array or topics
63+
func SanitizeAndValidateTopics(topics []string) (invalidTopics []string) {
64+
invalidTopics = make([]string, 0)
65+
66+
i := 0
67+
for _, topic := range topics {
68+
topic = strings.TrimSpace(strings.ToLower(topic))
69+
// ignore empty string
70+
if len(topic) == 0 {
71+
continue
72+
}
73+
topics[i] = topic
74+
i++
75+
if !ValidateTopic(topic) {
76+
invalidTopics = append(invalidTopics, topic)
77+
}
78+
}
79+
topics = topics[:i]
80+
81+
return invalidTopics
82+
}
83+
6284
// GetTopicByName retrieves topic by name
6385
func GetTopicByName(name string) (*Topic, error) {
6486
var topic Topic
@@ -149,13 +171,16 @@ func FindTopics(opts *FindTopicOptions) (topics []*Topic, err error) {
149171
return topics, sess.Desc("topic.repo_count").Find(&topics)
150172
}
151173

152-
// GetRepoTopicByName retrives topic from name for a repo if it exist
153-
func GetRepoTopicByName(repoID int64, topicName string) (topic *Topic, err error) {
154-
sess := x.Select("topic.*").Where("repo_topic.repo_id = ?", repoID).And("topic.name = ?", topicName)
174+
// GetRepoTopic retrives topic from name for a repo if it exist
175+
func GetRepoTopicByName(repoID int64, topicName string) (*Topic, error) {
176+
var cond = builder.NewCond()
177+
var topic Topic
178+
cond = cond.And(builder.Eq{"repo_topic.repo_id": repoID}).And(builder.Eq{"topic.name": topicName})
179+
sess := x.Table("topic").Where(cond)
155180
sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id")
156181
has, err := sess.Get(&topic)
157182
if has {
158-
return topic, err
183+
return &topic, err
159184
}
160185
return nil, err
161186
}

modules/structs/repo_topic.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,9 @@ type TopicResponse struct {
1616
Created time.Time `json:"created"`
1717
Updated time.Time `json:"updated"`
1818
}
19+
20+
// RepoTopicOptions a collection of repo topic names
21+
type RepoTopicOptions struct {
22+
// list of topic names
23+
Topics []string `json:"topics"`
24+
}

routers/api/v1/api.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -776,10 +776,12 @@ func RegisterRoutes(m *macaron.Macaron) {
776776
}, reqRepoWriter(models.UnitTypeCode), reqToken())
777777
}, reqRepoReader(models.UnitTypeCode))
778778
m.Group("/topics", func() {
779-
m.Get("", repo.ListTopics)
780-
m.Combo("/:topic").Get(repo.HasTopic).
781-
Put(reqToken(), reqRepoWriter(models.UnitTypeCode), repo.AddTopic).
782-
Delete(reqToken(), reqRepoWriter(models.UnitTypeCode), repo.DeleteTopic)
779+
m.Combo("").Get(repo.ListTopics).
780+
Put(reqToken(), reqRepoWriter(models.UnitTypeCode), bind(api.RepoTopicOptions{}), repo.UpdateTopics)
781+
m.Group("/:topic", func() {
782+
m.Combo("").Put(reqToken(), reqRepoWriter(models.UnitTypeCode), repo.AddTopic).
783+
Delete(reqToken(), reqRepoWriter(models.UnitTypeCode), repo.DeleteTopic)
784+
})
783785
}, reqRepoReader(models.UnitTypeCode))
784786
}, repoAssignment())
785787
})

routers/api/v1/repo/topic.go

Lines changed: 56 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -48,20 +48,20 @@ func ListTopics(ctx *context.APIContext) {
4848
return
4949
}
5050

51-
topicResponses := make([]*api.TopicResponse, len(topics))
51+
topicNames := make([]*string, len(topics))
5252
for i, topic := range topics {
53-
topicResponses[i] = convert.ToTopicResponse(topic)
53+
topicNames[i] = &topic.Name
5454
}
5555
ctx.JSON(200, map[string]interface{}{
56-
"topics": topicResponses,
56+
"topics": topicNames,
5757
})
5858
}
5959

60-
// HasTopic check if repo has topic name
61-
func HasTopic(ctx *context.APIContext) {
62-
// swagger:operation GET /repos/{owner}/{repo}/topics/{topic} repository repoHasTopic
60+
// UpdateTopics updates repo with a new set of topics
61+
func UpdateTopics(ctx *context.APIContext, form api.RepoTopicOptions) {
62+
// swagger:operation PUT /repos/{owner}/{repo}/topics repository repoUpdateTopics
6363
// ---
64-
// summary: Check if a repository has topic
64+
// summary: Replace list of topics for a repository
6565
// produces:
6666
// - application/json
6767
// parameters:
@@ -75,34 +75,43 @@ func HasTopic(ctx *context.APIContext) {
7575
// description: name of the repo
7676
// type: string
7777
// required: true
78-
// - name: topic
79-
// in: path
80-
// description: name of the topic to check for
81-
// type: string
82-
// required: true
78+
// - name: body
79+
// in: body
80+
// schema:
81+
// "$ref": "#/definitions/RepoTopicOptions"
8382
// responses:
84-
// "201":
85-
// "$ref": "#/responses/TopicResponse"
86-
// "404":
83+
// "200":
8784
// "$ref": "#/responses/empty"
88-
topicName := strings.TrimSpace(strings.ToLower(ctx.Params(":topic")))
8985

90-
topic, err := models.GetRepoTopicByName(ctx.Repo.Repository.ID, topicName)
91-
if err != nil {
92-
log.Error("HasTopic failed: %v", err)
93-
ctx.JSON(500, map[string]interface{}{
94-
"message": "HasTopic failed.",
86+
topicNames := form.Topics
87+
invalidTopics := models.SanitizeAndValidateTopics(topicNames)
88+
89+
if len(topicNames) > 25 {
90+
ctx.JSON(422, map[string]interface{}{
91+
"invalidTopics": topicNames[:0],
92+
"message": "Exceeding maximum number of topics per repo",
9593
})
9694
return
9795
}
9896

99-
if topic == nil {
100-
ctx.NotFound()
97+
if len(invalidTopics) > 0 {
98+
ctx.JSON(422, map[string]interface{}{
99+
"invalidTopics": invalidTopics,
100+
"message": "Topic names are invalid",
101+
})
102+
return
101103
}
102104

103-
ctx.JSON(200, map[string]interface{}{
104-
"topic": convert.ToTopicResponse(topic),
105-
})
105+
err := models.SaveTopics(ctx.Repo.Repository.ID, topicNames...)
106+
if err != nil {
107+
log.Error("SaveTopics failed: %v", err)
108+
ctx.JSON(500, map[string]interface{}{
109+
"message": "Save topics failed.",
110+
})
111+
return
112+
}
113+
114+
ctx.Status(204)
106115
}
107116

108117
// AddTopic adds a topic name to a repo
@@ -139,7 +148,25 @@ func AddTopic(ctx *context.APIContext) {
139148
return
140149
}
141150

142-
topic, err := models.AddTopic(ctx.Repo.Repository.ID, topicName)
151+
// Prevent adding more topics than allowed to repo
152+
topics, err := models.FindTopics(&models.FindTopicOptions{
153+
RepoID: ctx.Repo.Repository.ID,
154+
})
155+
if err != nil {
156+
log.Error("AddTopic failed: %v", err)
157+
ctx.JSON(500, map[string]interface{}{
158+
"message": "ListTopics failed.",
159+
})
160+
return
161+
}
162+
if len(topics) >= 25 {
163+
ctx.JSON(422, map[string]interface{}{
164+
"message": "Exceeding maximum allowed topics per repo.",
165+
})
166+
return
167+
}
168+
169+
_, err = models.AddTopic(ctx.Repo.Repository.ID, topicName)
143170
if err != nil {
144171
log.Error("AddTopic failed: %v", err)
145172
ctx.JSON(500, map[string]interface{}{
@@ -148,9 +175,7 @@ func AddTopic(ctx *context.APIContext) {
148175
return
149176
}
150177

151-
ctx.JSON(201, map[string]interface{}{
152-
"topic": convert.ToTopicResponse(topic),
153-
})
178+
ctx.Status(204)
154179
}
155180

156181
// DeleteTopic removes topic name from repo
@@ -199,9 +224,7 @@ func DeleteTopic(ctx *context.APIContext) {
199224
ctx.NotFound()
200225
}
201226

202-
ctx.JSON(201, map[string]interface{}{
203-
"topic": convert.ToTopicResponse(topic),
204-
})
227+
ctx.Status(204)
205228
}
206229

207230
// TopicSearch search for creating topic

routers/api/v1/swagger/options.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,7 @@ type swaggerParameterBodies struct {
117117

118118
// in:body
119119
DeleteFileOptions api.DeleteFileOptions
120+
121+
// in:body
122+
RepoTopicOptions api.RepoTopicOptions
120123
}

routers/repo/topic.go

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,7 @@ func TopicsPost(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]
30+
invalidTopics := models.SanitizeAndValidateTopics(topics)
4431

4532
if len(topics) > 25 {
4633
ctx.JSON(422, map[string]interface{}{

templates/swagger/v1_json.tmpl

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5425,18 +5425,16 @@
54255425
"$ref": "#/responses/TopicListResponse"
54265426
}
54275427
}
5428-
}
5429-
},
5430-
"/repos/{owner}/{repo}/topics/{topic}": {
5431-
"get": {
5428+
},
5429+
"put": {
54325430
"produces": [
54335431
"application/json"
54345432
],
54355433
"tags": [
54365434
"repository"
54375435
],
5438-
"summary": "Check if a repository has topic",
5439-
"operationId": "repoHasTopic",
5436+
"summary": "Replace list of topics for a repository",
5437+
"operationId": "repoUpdateTopics",
54405438
"parameters": [
54415439
{
54425440
"type": "string",
@@ -5453,22 +5451,21 @@
54535451
"required": true
54545452
},
54555453
{
5456-
"type": "string",
5457-
"description": "name of the topic to check for",
5458-
"name": "topic",
5459-
"in": "path",
5460-
"required": true
5454+
"name": "body",
5455+
"in": "body",
5456+
"schema": {
5457+
"$ref": "#/definitions/RepoTopicOptions"
5458+
}
54615459
}
54625460
],
54635461
"responses": {
5464-
"201": {
5465-
"$ref": "#/responses/TopicResponse"
5466-
},
5467-
"404": {
5462+
"200": {
54685463
"$ref": "#/responses/empty"
54695464
}
54705465
}
5471-
},
5466+
}
5467+
},
5468+
"/repos/{owner}/{repo}/topics/{topic}": {
54725469
"put": {
54735470
"produces": [
54745471
"application/json"
@@ -9617,6 +9614,21 @@
96179614
},
96189615
"x-go-package": "code.gitea.io/gitea/modules/structs"
96199616
},
9617+
"RepoTopicOptions": {
9618+
"description": "RepoTopicOptions a collection of repo topic names",
9619+
"type": "object",
9620+
"properties": {
9621+
"topics": {
9622+
"description": "list of topic names",
9623+
"type": "array",
9624+
"items": {
9625+
"type": "string"
9626+
},
9627+
"x-go-name": "Topics"
9628+
}
9629+
},
9630+
"x-go-package": "code.gitea.io/gitea/modules/structs"
9631+
},
96209632
"Repository": {
96219633
"description": "Repository represents a repository",
96229634
"type": "object",
@@ -10621,7 +10633,7 @@
1062110633
"parameterBodies": {
1062210634
"description": "parameterBodies",
1062310635
"schema": {
10624-
"$ref": "#/definitions/DeleteFileOptions"
10636+
"$ref": "#/definitions/RepoTopicOptions"
1062510637
}
1062610638
},
1062710639
"redirect": {

0 commit comments

Comments
 (0)