summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRichard Mahn <richmahn@users.noreply.github.com>2019-05-30 11:09:05 -0400
committertechknowlogick <hello@techknowlogick.com>2019-05-30 11:09:05 -0400
commit1831b3b57144e87ccfc4f6322eefc88a49b2300e (patch)
tree225fa569e3908c153683009d0d75c42f9dacc956
parentcdd10f145be0b5e9b94c19f1303dc01c6e9c8c29 (diff)
downloadgitea-1831b3b57144e87ccfc4f6322eefc88a49b2300e.tar.gz
gitea-1831b3b57144e87ccfc4f6322eefc88a49b2300e.zip
Fixes #5960 - Adds API Endpoint for Repo Edit (#7006)
* 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
-rw-r--r--integrations/api_repo_edit_test.go225
-rw-r--r--integrations/api_repo_file_delete_test.go2
-rw-r--r--models/org.go4
-rw-r--r--models/repo.go111
-rw-r--r--models/unit.go8
-rw-r--r--modules/structs/repo.go58
-rw-r--r--routers/api/v1/api.go3
-rw-r--r--routers/api/v1/repo/repo.go278
-rw-r--r--routers/api/v1/repo/repo_test.go82
-rw-r--r--routers/api/v1/swagger/options.go2
-rw-r--r--templates/swagger/v1_json.tmpl161
11 files changed, 868 insertions, 66 deletions
diff --git a/integrations/api_repo_edit_test.go b/integrations/api_repo_edit_test.go
new file mode 100644
index 0000000000..3b2c916ab0
--- /dev/null
+++ b/integrations/api_repo_edit_test.go
@@ -0,0 +1,225 @@
+// 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 integrations
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "testing"
+
+ "code.gitea.io/gitea/models"
+ api "code.gitea.io/gitea/modules/structs"
+
+ "github.com/stretchr/testify/assert"
+)
+
+// getRepoEditOptionFromRepo gets the options for an existing repo exactly as is
+func getRepoEditOptionFromRepo(repo *models.Repository) *api.EditRepoOption {
+ name := repo.Name
+ description := repo.Description
+ website := repo.Website
+ private := repo.IsPrivate
+ hasIssues := false
+ if _, err := repo.GetUnit(models.UnitTypeIssues); err == nil {
+ hasIssues = true
+ }
+ hasWiki := false
+ if _, err := repo.GetUnit(models.UnitTypeWiki); err == nil {
+ hasWiki = true
+ }
+ defaultBranch := repo.DefaultBranch
+ hasPullRequests := false
+ ignoreWhitespaceConflicts := false
+ allowMerge := false
+ allowRebase := false
+ allowRebaseMerge := false
+ allowSquash := false
+ if unit, err := repo.GetUnit(models.UnitTypePullRequests); err == nil {
+ config := unit.PullRequestsConfig()
+ hasPullRequests = true
+ ignoreWhitespaceConflicts = config.IgnoreWhitespaceConflicts
+ allowMerge = config.AllowMerge
+ allowRebase = config.AllowRebase
+ allowRebaseMerge = config.AllowRebaseMerge
+ allowSquash = config.AllowSquash
+ }
+ archived := repo.IsArchived
+ return &api.EditRepoOption{
+ Name: &name,
+ Description: &description,
+ Website: &website,
+ Private: &private,
+ HasIssues: &hasIssues,
+ HasWiki: &hasWiki,
+ DefaultBranch: &defaultBranch,
+ HasPullRequests: &hasPullRequests,
+ IgnoreWhitespaceConflicts: &ignoreWhitespaceConflicts,
+ AllowMerge: &allowMerge,
+ AllowRebase: &allowRebase,
+ AllowRebaseMerge: &allowRebaseMerge,
+ AllowSquash: &allowSquash,
+ Archived: &archived,
+ }
+}
+
+// getNewRepoEditOption Gets the options to change everything about an existing repo by adding to strings or changing
+// the boolean
+func getNewRepoEditOption(opts *api.EditRepoOption) *api.EditRepoOption {
+ // Gives a new property to everything
+ name := *opts.Name + "renamed"
+ description := "new description"
+ website := "http://wwww.newwebsite.com"
+ private := !*opts.Private
+ hasIssues := !*opts.HasIssues
+ hasWiki := !*opts.HasWiki
+ defaultBranch := "master"
+ hasPullRequests := !*opts.HasPullRequests
+ ignoreWhitespaceConflicts := !*opts.IgnoreWhitespaceConflicts
+ allowMerge := !*opts.AllowMerge
+ allowRebase := !*opts.AllowRebase
+ allowRebaseMerge := !*opts.AllowRebaseMerge
+ allowSquash := !*opts.AllowSquash
+ archived := !*opts.Archived
+
+ return &api.EditRepoOption{
+ Name: &name,
+ Description: &description,
+ Website: &website,
+ Private: &private,
+ DefaultBranch: &defaultBranch,
+ HasIssues: &hasIssues,
+ HasWiki: &hasWiki,
+ HasPullRequests: &hasPullRequests,
+ IgnoreWhitespaceConflicts: &ignoreWhitespaceConflicts,
+ AllowMerge: &allowMerge,
+ AllowRebase: &allowRebase,
+ AllowRebaseMerge: &allowRebaseMerge,
+ AllowSquash: &allowSquash,
+ Archived: &archived,
+ }
+}
+
+func TestAPIRepoEdit(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ user2 := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) // owner of the repo1 & repo16
+ user3 := models.AssertExistsAndLoadBean(t, &models.User{ID: 3}).(*models.User) // owner of the repo3, is an org
+ user4 := models.AssertExistsAndLoadBean(t, &models.User{ID: 4}).(*models.User) // owner of neither repos
+ repo1 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository) // public repo
+ repo3 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 3}).(*models.Repository) // public repo
+ repo16 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 16}).(*models.Repository) // private repo
+
+ // Get user2's token
+ session := loginUser(t, user2.Name)
+ token2 := getTokenForLoggedInUser(t, session)
+ session = emptyTestSession(t)
+ // Get user4's token
+ session = loginUser(t, user4.Name)
+ token4 := getTokenForLoggedInUser(t, session)
+ session = emptyTestSession(t)
+
+ // Test editing a repo1 which user2 owns, changing name and many properties
+ origRepoEditOption := getRepoEditOptionFromRepo(repo1)
+ repoEditOption := getNewRepoEditOption(origRepoEditOption)
+ url := fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user2.Name, repo1.Name, token2)
+ req := NewRequestWithJSON(t, "PATCH", url, &repoEditOption)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ var repo api.Repository
+ DecodeJSON(t, resp, &repo)
+ assert.NotNil(t, repo)
+ // check response
+ assert.Equal(t, *repoEditOption.Name, repo.Name)
+ assert.Equal(t, *repoEditOption.Description, repo.Description)
+ assert.Equal(t, *repoEditOption.Website, repo.Website)
+ assert.Equal(t, *repoEditOption.Archived, repo.Archived)
+ // check repo1 from database
+ repo1edited := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
+ repo1editedOption := getRepoEditOptionFromRepo(repo1edited)
+ assert.Equal(t, *repoEditOption.Name, *repo1editedOption.Name)
+ assert.Equal(t, *repoEditOption.Description, *repo1editedOption.Description)
+ assert.Equal(t, *repoEditOption.Website, *repo1editedOption.Website)
+ assert.Equal(t, *repoEditOption.Archived, *repo1editedOption.Archived)
+ assert.Equal(t, *repoEditOption.Private, *repo1editedOption.Private)
+ assert.Equal(t, *repoEditOption.HasWiki, *repo1editedOption.HasWiki)
+ // reset repo in db
+ url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user2.Name, *repoEditOption.Name, token2)
+ req = NewRequestWithJSON(t, "PATCH", url, &origRepoEditOption)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+
+ // Test editing a non-existing repo
+ name := "repodoesnotexist"
+ url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user2.Name, name, token2)
+ req = NewRequestWithJSON(t, "PATCH", url, &api.EditRepoOption{Name: &name})
+ resp = session.MakeRequest(t, req, http.StatusNotFound)
+
+ // Test editing repo16 by user4 who does not have write access
+ origRepoEditOption = getRepoEditOptionFromRepo(repo16)
+ repoEditOption = getNewRepoEditOption(origRepoEditOption)
+ url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user2.Name, repo16.Name, token4)
+ req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption)
+ session.MakeRequest(t, req, http.StatusNotFound)
+
+ // Tests a repo with no token given so will fail
+ origRepoEditOption = getRepoEditOptionFromRepo(repo16)
+ repoEditOption = getNewRepoEditOption(origRepoEditOption)
+ url = fmt.Sprintf("/api/v1/repos/%s/%s", user2.Name, repo16.Name)
+ req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption)
+ resp = session.MakeRequest(t, req, http.StatusNotFound)
+
+ // Test using access token for a private repo that the user of the token owns
+ origRepoEditOption = getRepoEditOptionFromRepo(repo16)
+ repoEditOption = getNewRepoEditOption(origRepoEditOption)
+ url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user2.Name, repo16.Name, token2)
+ req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ // reset repo in db
+ url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user2.Name, *repoEditOption.Name, token2)
+ req = NewRequestWithJSON(t, "PATCH", url, &origRepoEditOption)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+
+ // Test making a repo public that is private
+ repo16 = models.AssertExistsAndLoadBean(t, &models.Repository{ID: 16}).(*models.Repository)
+ assert.True(t, repo16.IsPrivate)
+ private := false
+ repoEditOption = &api.EditRepoOption{
+ Private: &private,
+ }
+ url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user2.Name, repo16.Name, token2)
+ req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ repo16 = models.AssertExistsAndLoadBean(t, &models.Repository{ID: 16}).(*models.Repository)
+ assert.False(t, repo16.IsPrivate)
+ // Make it private again
+ private = true
+ repoEditOption.Private = &private
+ req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+
+ // Test using org repo "user3/repo3" where user2 is a collaborator
+ origRepoEditOption = getRepoEditOptionFromRepo(repo3)
+ repoEditOption = getNewRepoEditOption(origRepoEditOption)
+ url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user3.Name, repo3.Name, token2)
+ req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption)
+ session.MakeRequest(t, req, http.StatusOK)
+ // reset repo in db
+ url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user3.Name, *repoEditOption.Name, token2)
+ req = NewRequestWithJSON(t, "PATCH", url, &origRepoEditOption)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+
+ // Test using org repo "user3/repo3" with no user token
+ origRepoEditOption = getRepoEditOptionFromRepo(repo3)
+ repoEditOption = getNewRepoEditOption(origRepoEditOption)
+ url = fmt.Sprintf("/api/v1/repos/%s/%s", user3.Name, repo3.Name)
+ req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption)
+ session.MakeRequest(t, req, http.StatusNotFound)
+
+ // Test using repo "user2/repo1" where user4 is a NOT collaborator
+ origRepoEditOption = getRepoEditOptionFromRepo(repo1)
+ repoEditOption = getNewRepoEditOption(origRepoEditOption)
+ url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user2.Name, repo1.Name, token4)
+ req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption)
+ session.MakeRequest(t, req, http.StatusForbidden)
+ })
+}
diff --git a/integrations/api_repo_file_delete_test.go b/integrations/api_repo_file_delete_test.go
index 57e2539e19..e9029a669b 100644
--- a/integrations/api_repo_file_delete_test.go
+++ b/integrations/api_repo_file_delete_test.go
@@ -108,7 +108,7 @@ func TestAPIDeleteFile(t *testing.T) {
DecodeJSON(t, resp, &apiError)
assert.Equal(t, expectedAPIError, apiError)
- // Test creating a file in repo1 by user4 who does not have write access
+ // Test creating a file in repo16 by user4 who does not have write access
fileID++
treePath = fmt.Sprintf("delete/file%d.txt", fileID)
createFile(user2, repo16, treePath)
diff --git a/models/org.go b/models/org.go
index b7db32ef16..6511072e2b 100644
--- a/models/org.go
+++ b/models/org.go
@@ -162,8 +162,8 @@ func CreateOrganization(org, owner *User) (err error) {
}
// insert units for team
- var units = make([]TeamUnit, 0, len(allRepUnitTypes))
- for _, tp := range allRepUnitTypes {
+ var units = make([]TeamUnit, 0, len(AllRepoUnitTypes))
+ for _, tp := range AllRepoUnitTypes {
units = append(units, TeamUnit{
OrgID: org.ID,
TeamID: t.ID,
diff --git a/models/repo.go b/models/repo.go
index b8a3714abf..16684bdeef 100644
--- a/models/repo.go
+++ b/models/repo.go
@@ -274,32 +274,64 @@ func (repo *Repository) innerAPIFormat(e Engine, mode AccessMode, isParent bool)
parent = repo.BaseRepo.innerAPIFormat(e, mode, true)
}
}
+ hasIssues := false
+ if _, err := repo.getUnit(e, UnitTypeIssues); err == nil {
+ hasIssues = true
+ }
+ hasWiki := false
+ if _, err := repo.getUnit(e, UnitTypeWiki); err == nil {
+ hasWiki = true
+ }
+ hasPullRequests := false
+ ignoreWhitespaceConflicts := false
+ allowMerge := false
+ allowRebase := false
+ allowRebaseMerge := false
+ allowSquash := false
+ if unit, err := repo.getUnit(e, UnitTypePullRequests); err == nil {
+ config := unit.PullRequestsConfig()
+ hasPullRequests = true
+ ignoreWhitespaceConflicts = config.IgnoreWhitespaceConflicts
+ allowMerge = config.AllowMerge
+ allowRebase = config.AllowRebase
+ allowRebaseMerge = config.AllowRebaseMerge
+ allowSquash = config.AllowSquash
+ }
+
return &api.Repository{
- ID: repo.ID,
- Owner: repo.Owner.APIFormat(),
- Name: repo.Name,
- FullName: repo.FullName(),
- Description: repo.Description,
- Private: repo.IsPrivate,
- Empty: repo.IsEmpty,
- Archived: repo.IsArchived,
- Size: int(repo.Size / 1024),
- Fork: repo.IsFork,
- Parent: parent,
- Mirror: repo.IsMirror,
- HTMLURL: repo.HTMLURL(),
- SSHURL: cloneLink.SSH,
- CloneURL: cloneLink.HTTPS,
- Website: repo.Website,
- Stars: repo.NumStars,
- Forks: repo.NumForks,
- Watchers: repo.NumWatches,
- OpenIssues: repo.NumOpenIssues,
- DefaultBranch: repo.DefaultBranch,
- Created: repo.CreatedUnix.AsTime(),
- Updated: repo.UpdatedUnix.AsTime(),
- Permissions: permission,
- AvatarURL: repo.AvatarLink(),
+ ID: repo.ID,
+ Owner: repo.Owner.APIFormat(),
+ Name: repo.Name,
+ FullName: repo.FullName(),
+ Description: repo.Description,
+ Private: repo.IsPrivate,
+ Empty: repo.IsEmpty,
+ Archived: repo.IsArchived,
+ Size: int(repo.Size / 1024),
+ Fork: repo.IsFork,
+ Parent: parent,
+ Mirror: repo.IsMirror,
+ HTMLURL: repo.HTMLURL(),
+ SSHURL: cloneLink.SSH,
+ CloneURL: cloneLink.HTTPS,
+ Website: repo.Website,
+ Stars: repo.NumStars,
+ Forks: repo.NumForks,
+ Watchers: repo.NumWatches,
+ OpenIssues: repo.NumOpenIssues,
+ DefaultBranch: repo.DefaultBranch,
+ Created: repo.CreatedUnix.AsTime(),
+ Updated: repo.UpdatedUnix.AsTime(),
+ Permissions: permission,
+ HasIssues: hasIssues,
+ HasWiki: hasWiki,
+ HasPullRequests: hasPullRequests,
+ IgnoreWhitespaceConflicts: ignoreWhitespaceConflicts,
+ AllowMerge: allowMerge,
+ AllowRebase: allowRebase,
+ AllowRebaseMerge: allowRebaseMerge,
+ AllowSquash: allowSquash,
+ AvatarURL: repo.AvatarLink(),
}
}
@@ -346,10 +378,20 @@ func (repo *Repository) UnitEnabled(tp UnitType) bool {
return false
}
-var (
- // ErrUnitNotExist organization does not exist
- ErrUnitNotExist = errors.New("Unit does not exist")
-)
+// ErrUnitTypeNotExist represents a "UnitTypeNotExist" kind of error.
+type ErrUnitTypeNotExist struct {
+ UT UnitType
+}
+
+// IsErrUnitTypeNotExist checks if an error is a ErrUnitNotExist.
+func IsErrUnitTypeNotExist(err error) bool {
+ _, ok := err.(ErrUnitTypeNotExist)
+ return ok
+}
+
+func (err ErrUnitTypeNotExist) Error() string {
+ return fmt.Sprintf("Unit type does not exist: %s", err.UT.String())
+}
// MustGetUnit always returns a RepoUnit object
func (repo *Repository) MustGetUnit(tp UnitType) *RepoUnit {
@@ -373,6 +415,11 @@ func (repo *Repository) MustGetUnit(tp UnitType) *RepoUnit {
Type: tp,
Config: new(PullRequestsConfig),
}
+ } else if tp == UnitTypeIssues {
+ return &RepoUnit{
+ Type: tp,
+ Config: new(IssuesConfig),
+ }
}
return &RepoUnit{
Type: tp,
@@ -394,7 +441,7 @@ func (repo *Repository) getUnit(e Engine, tp UnitType) (*RepoUnit, error) {
return unit, nil
}
}
- return nil, ErrUnitNotExist
+ return nil, ErrUnitTypeNotExist{tp}
}
func (repo *Repository) getOwner(e Engine) (err error) {
@@ -1232,8 +1279,8 @@ func createRepository(e *xorm.Session, doer, u *User, repo *Repository) (err err
}
// insert units for repo
- var units = make([]RepoUnit, 0, len(defaultRepoUnits))
- for _, tp := range defaultRepoUnits {
+ var units = make([]RepoUnit, 0, len(DefaultRepoUnits))
+ for _, tp := range DefaultRepoUnits {
if tp == UnitTypeIssues {
units = append(units, RepoUnit{
RepoID: repo.ID,
diff --git a/models/unit.go b/models/unit.go
index 697df696bc..9f5c8d3cbb 100644
--- a/models/unit.go
+++ b/models/unit.go
@@ -58,8 +58,8 @@ func (u UnitType) ColorFormat(s fmt.State) {
}
var (
- // allRepUnitTypes contains all the unit types
- allRepUnitTypes = []UnitType{
+ // AllRepoUnitTypes contains all the unit types
+ AllRepoUnitTypes = []UnitType{
UnitTypeCode,
UnitTypeIssues,
UnitTypePullRequests,
@@ -69,8 +69,8 @@ var (
UnitTypeExternalTracker,
}
- // defaultRepoUnits contains the default unit types
- defaultRepoUnits = []UnitType{
+ // DefaultRepoUnits contains the default unit types
+ DefaultRepoUnits = []UnitType{
UnitTypeCode,
UnitTypeIssues,
UnitTypePullRequests,
diff --git a/modules/structs/repo.go b/modules/structs/repo.go
index 19f5ff8afe..b4d162b776 100644
--- a/modules/structs/repo.go
+++ b/modules/structs/repo.go
@@ -41,9 +41,17 @@ type Repository struct {
// swagger:strfmt date-time
Created time.Time `json:"created_at"`
// swagger:strfmt date-time
- Updated time.Time `json:"updated_at"`
- Permissions *Permission `json:"permissions,omitempty"`
- AvatarURL string `json:"avatar_url"`
+ Updated time.Time `json:"updated_at"`
+ Permissions *Permission `json:"permissions,omitempty"`
+ HasIssues bool `json:"has_issues"`
+ HasWiki bool `json:"has_wiki"`
+ HasPullRequests bool `json:"has_pull_requests"`
+ IgnoreWhitespaceConflicts bool `json:"ignore_whitespace_conflicts"`
+ AllowMerge bool `json:"allow_merge_commits"`
+ AllowRebase bool `json:"allow_rebase"`
+ AllowRebaseMerge bool `json:"allow_rebase_explicit"`
+ AllowSquash bool `json:"allow_squash_merge"`
+ AvatarURL string `json:"avatar_url"`
}
// CreateRepoOption options when creating repository
@@ -71,38 +79,36 @@ type CreateRepoOption struct {
// EditRepoOption options when editing a repository's properties
// swagger:model
type EditRepoOption struct {
- // Name of the repository
- //
- // required: true
+ // name of the repository
// unique: true
- Name *string `json:"name" binding:"Required;AlphaDashDot;MaxSize(100)"`
- // A short description of the repository.
+ Name *string `json:"name,omitempty" binding:"OmitEmpty;AlphaDashDot;MaxSize(100);"`
+ // a short description of the repository.
Description *string `json:"description,omitempty" binding:"MaxSize(255)"`
- // A URL with more information about the repository.
+ // a URL with more information about the repository.
Website *string `json:"website,omitempty" binding:"MaxSize(255)"`
- // Either `true` to make the repository private or `false` to make it public.
- // Note: You will get a 422 error if the organization restricts changing repository visibility to organization
+ // either `true` to make the repository private or `false` to make it public.
+ // Note: you will get a 422 error if the organization restricts changing repository visibility to organization
// owners and a non-owner tries to change the value of private.
Private *bool `json:"private,omitempty"`
- // Either `true` to enable issues for this repository or `false` to disable them.
- EnableIssues *bool `json:"enable_issues,omitempty"`
- // Either `true` to enable the wiki for this repository or `false` to disable it.
- EnableWiki *bool `json:"enable_wiki,omitempty"`
- // Updates the default branch for this repository.
+ // either `true` to enable issues for this repository or `false` to disable them.
+ HasIssues *bool `json:"has_issues,omitempty"`
+ // either `true` to enable the wiki for this repository or `false` to disable it.
+ HasWiki *bool `json:"has_wiki,omitempty"`
+ // sets the default branch for this repository.
DefaultBranch *string `json:"default_branch,omitempty"`
- // Either `true` to allow pull requests, or `false` to prevent pull request.
- EnablePullRequests *bool `json:"enable_pull_requests,omitempty"`
- // Either `true` to ignore whitepace for conflicts, or `false` to not ignore whitespace. `enabled_pull_requests` must be `true`.
- IgnoreWhitespaceConflicts *bool `json:"ignore_whitespace,omitempty"`
- // Either `true` to allow merging pull requests with a merge commit, or `false` to prevent merging pull requests with merge commits. `enabled_pull_requests` must be `true`.
+ // either `true` to allow pull requests, or `false` to prevent pull request.
+ HasPullRequests *bool `json:"has_pull_requests,omitempty"`
+ // either `true` to ignore whitespace for conflicts, or `false` to not ignore whitespace. `has_pull_requests` must be `true`.
+ IgnoreWhitespaceConflicts *bool `json:"ignore_whitespace_conflicts,omitempty"`
+ // either `true` to allow merging pull requests with a merge commit, or `false` to prevent merging pull requests with merge commits. `has_pull_requests` must be `true`.
AllowMerge *bool `json:"allow_merge_commits,omitempty"`
- // Either `true` to allow rebase-merging pull requests, or `false` to prevent rebase-merging. `enabled_pull_requests` must be `true`.
+ // either `true` to allow rebase-merging pull requests, or `false` to prevent rebase-merging. `has_pull_requests` must be `true`.
AllowRebase *bool `json:"allow_rebase,omitempty"`
- // Either `true` to allow rebase with explicit merge commits (--no-ff), or `false` to prevent rebase with explicit merge commits. `enabled_pull_requests` must be `true`.
+ // either `true` to allow rebase with explicit merge commits (--no-ff), or `false` to prevent rebase with explicit merge commits. `has_pull_requests` must be `true`.
AllowRebaseMerge *bool `json:"allow_rebase_explicit,omitempty"`
- // Either `true` to allow squash-merging pull requests, or `false` to prevent squash-merging. `enabled_pull_requests` must be `true`.
- AllowSquashMerge *bool `json:"allow_squash_merge,omitempty"`
- // `true` to archive this repository. Note: You cannot unarchive repositories through the API.
+ // either `true` to allow squash-merging pull requests, or `false` to prevent squash-merging. `has_pull_requests` must be `true`.
+ AllowSquash *bool `json:"allow_squash_merge,omitempty"`
+ // set to `true` to archive this repository.
Archived *bool `json:"archived,omitempty"`
}
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index ae64e887ca..c1561200cd 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -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)
diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go
index 62153893a6..f8df3e9fa1 100644
--- a/routers/api/v1/repo/repo.go
+++ b/routers/api/v1/repo/repo.go
@@ -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
diff --git a/routers/api/v1/repo/repo_test.go b/routers/api/v1/repo/repo_test.go
new file mode 100644
index 0000000000..053134ec61
--- /dev/null
+++ b/routers/api/v1/repo/repo_test.go
@@ -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))
+}
diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go
index 2df97304aa..c1196eeb71 100644
--- a/routers/api/v1/swagger/options.go
+++ b/routers/api/v1/swagger/options.go
@@ -82,6 +82,8 @@ type swaggerParameterBodies struct {
// in:body
CreateRepoOption api.CreateRepoOption
// in:body
+ EditRepoOption api.EditRepoOption
+ // in:body
CreateForkOption api.CreateForkOption
// in:body
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 7307d1284b..a3090d1d52 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -1210,6 +1210,51 @@
"$ref": "#/responses/forbidden"
}
}
+ },
+ "patch": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "repository"
+ ],
+ "summary": "Edit a repository's properties. Only fields that are set will be changed.",
+ "operationId": "repoEdit",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "owner of the repo to edit",
+ "name": "owner",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "name of the repo to edit",
+ "name": "repo",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "Properties of a repo that you can edit",
+ "name": "body",
+ "in": "body",
+ "schema": {
+ "$ref": "#/definitions/EditRepoOption"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/Repository"
+ },
+ "403": {
+ "$ref": "#/responses/forbidden"
+ },
+ "422": {
+ "$ref": "#/responses/validationError"
+ }
+ }
}
},
"/repos/{owner}/{repo}/archive/{archive}": {
@@ -6037,6 +6082,12 @@
"responses": {
"201": {
"$ref": "#/responses/Repository"
+ },
+ "409": {
+ "description": "The repository with the same name already exists."
+ },
+ "422": {
+ "$ref": "#/responses/validationError"
}
}
}
@@ -7738,6 +7789,84 @@
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
+ "EditRepoOption": {
+ "description": "EditRepoOption options when editing a repository's properties",
+ "type": "object",
+ "properties": {
+ "allow_merge_commits": {
+ "description": "either `true` to allow merging pull requests with a merge commit, or `false` to prevent merging pull requests with merge commits. `has_pull_requests` must be `true`.",
+ "type": "boolean",
+ "x-go-name": "AllowMerge"
+ },
+ "allow_rebase": {
+ "description": "either `true` to allow rebase-merging pull requests, or `false` to prevent rebase-merging. `has_pull_requests` must be `true`.",
+ "type": "boolean",
+ "x-go-name": "AllowRebase"
+ },
+ "allow_rebase_explicit": {
+ "description": "either `true` to allow rebase with explicit merge commits (--no-ff), or `false` to prevent rebase with explicit merge commits. `has_pull_requests` must be `true`.",
+ "type": "boolean",
+ "x-go-name": "AllowRebaseMerge"
+ },
+ "allow_squash_merge": {
+ "description": "either `true` to allow squash-merging pull requests, or `false` to prevent squash-merging. `has_pull_requests` must be `true`.",
+ "type": "boolean",
+ "x-go-name": "AllowSquash"
+ },
+ "archived": {
+ "description": "set to `true` to archive this repository.",
+ "type": "boolean",
+ "x-go-name": "Archived"
+ },
+ "default_branch": {
+ "description": "sets the default branch for this repository.",
+ "type": "string",
+ "x-go-name": "DefaultBranch"
+ },
+ "description": {
+ "description": "a short description of the repository.",
+ "type": "string",
+ "x-go-name": "Description"
+ },
+ "has_issues": {
+ "description": "either `true` to enable issues for this repository or `false` to disable them.",
+ "type": "boolean",
+ "x-go-name": "HasIssues"
+ },
+ "has_pull_requests": {
+ "description": "either `true` to allow pull requests, or `false` to prevent pull request.",
+ "type": "boolean",
+ "x-go-name": "HasPullRequests"
+ },
+ "has_wiki": {
+ "description": "either `true` to enable the wiki for this repository or `false` to disable it.",
+ "type": "boolean",
+ "x-go-name": "HasWiki"
+ },
+ "ignore_whitespace_conflicts": {
+ "description": "either `true` to ignore whitespace for conflicts, or `false` to not ignore whitespace. `has_pull_requests` must be `true`.",
+ "type": "boolean",
+ "x-go-name": "IgnoreWhitespaceConflicts"
+ },
+ "name": {
+ "description": "name of the repository",
+ "type": "string",
+ "uniqueItems": true,
+ "x-go-name": "Name"
+ },
+ "private": {
+ "description": "either `true` to make the repository private or `false` to make it public.\nNote: you will get a 422 error if the organization restricts changing repository visibility to organization\nowners and a non-owner tries to change the value of private.",
+ "type": "boolean",
+ "x-go-name": "Private"
+ },
+ "website": {
+ "description": "a URL with more information about the repository.",
+ "type": "string",
+ "x-go-name": "Website"
+ }
+ },
+ "x-go-package": "code.gitea.io/gitea/modules/structs"
+ },
"EditTeamOption": {
"description": "EditTeamOption options for editing a team",
"type": "object",
@@ -9062,6 +9191,22 @@
"description": "Repository represents a repository",
"type": "object",
"properties": {
+ "allow_merge_commits": {
+ "type": "boolean",
+ "x-go-name": "AllowMerge"
+ },
+ "allow_rebase": {
+ "type": "boolean",
+ "x-go-name": "AllowRebase"
+ },
+ "allow_rebase_explicit": {
+ "type": "boolean",
+ "x-go-name": "AllowRebaseMerge"
+ },
+ "allow_squash_merge": {
+ "type": "boolean",
+ "x-go-name": "AllowSquash"
+ },
"archived": {
"type": "boolean",
"x-go-name": "Archived"
@@ -9104,6 +9249,18 @@
"type": "string",
"x-go-name": "FullName"
},
+ "has_issues": {
+ "type": "boolean",
+ "x-go-name": "HasIssues"
+ },
+ "has_pull_requests": {
+ "type": "boolean",
+ "x-go-name": "HasPullRequests"
+ },
+ "has_wiki": {
+ "type": "boolean",
+ "x-go-name": "HasWiki"
+ },
"html_url": {
"type": "string",
"x-go-name": "HTMLURL"
@@ -9113,6 +9270,10 @@
"format": "int64",
"x-go-name": "ID"
},
+ "ignore_whitespace_conflicts": {
+ "type": "boolean",
+ "x-go-name": "IgnoreWhitespaceConflicts"
+ },
"mirror": {
"type": "boolean",
"x-go-name": "Mirror"