mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 16:40:24 +08:00 
			
		
		
		
	Push to create repo (#8419)
* Refactor Signed-off-by: jolheiser <john.olheiser@gmail.com> * Add push-create to SSH serv Signed-off-by: jolheiser <john.olheiser@gmail.com> * Cannot push for another user unless admin Signed-off-by: jolheiser <john.olheiser@gmail.com> * Get owner in case admin pushes for another user Signed-off-by: jolheiser <john.olheiser@gmail.com> * Set new repo ID in result Signed-off-by: jolheiser <john.olheiser@gmail.com> * Update to service and use new org perms Signed-off-by: jolheiser <john.olheiser@gmail.com> * Move pushCreateRepo to services Signed-off-by: jolheiser <john.olheiser@gmail.com> * Fix import order Signed-off-by: jolheiser <john.olheiser@gmail.com> * Changes for @guillep2k * Check owner (not user) in SSH * Add basic tests for created repos (private, not empty) Signed-off-by: jolheiser <john.olheiser@gmail.com>
This commit is contained in:
		
				
					committed by
					
						
						Lunny Xiao
					
				
			
			
				
	
			
			
			
						parent
						
							47c24be293
						
					
				
				
					commit
					6715677b2b
				
			@@ -39,6 +39,9 @@ ACCESS_CONTROL_ALLOW_ORIGIN =
 | 
			
		||||
USE_COMPAT_SSH_URI = false
 | 
			
		||||
; Close issues as long as a commit on any branch marks it as fixed
 | 
			
		||||
DEFAULT_CLOSE_ISSUES_VIA_COMMITS_IN_ANY_BRANCH = false
 | 
			
		||||
; Allow users to push local repositories to Gitea and have them automatically created for a user or an org
 | 
			
		||||
ENABLE_PUSH_CREATE_USER = false
 | 
			
		||||
ENABLE_PUSH_CREATE_ORG = false
 | 
			
		||||
 | 
			
		||||
[repository.editor]
 | 
			
		||||
; List of file extensions for which lines should be wrapped in the CodeMirror editor
 | 
			
		||||
 
 | 
			
		||||
@@ -66,6 +66,8 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
 | 
			
		||||
   default is not to present. **WARNING**: This maybe harmful to you website if you do not
 | 
			
		||||
   give it a right value.
 | 
			
		||||
- `DEFAULT_CLOSE_ISSUES_VIA_COMMITS_IN_ANY_BRANCH`:  **false**: Close an issue if a commit on a non default branch marks it as closed.
 | 
			
		||||
- `ENABLE_PUSH_CREATE_USER`:  **false**: Allow users to push local repositories to Gitea and have them automatically created for a user.
 | 
			
		||||
- `ENABLE_PUSH_CREATE_ORG`:  **false**: Allow users to push local repositories to Gitea and have them automatically created for an org.
 | 
			
		||||
 | 
			
		||||
### Repository - Pull Request (`repository.pull-request`)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -75,6 +75,8 @@ func testGit(t *testing.T, u *url.URL) {
 | 
			
		||||
			rawTest(t, &forkedUserCtx, little, big, littleLFS, bigLFS)
 | 
			
		||||
			mediaTest(t, &forkedUserCtx, little, big, littleLFS, bigLFS)
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		t.Run("PushCreate", doPushCreate(httpContext, u))
 | 
			
		||||
	})
 | 
			
		||||
	t.Run("SSH", func(t *testing.T) {
 | 
			
		||||
		defer PrintCurrentTest(t)()
 | 
			
		||||
@@ -113,6 +115,8 @@ func testGit(t *testing.T, u *url.URL) {
 | 
			
		||||
				rawTest(t, &forkedUserCtx, little, big, littleLFS, bigLFS)
 | 
			
		||||
				mediaTest(t, &forkedUserCtx, little, big, littleLFS, bigLFS)
 | 
			
		||||
			})
 | 
			
		||||
 | 
			
		||||
			t.Run("PushCreate", doPushCreate(sshContext, sshURL))
 | 
			
		||||
		})
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
@@ -408,3 +412,57 @@ func doMergeFork(ctx, baseCtx APITestContext, baseBranch, headBranch string) fun
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func doPushCreate(ctx APITestContext, u *url.URL) func(t *testing.T) {
 | 
			
		||||
	return func(t *testing.T) {
 | 
			
		||||
		defer PrintCurrentTest(t)()
 | 
			
		||||
		ctx.Reponame = fmt.Sprintf("repo-tmp-push-create-%s", u.Scheme)
 | 
			
		||||
		u.Path = ctx.GitPath()
 | 
			
		||||
 | 
			
		||||
		tmpDir, err := ioutil.TempDir("", ctx.Reponame)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		err = git.InitRepository(tmpDir, false)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		_, err = os.Create(filepath.Join(tmpDir, "test.txt"))
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		err = git.AddChanges(tmpDir, true)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		err = git.CommitChanges(tmpDir, git.CommitChangesOptions{
 | 
			
		||||
			Committer: &git.Signature{
 | 
			
		||||
				Email: "user2@example.com",
 | 
			
		||||
				Name:  "User Two",
 | 
			
		||||
				When:  time.Now(),
 | 
			
		||||
			},
 | 
			
		||||
			Author: &git.Signature{
 | 
			
		||||
				Email: "user2@example.com",
 | 
			
		||||
				Name:  "User Two",
 | 
			
		||||
				When:  time.Now(),
 | 
			
		||||
			},
 | 
			
		||||
			Message: fmt.Sprintf("Testing push create @ %v", time.Now()),
 | 
			
		||||
		})
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		_, err = git.NewCommand("remote", "add", "origin", u.String()).RunInDir(tmpDir)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		// Push to create disabled
 | 
			
		||||
		setting.Repository.EnablePushCreateUser = false
 | 
			
		||||
		_, err = git.NewCommand("push", "origin", "master").RunInDir(tmpDir)
 | 
			
		||||
		assert.Error(t, err)
 | 
			
		||||
 | 
			
		||||
		// Push to create enabled
 | 
			
		||||
		setting.Repository.EnablePushCreateUser = true
 | 
			
		||||
		_, err = git.NewCommand("push", "origin", "master").RunInDir(tmpDir)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		// Fetch repo from database
 | 
			
		||||
		repo, err := models.GetRepositoryByOwnerAndName(ctx.Username, ctx.Reponame)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.False(t, repo.IsEmpty)
 | 
			
		||||
		assert.True(t, repo.IsPrivate)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -35,6 +35,8 @@ var (
 | 
			
		||||
		AccessControlAllowOrigin                string
 | 
			
		||||
		UseCompatSSHURI                         bool
 | 
			
		||||
		DefaultCloseIssuesViaCommitsInAnyBranch bool
 | 
			
		||||
		EnablePushCreateUser                    bool
 | 
			
		||||
		EnablePushCreateOrg                     bool
 | 
			
		||||
 | 
			
		||||
		// Repository editor settings
 | 
			
		||||
		Editor struct {
 | 
			
		||||
@@ -89,6 +91,8 @@ var (
 | 
			
		||||
		AccessControlAllowOrigin:                "",
 | 
			
		||||
		UseCompatSSHURI:                         false,
 | 
			
		||||
		DefaultCloseIssuesViaCommitsInAnyBranch: false,
 | 
			
		||||
		EnablePushCreateUser:                    false,
 | 
			
		||||
		EnablePushCreateOrg:                     false,
 | 
			
		||||
 | 
			
		||||
		// Repository editor settings
 | 
			
		||||
		Editor: struct {
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/private"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	repo_service "code.gitea.io/gitea/services/repository"
 | 
			
		||||
 | 
			
		||||
	"gitea.com/macaron/macaron"
 | 
			
		||||
)
 | 
			
		||||
@@ -98,16 +99,12 @@ func ServCommand(ctx *macaron.Context) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Now get the Repository and set the results section
 | 
			
		||||
	repoExist := true
 | 
			
		||||
	repo, err := models.GetRepositoryByOwnerAndName(results.OwnerName, results.RepoName)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if models.IsErrRepoNotExist(err) {
 | 
			
		||||
			ctx.JSON(http.StatusNotFound, map[string]interface{}{
 | 
			
		||||
				"results": results,
 | 
			
		||||
				"type":    "ErrRepoNotExist",
 | 
			
		||||
				"err":     fmt.Sprintf("Cannot find repository %s/%s", results.OwnerName, results.RepoName),
 | 
			
		||||
			})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
			repoExist = false
 | 
			
		||||
		} else {
 | 
			
		||||
			log.Error("Unable to get repository: %s/%s Error: %v", results.OwnerName, results.RepoName, err)
 | 
			
		||||
			ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
 | 
			
		||||
				"results": results,
 | 
			
		||||
@@ -116,6 +113,9 @@ func ServCommand(ctx *macaron.Context) {
 | 
			
		||||
			})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if repoExist {
 | 
			
		||||
		repo.OwnerName = ownerName
 | 
			
		||||
		results.RepoID = repo.ID
 | 
			
		||||
 | 
			
		||||
@@ -137,6 +137,7 @@ func ServCommand(ctx *macaron.Context) {
 | 
			
		||||
			})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Get the Public Key represented by the keyID
 | 
			
		||||
	key, err := models.GetPublicKeyByID(keyID)
 | 
			
		||||
@@ -161,6 +162,16 @@ func ServCommand(ctx *macaron.Context) {
 | 
			
		||||
	results.KeyID = key.ID
 | 
			
		||||
	results.UserID = key.OwnerID
 | 
			
		||||
 | 
			
		||||
	// If repo doesn't exist, deploy key doesn't make sense
 | 
			
		||||
	if !repoExist && key.Type == models.KeyTypeDeploy {
 | 
			
		||||
		ctx.JSON(http.StatusNotFound, map[string]interface{}{
 | 
			
		||||
			"results": results,
 | 
			
		||||
			"type":    "ErrRepoNotExist",
 | 
			
		||||
			"err":     fmt.Sprintf("Cannot find repository %s/%s", results.OwnerName, results.RepoName),
 | 
			
		||||
		})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Deploy Keys have ownerID set to 0 therefore we can't use the owner
 | 
			
		||||
	// So now we need to check if the key is a deploy key
 | 
			
		||||
	// We'll keep hold of the deploy key here for permissions checking
 | 
			
		||||
@@ -220,7 +231,7 @@ func ServCommand(ctx *macaron.Context) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Don't allow pushing if the repo is archived
 | 
			
		||||
	if mode > models.AccessModeRead && repo.IsArchived {
 | 
			
		||||
	if repoExist && mode > models.AccessModeRead && repo.IsArchived {
 | 
			
		||||
		ctx.JSON(http.StatusUnauthorized, map[string]interface{}{
 | 
			
		||||
			"results": results,
 | 
			
		||||
			"type":    "ErrRepoIsArchived",
 | 
			
		||||
@@ -230,7 +241,7 @@ func ServCommand(ctx *macaron.Context) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Permissions checking:
 | 
			
		||||
	if mode > models.AccessModeRead || repo.IsPrivate || setting.Service.RequireSignInView {
 | 
			
		||||
	if repoExist && (mode > models.AccessModeRead || repo.IsPrivate || setting.Service.RequireSignInView) {
 | 
			
		||||
		if key.Type == models.KeyTypeDeploy {
 | 
			
		||||
			if deployKey.Mode < mode {
 | 
			
		||||
				ctx.JSON(http.StatusUnauthorized, map[string]interface{}{
 | 
			
		||||
@@ -265,6 +276,48 @@ func ServCommand(ctx *macaron.Context) {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// We already know we aren't using a deploy key
 | 
			
		||||
	if !repoExist {
 | 
			
		||||
		owner, err := models.GetUserByName(ownerName)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
 | 
			
		||||
				"results": results,
 | 
			
		||||
				"type":    "InternalServerError",
 | 
			
		||||
				"err":     fmt.Sprintf("Unable to get owner: %s %v", results.OwnerName, err),
 | 
			
		||||
			})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if owner.IsOrganization() && !setting.Repository.EnablePushCreateOrg {
 | 
			
		||||
			ctx.JSON(http.StatusForbidden, map[string]interface{}{
 | 
			
		||||
				"results": results,
 | 
			
		||||
				"type":    "ErrForbidden",
 | 
			
		||||
				"err":     "Push to create is not enabled for organizations.",
 | 
			
		||||
			})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		if !owner.IsOrganization() && !setting.Repository.EnablePushCreateUser {
 | 
			
		||||
			ctx.JSON(http.StatusForbidden, map[string]interface{}{
 | 
			
		||||
				"results": results,
 | 
			
		||||
				"type":    "ErrForbidden",
 | 
			
		||||
				"err":     "Push to create is not enabled for users.",
 | 
			
		||||
			})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		repo, err = repo_service.PushCreateRepo(user, owner, results.RepoName)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Error("pushCreateRepo: %v", err)
 | 
			
		||||
			ctx.JSON(http.StatusNotFound, map[string]interface{}{
 | 
			
		||||
				"results": results,
 | 
			
		||||
				"type":    "ErrRepoNotExist",
 | 
			
		||||
				"err":     fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName),
 | 
			
		||||
			})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		results.RepoID = repo.ID
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Finally if we're trying to touch the wiki we should init it
 | 
			
		||||
	if results.IsWiki {
 | 
			
		||||
		if err = repo.InitWiki(); err != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -28,6 +28,7 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/process"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
			
		||||
	repo_service "code.gitea.io/gitea/services/repository"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// HTTP implmentation git smart HTTP protocol
 | 
			
		||||
@@ -100,29 +101,29 @@ func HTTP(ctx *context.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	repoExist := true
 | 
			
		||||
	repo, err := models.GetRepositoryByName(owner.ID, reponame)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if models.IsErrRepoNotExist(err) {
 | 
			
		||||
			redirectRepoID, err := models.LookupRepoRedirect(owner.ID, reponame)
 | 
			
		||||
			if err == nil {
 | 
			
		||||
			if redirectRepoID, err := models.LookupRepoRedirect(owner.ID, reponame); err == nil {
 | 
			
		||||
				context.RedirectToRepo(ctx, redirectRepoID)
 | 
			
		||||
			} else {
 | 
			
		||||
				ctx.NotFoundOrServerError("GetRepositoryByName", models.IsErrRepoRedirectNotExist, err)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			repoExist = false
 | 
			
		||||
		} else {
 | 
			
		||||
			ctx.ServerError("GetRepositoryByName", err)
 | 
			
		||||
		}
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Don't allow pushing if the repo is archived
 | 
			
		||||
	if repo.IsArchived && !isPull {
 | 
			
		||||
	if repoExist && repo.IsArchived && !isPull {
 | 
			
		||||
		ctx.HandleText(http.StatusForbidden, "This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Only public pull don't need auth.
 | 
			
		||||
	isPublicPull := !repo.IsPrivate && isPull
 | 
			
		||||
	isPublicPull := repoExist && !repo.IsPrivate && isPull
 | 
			
		||||
	var (
 | 
			
		||||
		askAuth      = !isPublicPull || setting.Service.RequireSignInView
 | 
			
		||||
		authUser     *models.User
 | 
			
		||||
@@ -243,6 +244,7 @@ func HTTP(ctx *context.Context) {
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if repoExist {
 | 
			
		||||
			perm, err := models.GetUserRepoPermission(repo, authUser)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				ctx.ServerError("GetUserRepoPermission", err)
 | 
			
		||||
@@ -258,13 +260,13 @@ func HTTP(ctx *context.Context) {
 | 
			
		||||
				ctx.HandleText(http.StatusForbidden, "mirror repository is read-only")
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		environ = []string{
 | 
			
		||||
			models.EnvRepoUsername + "=" + username,
 | 
			
		||||
			models.EnvRepoName + "=" + reponame,
 | 
			
		||||
			models.EnvPusherName + "=" + authUser.Name,
 | 
			
		||||
			models.EnvPusherID + fmt.Sprintf("=%d", authUser.ID),
 | 
			
		||||
			models.ProtectedBranchRepoID + fmt.Sprintf("=%d", repo.ID),
 | 
			
		||||
			models.EnvIsDeployKey + "=false",
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@@ -279,6 +281,25 @@ func HTTP(ctx *context.Context) {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !repoExist {
 | 
			
		||||
		if owner.IsOrganization() && !setting.Repository.EnablePushCreateOrg {
 | 
			
		||||
			ctx.HandleText(http.StatusForbidden, "Push to create is not enabled for organizations.")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		if !owner.IsOrganization() && !setting.Repository.EnablePushCreateUser {
 | 
			
		||||
			ctx.HandleText(http.StatusForbidden, "Push to create is not enabled for users.")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		repo, err = repo_service.PushCreateRepo(authUser, owner, reponame)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Error("pushCreateRepo: %v", err)
 | 
			
		||||
			ctx.Status(http.StatusNotFound)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	environ = append(environ, models.ProtectedBranchRepoID+fmt.Sprintf("=%d", repo.ID))
 | 
			
		||||
 | 
			
		||||
	w := ctx.Resp
 | 
			
		||||
	r := ctx.Req.Request
 | 
			
		||||
	cfg := &serviceConfig{
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,8 @@
 | 
			
		||||
package repository
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/notification"
 | 
			
		||||
@@ -54,3 +56,28 @@ func DeleteRepository(doer *models.User, repo *models.Repository) error {
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PushCreateRepo creates a repository when a new repository is pushed to an appropriate namespace
 | 
			
		||||
func PushCreateRepo(authUser, owner *models.User, repoName string) (*models.Repository, error) {
 | 
			
		||||
	if !authUser.IsAdmin {
 | 
			
		||||
		if owner.IsOrganization() {
 | 
			
		||||
			if ok, err := owner.CanCreateOrgRepo(authUser.ID); err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			} else if !ok {
 | 
			
		||||
				return nil, fmt.Errorf("cannot push-create repository for org")
 | 
			
		||||
			}
 | 
			
		||||
		} else if authUser.ID != owner.ID {
 | 
			
		||||
			return nil, fmt.Errorf("cannot push-create repository for another user")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	repo, err := CreateRepository(authUser, owner, models.CreateRepoOptions{
 | 
			
		||||
		Name:      repoName,
 | 
			
		||||
		IsPrivate: true,
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return repo, nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user