diff options
Diffstat (limited to '')
-rw-r--r-- | routers/api/v1/repo/repo.go | 1338 |
1 files changed, 1338 insertions, 0 deletions
diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go new file mode 100644 index 0000000..f39e582 --- /dev/null +++ b/routers/api/v1/repo/repo.go @@ -0,0 +1,1338 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Copyright 2018 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "fmt" + "net/http" + "slices" + "strings" + "time" + + actions_model "code.gitea.io/gitea/models/actions" + activities_model "code.gitea.io/gitea/models/activities" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/organization" + "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" + quota_model "code.gitea.io/gitea/models/quota" + repo_model "code.gitea.io/gitea/models/repo" + unit_model "code.gitea.io/gitea/models/unit" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" + "code.gitea.io/gitea/modules/label" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" + repo_module "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/validation" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/api/v1/utils" + actions_service "code.gitea.io/gitea/services/actions" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/convert" + "code.gitea.io/gitea/services/issue" + repo_service "code.gitea.io/gitea/services/repository" + wiki_service "code.gitea.io/gitea/services/wiki" +) + +// Search repositories via options +func Search(ctx *context.APIContext) { + // swagger:operation GET /repos/search repository repoSearch + // --- + // summary: Search for repositories + // produces: + // - application/json + // parameters: + // - name: q + // in: query + // description: keyword + // type: string + // - name: topic + // in: query + // description: Limit search to repositories with keyword as topic + // type: boolean + // - name: includeDesc + // in: query + // description: include search of keyword within repository description + // type: boolean + // - name: uid + // in: query + // description: search only for repos that the user with the given id owns or contributes to + // type: integer + // format: int64 + // - name: priority_owner_id + // in: query + // description: repo owner to prioritize in the results + // type: integer + // format: int64 + // - name: team_id + // in: query + // description: search only for repos that belong to the given team id + // type: integer + // format: int64 + // - name: starredBy + // in: query + // description: search only for repos that the user with the given id has starred + // type: integer + // format: int64 + // - name: private + // in: query + // description: include private repositories this user has access to (defaults to true) + // type: boolean + // - name: is_private + // in: query + // description: show only pubic, private or all repositories (defaults to all) + // type: boolean + // - name: template + // in: query + // description: include template repositories this user has access to (defaults to true) + // type: boolean + // - name: archived + // in: query + // description: show only archived, non-archived or all repositories (defaults to all) + // type: boolean + // - name: mode + // in: query + // description: type of repository to search for. Supported values are + // "fork", "source", "mirror" and "collaborative" + // type: string + // - name: exclusive + // in: query + // description: if `uid` is given, search only for repos that the user owns + // type: boolean + // - name: sort + // in: query + // description: sort repos by attribute. Supported values are + // "alpha", "created", "updated", "size", "git_size", "lfs_size", "stars", "forks" and "id". + // Default is "alpha" + // type: string + // - name: order + // in: query + // description: sort order, either "asc" (ascending) or "desc" (descending). + // Default is "asc", ignored if "sort" is not specified. + // type: string + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/SearchResults" + // "422": + // "$ref": "#/responses/validationError" + + private := ctx.IsSigned && (ctx.FormString("private") == "" || ctx.FormBool("private")) + if ctx.PublicOnly { + private = false + } + + opts := &repo_model.SearchRepoOptions{ + ListOptions: utils.GetListOptions(ctx), + Actor: ctx.Doer, + Keyword: ctx.FormTrim("q"), + OwnerID: ctx.FormInt64("uid"), + PriorityOwnerID: ctx.FormInt64("priority_owner_id"), + TeamID: ctx.FormInt64("team_id"), + TopicOnly: ctx.FormBool("topic"), + Collaborate: optional.None[bool](), + Private: private, + Template: optional.None[bool](), + StarredByID: ctx.FormInt64("starredBy"), + IncludeDescription: ctx.FormBool("includeDesc"), + } + + if ctx.FormString("template") != "" { + opts.Template = optional.Some(ctx.FormBool("template")) + } + + if ctx.FormBool("exclusive") { + opts.Collaborate = optional.Some(false) + } + + mode := ctx.FormString("mode") + switch mode { + case "source": + opts.Fork = optional.Some(false) + opts.Mirror = optional.Some(false) + case "fork": + opts.Fork = optional.Some(true) + case "mirror": + opts.Mirror = optional.Some(true) + case "collaborative": + opts.Mirror = optional.Some(false) + opts.Collaborate = optional.Some(true) + case "": + default: + ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid search mode: \"%s\"", mode)) + return + } + + if ctx.FormString("archived") != "" { + opts.Archived = optional.Some(ctx.FormBool("archived")) + } + + if ctx.FormString("is_private") != "" { + opts.IsPrivate = optional.Some(ctx.FormBool("is_private")) + } + + sortMode := ctx.FormString("sort") + if len(sortMode) > 0 { + sortOrder := ctx.FormString("order") + if len(sortOrder) == 0 { + sortOrder = "asc" + } + if searchModeMap, ok := repo_model.OrderByMap[sortOrder]; ok { + if orderBy, ok := searchModeMap[sortMode]; ok { + opts.OrderBy = orderBy + } else { + ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid sort mode: \"%s\"", sortMode)) + return + } + } else { + ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid sort order: \"%s\"", sortOrder)) + return + } + } + + var err error + repos, count, err := repo_model.SearchRepository(ctx, opts) + if err != nil { + ctx.JSON(http.StatusInternalServerError, api.SearchError{ + OK: false, + Error: err.Error(), + }) + return + } + + results := make([]*api.Repository, len(repos)) + for i, repo := range repos { + if err = repo.LoadOwner(ctx); err != nil { + ctx.JSON(http.StatusInternalServerError, api.SearchError{ + OK: false, + Error: err.Error(), + }) + return + } + permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) + if err != nil { + ctx.JSON(http.StatusInternalServerError, api.SearchError{ + OK: false, + Error: err.Error(), + }) + } + results[i] = convert.ToRepo(ctx, repo, permission) + } + ctx.SetLinkHeader(int(count), opts.PageSize) + ctx.SetTotalCountHeader(count) + ctx.JSON(http.StatusOK, api.SearchResults{ + OK: true, + Data: results, + }) +} + +// CreateUserRepo create a repository for a user +func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.CreateRepoOption) { + if opt.AutoInit && opt.Readme == "" { + opt.Readme = "Default" + } + + // If the readme template does not exist, a 400 will be returned. + if opt.AutoInit && len(opt.Readme) > 0 && !slices.Contains(repo_module.Readmes, opt.Readme) { + ctx.Error(http.StatusBadRequest, "", fmt.Errorf("readme template does not exist, available templates: %v", repo_module.Readmes)) + return + } + + repo, err := repo_service.CreateRepository(ctx, ctx.Doer, owner, repo_service.CreateRepoOptions{ + Name: opt.Name, + Description: opt.Description, + IssueLabels: opt.IssueLabels, + Gitignores: opt.Gitignores, + License: opt.License, + Readme: opt.Readme, + IsPrivate: opt.Private || setting.Repository.ForcePrivate, + AutoInit: opt.AutoInit, + DefaultBranch: opt.DefaultBranch, + TrustModel: repo_model.ToTrustModel(opt.TrustModel), + IsTemplate: opt.Template, + ObjectFormatName: opt.ObjectFormatName, + }) + if err != nil { + if repo_model.IsErrRepoAlreadyExist(err) { + ctx.Error(http.StatusConflict, "", "The repository with the same name already exists.") + } else if db.IsErrNameReserved(err) || + db.IsErrNamePatternNotAllowed(err) || + label.IsErrTemplateLoad(err) { + ctx.Error(http.StatusUnprocessableEntity, "", err) + } else { + ctx.Error(http.StatusInternalServerError, "CreateRepository", err) + } + return + } + + // reload repo from db to get a real state after creation + repo, err = repo_model.GetRepositoryByID(ctx, repo.ID) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetRepositoryByID", err) + } + + ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner})) +} + +// Create one repository of mine +func Create(ctx *context.APIContext) { + // swagger:operation POST /user/repos repository user createCurrentUserRepo + // --- + // summary: Create a repository + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/CreateRepoOption" + // responses: + // "201": + // "$ref": "#/responses/Repository" + // "400": + // "$ref": "#/responses/error" + // "401": + // "$ref": "#/responses/unauthorized" + // "403": + // "$ref": "#/responses/forbidden" + // "409": + // description: The repository with the same name already exists. + // "413": + // "$ref": "#/responses/quotaExceeded" + // "422": + // "$ref": "#/responses/validationError" + opt := web.GetForm(ctx).(*api.CreateRepoOption) + if ctx.Doer.IsOrganization() { + // Shouldn't reach this condition, but just in case. + ctx.Error(http.StatusUnprocessableEntity, "", "not allowed creating repository for organization") + return + } + CreateUserRepo(ctx, ctx.Doer, *opt) +} + +// Generate Create a repository using a template +func Generate(ctx *context.APIContext) { + // swagger:operation POST /repos/{template_owner}/{template_repo}/generate repository generateRepo + // --- + // summary: Create a repository using a template + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: template_owner + // in: path + // description: name of the template repository owner + // type: string + // required: true + // - name: template_repo + // in: path + // description: name of the template repository + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/GenerateRepoOption" + // responses: + // "201": + // "$ref": "#/responses/Repository" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + // "409": + // description: The repository with the same name already exists. + // "413": + // "$ref": "#/responses/quotaExceeded" + // "422": + // "$ref": "#/responses/validationError" + form := web.GetForm(ctx).(*api.GenerateRepoOption) + + if !ctx.Repo.Repository.IsTemplate { + ctx.Error(http.StatusUnprocessableEntity, "", "this is not a template repo") + return + } + + if ctx.Doer.IsOrganization() { + ctx.Error(http.StatusUnprocessableEntity, "", "not allowed creating repository for organization") + return + } + + opts := repo_service.GenerateRepoOptions{ + Name: form.Name, + DefaultBranch: form.DefaultBranch, + Description: form.Description, + Private: form.Private || setting.Repository.ForcePrivate, + GitContent: form.GitContent, + Topics: form.Topics, + GitHooks: form.GitHooks, + Webhooks: form.Webhooks, + Avatar: form.Avatar, + IssueLabels: form.Labels, + ProtectedBranch: form.ProtectedBranch, + } + + if !opts.IsValid() { + ctx.Error(http.StatusUnprocessableEntity, "", "must select at least one template item") + return + } + + ctxUser := ctx.Doer + var err error + if form.Owner != ctxUser.Name { + ctxUser, err = user_model.GetUserByName(ctx, form.Owner) + if err != nil { + if user_model.IsErrUserNotExist(err) { + ctx.JSON(http.StatusNotFound, map[string]any{ + "error": "request owner `" + form.Owner + "` does not exist", + }) + return + } + + ctx.Error(http.StatusInternalServerError, "GetUserByName", err) + return + } + + if !ctx.Doer.IsAdmin && !ctxUser.IsOrganization() { + ctx.Error(http.StatusForbidden, "", "Only admin can generate repository for other user.") + return + } + + if !ctx.Doer.IsAdmin { + canCreate, err := organization.OrgFromUser(ctxUser).CanCreateOrgRepo(ctx, ctx.Doer.ID) + if err != nil { + ctx.ServerError("CanCreateOrgRepo", err) + return + } else if !canCreate { + ctx.Error(http.StatusForbidden, "", "Given user is not allowed to create repository in organization.") + return + } + } + } + + if !ctx.CheckQuota(quota_model.LimitSubjectSizeReposAll, ctxUser.ID, ctxUser.Name) { + return + } + + repo, err := repo_service.GenerateRepository(ctx, ctx.Doer, ctxUser, ctx.Repo.Repository, opts) + if err != nil { + if repo_model.IsErrRepoAlreadyExist(err) { + ctx.Error(http.StatusConflict, "", "The repository with the same name already exists.") + } else if db.IsErrNameReserved(err) || + db.IsErrNamePatternNotAllowed(err) { + ctx.Error(http.StatusUnprocessableEntity, "", err) + } else { + ctx.Error(http.StatusInternalServerError, "CreateRepository", err) + } + return + } + log.Trace("Repository generated [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name) + + ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner})) +} + +// CreateOrgRepoDeprecated create one repository of the organization +func CreateOrgRepoDeprecated(ctx *context.APIContext) { + // swagger:operation POST /org/{org}/repos organization createOrgRepoDeprecated + // --- + // summary: Create a repository in an organization + // deprecated: true + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of organization + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/CreateRepoOption" + // responses: + // "201": + // "$ref": "#/responses/Repository" + // "422": + // "$ref": "#/responses/validationError" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + + CreateOrgRepo(ctx) +} + +// CreateOrgRepo create one repository of the organization +func CreateOrgRepo(ctx *context.APIContext) { + // swagger:operation POST /orgs/{org}/repos organization createOrgRepo + // --- + // summary: Create a repository in an organization + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of organization + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/CreateRepoOption" + // responses: + // "201": + // "$ref": "#/responses/Repository" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + // "403": + // "$ref": "#/responses/forbidden" + opt := web.GetForm(ctx).(*api.CreateRepoOption) + org, err := organization.GetOrgByName(ctx, ctx.Params(":org")) + if err != nil { + if organization.IsErrOrgNotExist(err) { + ctx.Error(http.StatusUnprocessableEntity, "", err) + } else { + ctx.Error(http.StatusInternalServerError, "GetOrgByName", err) + } + return + } + + if !organization.HasOrgOrUserVisible(ctx, org.AsUser(), ctx.Doer) { + ctx.NotFound("HasOrgOrUserVisible", nil) + return + } + + if !ctx.Doer.IsAdmin { + canCreate, err := org.CanCreateOrgRepo(ctx, ctx.Doer.ID) + if err != nil { + ctx.Error(http.StatusInternalServerError, "CanCreateOrgRepo", err) + return + } else if !canCreate { + ctx.Error(http.StatusForbidden, "", "Given user is not allowed to create repository in organization.") + return + } + } + CreateUserRepo(ctx, org.AsUser(), *opt) +} + +// Get one repository +func Get(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo} repository repoGet + // --- + // summary: Get a repository + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/Repository" + // "404": + // "$ref": "#/responses/notFound" + + if err := ctx.Repo.Repository.LoadAttributes(ctx); err != nil { + ctx.Error(http.StatusInternalServerError, "Repository.LoadAttributes", err) + return + } + + ctx.JSON(http.StatusOK, convert.ToRepo(ctx, ctx.Repo.Repository, ctx.Repo.Permission)) +} + +// GetByID returns a single Repository +func GetByID(ctx *context.APIContext) { + // swagger:operation GET /repositories/{id} repository repoGetByID + // --- + // summary: Get a repository by id + // produces: + // - application/json + // parameters: + // - name: id + // in: path + // description: id of the repo to get + // type: integer + // format: int64 + // required: true + // responses: + // "200": + // "$ref": "#/responses/Repository" + // "404": + // "$ref": "#/responses/notFound" + + repo, err := repo_model.GetRepositoryByID(ctx, ctx.ParamsInt64(":id")) + if err != nil { + if repo_model.IsErrRepoNotExist(err) { + ctx.NotFound() + } else { + ctx.Error(http.StatusInternalServerError, "GetRepositoryByID", err) + } + return + } + + permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) + return + } else if !permission.HasAccess() { + ctx.NotFound() + return + } + ctx.JSON(http.StatusOK, convert.ToRepo(ctx, repo, permission)) +} + +// Edit edit repository properties +func Edit(ctx *context.APIContext) { + // swagger:operation PATCH /repos/{owner}/{repo} repository repoEdit + // --- + // summary: Edit a repository's properties. Only fields that are set will be changed. + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo to edit + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo to edit + // type: string + // required: true + // - name: body + // in: body + // description: "Properties of a repo that you can edit" + // schema: + // "$ref": "#/definitions/EditRepoOption" + // responses: + // "200": + // "$ref": "#/responses/Repository" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" + + opts := *web.GetForm(ctx).(*api.EditRepoOption) + + if err := updateBasicProperties(ctx, opts); err != nil { + return + } + + if err := updateRepoUnits(ctx, opts); err != nil { + return + } + + if opts.Archived != nil { + if err := updateRepoArchivedState(ctx, opts); err != nil { + return + } + } + + if opts.MirrorInterval != nil || opts.EnablePrune != nil { + if err := updateMirror(ctx, opts); err != nil { + return + } + } + + repo, err := repo_model.GetRepositoryByID(ctx, ctx.Repo.Repository.ID) + if err != nil { + ctx.InternalServerError(err) + return + } + + ctx.JSON(http.StatusOK, convert.ToRepo(ctx, repo, ctx.Repo.Permission)) +} + +// updateBasicProperties updates the basic properties of a repo: Name, Description, Website and Visibility +func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) error { + owner := ctx.Repo.Owner + repo := ctx.Repo.Repository + newRepoName := repo.Name + if opts.Name != nil { + newRepoName = *opts.Name + } + // Check if repository name has been changed and not just a case change + if repo.LowerName != strings.ToLower(newRepoName) { + if err := repo_service.ChangeRepositoryName(ctx, ctx.Doer, repo, newRepoName); err != nil { + switch { + case repo_model.IsErrRepoAlreadyExist(err): + ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("repo name is already taken [name: %s]", newRepoName), err) + case db.IsErrNameReserved(err): + ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("repo name is reserved [name: %s]", newRepoName), err) + case db.IsErrNamePatternNotAllowed(err): + ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("repo name's pattern is not allowed [name: %s, pattern: %s]", newRepoName, err.(db.ErrNamePatternNotAllowed).Pattern), err) + default: + ctx.Error(http.StatusUnprocessableEntity, "ChangeRepositoryName", err) + } + return err + } + + log.Trace("Repository name changed: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newRepoName) + } + // Update the name in the repo object for the response + repo.Name = newRepoName + repo.LowerName = strings.ToLower(newRepoName) + + if opts.Description != nil { + repo.Description = *opts.Description + } + + if opts.Website != nil { + repo.Website = *opts.Website + } + + visibilityChanged := false + if opts.Private != nil { + // Visibility of forked repository is forced sync with base repository. + if repo.IsFork { + if err := repo.GetBaseRepo(ctx); err != nil { + ctx.Error(http.StatusInternalServerError, "Unable to load base repository", err) + return err + } + *opts.Private = repo.BaseRepo.IsPrivate + } + + visibilityChanged = repo.IsPrivate != *opts.Private + // when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public + if visibilityChanged && setting.Repository.ForcePrivate && !*opts.Private && !ctx.Doer.IsAdmin { + err := fmt.Errorf("cannot change private repository to public") + ctx.Error(http.StatusUnprocessableEntity, "Force Private enabled", err) + return err + } + + repo.IsPrivate = *opts.Private + } + + if opts.Template != nil { + repo.IsTemplate = *opts.Template + } + + if ctx.Repo.GitRepo == nil && !repo.IsEmpty { + var err error + ctx.Repo.GitRepo, err = gitrepo.OpenRepository(ctx, repo) + if err != nil { + ctx.Error(http.StatusInternalServerError, "Unable to OpenRepository", err) + return err + } + defer ctx.Repo.GitRepo.Close() + } + + // Default branch only updated if changed and exist or the repository is empty + if opts.DefaultBranch != nil && repo.DefaultBranch != *opts.DefaultBranch && (repo.IsEmpty || ctx.Repo.GitRepo.IsBranchExist(*opts.DefaultBranch)) { + if !repo.IsEmpty { + if err := gitrepo.SetDefaultBranch(ctx, ctx.Repo.Repository, *opts.DefaultBranch); err != nil { + if !git.IsErrUnsupportedVersion(err) { + ctx.Error(http.StatusInternalServerError, "SetDefaultBranch", err) + return err + } + } + } + repo.DefaultBranch = *opts.DefaultBranch + } + + // Wiki branch is updated if changed + if opts.WikiBranch != nil && repo.WikiBranch != *opts.WikiBranch { + if err := wiki_service.NormalizeWikiBranch(ctx, repo, *opts.WikiBranch); err != nil { + ctx.Error(http.StatusInternalServerError, "NormalizeWikiBranch", err) + return err + } + // While NormalizeWikiBranch updates the db, we need to update *this* + // instance of `repo`, so that the `UpdateRepository` below will not + // reset the branch back. + repo.WikiBranch = *opts.WikiBranch + } + + if err := repo_service.UpdateRepository(ctx, repo, visibilityChanged); err != nil { + ctx.Error(http.StatusInternalServerError, "UpdateRepository", err) + return err + } + + log.Trace("Repository basic settings updated: %s/%s", owner.Name, repo.Name) + return nil +} + +// updateRepoUnits updates repo units: Issue settings, Wiki settings, PR settings +func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { + owner := ctx.Repo.Owner + repo := ctx.Repo.Repository + + var units []repo_model.RepoUnit + var deleteUnitTypes []unit_model.Type + + currHasIssues := repo.UnitEnabled(ctx, unit_model.TypeIssues) + newHasIssues := currHasIssues + if opts.HasIssues != nil { + newHasIssues = *opts.HasIssues + } + if currHasIssues || newHasIssues { + if newHasIssues && opts.ExternalTracker != nil && !unit_model.TypeExternalTracker.UnitGlobalDisabled() { + // Check that values are valid + if !validation.IsValidExternalURL(opts.ExternalTracker.ExternalTrackerURL) { + err := fmt.Errorf("External tracker URL not valid") + ctx.Error(http.StatusUnprocessableEntity, "Invalid external tracker URL", err) + return err + } + if len(opts.ExternalTracker.ExternalTrackerFormat) != 0 && !validation.IsValidExternalTrackerURLFormat(opts.ExternalTracker.ExternalTrackerFormat) { + err := fmt.Errorf("External tracker URL format not valid") + ctx.Error(http.StatusUnprocessableEntity, "Invalid external tracker URL format", err) + return err + } + + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: unit_model.TypeExternalTracker, + Config: &repo_model.ExternalTrackerConfig{ + ExternalTrackerURL: opts.ExternalTracker.ExternalTrackerURL, + ExternalTrackerFormat: opts.ExternalTracker.ExternalTrackerFormat, + ExternalTrackerStyle: opts.ExternalTracker.ExternalTrackerStyle, + ExternalTrackerRegexpPattern: opts.ExternalTracker.ExternalTrackerRegexpPattern, + }, + }) + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues) + } else if newHasIssues && opts.ExternalTracker == nil && !unit_model.TypeIssues.UnitGlobalDisabled() { + // Default to built-in tracker + var config *repo_model.IssuesConfig + + if opts.InternalTracker != nil { + config = &repo_model.IssuesConfig{ + EnableTimetracker: opts.InternalTracker.EnableTimeTracker, + AllowOnlyContributorsToTrackTime: opts.InternalTracker.AllowOnlyContributorsToTrackTime, + EnableDependencies: opts.InternalTracker.EnableIssueDependencies, + } + } else if unit, err := repo.GetUnit(ctx, unit_model.TypeIssues); err != nil { + // Unit type doesn't exist so we make a new config file with default values + config = &repo_model.IssuesConfig{ + EnableTimetracker: true, + AllowOnlyContributorsToTrackTime: true, + EnableDependencies: true, + } + } else { + config = unit.IssuesConfig() + } + + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: unit_model.TypeIssues, + Config: config, + }) + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker) + } else if !newHasIssues { + if !unit_model.TypeExternalTracker.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker) + } + if !unit_model.TypeIssues.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues) + } + } + } + + currHasWiki := repo.UnitEnabled(ctx, unit_model.TypeWiki) + newHasWiki := currHasWiki + if opts.HasWiki != nil { + newHasWiki = *opts.HasWiki + } + if currHasWiki || newHasWiki { + wikiPermissions := repo.MustGetUnit(ctx, unit_model.TypeWiki).DefaultPermissions + if opts.GloballyEditableWiki != nil { + if *opts.GloballyEditableWiki { + wikiPermissions = repo_model.UnitAccessModeWrite + } else { + wikiPermissions = repo_model.UnitAccessModeRead + } + } + + if newHasWiki && opts.ExternalWiki != nil && !unit_model.TypeExternalWiki.UnitGlobalDisabled() { + // Check that values are valid + if !validation.IsValidExternalURL(opts.ExternalWiki.ExternalWikiURL) { + err := fmt.Errorf("External wiki URL not valid") + ctx.Error(http.StatusUnprocessableEntity, "", "Invalid external wiki URL") + return err + } + + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: unit_model.TypeExternalWiki, + Config: &repo_model.ExternalWikiConfig{ + ExternalWikiURL: opts.ExternalWiki.ExternalWikiURL, + }, + }) + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki) + } else if newHasWiki && opts.ExternalWiki == nil && !unit_model.TypeWiki.UnitGlobalDisabled() { + config := &repo_model.UnitConfig{} + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: unit_model.TypeWiki, + Config: config, + DefaultPermissions: wikiPermissions, + }) + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki) + } else if !newHasWiki { + if !unit_model.TypeExternalWiki.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki) + } + if !unit_model.TypeWiki.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki) + } + } else if *opts.GloballyEditableWiki { + config := &repo_model.UnitConfig{} + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: unit_model.TypeWiki, + Config: config, + DefaultPermissions: wikiPermissions, + }) + } + } + + currHasPullRequests := repo.UnitEnabled(ctx, unit_model.TypePullRequests) + newHasPullRequests := currHasPullRequests + if opts.HasPullRequests != nil { + newHasPullRequests = *opts.HasPullRequests + } + if currHasPullRequests || newHasPullRequests { + if newHasPullRequests && !unit_model.TypePullRequests.UnitGlobalDisabled() { + // We do allow setting individual PR settings through the API, so + // we get the config settings and then set them + // if those settings were provided in the opts. + unit, err := repo.GetUnit(ctx, unit_model.TypePullRequests) + var config *repo_model.PullRequestsConfig + if err != nil { + // Unit type doesn't exist so we make a new config file with default values + config = &repo_model.PullRequestsConfig{ + IgnoreWhitespaceConflicts: false, + AllowMerge: true, + AllowRebase: true, + AllowRebaseMerge: true, + AllowSquash: true, + AllowFastForwardOnly: true, + AllowManualMerge: true, + AutodetectManualMerge: false, + AllowRebaseUpdate: true, + DefaultDeleteBranchAfterMerge: false, + DefaultMergeStyle: repo_model.MergeStyleMerge, + DefaultAllowMaintainerEdit: false, + } + } else { + config = unit.PullRequestsConfig() + } + + if opts.IgnoreWhitespaceConflicts != nil { + config.IgnoreWhitespaceConflicts = *opts.IgnoreWhitespaceConflicts + } + if opts.AllowMerge != nil { + config.AllowMerge = *opts.AllowMerge + } + if opts.AllowRebase != nil { + config.AllowRebase = *opts.AllowRebase + } + if opts.AllowRebaseMerge != nil { + config.AllowRebaseMerge = *opts.AllowRebaseMerge + } + if opts.AllowSquash != nil { + config.AllowSquash = *opts.AllowSquash + } + if opts.AllowFastForwardOnly != nil { + config.AllowFastForwardOnly = *opts.AllowFastForwardOnly + } + if opts.AllowManualMerge != nil { + config.AllowManualMerge = *opts.AllowManualMerge + } + if opts.AutodetectManualMerge != nil { + config.AutodetectManualMerge = *opts.AutodetectManualMerge + } + if opts.AllowRebaseUpdate != nil { + config.AllowRebaseUpdate = *opts.AllowRebaseUpdate + } + if opts.DefaultDeleteBranchAfterMerge != nil { + config.DefaultDeleteBranchAfterMerge = *opts.DefaultDeleteBranchAfterMerge + } + if opts.DefaultMergeStyle != nil { + config.DefaultMergeStyle = repo_model.MergeStyle(*opts.DefaultMergeStyle) + } + if opts.DefaultAllowMaintainerEdit != nil { + config.DefaultAllowMaintainerEdit = *opts.DefaultAllowMaintainerEdit + } + + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: unit_model.TypePullRequests, + Config: config, + }) + } else if !newHasPullRequests && !unit_model.TypePullRequests.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePullRequests) + } + } + + if opts.HasProjects != nil && !unit_model.TypeProjects.UnitGlobalDisabled() { + if *opts.HasProjects { + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: unit_model.TypeProjects, + }) + } else { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects) + } + } + + if opts.HasReleases != nil && !unit_model.TypeReleases.UnitGlobalDisabled() { + if *opts.HasReleases { + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: unit_model.TypeReleases, + }) + } else { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeReleases) + } + } + + if opts.HasPackages != nil && !unit_model.TypePackages.UnitGlobalDisabled() { + if *opts.HasPackages { + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: unit_model.TypePackages, + }) + } else { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePackages) + } + } + + if opts.HasActions != nil && !unit_model.TypeActions.UnitGlobalDisabled() { + if *opts.HasActions { + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: unit_model.TypeActions, + }) + } else { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeActions) + } + } + + if len(units)+len(deleteUnitTypes) > 0 { + if err := repo_service.UpdateRepositoryUnits(ctx, repo, units, deleteUnitTypes); err != nil { + ctx.Error(http.StatusInternalServerError, "UpdateRepositoryUnits", err) + return err + } + } + + log.Trace("Repository advanced settings updated: %s/%s", owner.Name, repo.Name) + return nil +} + +// updateRepoArchivedState updates repo's archive state +func updateRepoArchivedState(ctx *context.APIContext, opts api.EditRepoOption) error { + repo := ctx.Repo.Repository + // archive / un-archive + if opts.Archived != nil { + if repo.IsMirror { + err := fmt.Errorf("repo is a mirror, cannot archive/un-archive") + ctx.Error(http.StatusUnprocessableEntity, err.Error(), err) + return err + } + if *opts.Archived { + if err := repo_model.SetArchiveRepoState(ctx, repo, *opts.Archived); err != nil { + log.Error("Tried to archive a repo: %s", err) + ctx.Error(http.StatusInternalServerError, "ArchiveRepoState", err) + return err + } + if err := actions_model.CleanRepoScheduleTasks(ctx, repo, true); err != nil { + log.Error("CleanRepoScheduleTasks for archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err) + } + log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name) + } else { + if err := repo_model.SetArchiveRepoState(ctx, repo, *opts.Archived); err != nil { + log.Error("Tried to un-archive a repo: %s", err) + ctx.Error(http.StatusInternalServerError, "ArchiveRepoState", err) + return err + } + if ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypeActions) { + if err := actions_service.DetectAndHandleSchedules(ctx, repo); err != nil { + log.Error("DetectAndHandleSchedules for un-archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err) + } + } + log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name) + } + } + return nil +} + +// updateMirror updates a repo's mirror Interval and EnablePrune +func updateMirror(ctx *context.APIContext, opts api.EditRepoOption) error { + repo := ctx.Repo.Repository + + // Skip this update if the repo is not a mirror, do not return error. + // Because reporting errors only makes the logic more complex&fragile, it doesn't really help end users. + if !repo.IsMirror { + return nil + } + + // get the mirror from the repo + mirror, err := repo_model.GetMirrorByRepoID(ctx, repo.ID) + if err != nil { + log.Error("Failed to get mirror: %s", err) + ctx.Error(http.StatusInternalServerError, "MirrorInterval", err) + return err + } + + // update MirrorInterval + if opts.MirrorInterval != nil { + // MirrorInterval should be a duration + interval, err := time.ParseDuration(*opts.MirrorInterval) + if err != nil { + log.Error("Wrong format for MirrorInternal Sent: %s", err) + ctx.Error(http.StatusUnprocessableEntity, "MirrorInterval", err) + return err + } + + // Ensure the provided duration is not too short + if interval != 0 && interval < setting.Mirror.MinInterval { + err := fmt.Errorf("invalid mirror interval: %s is below minimum interval: %s", interval, setting.Mirror.MinInterval) + ctx.Error(http.StatusUnprocessableEntity, "MirrorInterval", err) + return err + } + + mirror.Interval = interval + mirror.Repo = repo + mirror.ScheduleNextUpdate() + log.Trace("Repository %s Mirror[%d] Set Interval: %s NextUpdateUnix: %s", repo.FullName(), mirror.ID, interval, mirror.NextUpdateUnix) + } + + // update EnablePrune + if opts.EnablePrune != nil { + mirror.EnablePrune = *opts.EnablePrune + log.Trace("Repository %s Mirror[%d] Set EnablePrune: %t", repo.FullName(), mirror.ID, mirror.EnablePrune) + } + + // finally update the mirror in the DB + if err := repo_model.UpdateMirror(ctx, mirror); err != nil { + log.Error("Failed to Set Mirror Interval: %s", err) + ctx.Error(http.StatusUnprocessableEntity, "MirrorInterval", err) + return err + } + + return nil +} + +// Delete one repository +func Delete(ctx *context.APIContext) { + // swagger:operation DELETE /repos/{owner}/{repo} repository repoDelete + // --- + // summary: Delete a repository + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo to delete + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo to delete + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + + owner := ctx.Repo.Owner + repo := ctx.Repo.Repository + + canDelete, err := repo_module.CanUserDelete(ctx, repo, ctx.Doer) + if err != nil { + ctx.Error(http.StatusInternalServerError, "CanUserDelete", err) + return + } else if !canDelete { + ctx.Error(http.StatusForbidden, "", "Given user is not owner of organization.") + return + } + + if ctx.Repo.GitRepo != nil { + ctx.Repo.GitRepo.Close() + } + + if err := repo_service.DeleteRepository(ctx, ctx.Doer, repo, true); err != nil { + ctx.Error(http.StatusInternalServerError, "DeleteRepository", err) + return + } + + log.Trace("Repository deleted: %s/%s", owner.Name, repo.Name) + ctx.Status(http.StatusNoContent) +} + +// GetIssueTemplates returns the issue templates for a repository +func GetIssueTemplates(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/issue_templates repository repoGetIssueTemplates + // --- + // summary: Get available issue templates for a repository + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/IssueTemplates" + // "404": + // "$ref": "#/responses/notFound" + ret, _ := issue.GetTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo) + ctx.JSON(http.StatusOK, ret) +} + +// GetIssueConfig returns the issue config for a repo +func GetIssueConfig(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/issue_config repository repoGetIssueConfig + // --- + // summary: Returns the issue config for a repo + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/RepoIssueConfig" + // "404": + // "$ref": "#/responses/notFound" + issueConfig, _ := issue.GetTemplateConfigFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo) + ctx.JSON(http.StatusOK, issueConfig) +} + +// ValidateIssueConfig returns validation errors for the issue config +func ValidateIssueConfig(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/issue_config/validate repository repoValidateIssueConfig + // --- + // summary: Returns the validation information for a issue config + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/RepoIssueConfigValidation" + // "404": + // "$ref": "#/responses/notFound" + _, err := issue.GetTemplateConfigFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo) + + if err == nil { + ctx.JSON(http.StatusOK, api.IssueConfigValidation{Valid: true, Message: ""}) + } else { + ctx.JSON(http.StatusOK, api.IssueConfigValidation{Valid: false, Message: err.Error()}) + } +} + +func ListRepoActivityFeeds(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/activities/feeds repository repoListActivityFeeds + // --- + // summary: List a repository's activity feeds + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: date + // in: query + // description: the date of the activities to be found + // type: string + // format: date + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/ActivityFeedsList" + // "404": + // "$ref": "#/responses/notFound" + + listOptions := utils.GetListOptions(ctx) + + opts := activities_model.GetFeedsOptions{ + RequestedRepo: ctx.Repo.Repository, + OnlyPerformedByActor: true, + Actor: ctx.Doer, + IncludePrivate: true, + Date: ctx.FormString("date"), + ListOptions: listOptions, + } + + feeds, count, err := activities_model.GetFeeds(ctx, opts) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetFeeds", err) + return + } + ctx.SetTotalCountHeader(count) + + ctx.JSON(http.StatusOK, convert.ToActivities(ctx, feeds, ctx.Doer)) +} |