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