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

     1  // Copyright 2014 The Gogs Authors. All rights reserved.
     2  // Copyright 2018 The Gitea Authors. All rights reserved.
     3  // SPDX-License-Identifier: MIT
     4  
     5  package repo
     6  
     7  import (
     8  	"fmt"
     9  	"net/http"
    10  	"slices"
    11  	"strconv"
    12  	"strings"
    13  	"time"
    14  
    15  	actions_model "code.gitea.io/gitea/models/actions"
    16  	activities_model "code.gitea.io/gitea/models/activities"
    17  	"code.gitea.io/gitea/models/db"
    18  	"code.gitea.io/gitea/models/organization"
    19  	"code.gitea.io/gitea/models/perm"
    20  	access_model "code.gitea.io/gitea/models/perm/access"
    21  	repo_model "code.gitea.io/gitea/models/repo"
    22  	unit_model "code.gitea.io/gitea/models/unit"
    23  	user_model "code.gitea.io/gitea/models/user"
    24  	"code.gitea.io/gitea/modules/git"
    25  	"code.gitea.io/gitea/modules/gitrepo"
    26  	"code.gitea.io/gitea/modules/label"
    27  	"code.gitea.io/gitea/modules/log"
    28  	"code.gitea.io/gitea/modules/optional"
    29  	repo_module "code.gitea.io/gitea/modules/repository"
    30  	"code.gitea.io/gitea/modules/setting"
    31  	api "code.gitea.io/gitea/modules/structs"
    32  	"code.gitea.io/gitea/modules/validation"
    33  	"code.gitea.io/gitea/modules/web"
    34  	"code.gitea.io/gitea/routers/api/v1/utils"
    35  	actions_service "code.gitea.io/gitea/services/actions"
    36  	"code.gitea.io/gitea/services/context"
    37  	"code.gitea.io/gitea/services/convert"
    38  	"code.gitea.io/gitea/services/issue"
    39  	repo_service "code.gitea.io/gitea/services/repository"
    40  )
    41  
    42  // Search repositories via options
    43  func Search(ctx *context.APIContext) {
    44  	// swagger:operation GET /repos/search repository repoSearch
    45  	// ---
    46  	// summary: Search for repositories
    47  	// produces:
    48  	// - application/json
    49  	// parameters:
    50  	// - name: q
    51  	//   in: query
    52  	//   description: keyword
    53  	//   type: string
    54  	// - name: topic
    55  	//   in: query
    56  	//   description: Limit search to repositories with keyword as topic
    57  	//   type: boolean
    58  	// - name: includeDesc
    59  	//   in: query
    60  	//   description: include search of keyword within repository description
    61  	//   type: boolean
    62  	// - name: uid
    63  	//   in: query
    64  	//   description: search only for repos that the user with the given id owns or contributes to
    65  	//   type: integer
    66  	//   format: int64
    67  	// - name: priority_owner_id
    68  	//   in: query
    69  	//   description: repo owner to prioritize in the results
    70  	//   type: integer
    71  	//   format: int64
    72  	// - name: team_id
    73  	//   in: query
    74  	//   description: search only for repos that belong to the given team id
    75  	//   type: integer
    76  	//   format: int64
    77  	// - name: starredBy
    78  	//   in: query
    79  	//   description: search only for repos that the user with the given id has starred
    80  	//   type: integer
    81  	//   format: int64
    82  	// - name: private
    83  	//   in: query
    84  	//   description: include private repositories this user has access to (defaults to true)
    85  	//   type: boolean
    86  	// - name: is_private
    87  	//   in: query
    88  	//   description: show only pubic, private or all repositories (defaults to all)
    89  	//   type: boolean
    90  	// - name: template
    91  	//   in: query
    92  	//   description: include template repositories this user has access to (defaults to true)
    93  	//   type: boolean
    94  	// - name: archived
    95  	//   in: query
    96  	//   description: show only archived, non-archived or all repositories (defaults to all)
    97  	//   type: boolean
    98  	// - name: mode
    99  	//   in: query
   100  	//   description: type of repository to search for. Supported values are
   101  	//                "fork", "source", "mirror" and "collaborative"
   102  	//   type: string
   103  	// - name: exclusive
   104  	//   in: query
   105  	//   description: if `uid` is given, search only for repos that the user owns
   106  	//   type: boolean
   107  	// - name: sort
   108  	//   in: query
   109  	//   description: sort repos by attribute. Supported values are
   110  	//                "alpha", "created", "updated", "size", and "id".
   111  	//                Default is "alpha"
   112  	//   type: string
   113  	// - name: order
   114  	//   in: query
   115  	//   description: sort order, either "asc" (ascending) or "desc" (descending).
   116  	//                Default is "asc", ignored if "sort" is not specified.
   117  	//   type: string
   118  	// - name: page
   119  	//   in: query
   120  	//   description: page number of results to return (1-based)
   121  	//   type: integer
   122  	// - name: limit
   123  	//   in: query
   124  	//   description: page size of results
   125  	//   type: integer
   126  	// responses:
   127  	//   "200":
   128  	//     "$ref": "#/responses/SearchResults"
   129  	//   "422":
   130  	//     "$ref": "#/responses/validationError"
   131  
   132  	private := ctx.IsSigned && (ctx.FormString("private") == "" || ctx.FormBool("private"))
   133  	if ctx.PublicOnly {
   134  		private = false
   135  	}
   136  
   137  	opts := &repo_model.SearchRepoOptions{
   138  		ListOptions:        utils.GetListOptions(ctx),
   139  		Actor:              ctx.Doer,
   140  		Keyword:            ctx.FormTrim("q"),
   141  		OwnerID:            ctx.FormInt64("uid"),
   142  		PriorityOwnerID:    ctx.FormInt64("priority_owner_id"),
   143  		TeamID:             ctx.FormInt64("team_id"),
   144  		TopicOnly:          ctx.FormBool("topic"),
   145  		Collaborate:        optional.None[bool](),
   146  		Private:            private,
   147  		Template:           optional.None[bool](),
   148  		StarredByID:        ctx.FormInt64("starredBy"),
   149  		IncludeDescription: ctx.FormBool("includeDesc"),
   150  	}
   151  
   152  	if ctx.FormString("template") != "" {
   153  		opts.Template = optional.Some(ctx.FormBool("template"))
   154  	}
   155  
   156  	if ctx.FormBool("exclusive") {
   157  		opts.Collaborate = optional.Some(false)
   158  	}
   159  
   160  	mode := ctx.FormString("mode")
   161  	switch mode {
   162  	case "source":
   163  		opts.Fork = optional.Some(false)
   164  		opts.Mirror = optional.Some(false)
   165  	case "fork":
   166  		opts.Fork = optional.Some(true)
   167  	case "mirror":
   168  		opts.Mirror = optional.Some(true)
   169  	case "collaborative":
   170  		opts.Mirror = optional.Some(false)
   171  		opts.Collaborate = optional.Some(true)
   172  	case "":
   173  	default:
   174  		ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid search mode: \"%s\"", mode))
   175  		return
   176  	}
   177  
   178  	if ctx.FormString("archived") != "" {
   179  		opts.Archived = optional.Some(ctx.FormBool("archived"))
   180  	}
   181  
   182  	if ctx.FormString("is_private") != "" {
   183  		opts.IsPrivate = optional.Some(ctx.FormBool("is_private"))
   184  	}
   185  
   186  	sortMode := ctx.FormString("sort")
   187  	if len(sortMode) > 0 {
   188  		sortOrder := ctx.FormString("order")
   189  		if len(sortOrder) == 0 {
   190  			sortOrder = "asc"
   191  		}
   192  		if searchModeMap, ok := repo_model.SearchOrderByMap[sortOrder]; ok {
   193  			if orderBy, ok := searchModeMap[sortMode]; ok {
   194  				opts.OrderBy = orderBy
   195  			} else {
   196  				ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid sort mode: \"%s\"", sortMode))
   197  				return
   198  			}
   199  		} else {
   200  			ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid sort order: \"%s\"", sortOrder))
   201  			return
   202  		}
   203  	}
   204  
   205  	var err error
   206  	repos, count, err := repo_model.SearchRepository(ctx, opts)
   207  	if err != nil {
   208  		ctx.JSON(http.StatusInternalServerError, api.SearchError{
   209  			OK:    false,
   210  			Error: err.Error(),
   211  		})
   212  		return
   213  	}
   214  
   215  	results := make([]*api.Repository, len(repos))
   216  	for i, repo := range repos {
   217  		if err = repo.LoadOwner(ctx); err != nil {
   218  			ctx.JSON(http.StatusInternalServerError, api.SearchError{
   219  				OK:    false,
   220  				Error: err.Error(),
   221  			})
   222  			return
   223  		}
   224  		permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
   225  		if err != nil {
   226  			ctx.JSON(http.StatusInternalServerError, api.SearchError{
   227  				OK:    false,
   228  				Error: err.Error(),
   229  			})
   230  		}
   231  		results[i] = convert.ToRepo(ctx, repo, permission)
   232  	}
   233  	ctx.SetLinkHeader(int(count), opts.PageSize)
   234  	ctx.SetTotalCountHeader(count)
   235  	ctx.JSON(http.StatusOK, api.SearchResults{
   236  		OK:   true,
   237  		Data: results,
   238  	})
   239  }
   240  
   241  // CreateUserRepo create a repository for a user
   242  func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.CreateRepoOption) {
   243  	if opt.AutoInit && opt.Readme == "" {
   244  		opt.Readme = "Default"
   245  	}
   246  
   247  	// If the readme template does not exist, a 400 will be returned.
   248  	if opt.AutoInit && len(opt.Readme) > 0 && !slices.Contains(repo_module.Readmes, opt.Readme) {
   249  		ctx.Error(http.StatusBadRequest, "", fmt.Errorf("readme template does not exist, available templates: %v", repo_module.Readmes))
   250  		return
   251  	}
   252  
   253  	repo, err := repo_service.CreateRepository(ctx, ctx.Doer, owner, repo_service.CreateRepoOptions{
   254  		Name:             opt.Name,
   255  		Description:      opt.Description,
   256  		IssueLabels:      opt.IssueLabels,
   257  		Gitignores:       opt.Gitignores,
   258  		License:          opt.License,
   259  		Readme:           opt.Readme,
   260  		IsPrivate:        opt.Private || setting.Repository.ForcePrivate,
   261  		AutoInit:         opt.AutoInit,
   262  		DefaultBranch:    opt.DefaultBranch,
   263  		TrustModel:       repo_model.ToTrustModel(opt.TrustModel),
   264  		IsTemplate:       opt.Template,
   265  		ObjectFormatName: opt.ObjectFormatName,
   266  	})
   267  	if err != nil {
   268  		if repo_model.IsErrRepoAlreadyExist(err) {
   269  			ctx.Error(http.StatusConflict, "", "The repository with the same name already exists.")
   270  		} else if db.IsErrNameReserved(err) ||
   271  			db.IsErrNamePatternNotAllowed(err) ||
   272  			label.IsErrTemplateLoad(err) {
   273  			ctx.Error(http.StatusUnprocessableEntity, "", err)
   274  		} else {
   275  			ctx.Error(http.StatusInternalServerError, "CreateRepository", err)
   276  		}
   277  		return
   278  	}
   279  
   280  	// reload repo from db to get a real state after creation
   281  	repo, err = repo_model.GetRepositoryByID(ctx, repo.ID)
   282  	if err != nil {
   283  		ctx.Error(http.StatusInternalServerError, "GetRepositoryByID", err)
   284  	}
   285  
   286  	ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}))
   287  }
   288  
   289  // Create one repository of mine
   290  func Create(ctx *context.APIContext) {
   291  	// swagger:operation POST /user/repos repository user createCurrentUserRepo
   292  	// ---
   293  	// summary: Create a repository
   294  	// consumes:
   295  	// - application/json
   296  	// produces:
   297  	// - application/json
   298  	// parameters:
   299  	// - name: body
   300  	//   in: body
   301  	//   schema:
   302  	//     "$ref": "#/definitions/CreateRepoOption"
   303  	// responses:
   304  	//   "201":
   305  	//     "$ref": "#/responses/Repository"
   306  	//   "400":
   307  	//     "$ref": "#/responses/error"
   308  	//   "409":
   309  	//     description: The repository with the same name already exists.
   310  	//   "422":
   311  	//     "$ref": "#/responses/validationError"
   312  	opt := web.GetForm(ctx).(*api.CreateRepoOption)
   313  	if ctx.Doer.IsOrganization() {
   314  		// Shouldn't reach this condition, but just in case.
   315  		ctx.Error(http.StatusUnprocessableEntity, "", "not allowed creating repository for organization")
   316  		return
   317  	}
   318  	CreateUserRepo(ctx, ctx.Doer, *opt)
   319  }
   320  
   321  // Generate Create a repository using a template
   322  func Generate(ctx *context.APIContext) {
   323  	// swagger:operation POST /repos/{template_owner}/{template_repo}/generate repository generateRepo
   324  	// ---
   325  	// summary: Create a repository using a template
   326  	// consumes:
   327  	// - application/json
   328  	// produces:
   329  	// - application/json
   330  	// parameters:
   331  	// - name: template_owner
   332  	//   in: path
   333  	//   description: name of the template repository owner
   334  	//   type: string
   335  	//   required: true
   336  	// - name: template_repo
   337  	//   in: path
   338  	//   description: name of the template repository
   339  	//   type: string
   340  	//   required: true
   341  	// - name: body
   342  	//   in: body
   343  	//   schema:
   344  	//     "$ref": "#/definitions/GenerateRepoOption"
   345  	// responses:
   346  	//   "201":
   347  	//     "$ref": "#/responses/Repository"
   348  	//   "403":
   349  	//     "$ref": "#/responses/forbidden"
   350  	//   "404":
   351  	//     "$ref": "#/responses/notFound"
   352  	//   "409":
   353  	//     description: The repository with the same name already exists.
   354  	//   "422":
   355  	//     "$ref": "#/responses/validationError"
   356  	form := web.GetForm(ctx).(*api.GenerateRepoOption)
   357  
   358  	if !ctx.Repo.Repository.IsTemplate {
   359  		ctx.Error(http.StatusUnprocessableEntity, "", "this is not a template repo")
   360  		return
   361  	}
   362  
   363  	if ctx.Doer.IsOrganization() {
   364  		ctx.Error(http.StatusUnprocessableEntity, "", "not allowed creating repository for organization")
   365  		return
   366  	}
   367  
   368  	opts := repo_service.GenerateRepoOptions{
   369  		Name:            form.Name,
   370  		DefaultBranch:   form.DefaultBranch,
   371  		Description:     form.Description,
   372  		Private:         form.Private || setting.Repository.ForcePrivate,
   373  		GitContent:      form.GitContent,
   374  		Topics:          form.Topics,
   375  		GitHooks:        form.GitHooks,
   376  		Webhooks:        form.Webhooks,
   377  		Avatar:          form.Avatar,
   378  		IssueLabels:     form.Labels,
   379  		ProtectedBranch: form.ProtectedBranch,
   380  	}
   381  
   382  	if !opts.IsValid() {
   383  		ctx.Error(http.StatusUnprocessableEntity, "", "must select at least one template item")
   384  		return
   385  	}
   386  
   387  	ctxUser := ctx.Doer
   388  	var err error
   389  	if form.Owner != ctxUser.Name {
   390  		ctxUser, err = user_model.GetUserByName(ctx, form.Owner)
   391  		if err != nil {
   392  			if user_model.IsErrUserNotExist(err) {
   393  				ctx.JSON(http.StatusNotFound, map[string]any{
   394  					"error": "request owner `" + form.Owner + "` does not exist",
   395  				})
   396  				return
   397  			}
   398  
   399  			ctx.Error(http.StatusInternalServerError, "GetUserByName", err)
   400  			return
   401  		}
   402  
   403  		if !ctx.Doer.IsAdmin && !ctxUser.IsOrganization() {
   404  			ctx.Error(http.StatusForbidden, "", "Only admin can generate repository for other user.")
   405  			return
   406  		}
   407  
   408  		if !ctx.Doer.IsAdmin {
   409  			canCreate, err := organization.OrgFromUser(ctxUser).CanCreateOrgRepo(ctx, ctx.Doer.ID)
   410  			if err != nil {
   411  				ctx.ServerError("CanCreateOrgRepo", err)
   412  				return
   413  			} else if !canCreate {
   414  				ctx.Error(http.StatusForbidden, "", "Given user is not allowed to create repository in organization.")
   415  				return
   416  			}
   417  		}
   418  	}
   419  
   420  	repo, err := repo_service.GenerateRepository(ctx, ctx.Doer, ctxUser, ctx.Repo.Repository, opts)
   421  	if err != nil {
   422  		if repo_model.IsErrRepoAlreadyExist(err) {
   423  			ctx.Error(http.StatusConflict, "", "The repository with the same name already exists.")
   424  		} else if db.IsErrNameReserved(err) ||
   425  			db.IsErrNamePatternNotAllowed(err) {
   426  			ctx.Error(http.StatusUnprocessableEntity, "", err)
   427  		} else {
   428  			ctx.Error(http.StatusInternalServerError, "CreateRepository", err)
   429  		}
   430  		return
   431  	}
   432  	log.Trace("Repository generated [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name)
   433  
   434  	ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}))
   435  }
   436  
   437  // CreateOrgRepoDeprecated create one repository of the organization
   438  func CreateOrgRepoDeprecated(ctx *context.APIContext) {
   439  	// swagger:operation POST /org/{org}/repos organization createOrgRepoDeprecated
   440  	// ---
   441  	// summary: Create a repository in an organization
   442  	// deprecated: true
   443  	// consumes:
   444  	// - application/json
   445  	// produces:
   446  	// - application/json
   447  	// parameters:
   448  	// - name: org
   449  	//   in: path
   450  	//   description: name of organization
   451  	//   type: string
   452  	//   required: true
   453  	// - name: body
   454  	//   in: body
   455  	//   schema:
   456  	//     "$ref": "#/definitions/CreateRepoOption"
   457  	// responses:
   458  	//   "201":
   459  	//     "$ref": "#/responses/Repository"
   460  	//   "422":
   461  	//     "$ref": "#/responses/validationError"
   462  	//   "403":
   463  	//     "$ref": "#/responses/forbidden"
   464  	//   "404":
   465  	//     "$ref": "#/responses/notFound"
   466  
   467  	CreateOrgRepo(ctx)
   468  }
   469  
   470  // CreateOrgRepo create one repository of the organization
   471  func CreateOrgRepo(ctx *context.APIContext) {
   472  	// swagger:operation POST /orgs/{org}/repos organization createOrgRepo
   473  	// ---
   474  	// summary: Create a repository in an organization
   475  	// consumes:
   476  	// - application/json
   477  	// produces:
   478  	// - application/json
   479  	// parameters:
   480  	// - name: org
   481  	//   in: path
   482  	//   description: name of organization
   483  	//   type: string
   484  	//   required: true
   485  	// - name: body
   486  	//   in: body
   487  	//   schema:
   488  	//     "$ref": "#/definitions/CreateRepoOption"
   489  	// responses:
   490  	//   "201":
   491  	//     "$ref": "#/responses/Repository"
   492  	//   "400":
   493  	//     "$ref": "#/responses/error"
   494  	//   "404":
   495  	//     "$ref": "#/responses/notFound"
   496  	//   "403":
   497  	//     "$ref": "#/responses/forbidden"
   498  	opt := web.GetForm(ctx).(*api.CreateRepoOption)
   499  	org, err := organization.GetOrgByName(ctx, ctx.Params(":org"))
   500  	if err != nil {
   501  		if organization.IsErrOrgNotExist(err) {
   502  			ctx.Error(http.StatusUnprocessableEntity, "", err)
   503  		} else {
   504  			ctx.Error(http.StatusInternalServerError, "GetOrgByName", err)
   505  		}
   506  		return
   507  	}
   508  
   509  	if !organization.HasOrgOrUserVisible(ctx, org.AsUser(), ctx.Doer) {
   510  		ctx.NotFound("HasOrgOrUserVisible", nil)
   511  		return
   512  	}
   513  
   514  	if !ctx.Doer.IsAdmin {
   515  		canCreate, err := org.CanCreateOrgRepo(ctx, ctx.Doer.ID)
   516  		if err != nil {
   517  			ctx.Error(http.StatusInternalServerError, "CanCreateOrgRepo", err)
   518  			return
   519  		} else if !canCreate {
   520  			ctx.Error(http.StatusForbidden, "", "Given user is not allowed to create repository in organization.")
   521  			return
   522  		}
   523  	}
   524  	CreateUserRepo(ctx, org.AsUser(), *opt)
   525  }
   526  
   527  // Get one repository
   528  func Get(ctx *context.APIContext) {
   529  	// swagger:operation GET /repos/{owner}/{repo} repository repoGet
   530  	// ---
   531  	// summary: Get a repository
   532  	// produces:
   533  	// - application/json
   534  	// parameters:
   535  	// - name: owner
   536  	//   in: path
   537  	//   description: owner of the repo
   538  	//   type: string
   539  	//   required: true
   540  	// - name: repo
   541  	//   in: path
   542  	//   description: name of the repo
   543  	//   type: string
   544  	//   required: true
   545  	// responses:
   546  	//   "200":
   547  	//     "$ref": "#/responses/Repository"
   548  	//   "404":
   549  	//     "$ref": "#/responses/notFound"
   550  
   551  	if err := ctx.Repo.Repository.LoadAttributes(ctx); err != nil {
   552  		ctx.Error(http.StatusInternalServerError, "Repository.LoadAttributes", err)
   553  		return
   554  	}
   555  
   556  	ctx.JSON(http.StatusOK, convert.ToRepo(ctx, ctx.Repo.Repository, ctx.Repo.Permission))
   557  }
   558  
   559  // GetByID returns a single Repository
   560  func GetByID(ctx *context.APIContext) {
   561  	// swagger:operation GET /repositories/{id} repository repoGetByID
   562  	// ---
   563  	// summary: Get a repository by id
   564  	// produces:
   565  	// - application/json
   566  	// parameters:
   567  	// - name: id
   568  	//   in: path
   569  	//   description: id of the repo to get
   570  	//   type: integer
   571  	//   format: int64
   572  	//   required: true
   573  	// responses:
   574  	//   "200":
   575  	//     "$ref": "#/responses/Repository"
   576  	//   "404":
   577  	//     "$ref": "#/responses/notFound"
   578  
   579  	repo, err := repo_model.GetRepositoryByID(ctx, ctx.ParamsInt64(":id"))
   580  	if err != nil {
   581  		if repo_model.IsErrRepoNotExist(err) {
   582  			ctx.NotFound()
   583  		} else {
   584  			ctx.Error(http.StatusInternalServerError, "GetRepositoryByID", err)
   585  		}
   586  		return
   587  	}
   588  
   589  	permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
   590  	if err != nil {
   591  		ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
   592  		return
   593  	} else if !permission.HasAnyUnitAccess() {
   594  		ctx.NotFound()
   595  		return
   596  	}
   597  	ctx.JSON(http.StatusOK, convert.ToRepo(ctx, repo, permission))
   598  }
   599  
   600  // Edit edit repository properties
   601  func Edit(ctx *context.APIContext) {
   602  	// swagger:operation PATCH /repos/{owner}/{repo} repository repoEdit
   603  	// ---
   604  	// summary: Edit a repository's properties. Only fields that are set will be changed.
   605  	// produces:
   606  	// - application/json
   607  	// parameters:
   608  	// - name: owner
   609  	//   in: path
   610  	//   description: owner of the repo to edit
   611  	//   type: string
   612  	//   required: true
   613  	// - name: repo
   614  	//   in: path
   615  	//   description: name of the repo to edit
   616  	//   type: string
   617  	//   required: true
   618  	// - name: body
   619  	//   in: body
   620  	//   description: "Properties of a repo that you can edit"
   621  	//   schema:
   622  	//     "$ref": "#/definitions/EditRepoOption"
   623  	// responses:
   624  	//   "200":
   625  	//     "$ref": "#/responses/Repository"
   626  	//   "403":
   627  	//     "$ref": "#/responses/forbidden"
   628  	//   "404":
   629  	//     "$ref": "#/responses/notFound"
   630  	//   "422":
   631  	//     "$ref": "#/responses/validationError"
   632  
   633  	opts := *web.GetForm(ctx).(*api.EditRepoOption)
   634  
   635  	if err := updateBasicProperties(ctx, opts); err != nil {
   636  		return
   637  	}
   638  
   639  	if err := updateRepoUnits(ctx, opts); err != nil {
   640  		return
   641  	}
   642  
   643  	if opts.Archived != nil {
   644  		if err := updateRepoArchivedState(ctx, opts); err != nil {
   645  			return
   646  		}
   647  	}
   648  
   649  	if opts.MirrorInterval != nil || opts.EnablePrune != nil {
   650  		if err := updateMirror(ctx, opts); err != nil {
   651  			return
   652  		}
   653  	}
   654  
   655  	repo, err := repo_model.GetRepositoryByID(ctx, ctx.Repo.Repository.ID)
   656  	if err != nil {
   657  		ctx.InternalServerError(err)
   658  		return
   659  	}
   660  
   661  	ctx.JSON(http.StatusOK, convert.ToRepo(ctx, repo, ctx.Repo.Permission))
   662  }
   663  
   664  // updateBasicProperties updates the basic properties of a repo: Name, Description, Website and Visibility
   665  func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) error {
   666  	owner := ctx.Repo.Owner
   667  	repo := ctx.Repo.Repository
   668  	newRepoName := repo.Name
   669  	if opts.Name != nil {
   670  		newRepoName = *opts.Name
   671  	}
   672  	// Check if repository name has been changed and not just a case change
   673  	if repo.LowerName != strings.ToLower(newRepoName) {
   674  		if err := repo_service.ChangeRepositoryName(ctx, ctx.Doer, repo, newRepoName); err != nil {
   675  			switch {
   676  			case repo_model.IsErrRepoAlreadyExist(err):
   677  				ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("repo name is already taken [name: %s]", newRepoName), err)
   678  			case db.IsErrNameReserved(err):
   679  				ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("repo name is reserved [name: %s]", newRepoName), err)
   680  			case db.IsErrNamePatternNotAllowed(err):
   681  				ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("repo name's pattern is not allowed [name: %s, pattern: %s]", newRepoName, err.(db.ErrNamePatternNotAllowed).Pattern), err)
   682  			default:
   683  				ctx.Error(http.StatusUnprocessableEntity, "ChangeRepositoryName", err)
   684  			}
   685  			return err
   686  		}
   687  
   688  		log.Trace("Repository name changed: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newRepoName)
   689  	}
   690  	// Update the name in the repo object for the response
   691  	repo.Name = newRepoName
   692  	repo.LowerName = strings.ToLower(newRepoName)
   693  
   694  	if opts.Description != nil {
   695  		repo.Description = *opts.Description
   696  	}
   697  
   698  	if opts.Website != nil {
   699  		repo.Website = *opts.Website
   700  	}
   701  
   702  	visibilityChanged := false
   703  	if opts.Private != nil {
   704  		// Visibility of forked repository is forced sync with base repository.
   705  		if repo.IsFork {
   706  			if err := repo.GetBaseRepo(ctx); err != nil {
   707  				ctx.Error(http.StatusInternalServerError, "Unable to load base repository", err)
   708  				return err
   709  			}
   710  			*opts.Private = repo.BaseRepo.IsPrivate
   711  		}
   712  
   713  		visibilityChanged = repo.IsPrivate != *opts.Private
   714  		// when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public
   715  		if visibilityChanged && setting.Repository.ForcePrivate && !*opts.Private && !ctx.Doer.IsAdmin {
   716  			err := fmt.Errorf("cannot change private repository to public")
   717  			ctx.Error(http.StatusUnprocessableEntity, "Force Private enabled", err)
   718  			return err
   719  		}
   720  
   721  		repo.IsPrivate = *opts.Private
   722  	}
   723  
   724  	if opts.Template != nil {
   725  		repo.IsTemplate = *opts.Template
   726  	}
   727  
   728  	if ctx.Repo.GitRepo == nil && !repo.IsEmpty {
   729  		var err error
   730  		ctx.Repo.GitRepo, err = gitrepo.OpenRepository(ctx, repo)
   731  		if err != nil {
   732  			ctx.Error(http.StatusInternalServerError, "Unable to OpenRepository", err)
   733  			return err
   734  		}
   735  		defer ctx.Repo.GitRepo.Close()
   736  	}
   737  
   738  	// Default branch only updated if changed and exist or the repository is empty
   739  	if opts.DefaultBranch != nil && repo.DefaultBranch != *opts.DefaultBranch && (repo.IsEmpty || ctx.Repo.GitRepo.IsBranchExist(*opts.DefaultBranch)) {
   740  		if !repo.IsEmpty {
   741  			if err := gitrepo.SetDefaultBranch(ctx, ctx.Repo.Repository, *opts.DefaultBranch); err != nil {
   742  				if !git.IsErrUnsupportedVersion(err) {
   743  					ctx.Error(http.StatusInternalServerError, "SetDefaultBranch", err)
   744  					return err
   745  				}
   746  			}
   747  		}
   748  		repo.DefaultBranch = *opts.DefaultBranch
   749  	}
   750  
   751  	if err := repo_service.UpdateRepository(ctx, repo, visibilityChanged); err != nil {
   752  		ctx.Error(http.StatusInternalServerError, "UpdateRepository", err)
   753  		return err
   754  	}
   755  
   756  	log.Trace("Repository basic settings updated: %s/%s", owner.Name, repo.Name)
   757  	return nil
   758  }
   759  
   760  // updateRepoUnits updates repo units: Issue settings, Wiki settings, PR settings
   761  func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
   762  	owner := ctx.Repo.Owner
   763  	repo := ctx.Repo.Repository
   764  
   765  	var units []repo_model.RepoUnit
   766  	var deleteUnitTypes []unit_model.Type
   767  
   768  	currHasIssues := repo.UnitEnabled(ctx, unit_model.TypeIssues)
   769  	newHasIssues := currHasIssues
   770  	if opts.HasIssues != nil {
   771  		newHasIssues = *opts.HasIssues
   772  	}
   773  	if currHasIssues || newHasIssues {
   774  		if newHasIssues && opts.ExternalTracker != nil && !unit_model.TypeExternalTracker.UnitGlobalDisabled() {
   775  			// Check that values are valid
   776  			if !validation.IsValidExternalURL(opts.ExternalTracker.ExternalTrackerURL) {
   777  				err := fmt.Errorf("External tracker URL not valid")
   778  				ctx.Error(http.StatusUnprocessableEntity, "Invalid external tracker URL", err)
   779  				return err
   780  			}
   781  			if len(opts.ExternalTracker.ExternalTrackerFormat) != 0 && !validation.IsValidExternalTrackerURLFormat(opts.ExternalTracker.ExternalTrackerFormat) {
   782  				err := fmt.Errorf("External tracker URL format not valid")
   783  				ctx.Error(http.StatusUnprocessableEntity, "Invalid external tracker URL format", err)
   784  				return err
   785  			}
   786  
   787  			units = append(units, repo_model.RepoUnit{
   788  				RepoID: repo.ID,
   789  				Type:   unit_model.TypeExternalTracker,
   790  				Config: &repo_model.ExternalTrackerConfig{
   791  					ExternalTrackerURL:           opts.ExternalTracker.ExternalTrackerURL,
   792  					ExternalTrackerFormat:        opts.ExternalTracker.ExternalTrackerFormat,
   793  					ExternalTrackerStyle:         opts.ExternalTracker.ExternalTrackerStyle,
   794  					ExternalTrackerRegexpPattern: opts.ExternalTracker.ExternalTrackerRegexpPattern,
   795  				},
   796  			})
   797  			deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues)
   798  		} else if newHasIssues && opts.ExternalTracker == nil && !unit_model.TypeIssues.UnitGlobalDisabled() {
   799  			// Default to built-in tracker
   800  			var config *repo_model.IssuesConfig
   801  
   802  			if opts.InternalTracker != nil {
   803  				config = &repo_model.IssuesConfig{
   804  					EnableTimetracker:                opts.InternalTracker.EnableTimeTracker,
   805  					AllowOnlyContributorsToTrackTime: opts.InternalTracker.AllowOnlyContributorsToTrackTime,
   806  					EnableDependencies:               opts.InternalTracker.EnableIssueDependencies,
   807  				}
   808  			} else if unit, err := repo.GetUnit(ctx, unit_model.TypeIssues); err != nil {
   809  				// Unit type doesn't exist so we make a new config file with default values
   810  				config = &repo_model.IssuesConfig{
   811  					EnableTimetracker:                true,
   812  					AllowOnlyContributorsToTrackTime: true,
   813  					EnableDependencies:               true,
   814  				}
   815  			} else {
   816  				config = unit.IssuesConfig()
   817  			}
   818  
   819  			units = append(units, repo_model.RepoUnit{
   820  				RepoID: repo.ID,
   821  				Type:   unit_model.TypeIssues,
   822  				Config: config,
   823  			})
   824  			deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
   825  		} else if !newHasIssues {
   826  			if !unit_model.TypeExternalTracker.UnitGlobalDisabled() {
   827  				deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
   828  			}
   829  			if !unit_model.TypeIssues.UnitGlobalDisabled() {
   830  				deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues)
   831  			}
   832  		}
   833  	}
   834  
   835  	currHasWiki := repo.UnitEnabled(ctx, unit_model.TypeWiki)
   836  	newHasWiki := currHasWiki
   837  	if opts.HasWiki != nil {
   838  		newHasWiki = *opts.HasWiki
   839  	}
   840  	if currHasWiki || newHasWiki {
   841  		if newHasWiki && opts.ExternalWiki != nil && !unit_model.TypeExternalWiki.UnitGlobalDisabled() {
   842  			// Check that values are valid
   843  			if !validation.IsValidExternalURL(opts.ExternalWiki.ExternalWikiURL) {
   844  				err := fmt.Errorf("External wiki URL not valid")
   845  				ctx.Error(http.StatusUnprocessableEntity, "", "Invalid external wiki URL")
   846  				return err
   847  			}
   848  
   849  			units = append(units, repo_model.RepoUnit{
   850  				RepoID: repo.ID,
   851  				Type:   unit_model.TypeExternalWiki,
   852  				Config: &repo_model.ExternalWikiConfig{
   853  					ExternalWikiURL: opts.ExternalWiki.ExternalWikiURL,
   854  				},
   855  			})
   856  			deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki)
   857  		} else if newHasWiki && opts.ExternalWiki == nil && !unit_model.TypeWiki.UnitGlobalDisabled() {
   858  			config := &repo_model.UnitConfig{}
   859  			units = append(units, repo_model.RepoUnit{
   860  				RepoID: repo.ID,
   861  				Type:   unit_model.TypeWiki,
   862  				Config: config,
   863  			})
   864  			deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki)
   865  		} else if !newHasWiki {
   866  			if !unit_model.TypeExternalWiki.UnitGlobalDisabled() {
   867  				deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki)
   868  			}
   869  			if !unit_model.TypeWiki.UnitGlobalDisabled() {
   870  				deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki)
   871  			}
   872  		}
   873  	}
   874  
   875  	currHasPullRequests := repo.UnitEnabled(ctx, unit_model.TypePullRequests)
   876  	newHasPullRequests := currHasPullRequests
   877  	if opts.HasPullRequests != nil {
   878  		newHasPullRequests = *opts.HasPullRequests
   879  	}
   880  	if currHasPullRequests || newHasPullRequests {
   881  		if newHasPullRequests && !unit_model.TypePullRequests.UnitGlobalDisabled() {
   882  			// We do allow setting individual PR settings through the API, so
   883  			// we get the config settings and then set them
   884  			// if those settings were provided in the opts.
   885  			unit, err := repo.GetUnit(ctx, unit_model.TypePullRequests)
   886  			var config *repo_model.PullRequestsConfig
   887  			if err != nil {
   888  				// Unit type doesn't exist so we make a new config file with default values
   889  				config = &repo_model.PullRequestsConfig{
   890  					IgnoreWhitespaceConflicts:     false,
   891  					AllowMerge:                    true,
   892  					AllowRebase:                   true,
   893  					AllowRebaseMerge:              true,
   894  					AllowSquash:                   true,
   895  					AllowFastForwardOnly:          true,
   896  					AllowManualMerge:              true,
   897  					AutodetectManualMerge:         false,
   898  					AllowRebaseUpdate:             true,
   899  					DefaultDeleteBranchAfterMerge: false,
   900  					DefaultMergeStyle:             repo_model.MergeStyleMerge,
   901  					DefaultAllowMaintainerEdit:    false,
   902  				}
   903  			} else {
   904  				config = unit.PullRequestsConfig()
   905  			}
   906  
   907  			if opts.IgnoreWhitespaceConflicts != nil {
   908  				config.IgnoreWhitespaceConflicts = *opts.IgnoreWhitespaceConflicts
   909  			}
   910  			if opts.AllowMerge != nil {
   911  				config.AllowMerge = *opts.AllowMerge
   912  			}
   913  			if opts.AllowRebase != nil {
   914  				config.AllowRebase = *opts.AllowRebase
   915  			}
   916  			if opts.AllowRebaseMerge != nil {
   917  				config.AllowRebaseMerge = *opts.AllowRebaseMerge
   918  			}
   919  			if opts.AllowSquash != nil {
   920  				config.AllowSquash = *opts.AllowSquash
   921  			}
   922  			if opts.AllowFastForwardOnly != nil {
   923  				config.AllowFastForwardOnly = *opts.AllowFastForwardOnly
   924  			}
   925  			if opts.AllowManualMerge != nil {
   926  				config.AllowManualMerge = *opts.AllowManualMerge
   927  			}
   928  			if opts.AutodetectManualMerge != nil {
   929  				config.AutodetectManualMerge = *opts.AutodetectManualMerge
   930  			}
   931  			if opts.AllowRebaseUpdate != nil {
   932  				config.AllowRebaseUpdate = *opts.AllowRebaseUpdate
   933  			}
   934  			if opts.DefaultDeleteBranchAfterMerge != nil {
   935  				config.DefaultDeleteBranchAfterMerge = *opts.DefaultDeleteBranchAfterMerge
   936  			}
   937  			if opts.DefaultMergeStyle != nil {
   938  				config.DefaultMergeStyle = repo_model.MergeStyle(*opts.DefaultMergeStyle)
   939  			}
   940  			if opts.DefaultAllowMaintainerEdit != nil {
   941  				config.DefaultAllowMaintainerEdit = *opts.DefaultAllowMaintainerEdit
   942  			}
   943  
   944  			units = append(units, repo_model.RepoUnit{
   945  				RepoID: repo.ID,
   946  				Type:   unit_model.TypePullRequests,
   947  				Config: config,
   948  			})
   949  		} else if !newHasPullRequests && !unit_model.TypePullRequests.UnitGlobalDisabled() {
   950  			deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePullRequests)
   951  		}
   952  	}
   953  
   954  	currHasProjects := repo.UnitEnabled(ctx, unit_model.TypeProjects)
   955  	newHasProjects := currHasProjects
   956  	if opts.HasProjects != nil {
   957  		newHasProjects = *opts.HasProjects
   958  	}
   959  	if currHasProjects || newHasProjects {
   960  		if newHasProjects && !unit_model.TypeProjects.UnitGlobalDisabled() {
   961  			unit, err := repo.GetUnit(ctx, unit_model.TypeProjects)
   962  			var config *repo_model.ProjectsConfig
   963  			if err != nil {
   964  				config = &repo_model.ProjectsConfig{
   965  					ProjectsMode: repo_model.ProjectsModeAll,
   966  				}
   967  			} else {
   968  				config = unit.ProjectsConfig()
   969  			}
   970  
   971  			if opts.ProjectsMode != nil {
   972  				config.ProjectsMode = repo_model.ProjectsMode(*opts.ProjectsMode)
   973  			}
   974  
   975  			units = append(units, repo_model.RepoUnit{
   976  				RepoID: repo.ID,
   977  				Type:   unit_model.TypeProjects,
   978  				Config: config,
   979  			})
   980  		} else if !newHasProjects && !unit_model.TypeProjects.UnitGlobalDisabled() {
   981  			deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects)
   982  		}
   983  	}
   984  
   985  	if opts.HasReleases != nil && !unit_model.TypeReleases.UnitGlobalDisabled() {
   986  		if *opts.HasReleases {
   987  			units = append(units, repo_model.RepoUnit{
   988  				RepoID: repo.ID,
   989  				Type:   unit_model.TypeReleases,
   990  			})
   991  		} else {
   992  			deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeReleases)
   993  		}
   994  	}
   995  
   996  	if opts.HasPackages != nil && !unit_model.TypePackages.UnitGlobalDisabled() {
   997  		if *opts.HasPackages {
   998  			units = append(units, repo_model.RepoUnit{
   999  				RepoID: repo.ID,
  1000  				Type:   unit_model.TypePackages,
  1001  			})
  1002  		} else {
  1003  			deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePackages)
  1004  		}
  1005  	}
  1006  
  1007  	if opts.HasActions != nil && !unit_model.TypeActions.UnitGlobalDisabled() {
  1008  		if *opts.HasActions {
  1009  			units = append(units, repo_model.RepoUnit{
  1010  				RepoID: repo.ID,
  1011  				Type:   unit_model.TypeActions,
  1012  			})
  1013  		} else {
  1014  			deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeActions)
  1015  		}
  1016  	}
  1017  
  1018  	if len(units)+len(deleteUnitTypes) > 0 {
  1019  		if err := repo_service.UpdateRepositoryUnits(ctx, repo, units, deleteUnitTypes); err != nil {
  1020  			ctx.Error(http.StatusInternalServerError, "UpdateRepositoryUnits", err)
  1021  			return err
  1022  		}
  1023  	}
  1024  
  1025  	log.Trace("Repository advanced settings updated: %s/%s", owner.Name, repo.Name)
  1026  	return nil
  1027  }
  1028  
  1029  // updateRepoArchivedState updates repo's archive state
  1030  func updateRepoArchivedState(ctx *context.APIContext, opts api.EditRepoOption) error {
  1031  	repo := ctx.Repo.Repository
  1032  	// archive / un-archive
  1033  	if opts.Archived != nil {
  1034  		if repo.IsMirror {
  1035  			err := fmt.Errorf("repo is a mirror, cannot archive/un-archive")
  1036  			ctx.Error(http.StatusUnprocessableEntity, err.Error(), err)
  1037  			return err
  1038  		}
  1039  		if *opts.Archived {
  1040  			if err := repo_model.SetArchiveRepoState(ctx, repo, *opts.Archived); err != nil {
  1041  				log.Error("Tried to archive a repo: %s", err)
  1042  				ctx.Error(http.StatusInternalServerError, "ArchiveRepoState", err)
  1043  				return err
  1044  			}
  1045  			if err := actions_model.CleanRepoScheduleTasks(ctx, repo); err != nil {
  1046  				log.Error("CleanRepoScheduleTasks for archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
  1047  			}
  1048  			log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
  1049  		} else {
  1050  			if err := repo_model.SetArchiveRepoState(ctx, repo, *opts.Archived); err != nil {
  1051  				log.Error("Tried to un-archive a repo: %s", err)
  1052  				ctx.Error(http.StatusInternalServerError, "ArchiveRepoState", err)
  1053  				return err
  1054  			}
  1055  			if ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypeActions) {
  1056  				if err := actions_service.DetectAndHandleSchedules(ctx, repo); err != nil {
  1057  					log.Error("DetectAndHandleSchedules for un-archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
  1058  				}
  1059  			}
  1060  			log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
  1061  		}
  1062  	}
  1063  	return nil
  1064  }
  1065  
  1066  // updateMirror updates a repo's mirror Interval and EnablePrune
  1067  func updateMirror(ctx *context.APIContext, opts api.EditRepoOption) error {
  1068  	repo := ctx.Repo.Repository
  1069  
  1070  	// Skip this update if the repo is not a mirror, do not return error.
  1071  	// Because reporting errors only makes the logic more complex&fragile, it doesn't really help end users.
  1072  	if !repo.IsMirror {
  1073  		return nil
  1074  	}
  1075  
  1076  	// get the mirror from the repo
  1077  	mirror, err := repo_model.GetMirrorByRepoID(ctx, repo.ID)
  1078  	if err != nil {
  1079  		log.Error("Failed to get mirror: %s", err)
  1080  		ctx.Error(http.StatusInternalServerError, "MirrorInterval", err)
  1081  		return err
  1082  	}
  1083  
  1084  	// update MirrorInterval
  1085  	if opts.MirrorInterval != nil {
  1086  		// MirrorInterval should be a duration
  1087  		interval, err := time.ParseDuration(*opts.MirrorInterval)
  1088  		if err != nil {
  1089  			log.Error("Wrong format for MirrorInternal Sent: %s", err)
  1090  			ctx.Error(http.StatusUnprocessableEntity, "MirrorInterval", err)
  1091  			return err
  1092  		}
  1093  
  1094  		// Ensure the provided duration is not too short
  1095  		if interval != 0 && interval < setting.Mirror.MinInterval {
  1096  			err := fmt.Errorf("invalid mirror interval: %s is below minimum interval: %s", interval, setting.Mirror.MinInterval)
  1097  			ctx.Error(http.StatusUnprocessableEntity, "MirrorInterval", err)
  1098  			return err
  1099  		}
  1100  
  1101  		mirror.Interval = interval
  1102  		mirror.Repo = repo
  1103  		mirror.ScheduleNextUpdate()
  1104  		log.Trace("Repository %s Mirror[%d] Set Interval: %s NextUpdateUnix: %s", repo.FullName(), mirror.ID, interval, mirror.NextUpdateUnix)
  1105  	}
  1106  
  1107  	// update EnablePrune
  1108  	if opts.EnablePrune != nil {
  1109  		mirror.EnablePrune = *opts.EnablePrune
  1110  		log.Trace("Repository %s Mirror[%d] Set EnablePrune: %t", repo.FullName(), mirror.ID, mirror.EnablePrune)
  1111  	}
  1112  
  1113  	// finally update the mirror in the DB
  1114  	if err := repo_model.UpdateMirror(ctx, mirror); err != nil {
  1115  		log.Error("Failed to Set Mirror Interval: %s", err)
  1116  		ctx.Error(http.StatusUnprocessableEntity, "MirrorInterval", err)
  1117  		return err
  1118  	}
  1119  
  1120  	return nil
  1121  }
  1122  
  1123  // Delete one repository
  1124  func Delete(ctx *context.APIContext) {
  1125  	// swagger:operation DELETE /repos/{owner}/{repo} repository repoDelete
  1126  	// ---
  1127  	// summary: Delete a repository
  1128  	// produces:
  1129  	// - application/json
  1130  	// parameters:
  1131  	// - name: owner
  1132  	//   in: path
  1133  	//   description: owner of the repo to delete
  1134  	//   type: string
  1135  	//   required: true
  1136  	// - name: repo
  1137  	//   in: path
  1138  	//   description: name of the repo to delete
  1139  	//   type: string
  1140  	//   required: true
  1141  	// responses:
  1142  	//   "204":
  1143  	//     "$ref": "#/responses/empty"
  1144  	//   "403":
  1145  	//     "$ref": "#/responses/forbidden"
  1146  	//   "404":
  1147  	//     "$ref": "#/responses/notFound"
  1148  
  1149  	owner := ctx.Repo.Owner
  1150  	repo := ctx.Repo.Repository
  1151  
  1152  	canDelete, err := repo_module.CanUserDelete(ctx, repo, ctx.Doer)
  1153  	if err != nil {
  1154  		ctx.Error(http.StatusInternalServerError, "CanUserDelete", err)
  1155  		return
  1156  	} else if !canDelete {
  1157  		ctx.Error(http.StatusForbidden, "", "Given user is not owner of organization.")
  1158  		return
  1159  	}
  1160  
  1161  	if ctx.Repo.GitRepo != nil {
  1162  		ctx.Repo.GitRepo.Close()
  1163  	}
  1164  
  1165  	if err := repo_service.DeleteRepository(ctx, ctx.Doer, repo, true); err != nil {
  1166  		ctx.Error(http.StatusInternalServerError, "DeleteRepository", err)
  1167  		return
  1168  	}
  1169  
  1170  	log.Trace("Repository deleted: %s/%s", owner.Name, repo.Name)
  1171  	ctx.Status(http.StatusNoContent)
  1172  }
  1173  
  1174  // GetIssueTemplates returns the issue templates for a repository
  1175  func GetIssueTemplates(ctx *context.APIContext) {
  1176  	// swagger:operation GET /repos/{owner}/{repo}/issue_templates repository repoGetIssueTemplates
  1177  	// ---
  1178  	// summary: Get available issue templates for a repository
  1179  	// produces:
  1180  	// - application/json
  1181  	// parameters:
  1182  	// - name: owner
  1183  	//   in: path
  1184  	//   description: owner of the repo
  1185  	//   type: string
  1186  	//   required: true
  1187  	// - name: repo
  1188  	//   in: path
  1189  	//   description: name of the repo
  1190  	//   type: string
  1191  	//   required: true
  1192  	// responses:
  1193  	//   "200":
  1194  	//     "$ref": "#/responses/IssueTemplates"
  1195  	//   "404":
  1196  	//     "$ref": "#/responses/notFound"
  1197  	ret := issue.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
  1198  	if cnt := len(ret.TemplateErrors); cnt != 0 {
  1199  		ctx.Resp.Header().Add("X-Gitea-Warning", "error occurs when parsing issue template: count="+strconv.Itoa(cnt))
  1200  	}
  1201  	ctx.JSON(http.StatusOK, ret.IssueTemplates)
  1202  }
  1203  
  1204  // GetIssueConfig returns the issue config for a repo
  1205  func GetIssueConfig(ctx *context.APIContext) {
  1206  	// swagger:operation GET /repos/{owner}/{repo}/issue_config repository repoGetIssueConfig
  1207  	// ---
  1208  	// summary: Returns the issue config for a repo
  1209  	// produces:
  1210  	// - application/json
  1211  	// parameters:
  1212  	// - name: owner
  1213  	//   in: path
  1214  	//   description: owner of the repo
  1215  	//   type: string
  1216  	//   required: true
  1217  	// - name: repo
  1218  	//   in: path
  1219  	//   description: name of the repo
  1220  	//   type: string
  1221  	//   required: true
  1222  	// responses:
  1223  	//   "200":
  1224  	//     "$ref": "#/responses/RepoIssueConfig"
  1225  	//   "404":
  1226  	//     "$ref": "#/responses/notFound"
  1227  	issueConfig, _ := issue.GetTemplateConfigFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
  1228  	ctx.JSON(http.StatusOK, issueConfig)
  1229  }
  1230  
  1231  // ValidateIssueConfig returns validation errors for the issue config
  1232  func ValidateIssueConfig(ctx *context.APIContext) {
  1233  	// swagger:operation GET /repos/{owner}/{repo}/issue_config/validate repository repoValidateIssueConfig
  1234  	// ---
  1235  	// summary: Returns the validation information for a issue config
  1236  	// produces:
  1237  	// - application/json
  1238  	// parameters:
  1239  	// - name: owner
  1240  	//   in: path
  1241  	//   description: owner of the repo
  1242  	//   type: string
  1243  	//   required: true
  1244  	// - name: repo
  1245  	//   in: path
  1246  	//   description: name of the repo
  1247  	//   type: string
  1248  	//   required: true
  1249  	// responses:
  1250  	//   "200":
  1251  	//     "$ref": "#/responses/RepoIssueConfigValidation"
  1252  	//   "404":
  1253  	//     "$ref": "#/responses/notFound"
  1254  	_, err := issue.GetTemplateConfigFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
  1255  
  1256  	if err == nil {
  1257  		ctx.JSON(http.StatusOK, api.IssueConfigValidation{Valid: true, Message: ""})
  1258  	} else {
  1259  		ctx.JSON(http.StatusOK, api.IssueConfigValidation{Valid: false, Message: err.Error()})
  1260  	}
  1261  }
  1262  
  1263  func ListRepoActivityFeeds(ctx *context.APIContext) {
  1264  	// swagger:operation GET /repos/{owner}/{repo}/activities/feeds repository repoListActivityFeeds
  1265  	// ---
  1266  	// summary: List a repository's activity feeds
  1267  	// produces:
  1268  	// - application/json
  1269  	// parameters:
  1270  	// - name: owner
  1271  	//   in: path
  1272  	//   description: owner of the repo
  1273  	//   type: string
  1274  	//   required: true
  1275  	// - name: repo
  1276  	//   in: path
  1277  	//   description: name of the repo
  1278  	//   type: string
  1279  	//   required: true
  1280  	// - name: date
  1281  	//   in: query
  1282  	//   description: the date of the activities to be found
  1283  	//   type: string
  1284  	//   format: date
  1285  	// - name: page
  1286  	//   in: query
  1287  	//   description: page number of results to return (1-based)
  1288  	//   type: integer
  1289  	// - name: limit
  1290  	//   in: query
  1291  	//   description: page size of results
  1292  	//   type: integer
  1293  	// responses:
  1294  	//   "200":
  1295  	//     "$ref": "#/responses/ActivityFeedsList"
  1296  	//   "404":
  1297  	//     "$ref": "#/responses/notFound"
  1298  
  1299  	listOptions := utils.GetListOptions(ctx)
  1300  
  1301  	opts := activities_model.GetFeedsOptions{
  1302  		RequestedRepo:  ctx.Repo.Repository,
  1303  		Actor:          ctx.Doer,
  1304  		IncludePrivate: true,
  1305  		Date:           ctx.FormString("date"),
  1306  		ListOptions:    listOptions,
  1307  	}
  1308  
  1309  	feeds, count, err := activities_model.GetFeeds(ctx, opts)
  1310  	if err != nil {
  1311  		ctx.Error(http.StatusInternalServerError, "GetFeeds", err)
  1312  		return
  1313  	}
  1314  	ctx.SetTotalCountHeader(count)
  1315  
  1316  	ctx.JSON(http.StatusOK, convert.ToActivities(ctx, feeds, ctx.Doer))
  1317  }