* Feature - #5960 - API Endpoint for Repo Editing * Revert from merge * Adds integration testing * Updates to integration tests * Revert changes * Update year in file header * Misspell fix * XORM = test * XORM = test * revert XORM = file * Makes RepoUnit.ID be pk and autoincr * Fix to units * revert header * Remove print statement * Adds other responses * Improves swagger for creating repo * Fixes import order * Better Unit Type does not exist error * Adds editable repo properties to the response repo structure * Fix to api_repo_edit_test.go * Fixes repo test * Changes per review * Fixes typo and standardizes comments in the EditRepoOption struct for swagger * Fixes typo and standardizes comments in the EditRepoOption struct for swagger * Actually can unarchive through the API * Unlike delete, user doesn't have to be the owner of the org, just admin to the repo * Fix to swagger comments for field name change * Update to swagger docs * Update swagger * Changes allow_pull_requests to has_pull_requests
This commit is contained in:
parent
cdd10f145b
commit
1831b3b571
11 changed files with 868 additions and 66 deletions
|
@ -608,7 +608,8 @@ func RegisterRoutes(m *macaron.Macaron) {
|
|||
|
||||
m.Group("/:username/:reponame", func() {
|
||||
m.Combo("").Get(reqAnyRepoReader(), repo.Get).
|
||||
Delete(reqToken(), reqOwner(), repo.Delete)
|
||||
Delete(reqToken(), reqOwner(), repo.Delete).
|
||||
Patch(reqToken(), reqAdmin(), bind(api.EditRepoOption{}), repo.Edit)
|
||||
m.Group("/hooks", func() {
|
||||
m.Combo("").Get(repo.ListHooks).
|
||||
Post(bind(api.CreateHookOption{}), repo.CreateHook)
|
||||
|
|
|
@ -240,6 +240,10 @@ func Create(ctx *context.APIContext, opt api.CreateRepoOption) {
|
|||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/Repository"
|
||||
// "409":
|
||||
// description: The repository with the same name already exists.
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
if ctx.User.IsOrganization() {
|
||||
// Shouldn't reach this condition, but just in case.
|
||||
ctx.Error(422, "", "not allowed creating repository for organization")
|
||||
|
@ -500,6 +504,280 @@ func GetByID(ctx *context.APIContext) {
|
|||
ctx.JSON(200, repo.APIFormat(perm.AccessMode))
|
||||
}
|
||||
|
||||
// Edit edit repository properties
|
||||
func Edit(ctx *context.APIContext, opts api.EditRepoOption) {
|
||||
// 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
|
||||
// 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"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, ctx.Repo.Repository.APIFormat(ctx.Repo.AccessMode))
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
oldRepoName := repo.Name
|
||||
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 := models.ChangeRepositoryName(ctx.Repo.Owner, repo.Name, newRepoName); err != nil {
|
||||
switch {
|
||||
case models.IsErrRepoAlreadyExist(err):
|
||||
ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("repo name is already taken [name: %s]", newRepoName), err)
|
||||
case models.IsErrNameReserved(err):
|
||||
ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("repo name is reserved [name: %s]", newRepoName), err)
|
||||
case models.IsErrNamePatternNotAllowed(err):
|
||||
ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("repo name's pattern is not allowed [name: %s, pattern: %s]", newRepoName, err.(models.ErrNamePatternNotAllowed).Pattern), err)
|
||||
default:
|
||||
ctx.Error(http.StatusUnprocessableEntity, "ChangeRepositoryName", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
err := models.NewRepoRedirect(ctx.Repo.Owner.ID, repo.ID, repo.Name, newRepoName)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "NewRepoRedirect", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := models.RenameRepoAction(ctx.User, oldRepoName, repo); err != nil {
|
||||
log.Error("RenameRepoAction: %v", err)
|
||||
ctx.Error(http.StatusInternalServerError, "RenameRepoActions", 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 {
|
||||
*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.User.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 err := models.UpdateRepository(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
|
||||
}
|
||||
|
||||
func unitTypeInTypes(unitType models.UnitType, unitTypes []models.UnitType) bool {
|
||||
for _, tp := range unitTypes {
|
||||
if unitType == tp {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 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 []models.RepoUnit
|
||||
|
||||
for _, tp := range models.MustRepoUnits {
|
||||
units = append(units, models.RepoUnit{
|
||||
RepoID: repo.ID,
|
||||
Type: tp,
|
||||
Config: new(models.UnitConfig),
|
||||
})
|
||||
}
|
||||
|
||||
if opts.HasIssues != nil {
|
||||
if *opts.HasIssues {
|
||||
// We don't currently allow setting individual issue settings through the API,
|
||||
// only can enable/disable issues, so when enabling issues,
|
||||
// we either get the existing config which means it was already enabled,
|
||||
// or create a new config since it doesn't exist.
|
||||
unit, err := repo.GetUnit(models.UnitTypeIssues)
|
||||
var config *models.IssuesConfig
|
||||
if err != nil {
|
||||
// Unit type doesn't exist so we make a new config file with default values
|
||||
config = &models.IssuesConfig{
|
||||
EnableTimetracker: true,
|
||||
AllowOnlyContributorsToTrackTime: true,
|
||||
EnableDependencies: true,
|
||||
}
|
||||
} else {
|
||||
config = unit.IssuesConfig()
|
||||
}
|
||||
units = append(units, models.RepoUnit{
|
||||
RepoID: repo.ID,
|
||||
Type: models.UnitTypeIssues,
|
||||
Config: config,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if opts.HasWiki != nil {
|
||||
if *opts.HasWiki {
|
||||
// We don't currently allow setting individual wiki settings through the API,
|
||||
// only can enable/disable the wiki, so when enabling the wiki,
|
||||
// we either get the existing config which means it was already enabled,
|
||||
// or create a new config since it doesn't exist.
|
||||
config := &models.UnitConfig{}
|
||||
units = append(units, models.RepoUnit{
|
||||
RepoID: repo.ID,
|
||||
Type: models.UnitTypeWiki,
|
||||
Config: config,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if opts.HasPullRequests != nil {
|
||||
if *opts.HasPullRequests {
|
||||
// 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(models.UnitTypePullRequests)
|
||||
var config *models.PullRequestsConfig
|
||||
if err != nil {
|
||||
// Unit type doesn't exist so we make a new config file with default values
|
||||
config = &models.PullRequestsConfig{
|
||||
IgnoreWhitespaceConflicts: false,
|
||||
AllowMerge: true,
|
||||
AllowRebase: true,
|
||||
AllowRebaseMerge: true,
|
||||
AllowSquash: true,
|
||||
}
|
||||
} 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
|
||||
}
|
||||
|
||||
units = append(units, models.RepoUnit{
|
||||
RepoID: repo.ID,
|
||||
Type: models.UnitTypePullRequests,
|
||||
Config: config,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if err := models.UpdateRepositoryUnits(repo, units); 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.SetArchiveRepoState(*opts.Archived); err != nil {
|
||||
log.Error("Tried to archive a repo: %s", err)
|
||||
ctx.Error(http.StatusInternalServerError, "ArchiveRepoState", err)
|
||||
return err
|
||||
}
|
||||
log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
|
||||
} else {
|
||||
if err := repo.SetArchiveRepoState(*opts.Archived); err != nil {
|
||||
log.Error("Tried to un-archive a repo: %s", err)
|
||||
ctx.Error(http.StatusInternalServerError, "ArchiveRepoState", err)
|
||||
return err
|
||||
}
|
||||
log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete one repository
|
||||
func Delete(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo} repository repoDelete
|
||||
|
|
82
routers/api/v1/repo/repo_test.go
Normal file
82
routers/api/v1/repo/repo_test.go
Normal file
|
@ -0,0 +1,82 @@
|
|||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRepoEdit(t *testing.T) {
|
||||
models.PrepareTestEnv(t)
|
||||
|
||||
ctx := test.MockContext(t, "user2/repo1")
|
||||
test.LoadRepo(t, ctx, 1)
|
||||
test.LoadUser(t, ctx, 2)
|
||||
ctx.Repo.Owner = ctx.User
|
||||
description := "new description"
|
||||
website := "http://wwww.newwebsite.com"
|
||||
private := true
|
||||
hasIssues := false
|
||||
hasWiki := false
|
||||
defaultBranch := "master"
|
||||
hasPullRequests := true
|
||||
ignoreWhitespaceConflicts := true
|
||||
allowMerge := false
|
||||
allowRebase := false
|
||||
allowRebaseMerge := false
|
||||
allowSquashMerge := false
|
||||
archived := true
|
||||
opts := api.EditRepoOption{
|
||||
Name: &ctx.Repo.Repository.Name,
|
||||
Description: &description,
|
||||
Website: &website,
|
||||
Private: &private,
|
||||
HasIssues: &hasIssues,
|
||||
HasWiki: &hasWiki,
|
||||
DefaultBranch: &defaultBranch,
|
||||
HasPullRequests: &hasPullRequests,
|
||||
IgnoreWhitespaceConflicts: &ignoreWhitespaceConflicts,
|
||||
AllowMerge: &allowMerge,
|
||||
AllowRebase: &allowRebase,
|
||||
AllowRebaseMerge: &allowRebaseMerge,
|
||||
AllowSquash: &allowSquashMerge,
|
||||
Archived: &archived,
|
||||
}
|
||||
|
||||
Edit(&context.APIContext{Context: ctx, Org: nil}, opts)
|
||||
|
||||
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
|
||||
models.AssertExistsAndLoadBean(t, &models.Repository{
|
||||
ID: 1,
|
||||
}, models.Cond("name = ? AND is_archived = 1", *opts.Name))
|
||||
}
|
||||
|
||||
func TestRepoEditNameChange(t *testing.T) {
|
||||
models.PrepareTestEnv(t)
|
||||
|
||||
ctx := test.MockContext(t, "user2/repo1")
|
||||
test.LoadRepo(t, ctx, 1)
|
||||
test.LoadUser(t, ctx, 2)
|
||||
ctx.Repo.Owner = ctx.User
|
||||
name := "newname"
|
||||
opts := api.EditRepoOption{
|
||||
Name: &name,
|
||||
}
|
||||
|
||||
Edit(&context.APIContext{Context: ctx, Org: nil}, opts)
|
||||
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
|
||||
|
||||
models.AssertExistsAndLoadBean(t, &models.Repository{
|
||||
ID: 1,
|
||||
}, models.Cond("name = ?", opts.Name))
|
||||
}
|
|
@ -82,6 +82,8 @@ type swaggerParameterBodies struct {
|
|||
// in:body
|
||||
CreateRepoOption api.CreateRepoOption
|
||||
// in:body
|
||||
EditRepoOption api.EditRepoOption
|
||||
// in:body
|
||||
CreateForkOption api.CreateForkOption
|
||||
|
||||
// in:body
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue