Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Push to create repo #8419

Merged
merged 21 commits into from
Dec 15, 2019
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions custom/conf/app.ini.sample
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions docs/content/doc/advanced/config-cheat-sheet.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)

Expand Down
52 changes: 52 additions & 0 deletions integrations/git_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)()
Expand Down Expand Up @@ -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))
})
})
}
Expand Down Expand Up @@ -407,3 +411,51 @@ 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)
jolheiser marked this conversation as resolved.
Show resolved Hide resolved
}
}
4 changes: 4 additions & 0 deletions modules/setting/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ var (
AccessControlAllowOrigin string
UseCompatSSHURI bool
DefaultCloseIssuesViaCommitsInAnyBranch bool
EnablePushCreateUser bool
EnablePushCreateOrg bool

// Repository editor settings
Editor struct {
Expand Down Expand Up @@ -89,6 +91,8 @@ var (
AccessControlAllowOrigin: "",
UseCompatSSHURI: false,
DefaultCloseIssuesViaCommitsInAnyBranch: false,
EnablePushCreateUser: false,
EnablePushCreateOrg: false,

// Repository editor settings
Editor: struct {
Expand Down
137 changes: 107 additions & 30 deletions routers/private/serv.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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",
Expand All @@ -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{}{
Expand Down Expand Up @@ -265,6 +276,48 @@ func ServCommand(ctx *macaron.Context) {
}
}

// We already know we aren't using a deploy key
if !repoExist {
if user.IsOrganization() && !setting.Repository.EnablePushCreateOrg {
jolheiser marked this conversation as resolved.
Show resolved Hide resolved
ctx.JSON(http.StatusForbidden, map[string]interface{}{
"results": results,
"type": "ErrForbidden",
"err": "Push to create is not enabled for organizations.",
})
return
}
if !user.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
}

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
}

repo, err = 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 {
Expand All @@ -291,3 +344,27 @@ func ServCommand(ctx *macaron.Context) {
ctx.JSON(http.StatusOK, results)
// We will update the keys in a different call.
}

func pushCreateRepo(authUser, owner *models.User, repoName string) (*models.Repository, error) {
jolheiser marked this conversation as resolved.
Show resolved Hide resolved
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 := repo_service.CreateRepository(authUser, owner, models.CreateRepoOptions{
Name: repoName,
IsPrivate: true,
})
if err != nil {
return nil, err
}

return repo, nil
}
Loading