code.gitea.io/gitea@v1.21.7/routers/api/v1/repo/topic.go (about)

     1  // Copyright 2019 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package repo
     5  
     6  import (
     7  	"net/http"
     8  	"strings"
     9  
    10  	repo_model "code.gitea.io/gitea/models/repo"
    11  	"code.gitea.io/gitea/modules/context"
    12  	"code.gitea.io/gitea/modules/log"
    13  	api "code.gitea.io/gitea/modules/structs"
    14  	"code.gitea.io/gitea/modules/web"
    15  	"code.gitea.io/gitea/routers/api/v1/utils"
    16  	"code.gitea.io/gitea/services/convert"
    17  )
    18  
    19  // ListTopics returns list of current topics for repo
    20  func ListTopics(ctx *context.APIContext) {
    21  	// swagger:operation GET /repos/{owner}/{repo}/topics repository repoListTopics
    22  	// ---
    23  	// summary: Get list of topics that a repository has
    24  	// produces:
    25  	//   - application/json
    26  	// parameters:
    27  	// - name: owner
    28  	//   in: path
    29  	//   description: owner of the repo
    30  	//   type: string
    31  	//   required: true
    32  	// - name: repo
    33  	//   in: path
    34  	//   description: name of the repo
    35  	//   type: string
    36  	//   required: true
    37  	// - name: page
    38  	//   in: query
    39  	//   description: page number of results to return (1-based)
    40  	//   type: integer
    41  	// - name: limit
    42  	//   in: query
    43  	//   description: page size of results
    44  	//   type: integer
    45  	// responses:
    46  	//   "200":
    47  	//     "$ref": "#/responses/TopicNames"
    48  	//   "404":
    49  	//     "$ref": "#/responses/notFound"
    50  
    51  	opts := &repo_model.FindTopicOptions{
    52  		ListOptions: utils.GetListOptions(ctx),
    53  		RepoID:      ctx.Repo.Repository.ID,
    54  	}
    55  
    56  	topics, total, err := repo_model.FindTopics(ctx, opts)
    57  	if err != nil {
    58  		ctx.InternalServerError(err)
    59  		return
    60  	}
    61  
    62  	topicNames := make([]string, len(topics))
    63  	for i, topic := range topics {
    64  		topicNames[i] = topic.Name
    65  	}
    66  
    67  	ctx.SetTotalCountHeader(total)
    68  	ctx.JSON(http.StatusOK, map[string]any{
    69  		"topics": topicNames,
    70  	})
    71  }
    72  
    73  // UpdateTopics updates repo with a new set of topics
    74  func UpdateTopics(ctx *context.APIContext) {
    75  	// swagger:operation PUT /repos/{owner}/{repo}/topics repository repoUpdateTopics
    76  	// ---
    77  	// summary: Replace list of topics for a repository
    78  	// produces:
    79  	//   - application/json
    80  	// parameters:
    81  	// - name: owner
    82  	//   in: path
    83  	//   description: owner of the repo
    84  	//   type: string
    85  	//   required: true
    86  	// - name: repo
    87  	//   in: path
    88  	//   description: name of the repo
    89  	//   type: string
    90  	//   required: true
    91  	// - name: body
    92  	//   in: body
    93  	//   schema:
    94  	//     "$ref": "#/definitions/RepoTopicOptions"
    95  	// responses:
    96  	//   "204":
    97  	//     "$ref": "#/responses/empty"
    98  	//   "404":
    99  	//     "$ref": "#/responses/notFound"
   100  	//   "422":
   101  	//     "$ref": "#/responses/invalidTopicsError"
   102  
   103  	form := web.GetForm(ctx).(*api.RepoTopicOptions)
   104  	topicNames := form.Topics
   105  	validTopics, invalidTopics := repo_model.SanitizeAndValidateTopics(topicNames)
   106  
   107  	if len(validTopics) > 25 {
   108  		ctx.JSON(http.StatusUnprocessableEntity, map[string]any{
   109  			"invalidTopics": nil,
   110  			"message":       "Exceeding maximum number of topics per repo",
   111  		})
   112  		return
   113  	}
   114  
   115  	if len(invalidTopics) > 0 {
   116  		ctx.JSON(http.StatusUnprocessableEntity, map[string]any{
   117  			"invalidTopics": invalidTopics,
   118  			"message":       "Topic names are invalid",
   119  		})
   120  		return
   121  	}
   122  
   123  	err := repo_model.SaveTopics(ctx, ctx.Repo.Repository.ID, validTopics...)
   124  	if err != nil {
   125  		log.Error("SaveTopics failed: %v", err)
   126  		ctx.InternalServerError(err)
   127  		return
   128  	}
   129  
   130  	ctx.Status(http.StatusNoContent)
   131  }
   132  
   133  // AddTopic adds a topic name to a repo
   134  func AddTopic(ctx *context.APIContext) {
   135  	// swagger:operation PUT /repos/{owner}/{repo}/topics/{topic} repository repoAddTopic
   136  	// ---
   137  	// summary: Add a topic to a repository
   138  	// produces:
   139  	//   - application/json
   140  	// parameters:
   141  	// - name: owner
   142  	//   in: path
   143  	//   description: owner of the repo
   144  	//   type: string
   145  	//   required: true
   146  	// - name: repo
   147  	//   in: path
   148  	//   description: name of the repo
   149  	//   type: string
   150  	//   required: true
   151  	// - name: topic
   152  	//   in: path
   153  	//   description: name of the topic to add
   154  	//   type: string
   155  	//   required: true
   156  	// responses:
   157  	//   "204":
   158  	//     "$ref": "#/responses/empty"
   159  	//   "404":
   160  	//     "$ref": "#/responses/notFound"
   161  	//   "422":
   162  	//     "$ref": "#/responses/invalidTopicsError"
   163  
   164  	topicName := strings.TrimSpace(strings.ToLower(ctx.Params(":topic")))
   165  
   166  	if !repo_model.ValidateTopic(topicName) {
   167  		ctx.JSON(http.StatusUnprocessableEntity, map[string]any{
   168  			"invalidTopics": topicName,
   169  			"message":       "Topic name is invalid",
   170  		})
   171  		return
   172  	}
   173  
   174  	// Prevent adding more topics than allowed to repo
   175  	count, err := repo_model.CountTopics(ctx, &repo_model.FindTopicOptions{
   176  		RepoID: ctx.Repo.Repository.ID,
   177  	})
   178  	if err != nil {
   179  		log.Error("CountTopics failed: %v", err)
   180  		ctx.InternalServerError(err)
   181  		return
   182  	}
   183  	if count >= 25 {
   184  		ctx.JSON(http.StatusUnprocessableEntity, map[string]any{
   185  			"message": "Exceeding maximum allowed topics per repo.",
   186  		})
   187  		return
   188  	}
   189  
   190  	_, err = repo_model.AddTopic(ctx, ctx.Repo.Repository.ID, topicName)
   191  	if err != nil {
   192  		log.Error("AddTopic failed: %v", err)
   193  		ctx.InternalServerError(err)
   194  		return
   195  	}
   196  
   197  	ctx.Status(http.StatusNoContent)
   198  }
   199  
   200  // DeleteTopic removes topic name from repo
   201  func DeleteTopic(ctx *context.APIContext) {
   202  	// swagger:operation DELETE /repos/{owner}/{repo}/topics/{topic} repository repoDeleteTopic
   203  	// ---
   204  	// summary: Delete a topic from a repository
   205  	// produces:
   206  	//   - application/json
   207  	// parameters:
   208  	// - name: owner
   209  	//   in: path
   210  	//   description: owner of the repo
   211  	//   type: string
   212  	//   required: true
   213  	// - name: repo
   214  	//   in: path
   215  	//   description: name of the repo
   216  	//   type: string
   217  	//   required: true
   218  	// - name: topic
   219  	//   in: path
   220  	//   description: name of the topic to delete
   221  	//   type: string
   222  	//   required: true
   223  	// responses:
   224  	//   "204":
   225  	//     "$ref": "#/responses/empty"
   226  	//   "404":
   227  	//     "$ref": "#/responses/notFound"
   228  	//   "422":
   229  	//     "$ref": "#/responses/invalidTopicsError"
   230  
   231  	topicName := strings.TrimSpace(strings.ToLower(ctx.Params(":topic")))
   232  
   233  	if !repo_model.ValidateTopic(topicName) {
   234  		ctx.JSON(http.StatusUnprocessableEntity, map[string]any{
   235  			"invalidTopics": topicName,
   236  			"message":       "Topic name is invalid",
   237  		})
   238  		return
   239  	}
   240  
   241  	topic, err := repo_model.DeleteTopic(ctx, ctx.Repo.Repository.ID, topicName)
   242  	if err != nil {
   243  		log.Error("DeleteTopic failed: %v", err)
   244  		ctx.InternalServerError(err)
   245  		return
   246  	}
   247  
   248  	if topic == nil {
   249  		ctx.NotFound()
   250  		return
   251  	}
   252  
   253  	ctx.Status(http.StatusNoContent)
   254  }
   255  
   256  // TopicSearch search for creating topic
   257  func TopicSearch(ctx *context.APIContext) {
   258  	// swagger:operation GET /topics/search repository topicSearch
   259  	// ---
   260  	// summary: search topics via keyword
   261  	// produces:
   262  	//   - application/json
   263  	// parameters:
   264  	//   - name: q
   265  	//     in: query
   266  	//     description: keywords to search
   267  	//     required: true
   268  	//     type: string
   269  	//   - name: page
   270  	//     in: query
   271  	//     description: page number of results to return (1-based)
   272  	//     type: integer
   273  	//   - name: limit
   274  	//     in: query
   275  	//     description: page size of results
   276  	//     type: integer
   277  	// responses:
   278  	//   "200":
   279  	//     "$ref": "#/responses/TopicListResponse"
   280  	//   "403":
   281  	//     "$ref": "#/responses/forbidden"
   282  	//   "404":
   283  	//     "$ref": "#/responses/notFound"
   284  
   285  	opts := &repo_model.FindTopicOptions{
   286  		Keyword:     ctx.FormString("q"),
   287  		ListOptions: utils.GetListOptions(ctx),
   288  	}
   289  
   290  	topics, total, err := repo_model.FindTopics(ctx, opts)
   291  	if err != nil {
   292  		ctx.InternalServerError(err)
   293  		return
   294  	}
   295  
   296  	topicResponses := make([]*api.TopicResponse, len(topics))
   297  	for i, topic := range topics {
   298  		topicResponses[i] = convert.ToTopicResponse(topic)
   299  	}
   300  
   301  	ctx.SetTotalCountHeader(total)
   302  	ctx.JSON(http.StatusOK, map[string]any{
   303  		"topics": topicResponses,
   304  	})
   305  }