* 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
				
			
		
							
								
								
									
										225
									
								
								integrations/api_repo_edit_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										225
									
								
								integrations/api_repo_edit_test.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -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) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | @ -108,7 +108,7 @@ func TestAPIDeleteFile(t *testing.T) { | ||||||
| 		DecodeJSON(t, resp, &apiError) | 		DecodeJSON(t, resp, &apiError) | ||||||
| 		assert.Equal(t, expectedAPIError, 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++ | 		fileID++ | ||||||
| 		treePath = fmt.Sprintf("delete/file%d.txt", fileID) | 		treePath = fmt.Sprintf("delete/file%d.txt", fileID) | ||||||
| 		createFile(user2, repo16, treePath) | 		createFile(user2, repo16, treePath) | ||||||
|  |  | ||||||
|  | @ -162,8 +162,8 @@ func CreateOrganization(org, owner *User) (err error) { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// insert units for team
 | 	// insert units for team
 | ||||||
| 	var units = make([]TeamUnit, 0, len(allRepUnitTypes)) | 	var units = make([]TeamUnit, 0, len(AllRepoUnitTypes)) | ||||||
| 	for _, tp := range allRepUnitTypes { | 	for _, tp := range AllRepoUnitTypes { | ||||||
| 		units = append(units, TeamUnit{ | 		units = append(units, TeamUnit{ | ||||||
| 			OrgID:  org.ID, | 			OrgID:  org.ID, | ||||||
| 			TeamID: t.ID, | 			TeamID: t.ID, | ||||||
|  |  | ||||||
							
								
								
									
										111
									
								
								models/repo.go
									
									
									
									
									
								
							
							
						
						
									
										111
									
								
								models/repo.go
									
									
									
									
									
								
							|  | @ -274,32 +274,64 @@ func (repo *Repository) innerAPIFormat(e Engine, mode AccessMode, isParent bool) | ||||||
| 			parent = repo.BaseRepo.innerAPIFormat(e, mode, true) | 			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{ | 	return &api.Repository{ | ||||||
| 		ID:            repo.ID, | 		ID:                        repo.ID, | ||||||
| 		Owner:         repo.Owner.APIFormat(), | 		Owner:                     repo.Owner.APIFormat(), | ||||||
| 		Name:          repo.Name, | 		Name:                      repo.Name, | ||||||
| 		FullName:      repo.FullName(), | 		FullName:                  repo.FullName(), | ||||||
| 		Description:   repo.Description, | 		Description:               repo.Description, | ||||||
| 		Private:       repo.IsPrivate, | 		Private:                   repo.IsPrivate, | ||||||
| 		Empty:         repo.IsEmpty, | 		Empty:                     repo.IsEmpty, | ||||||
| 		Archived:      repo.IsArchived, | 		Archived:                  repo.IsArchived, | ||||||
| 		Size:          int(repo.Size / 1024), | 		Size:                      int(repo.Size / 1024), | ||||||
| 		Fork:          repo.IsFork, | 		Fork:                      repo.IsFork, | ||||||
| 		Parent:        parent, | 		Parent:                    parent, | ||||||
| 		Mirror:        repo.IsMirror, | 		Mirror:                    repo.IsMirror, | ||||||
| 		HTMLURL:       repo.HTMLURL(), | 		HTMLURL:                   repo.HTMLURL(), | ||||||
| 		SSHURL:        cloneLink.SSH, | 		SSHURL:                    cloneLink.SSH, | ||||||
| 		CloneURL:      cloneLink.HTTPS, | 		CloneURL:                  cloneLink.HTTPS, | ||||||
| 		Website:       repo.Website, | 		Website:                   repo.Website, | ||||||
| 		Stars:         repo.NumStars, | 		Stars:                     repo.NumStars, | ||||||
| 		Forks:         repo.NumForks, | 		Forks:                     repo.NumForks, | ||||||
| 		Watchers:      repo.NumWatches, | 		Watchers:                  repo.NumWatches, | ||||||
| 		OpenIssues:    repo.NumOpenIssues, | 		OpenIssues:                repo.NumOpenIssues, | ||||||
| 		DefaultBranch: repo.DefaultBranch, | 		DefaultBranch:             repo.DefaultBranch, | ||||||
| 		Created:       repo.CreatedUnix.AsTime(), | 		Created:                   repo.CreatedUnix.AsTime(), | ||||||
| 		Updated:       repo.UpdatedUnix.AsTime(), | 		Updated:                   repo.UpdatedUnix.AsTime(), | ||||||
| 		Permissions:   permission, | 		Permissions:               permission, | ||||||
| 		AvatarURL:     repo.AvatarLink(), | 		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 | 	return false | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| var ( | // ErrUnitTypeNotExist represents a "UnitTypeNotExist" kind of error.
 | ||||||
| 	// ErrUnitNotExist organization does not exist
 | type ErrUnitTypeNotExist struct { | ||||||
| 	ErrUnitNotExist = errors.New("Unit does not exist") | 	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
 | // MustGetUnit always returns a RepoUnit object
 | ||||||
| func (repo *Repository) MustGetUnit(tp UnitType) *RepoUnit { | func (repo *Repository) MustGetUnit(tp UnitType) *RepoUnit { | ||||||
|  | @ -373,6 +415,11 @@ func (repo *Repository) MustGetUnit(tp UnitType) *RepoUnit { | ||||||
| 			Type:   tp, | 			Type:   tp, | ||||||
| 			Config: new(PullRequestsConfig), | 			Config: new(PullRequestsConfig), | ||||||
| 		} | 		} | ||||||
|  | 	} else if tp == UnitTypeIssues { | ||||||
|  | 		return &RepoUnit{ | ||||||
|  | 			Type:   tp, | ||||||
|  | 			Config: new(IssuesConfig), | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 	return &RepoUnit{ | 	return &RepoUnit{ | ||||||
| 		Type:   tp, | 		Type:   tp, | ||||||
|  | @ -394,7 +441,7 @@ func (repo *Repository) getUnit(e Engine, tp UnitType) (*RepoUnit, error) { | ||||||
| 			return unit, nil | 			return unit, nil | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	return nil, ErrUnitNotExist | 	return nil, ErrUnitTypeNotExist{tp} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (repo *Repository) getOwner(e Engine) (err error) { | 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
 | 	// insert units for repo
 | ||||||
| 	var units = make([]RepoUnit, 0, len(defaultRepoUnits)) | 	var units = make([]RepoUnit, 0, len(DefaultRepoUnits)) | ||||||
| 	for _, tp := range defaultRepoUnits { | 	for _, tp := range DefaultRepoUnits { | ||||||
| 		if tp == UnitTypeIssues { | 		if tp == UnitTypeIssues { | ||||||
| 			units = append(units, RepoUnit{ | 			units = append(units, RepoUnit{ | ||||||
| 				RepoID: repo.ID, | 				RepoID: repo.ID, | ||||||
|  |  | ||||||
|  | @ -58,8 +58,8 @@ func (u UnitType) ColorFormat(s fmt.State) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| var ( | var ( | ||||||
| 	// allRepUnitTypes contains all the unit types
 | 	// AllRepoUnitTypes contains all the unit types
 | ||||||
| 	allRepUnitTypes = []UnitType{ | 	AllRepoUnitTypes = []UnitType{ | ||||||
| 		UnitTypeCode, | 		UnitTypeCode, | ||||||
| 		UnitTypeIssues, | 		UnitTypeIssues, | ||||||
| 		UnitTypePullRequests, | 		UnitTypePullRequests, | ||||||
|  | @ -69,8 +69,8 @@ var ( | ||||||
| 		UnitTypeExternalTracker, | 		UnitTypeExternalTracker, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// defaultRepoUnits contains the default unit types
 | 	// DefaultRepoUnits contains the default unit types
 | ||||||
| 	defaultRepoUnits = []UnitType{ | 	DefaultRepoUnits = []UnitType{ | ||||||
| 		UnitTypeCode, | 		UnitTypeCode, | ||||||
| 		UnitTypeIssues, | 		UnitTypeIssues, | ||||||
| 		UnitTypePullRequests, | 		UnitTypePullRequests, | ||||||
|  |  | ||||||
|  | @ -41,9 +41,17 @@ type Repository struct { | ||||||
| 	// swagger:strfmt date-time
 | 	// swagger:strfmt date-time
 | ||||||
| 	Created time.Time `json:"created_at"` | 	Created time.Time `json:"created_at"` | ||||||
| 	// swagger:strfmt date-time
 | 	// swagger:strfmt date-time
 | ||||||
| 	Updated     time.Time   `json:"updated_at"` | 	Updated                   time.Time   `json:"updated_at"` | ||||||
| 	Permissions *Permission `json:"permissions,omitempty"` | 	Permissions               *Permission `json:"permissions,omitempty"` | ||||||
| 	AvatarURL   string      `json:"avatar_url"` | 	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
 | // CreateRepoOption options when creating repository
 | ||||||
|  | @ -71,38 +79,36 @@ type CreateRepoOption struct { | ||||||
| // EditRepoOption options when editing a repository's properties
 | // EditRepoOption options when editing a repository's properties
 | ||||||
| // swagger:model
 | // swagger:model
 | ||||||
| type EditRepoOption struct { | type EditRepoOption struct { | ||||||
| 	// Name of the repository
 | 	// name of the repository
 | ||||||
| 	//
 |  | ||||||
| 	// required: true
 |  | ||||||
| 	// unique: true
 | 	// unique: true
 | ||||||
| 	Name *string `json:"name" binding:"Required;AlphaDashDot;MaxSize(100)"` | 	Name *string `json:"name,omitempty" binding:"OmitEmpty;AlphaDashDot;MaxSize(100);"` | ||||||
| 	// A short description of the repository.
 | 	// a short description of the repository.
 | ||||||
| 	Description *string `json:"description,omitempty" binding:"MaxSize(255)"` | 	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)"` | 	Website *string `json:"website,omitempty" binding:"MaxSize(255)"` | ||||||
| 	// Either `true` to make the repository private or `false` to make it public.
 | 	// 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
 | 	// 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.
 | 	// owners and a non-owner tries to change the value of private.
 | ||||||
| 	Private *bool `json:"private,omitempty"` | 	Private *bool `json:"private,omitempty"` | ||||||
| 	// Either `true` to enable issues for this repository or `false` to disable them.
 | 	// either `true` to enable issues for this repository or `false` to disable them.
 | ||||||
| 	EnableIssues *bool `json:"enable_issues,omitempty"` | 	HasIssues *bool `json:"has_issues,omitempty"` | ||||||
| 	// Either `true` to enable the wiki for this repository or `false` to disable it.
 | 	// either `true` to enable the wiki for this repository or `false` to disable it.
 | ||||||
| 	EnableWiki *bool `json:"enable_wiki,omitempty"` | 	HasWiki *bool `json:"has_wiki,omitempty"` | ||||||
| 	// Updates the default branch for this repository.
 | 	// sets the default branch for this repository.
 | ||||||
| 	DefaultBranch *string `json:"default_branch,omitempty"` | 	DefaultBranch *string `json:"default_branch,omitempty"` | ||||||
| 	// Either `true` to allow pull requests, or `false` to prevent pull request.
 | 	// either `true` to allow pull requests, or `false` to prevent pull request.
 | ||||||
| 	EnablePullRequests *bool `json:"enable_pull_requests,omitempty"` | 	HasPullRequests *bool `json:"has_pull_requests,omitempty"` | ||||||
| 	// Either `true` to ignore whitepace for conflicts, or `false` to not ignore whitespace. `enabled_pull_requests` must be `true`.
 | 	// either `true` to ignore whitespace for conflicts, or `false` to not ignore whitespace. `has_pull_requests` must be `true`.
 | ||||||
| 	IgnoreWhitespaceConflicts *bool `json:"ignore_whitespace,omitempty"` | 	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. `enabled_pull_requests` must be `true`.
 | 	// 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"` | 	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"` | 	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"` | 	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`.
 | 	// either `true` to allow squash-merging pull requests, or `false` to prevent squash-merging. `has_pull_requests` must be `true`.
 | ||||||
| 	AllowSquashMerge *bool `json:"allow_squash_merge,omitempty"` | 	AllowSquash *bool `json:"allow_squash_merge,omitempty"` | ||||||
| 	// `true` to archive this repository. Note: You cannot unarchive repositories through the API.
 | 	// set to `true` to archive this repository.
 | ||||||
| 	Archived *bool `json:"archived,omitempty"` | 	Archived *bool `json:"archived,omitempty"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -608,7 +608,8 @@ func RegisterRoutes(m *macaron.Macaron) { | ||||||
| 
 | 
 | ||||||
| 			m.Group("/:username/:reponame", func() { | 			m.Group("/:username/:reponame", func() { | ||||||
| 				m.Combo("").Get(reqAnyRepoReader(), repo.Get). | 				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.Group("/hooks", func() { | ||||||
| 					m.Combo("").Get(repo.ListHooks). | 					m.Combo("").Get(repo.ListHooks). | ||||||
| 						Post(bind(api.CreateHookOption{}), repo.CreateHook) | 						Post(bind(api.CreateHookOption{}), repo.CreateHook) | ||||||
|  |  | ||||||
|  | @ -240,6 +240,10 @@ func Create(ctx *context.APIContext, opt api.CreateRepoOption) { | ||||||
| 	// responses:
 | 	// responses:
 | ||||||
| 	//   "201":
 | 	//   "201":
 | ||||||
| 	//     "$ref": "#/responses/Repository"
 | 	//     "$ref": "#/responses/Repository"
 | ||||||
|  | 	//   "409":
 | ||||||
|  | 	//     description: The repository with the same name already exists.
 | ||||||
|  | 	//   "422":
 | ||||||
|  | 	//     "$ref": "#/responses/validationError"
 | ||||||
| 	if ctx.User.IsOrganization() { | 	if ctx.User.IsOrganization() { | ||||||
| 		// Shouldn't reach this condition, but just in case.
 | 		// Shouldn't reach this condition, but just in case.
 | ||||||
| 		ctx.Error(422, "", "not allowed creating repository for organization") | 		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)) | 	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
 | // Delete one repository
 | ||||||
| func Delete(ctx *context.APIContext) { | func Delete(ctx *context.APIContext) { | ||||||
| 	// swagger:operation DELETE /repos/{owner}/{repo} repository repoDelete
 | 	// 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
 | 	// in:body
 | ||||||
| 	CreateRepoOption api.CreateRepoOption | 	CreateRepoOption api.CreateRepoOption | ||||||
| 	// in:body
 | 	// in:body
 | ||||||
|  | 	EditRepoOption api.EditRepoOption | ||||||
|  | 	// in:body
 | ||||||
| 	CreateForkOption api.CreateForkOption | 	CreateForkOption api.CreateForkOption | ||||||
| 
 | 
 | ||||||
| 	// in:body
 | 	// in:body
 | ||||||
|  |  | ||||||
|  | @ -1210,6 +1210,51 @@ | ||||||
|             "$ref": "#/responses/forbidden" |             "$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}": { |     "/repos/{owner}/{repo}/archive/{archive}": { | ||||||
|  | @ -6037,6 +6082,12 @@ | ||||||
|         "responses": { |         "responses": { | ||||||
|           "201": { |           "201": { | ||||||
|             "$ref": "#/responses/Repository" |             "$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" |       "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": { |     "EditTeamOption": { | ||||||
|       "description": "EditTeamOption options for editing a team", |       "description": "EditTeamOption options for editing a team", | ||||||
|       "type": "object", |       "type": "object", | ||||||
|  | @ -9062,6 +9191,22 @@ | ||||||
|       "description": "Repository represents a repository", |       "description": "Repository represents a repository", | ||||||
|       "type": "object", |       "type": "object", | ||||||
|       "properties": { |       "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": { |         "archived": { | ||||||
|           "type": "boolean", |           "type": "boolean", | ||||||
|           "x-go-name": "Archived" |           "x-go-name": "Archived" | ||||||
|  | @ -9104,6 +9249,18 @@ | ||||||
|           "type": "string", |           "type": "string", | ||||||
|           "x-go-name": "FullName" |           "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": { |         "html_url": { | ||||||
|           "type": "string", |           "type": "string", | ||||||
|           "x-go-name": "HTMLURL" |           "x-go-name": "HTMLURL" | ||||||
|  | @ -9113,6 +9270,10 @@ | ||||||
|           "format": "int64", |           "format": "int64", | ||||||
|           "x-go-name": "ID" |           "x-go-name": "ID" | ||||||
|         }, |         }, | ||||||
|  |         "ignore_whitespace_conflicts": { | ||||||
|  |           "type": "boolean", | ||||||
|  |           "x-go-name": "IgnoreWhitespaceConflicts" | ||||||
|  |         }, | ||||||
|         "mirror": { |         "mirror": { | ||||||
|           "type": "boolean", |           "type": "boolean", | ||||||
|           "x-go-name": "Mirror" |           "x-go-name": "Mirror" | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue