summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--custom/conf/app.ini.sample3
-rw-r--r--docs/content/doc/advanced/config-cheat-sheet.en-us.md2
-rw-r--r--integrations/git_test.go58
-rw-r--r--modules/setting/repository.go4
-rw-r--r--routers/private/serv.go113
-rw-r--r--routers/repo/http.go61
-rw-r--r--services/repository/repository.go27
7 files changed, 218 insertions, 50 deletions
diff --git a/custom/conf/app.ini.sample b/custom/conf/app.ini.sample
index 62ccda2306..76889484b5 100644
--- a/custom/conf/app.ini.sample
+++ b/custom/conf/app.ini.sample
@@ -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
diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
index 639f60fe5d..4174a87471 100644
--- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md
+++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
@@ -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`)
diff --git a/integrations/git_test.go b/integrations/git_test.go
index b504cd7524..7d37555f06 100644
--- a/integrations/git_test.go
+++ b/integrations/git_test.go
@@ -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)
+ }
+}
diff --git a/modules/setting/repository.go b/modules/setting/repository.go
index 3e183b6c98..3e7393efb6 100644
--- a/modules/setting/repository.go
+++ b/modules/setting/repository.go
@@ -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 {
diff --git a/routers/private/serv.go b/routers/private/serv.go
index c4508b4cb5..64fd671309 100644
--- a/routers/private/serv.go
+++ b/routers/private/serv.go
@@ -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,44 +99,44 @@ 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{}{
+ 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,
- "type": "ErrRepoNotExist",
- "err": fmt.Sprintf("Cannot find repository %s/%s", results.OwnerName, results.RepoName),
+ "type": "InternalServerError",
+ "err": fmt.Sprintf("Unable to get repository: %s/%s %v", results.OwnerName, results.RepoName, err),
})
return
}
- log.Error("Unable to get repository: %s/%s Error: %v", results.OwnerName, results.RepoName, err)
- ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
- "results": results,
- "type": "InternalServerError",
- "err": fmt.Sprintf("Unable to get repository: %s/%s %v", results.OwnerName, results.RepoName, err),
- })
- return
}
- repo.OwnerName = ownerName
- results.RepoID = repo.ID
- if repo.IsBeingCreated() {
- ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
- "results": results,
- "type": "InternalServerError",
- "err": "Repository is being created, you could retry after it finished",
- })
- return
- }
+ if repoExist {
+ repo.OwnerName = ownerName
+ results.RepoID = repo.ID
- // We can shortcut at this point if the repo is a mirror
- if mode > models.AccessModeRead && repo.IsMirror {
- ctx.JSON(http.StatusUnauthorized, map[string]interface{}{
- "results": results,
- "type": "ErrMirrorReadOnly",
- "err": fmt.Sprintf("Mirror Repository %s/%s is read-only", results.OwnerName, results.RepoName),
- })
- return
+ if repo.IsBeingCreated() {
+ ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
+ "results": results,
+ "type": "InternalServerError",
+ "err": "Repository is being created, you could retry after it finished",
+ })
+ return
+ }
+
+ // We can shortcut at this point if the repo is a mirror
+ if mode > models.AccessModeRead && repo.IsMirror {
+ ctx.JSON(http.StatusUnauthorized, map[string]interface{}{
+ "results": results,
+ "type": "ErrMirrorReadOnly",
+ "err": fmt.Sprintf("Mirror Repository %s/%s is read-only", results.OwnerName, results.RepoName),
+ })
+ return
+ }
}
// Get the Public Key represented by the 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 {
diff --git a/routers/repo/http.go b/routers/repo/http.go
index c66d7aae65..b97feed51d 100644
--- a/routers/repo/http.go
+++ b/routers/repo/http.go
@@ -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
}
- 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,20 +244,22 @@ func HTTP(ctx *context.Context) {
}
}
- perm, err := models.GetUserRepoPermission(repo, authUser)
- if err != nil {
- ctx.ServerError("GetUserRepoPermission", err)
- return
- }
+ if repoExist {
+ perm, err := models.GetUserRepoPermission(repo, authUser)
+ if err != nil {
+ ctx.ServerError("GetUserRepoPermission", err)
+ return
+ }
- if !perm.CanAccess(accessMode, unitType) {
- ctx.HandleText(http.StatusForbidden, "User permission denied")
- return
- }
+ if !perm.CanAccess(accessMode, unitType) {
+ ctx.HandleText(http.StatusForbidden, "User permission denied")
+ return
+ }
- if !isPull && repo.IsMirror {
- ctx.HandleText(http.StatusForbidden, "mirror repository is read-only")
- return
+ if !isPull && repo.IsMirror {
+ ctx.HandleText(http.StatusForbidden, "mirror repository is read-only")
+ return
+ }
}
environ = []string{
@@ -264,7 +267,6 @@ func HTTP(ctx *context.Context) {
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{
diff --git a/services/repository/repository.go b/services/repository/repository.go
index 5135435c78..2fb45bb617 100644
--- a/services/repository/repository.go
+++ b/services/repository/repository.go
@@ -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
+}