]> source.dussan.org Git - gitea.git/commitdiff
Clean-up HookPreReceive and restore functionality for pushing non-standard refs ...
authorzeripath <art27@cantab.net>
Thu, 16 Sep 2021 13:34:54 +0000 (14:34 +0100)
committerGitHub <noreply@github.com>
Thu, 16 Sep 2021 13:34:54 +0000 (15:34 +0200)
* Clean-up HookPreReceive and restore functionality for pushing non-standard refs

There was an inadvertent breaking change in #15629 meaning that notes refs and other
git extension refs will be automatically rejected.

Further following #14295 and #15629 the pre-recieve hook code is untenably long and
too complex.

This PR refactors the hook code and removes the incorrect forced rejection of
non-standard refs.

Fix #16688

Signed-off-by: Andrew Thornton <art27@cantab.net>
modules/web/route.go
routers/private/default_branch.go [new file with mode: 0644]
routers/private/hook.go [deleted file]
routers/private/hook_post_receive.go [new file with mode: 0644]
routers/private/hook_pre_receive.go [new file with mode: 0644]
routers/private/hook_proc_receive.go [new file with mode: 0644]
routers/private/hook_verification.go [new file with mode: 0644]
routers/private/internal.go
routers/private/internal_repo.go [new file with mode: 0644]

index 3c6513da626ce2226d8b1a43c0cfbbf17d996aa7..9b08510264bfe196ee4197192f3ec3548ff40167 100644 (file)
@@ -31,6 +31,7 @@ func Wrap(handlers ...interface{}) http.HandlerFunc {
                        func(ctx *context.Context) goctx.CancelFunc,
                        func(*context.APIContext),
                        func(*context.PrivateContext),
+                       func(*context.PrivateContext) goctx.CancelFunc,
                        func(http.Handler) http.Handler:
                default:
                        panic(fmt.Sprintf("Unsupported handler type: %#v", t))
@@ -59,6 +60,15 @@ func Wrap(handlers ...interface{}) http.HandlerFunc {
                                if ctx.Written() {
                                        return
                                }
+                       case func(*context.PrivateContext) goctx.CancelFunc:
+                               ctx := context.GetPrivateContext(req)
+                               cancel := t(ctx)
+                               if cancel != nil {
+                                       defer cancel()
+                               }
+                               if ctx.Written() {
+                                       return
+                               }
                        case func(ctx *context.Context):
                                ctx := context.GetContext(req)
                                t(ctx)
diff --git a/routers/private/default_branch.go b/routers/private/default_branch.go
new file mode 100644 (file)
index 0000000..ec6adc4
--- /dev/null
@@ -0,0 +1,75 @@
+// Copyright 2021 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 private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead.
+package private
+
+import (
+       "fmt"
+       "net/http"
+
+       "code.gitea.io/gitea/models"
+       gitea_context "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/git"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/private"
+)
+
+// ________          _____             .__   __
+// \______ \   _____/ ____\____   __ __|  |_/  |_
+//  |    |  \_/ __ \   __\\__  \ |  |  \  |\   __\
+//  |    `   \  ___/|  |   / __ \|  |  /  |_|  |
+// /_______  /\___  >__|  (____  /____/|____/__|
+//         \/     \/           \/
+// __________                             .__
+// \______   \____________    ____   ____ |  |__
+//  |    |  _/\_  __ \__  \  /    \_/ ___\|  |  \
+//  |    |   \ |  | \// __ \|   |  \  \___|   Y  \
+//  |______  / |__|  (____  /___|  /\___  >___|  /
+//         \/             \/     \/     \/     \/
+
+// SetDefaultBranch updates the default branch
+func SetDefaultBranch(ctx *gitea_context.PrivateContext) {
+       ownerName := ctx.Params(":owner")
+       repoName := ctx.Params(":repo")
+       branch := ctx.Params(":branch")
+       repo, err := models.GetRepositoryByOwnerAndName(ownerName, repoName)
+       if err != nil {
+               log.Error("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err)
+               ctx.JSON(http.StatusInternalServerError, private.Response{
+                       Err: fmt.Sprintf("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err),
+               })
+               return
+       }
+       if repo.OwnerName == "" {
+               repo.OwnerName = ownerName
+       }
+
+       repo.DefaultBranch = branch
+       gitRepo, err := git.OpenRepository(repo.RepoPath())
+       if err != nil {
+               ctx.JSON(http.StatusInternalServerError, private.Response{
+                       Err: fmt.Sprintf("Failed to get git repository: %s/%s Error: %v", ownerName, repoName, err),
+               })
+               return
+       }
+       if err := gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil {
+               if !git.IsErrUnsupportedVersion(err) {
+                       gitRepo.Close()
+                       ctx.JSON(http.StatusInternalServerError, private.Response{
+                               Err: fmt.Sprintf("Unable to set default branch on repository: %s/%s Error: %v", ownerName, repoName, err),
+                       })
+                       return
+               }
+       }
+       gitRepo.Close()
+
+       if err := repo.UpdateDefaultBranch(); err != nil {
+               ctx.JSON(http.StatusInternalServerError, private.Response{
+                       Err: fmt.Sprintf("Unable to set default branch on repository: %s/%s Error: %v", ownerName, repoName, err),
+               })
+               return
+       }
+       ctx.PlainText(http.StatusOK, []byte("success"))
+}
diff --git a/routers/private/hook.go b/routers/private/hook.go
deleted file mode 100644 (file)
index d928dc4..0000000
+++ /dev/null
@@ -1,777 +0,0 @@
-// 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 private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead.
-package private
-
-import (
-       "bufio"
-       "context"
-       "fmt"
-       "io"
-       "net/http"
-       "os"
-       "strings"
-
-       "code.gitea.io/gitea/models"
-       gitea_context "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/git"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/private"
-       repo_module "code.gitea.io/gitea/modules/repository"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/util"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/services/agit"
-       pull_service "code.gitea.io/gitea/services/pull"
-       repo_service "code.gitea.io/gitea/services/repository"
-)
-
-func verifyCommits(oldCommitID, newCommitID string, repo *git.Repository, env []string) error {
-       stdoutReader, stdoutWriter, err := os.Pipe()
-       if err != nil {
-               log.Error("Unable to create os.Pipe for %s", repo.Path)
-               return err
-       }
-       defer func() {
-               _ = stdoutReader.Close()
-               _ = stdoutWriter.Close()
-       }()
-
-       // This is safe as force pushes are already forbidden
-       err = git.NewCommand("rev-list", oldCommitID+"..."+newCommitID).
-               RunInDirTimeoutEnvFullPipelineFunc(env, -1, repo.Path,
-                       stdoutWriter, nil, nil,
-                       func(ctx context.Context, cancel context.CancelFunc) error {
-                               _ = stdoutWriter.Close()
-                               err := readAndVerifyCommitsFromShaReader(stdoutReader, repo, env)
-                               if err != nil {
-                                       log.Error("%v", err)
-                                       cancel()
-                               }
-                               _ = stdoutReader.Close()
-                               return err
-                       })
-       if err != nil && !isErrUnverifiedCommit(err) {
-               log.Error("Unable to check commits from %s to %s in %s: %v", oldCommitID, newCommitID, repo.Path, err)
-       }
-       return err
-}
-
-func readAndVerifyCommitsFromShaReader(input io.ReadCloser, repo *git.Repository, env []string) error {
-       scanner := bufio.NewScanner(input)
-       for scanner.Scan() {
-               line := scanner.Text()
-               err := readAndVerifyCommit(line, repo, env)
-               if err != nil {
-                       log.Error("%v", err)
-                       return err
-               }
-       }
-       return scanner.Err()
-}
-
-func readAndVerifyCommit(sha string, repo *git.Repository, env []string) error {
-       stdoutReader, stdoutWriter, err := os.Pipe()
-       if err != nil {
-               log.Error("Unable to create pipe for %s: %v", repo.Path, err)
-               return err
-       }
-       defer func() {
-               _ = stdoutReader.Close()
-               _ = stdoutWriter.Close()
-       }()
-       hash := git.MustIDFromString(sha)
-
-       return git.NewCommand("cat-file", "commit", sha).
-               RunInDirTimeoutEnvFullPipelineFunc(env, -1, repo.Path,
-                       stdoutWriter, nil, nil,
-                       func(ctx context.Context, cancel context.CancelFunc) error {
-                               _ = stdoutWriter.Close()
-                               commit, err := git.CommitFromReader(repo, hash, stdoutReader)
-                               if err != nil {
-                                       return err
-                               }
-                               verification := models.ParseCommitWithSignature(commit)
-                               if !verification.Verified {
-                                       cancel()
-                                       return &errUnverifiedCommit{
-                                               commit.ID.String(),
-                                       }
-                               }
-                               return nil
-                       })
-}
-
-type errUnverifiedCommit struct {
-       sha string
-}
-
-func (e *errUnverifiedCommit) Error() string {
-       return fmt.Sprintf("Unverified commit: %s", e.sha)
-}
-
-func isErrUnverifiedCommit(err error) bool {
-       _, ok := err.(*errUnverifiedCommit)
-       return ok
-}
-
-// HookPreReceive checks whether a individual commit is acceptable
-func HookPreReceive(ctx *gitea_context.PrivateContext) {
-       opts := web.GetForm(ctx).(*private.HookOptions)
-       ownerName := ctx.Params(":owner")
-       repoName := ctx.Params(":repo")
-       repo, err := models.GetRepositoryByOwnerAndName(ownerName, repoName)
-       if err != nil {
-               log.Error("Unable to get repository: %s/%s Error: %v", ownerName, repoName, err)
-               ctx.JSON(http.StatusInternalServerError, private.Response{
-                       Err: err.Error(),
-               })
-               return
-       }
-       repo.OwnerName = ownerName
-       gitRepo, err := git.OpenRepository(repo.RepoPath())
-       if err != nil {
-               log.Error("Unable to get git repository for: %s/%s Error: %v", ownerName, repoName, err)
-               ctx.JSON(http.StatusInternalServerError, private.Response{
-                       Err: err.Error(),
-               })
-               return
-       }
-       defer gitRepo.Close()
-
-       // Generate git environment for checking commits
-       env := os.Environ()
-       if opts.GitAlternativeObjectDirectories != "" {
-               env = append(env,
-                       private.GitAlternativeObjectDirectories+"="+opts.GitAlternativeObjectDirectories)
-       }
-       if opts.GitObjectDirectory != "" {
-               env = append(env,
-                       private.GitObjectDirectory+"="+opts.GitObjectDirectory)
-       }
-       if opts.GitQuarantinePath != "" {
-               env = append(env,
-                       private.GitQuarantinePath+"="+opts.GitQuarantinePath)
-       }
-
-       if git.SupportProcReceive {
-               pusher, err := models.GetUserByID(opts.UserID)
-               if err != nil {
-                       log.Error("models.GetUserByID:%v", err)
-                       ctx.Error(http.StatusInternalServerError, "")
-                       return
-               }
-
-               perm, err := models.GetUserRepoPermission(repo, pusher)
-               if err != nil {
-                       log.Error("models.GetUserRepoPermission:%v", err)
-                       ctx.Error(http.StatusInternalServerError, "")
-                       return
-               }
-
-               canCreatePullRequest := perm.CanRead(models.UnitTypePullRequests)
-
-               for _, refFullName := range opts.RefFullNames {
-                       // if user want update other refs (branch or tag),
-                       // should check code write permission because
-                       // this check was delayed.
-                       if !strings.HasPrefix(refFullName, git.PullRequestPrefix) {
-                               if !perm.CanWrite(models.UnitTypeCode) {
-                                       ctx.JSON(http.StatusForbidden, map[string]interface{}{
-                                               "err": "User permission denied.",
-                                       })
-                                       return
-                               }
-
-                               break
-                       } else if repo.IsEmpty {
-                               ctx.JSON(http.StatusForbidden, map[string]interface{}{
-                                       "err": "Can't create pull request for an empty repository.",
-                               })
-                               return
-                       } else if !canCreatePullRequest {
-                               ctx.JSON(http.StatusForbidden, map[string]interface{}{
-                                       "err": "User permission denied.",
-                               })
-                               return
-                       } else if opts.IsWiki {
-                               // TODO: maybe can do it ...
-                               ctx.JSON(http.StatusForbidden, map[string]interface{}{
-                                       "err": "not support send pull request to wiki.",
-                               })
-                               return
-                       }
-               }
-       }
-
-       protectedTags, err := repo.GetProtectedTags()
-       if err != nil {
-               log.Error("Unable to get protected tags for %-v Error: %v", repo, err)
-               ctx.JSON(http.StatusInternalServerError, private.Response{
-                       Err: err.Error(),
-               })
-               return
-       }
-
-       // Iterate across the provided old commit IDs
-       for i := range opts.OldCommitIDs {
-               oldCommitID := opts.OldCommitIDs[i]
-               newCommitID := opts.NewCommitIDs[i]
-               refFullName := opts.RefFullNames[i]
-
-               if strings.HasPrefix(refFullName, git.BranchPrefix) {
-                       branchName := strings.TrimPrefix(refFullName, git.BranchPrefix)
-                       if branchName == repo.DefaultBranch && newCommitID == git.EmptySHA {
-                               log.Warn("Forbidden: Branch: %s is the default branch in %-v and cannot be deleted", branchName, repo)
-                               ctx.JSON(http.StatusForbidden, private.Response{
-                                       Err: fmt.Sprintf("branch %s is the default branch and cannot be deleted", branchName),
-                               })
-                               return
-                       }
-
-                       protectBranch, err := models.GetProtectedBranchBy(repo.ID, branchName)
-                       if err != nil {
-                               log.Error("Unable to get protected branch: %s in %-v Error: %v", branchName, repo, err)
-                               ctx.JSON(http.StatusInternalServerError, private.Response{
-                                       Err: err.Error(),
-                               })
-                               return
-                       }
-
-                       // Allow pushes to non-protected branches
-                       if protectBranch == nil || !protectBranch.IsProtected() {
-                               continue
-                       }
-
-                       // This ref is a protected branch.
-                       //
-                       // First of all we need to enforce absolutely:
-                       //
-                       // 1. Detect and prevent deletion of the branch
-                       if newCommitID == git.EmptySHA {
-                               log.Warn("Forbidden: Branch: %s in %-v is protected from deletion", branchName, repo)
-                               ctx.JSON(http.StatusForbidden, private.Response{
-                                       Err: fmt.Sprintf("branch %s is protected from deletion", branchName),
-                               })
-                               return
-                       }
-
-                       // 2. Disallow force pushes to protected branches
-                       if git.EmptySHA != oldCommitID {
-                               output, err := git.NewCommand("rev-list", "--max-count=1", oldCommitID, "^"+newCommitID).RunInDirWithEnv(repo.RepoPath(), env)
-                               if err != nil {
-                                       log.Error("Unable to detect force push between: %s and %s in %-v Error: %v", oldCommitID, newCommitID, repo, err)
-                                       ctx.JSON(http.StatusInternalServerError, private.Response{
-                                               Err: fmt.Sprintf("Fail to detect force push: %v", err),
-                                       })
-                                       return
-                               } else if len(output) > 0 {
-                                       log.Warn("Forbidden: Branch: %s in %-v is protected from force push", branchName, repo)
-                                       ctx.JSON(http.StatusForbidden, private.Response{
-                                               Err: fmt.Sprintf("branch %s is protected from force push", branchName),
-                                       })
-                                       return
-
-                               }
-                       }
-
-                       // 3. Enforce require signed commits
-                       if protectBranch.RequireSignedCommits {
-                               err := verifyCommits(oldCommitID, newCommitID, gitRepo, env)
-                               if err != nil {
-                                       if !isErrUnverifiedCommit(err) {
-                                               log.Error("Unable to check commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err)
-                                               ctx.JSON(http.StatusInternalServerError, private.Response{
-                                                       Err: fmt.Sprintf("Unable to check commits from %s to %s: %v", oldCommitID, newCommitID, err),
-                                               })
-                                               return
-                                       }
-                                       unverifiedCommit := err.(*errUnverifiedCommit).sha
-                                       log.Warn("Forbidden: Branch: %s in %-v is protected from unverified commit %s", branchName, repo, unverifiedCommit)
-                                       ctx.JSON(http.StatusForbidden, private.Response{
-                                               Err: fmt.Sprintf("branch %s is protected from unverified commit %s", branchName, unverifiedCommit),
-                                       })
-                                       return
-                               }
-                       }
-
-                       // Now there are several tests which can be overridden:
-                       //
-                       // 4. Check protected file patterns - this is overridable from the UI
-                       changedProtectedfiles := false
-                       protectedFilePath := ""
-
-                       globs := protectBranch.GetProtectedFilePatterns()
-                       if len(globs) > 0 {
-                               _, err := pull_service.CheckFileProtection(oldCommitID, newCommitID, globs, 1, env, gitRepo)
-                               if err != nil {
-                                       if !models.IsErrFilePathProtected(err) {
-                                               log.Error("Unable to check file protection for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err)
-                                               ctx.JSON(http.StatusInternalServerError, private.Response{
-                                                       Err: fmt.Sprintf("Unable to check file protection for commits from %s to %s: %v", oldCommitID, newCommitID, err),
-                                               })
-                                               return
-                                       }
-
-                                       changedProtectedfiles = true
-                                       protectedFilePath = err.(models.ErrFilePathProtected).Path
-                               }
-                       }
-
-                       // 5. Check if the doer is allowed to push
-                       canPush := false
-                       if opts.IsDeployKey {
-                               canPush = !changedProtectedfiles && protectBranch.CanPush && (!protectBranch.EnableWhitelist || protectBranch.WhitelistDeployKeys)
-                       } else {
-                               canPush = !changedProtectedfiles && protectBranch.CanUserPush(opts.UserID)
-                       }
-
-                       // 6. If we're not allowed to push directly
-                       if !canPush {
-                               // Is this is a merge from the UI/API?
-                               if opts.PullRequestID == 0 {
-                                       // 6a. If we're not merging from the UI/API then there are two ways we got here:
-                                       //
-                                       // We are changing a protected file and we're not allowed to do that
-                                       if changedProtectedfiles {
-                                               log.Warn("Forbidden: Branch: %s in %-v is protected from changing file %s", branchName, repo, protectedFilePath)
-                                               ctx.JSON(http.StatusForbidden, private.Response{
-                                                       Err: fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedFilePath),
-                                               })
-                                               return
-                                       }
-
-                                       // Allow commits that only touch unprotected files
-                                       globs := protectBranch.GetUnprotectedFilePatterns()
-                                       if len(globs) > 0 {
-                                               unprotectedFilesOnly, err := pull_service.CheckUnprotectedFiles(oldCommitID, newCommitID, globs, env, gitRepo)
-                                               if err != nil {
-                                                       log.Error("Unable to check file protection for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err)
-                                                       ctx.JSON(http.StatusInternalServerError, private.Response{
-                                                               Err: fmt.Sprintf("Unable to check file protection for commits from %s to %s: %v", oldCommitID, newCommitID, err),
-                                                       })
-                                                       return
-                                               }
-                                               if unprotectedFilesOnly {
-                                                       // Commit only touches unprotected files, this is allowed
-                                                       continue
-                                               }
-                                       }
-
-                                       // Or we're simply not able to push to this protected branch
-                                       log.Warn("Forbidden: User %d is not allowed to push to protected branch: %s in %-v", opts.UserID, branchName, repo)
-                                       ctx.JSON(http.StatusForbidden, private.Response{
-                                               Err: fmt.Sprintf("Not allowed to push to protected branch %s", branchName),
-                                       })
-                                       return
-                               }
-                               // 6b. Merge (from UI or API)
-
-                               // Get the PR, user and permissions for the user in the repository
-                               pr, err := models.GetPullRequestByID(opts.PullRequestID)
-                               if err != nil {
-                                       log.Error("Unable to get PullRequest %d Error: %v", opts.PullRequestID, err)
-                                       ctx.JSON(http.StatusInternalServerError, private.Response{
-                                               Err: fmt.Sprintf("Unable to get PullRequest %d Error: %v", opts.PullRequestID, err),
-                                       })
-                                       return
-                               }
-                               user, err := models.GetUserByID(opts.UserID)
-                               if err != nil {
-                                       log.Error("Unable to get User id %d Error: %v", opts.UserID, err)
-                                       ctx.JSON(http.StatusInternalServerError, private.Response{
-                                               Err: fmt.Sprintf("Unable to get User id %d Error: %v", opts.UserID, err),
-                                       })
-                                       return
-                               }
-                               perm, err := models.GetUserRepoPermission(repo, user)
-                               if err != nil {
-                                       log.Error("Unable to get Repo permission of repo %s/%s of User %s", repo.OwnerName, repo.Name, user.Name, err)
-                                       ctx.JSON(http.StatusInternalServerError, private.Response{
-                                               Err: fmt.Sprintf("Unable to get Repo permission of repo %s/%s of User %s: %v", repo.OwnerName, repo.Name, user.Name, err),
-                                       })
-                                       return
-                               }
-
-                               // Now check if the user is allowed to merge PRs for this repository
-                               allowedMerge, err := pull_service.IsUserAllowedToMerge(pr, perm, user)
-                               if err != nil {
-                                       log.Error("Error calculating if allowed to merge: %v", err)
-                                       ctx.JSON(http.StatusInternalServerError, private.Response{
-                                               Err: fmt.Sprintf("Error calculating if allowed to merge: %v", err),
-                                       })
-                                       return
-                               }
-
-                               if !allowedMerge {
-                                       log.Warn("Forbidden: User %d is not allowed to push to protected branch: %s in %-v and is not allowed to merge pr #%d", opts.UserID, branchName, repo, pr.Index)
-                                       ctx.JSON(http.StatusForbidden, private.Response{
-                                               Err: fmt.Sprintf("Not allowed to push to protected branch %s", branchName),
-                                       })
-                                       return
-                               }
-
-                               // If we're an admin for the repository we can ignore status checks, reviews and override protected files
-                               if perm.IsAdmin() {
-                                       continue
-                               }
-
-                               // Now if we're not an admin - we can't overwrite protected files so fail now
-                               if changedProtectedfiles {
-                                       log.Warn("Forbidden: Branch: %s in %-v is protected from changing file %s", branchName, repo, protectedFilePath)
-                                       ctx.JSON(http.StatusForbidden, private.Response{
-                                               Err: fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedFilePath),
-                                       })
-                                       return
-                               }
-
-                               // Check all status checks and reviews are ok
-                               if err := pull_service.CheckPRReadyToMerge(pr, true); err != nil {
-                                       if models.IsErrNotAllowedToMerge(err) {
-                                               log.Warn("Forbidden: User %d is not allowed push to protected branch %s in %-v and pr #%d is not ready to be merged: %s", opts.UserID, branchName, repo, pr.Index, err.Error())
-                                               ctx.JSON(http.StatusForbidden, private.Response{
-                                                       Err: fmt.Sprintf("Not allowed to push to protected branch %s and pr #%d is not ready to be merged: %s", branchName, opts.PullRequestID, err.Error()),
-                                               })
-                                               return
-                                       }
-                                       log.Error("Unable to check if mergable: protected branch %s in %-v and pr #%d. Error: %v", opts.UserID, branchName, repo, pr.Index, err)
-                                       ctx.JSON(http.StatusInternalServerError, private.Response{
-                                               Err: fmt.Sprintf("Unable to get status of pull request %d. Error: %v", opts.PullRequestID, err),
-                                       })
-                                       return
-                               }
-                       }
-               } else if strings.HasPrefix(refFullName, git.TagPrefix) {
-                       tagName := strings.TrimPrefix(refFullName, git.TagPrefix)
-
-                       isAllowed, err := models.IsUserAllowedToControlTag(protectedTags, tagName, opts.UserID)
-                       if err != nil {
-                               ctx.JSON(http.StatusInternalServerError, private.Response{
-                                       Err: err.Error(),
-                               })
-                               return
-                       }
-                       if !isAllowed {
-                               log.Warn("Forbidden: Tag %s in %-v is protected", tagName, repo)
-                               ctx.JSON(http.StatusForbidden, private.Response{
-                                       Err: fmt.Sprintf("Tag %s is protected", tagName),
-                               })
-                               return
-                       }
-               } else if git.SupportProcReceive && strings.HasPrefix(refFullName, git.PullRequestPrefix) {
-                       baseBranchName := opts.RefFullNames[i][len(git.PullRequestPrefix):]
-
-                       baseBranchExist := false
-                       if gitRepo.IsBranchExist(baseBranchName) {
-                               baseBranchExist = true
-                       }
-
-                       if !baseBranchExist {
-                               for p, v := range baseBranchName {
-                                       if v == '/' && gitRepo.IsBranchExist(baseBranchName[:p]) && p != len(baseBranchName)-1 {
-                                               baseBranchExist = true
-                                               break
-                                       }
-                               }
-                       }
-
-                       if !baseBranchExist {
-                               ctx.JSON(http.StatusForbidden, private.Response{
-                                       Err: fmt.Sprintf("Unexpected ref: %s", refFullName),
-                               })
-                               return
-                       }
-               } else {
-                       log.Error("Unexpected ref: %s", refFullName)
-                       ctx.JSON(http.StatusInternalServerError, private.Response{
-                               Err: fmt.Sprintf("Unexpected ref: %s", refFullName),
-                       })
-                       return
-               }
-       }
-
-       ctx.PlainText(http.StatusOK, []byte("ok"))
-}
-
-// HookPostReceive updates services and users
-func HookPostReceive(ctx *gitea_context.PrivateContext) {
-       opts := web.GetForm(ctx).(*private.HookOptions)
-       ownerName := ctx.Params(":owner")
-       repoName := ctx.Params(":repo")
-
-       var repo *models.Repository
-       updates := make([]*repo_module.PushUpdateOptions, 0, len(opts.OldCommitIDs))
-       wasEmpty := false
-
-       for i := range opts.OldCommitIDs {
-               refFullName := opts.RefFullNames[i]
-
-               // Only trigger activity updates for changes to branches or
-               // tags.  Updates to other refs (eg, refs/notes, refs/changes,
-               // or other less-standard refs spaces are ignored since there
-               // may be a very large number of them).
-               if strings.HasPrefix(refFullName, git.BranchPrefix) || strings.HasPrefix(refFullName, git.TagPrefix) {
-                       if repo == nil {
-                               var err error
-                               repo, err = models.GetRepositoryByOwnerAndName(ownerName, repoName)
-                               if err != nil {
-                                       log.Error("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err)
-                                       ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
-                                               Err: fmt.Sprintf("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err),
-                                       })
-                                       return
-                               }
-                               if repo.OwnerName == "" {
-                                       repo.OwnerName = ownerName
-                               }
-                               wasEmpty = repo.IsEmpty
-                       }
-
-                       option := repo_module.PushUpdateOptions{
-                               RefFullName:  refFullName,
-                               OldCommitID:  opts.OldCommitIDs[i],
-                               NewCommitID:  opts.NewCommitIDs[i],
-                               PusherID:     opts.UserID,
-                               PusherName:   opts.UserName,
-                               RepoUserName: ownerName,
-                               RepoName:     repoName,
-                       }
-                       updates = append(updates, &option)
-                       if repo.IsEmpty && option.IsBranch() && (option.BranchName() == "master" || option.BranchName() == "main") {
-                               // put the master/main branch first
-                               copy(updates[1:], updates)
-                               updates[0] = &option
-                       }
-               }
-       }
-
-       if repo != nil && len(updates) > 0 {
-               if err := repo_service.PushUpdates(updates); err != nil {
-                       log.Error("Failed to Update: %s/%s Total Updates: %d", ownerName, repoName, len(updates))
-                       for i, update := range updates {
-                               log.Error("Failed to Update: %s/%s Update: %d/%d: Branch: %s", ownerName, repoName, i, len(updates), update.BranchName())
-                       }
-                       log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
-
-                       ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
-                               Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err),
-                       })
-                       return
-               }
-       }
-
-       // Push Options
-       if repo != nil && len(opts.GitPushOptions) > 0 {
-               repo.IsPrivate = opts.GitPushOptions.Bool(private.GitPushOptionRepoPrivate, repo.IsPrivate)
-               repo.IsTemplate = opts.GitPushOptions.Bool(private.GitPushOptionRepoTemplate, repo.IsTemplate)
-               if err := models.UpdateRepositoryCols(repo, "is_private", "is_template"); err != nil {
-                       log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
-                       ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
-                               Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err),
-                       })
-               }
-       }
-
-       results := make([]private.HookPostReceiveBranchResult, 0, len(opts.OldCommitIDs))
-
-       // We have to reload the repo in case its state is changed above
-       repo = nil
-       var baseRepo *models.Repository
-
-       for i := range opts.OldCommitIDs {
-               refFullName := opts.RefFullNames[i]
-               newCommitID := opts.NewCommitIDs[i]
-
-               branch := git.RefEndName(opts.RefFullNames[i])
-
-               if newCommitID != git.EmptySHA && strings.HasPrefix(refFullName, git.BranchPrefix) {
-                       if repo == nil {
-                               var err error
-                               repo, err = models.GetRepositoryByOwnerAndName(ownerName, repoName)
-                               if err != nil {
-                                       log.Error("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err)
-                                       ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
-                                               Err:          fmt.Sprintf("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err),
-                                               RepoWasEmpty: wasEmpty,
-                                       })
-                                       return
-                               }
-                               if repo.OwnerName == "" {
-                                       repo.OwnerName = ownerName
-                               }
-
-                               if !repo.AllowsPulls() {
-                                       // We can stop there's no need to go any further
-                                       ctx.JSON(http.StatusOK, private.HookPostReceiveResult{
-                                               RepoWasEmpty: wasEmpty,
-                                       })
-                                       return
-                               }
-                               baseRepo = repo
-
-                               if repo.IsFork {
-                                       if err := repo.GetBaseRepo(); err != nil {
-                                               log.Error("Failed to get Base Repository of Forked repository: %-v Error: %v", repo, err)
-                                               ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
-                                                       Err:          fmt.Sprintf("Failed to get Base Repository of Forked repository: %-v Error: %v", repo, err),
-                                                       RepoWasEmpty: wasEmpty,
-                                               })
-                                               return
-                                       }
-                                       baseRepo = repo.BaseRepo
-                               }
-                       }
-
-                       if !repo.IsFork && branch == baseRepo.DefaultBranch {
-                               results = append(results, private.HookPostReceiveBranchResult{})
-                               continue
-                       }
-
-                       pr, err := models.GetUnmergedPullRequest(repo.ID, baseRepo.ID, branch, baseRepo.DefaultBranch, models.PullRequestFlowGithub)
-                       if err != nil && !models.IsErrPullRequestNotExist(err) {
-                               log.Error("Failed to get active PR in: %-v Branch: %s to: %-v Branch: %s Error: %v", repo, branch, baseRepo, baseRepo.DefaultBranch, err)
-                               ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
-                                       Err: fmt.Sprintf(
-                                               "Failed to get active PR in: %-v Branch: %s to: %-v Branch: %s Error: %v", repo, branch, baseRepo, baseRepo.DefaultBranch, err),
-                                       RepoWasEmpty: wasEmpty,
-                               })
-                               return
-                       }
-
-                       if pr == nil {
-                               if repo.IsFork {
-                                       branch = fmt.Sprintf("%s:%s", repo.OwnerName, branch)
-                               }
-                               results = append(results, private.HookPostReceiveBranchResult{
-                                       Message: setting.Git.PullRequestPushMessage && repo.AllowsPulls(),
-                                       Create:  true,
-                                       Branch:  branch,
-                                       URL:     fmt.Sprintf("%s/compare/%s...%s", baseRepo.HTMLURL(), util.PathEscapeSegments(baseRepo.DefaultBranch), util.PathEscapeSegments(branch)),
-                               })
-                       } else {
-                               results = append(results, private.HookPostReceiveBranchResult{
-                                       Message: setting.Git.PullRequestPushMessage && repo.AllowsPulls(),
-                                       Create:  false,
-                                       Branch:  branch,
-                                       URL:     fmt.Sprintf("%s/pulls/%d", baseRepo.HTMLURL(), pr.Index),
-                               })
-                       }
-               }
-       }
-       ctx.JSON(http.StatusOK, private.HookPostReceiveResult{
-               Results:      results,
-               RepoWasEmpty: wasEmpty,
-       })
-}
-
-// HookProcReceive proc-receive hook
-func HookProcReceive(ctx *gitea_context.PrivateContext) {
-       opts := web.GetForm(ctx).(*private.HookOptions)
-       if !git.SupportProcReceive {
-               ctx.Status(http.StatusNotFound)
-               return
-       }
-
-       cancel := loadRepositoryAndGitRepoByParams(ctx)
-       if ctx.Written() {
-               return
-       }
-       defer cancel()
-
-       results := agit.ProcRecive(ctx, opts)
-       if ctx.Written() {
-               return
-       }
-
-       ctx.JSON(http.StatusOK, private.HookProcReceiveResult{
-               Results: results,
-       })
-}
-
-// SetDefaultBranch updates the default branch
-func SetDefaultBranch(ctx *gitea_context.PrivateContext) {
-       ownerName := ctx.Params(":owner")
-       repoName := ctx.Params(":repo")
-       branch := ctx.Params(":branch")
-       repo, err := models.GetRepositoryByOwnerAndName(ownerName, repoName)
-       if err != nil {
-               log.Error("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err)
-               ctx.JSON(http.StatusInternalServerError, private.Response{
-                       Err: fmt.Sprintf("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err),
-               })
-               return
-       }
-       if repo.OwnerName == "" {
-               repo.OwnerName = ownerName
-       }
-
-       repo.DefaultBranch = branch
-       gitRepo, err := git.OpenRepository(repo.RepoPath())
-       if err != nil {
-               ctx.JSON(http.StatusInternalServerError, private.Response{
-                       Err: fmt.Sprintf("Failed to get git repository: %s/%s Error: %v", ownerName, repoName, err),
-               })
-               return
-       }
-       if err := gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil {
-               if !git.IsErrUnsupportedVersion(err) {
-                       gitRepo.Close()
-                       ctx.JSON(http.StatusInternalServerError, private.Response{
-                               Err: fmt.Sprintf("Unable to set default branch on repository: %s/%s Error: %v", ownerName, repoName, err),
-                       })
-                       return
-               }
-       }
-       gitRepo.Close()
-
-       if err := repo.UpdateDefaultBranch(); err != nil {
-               ctx.JSON(http.StatusInternalServerError, private.Response{
-                       Err: fmt.Sprintf("Unable to set default branch on repository: %s/%s Error: %v", ownerName, repoName, err),
-               })
-               return
-       }
-       ctx.PlainText(http.StatusOK, []byte("success"))
-}
-
-func loadRepositoryAndGitRepoByParams(ctx *gitea_context.PrivateContext) context.CancelFunc {
-       ownerName := ctx.Params(":owner")
-       repoName := ctx.Params(":repo")
-
-       repo, err := models.GetRepositoryByOwnerAndName(ownerName, repoName)
-       if err != nil {
-               log.Error("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err)
-               ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
-                       "Err": fmt.Sprintf("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err),
-               })
-               return nil
-       }
-       if repo.OwnerName == "" {
-               repo.OwnerName = ownerName
-       }
-
-       gitRepo, err := git.OpenRepository(repo.RepoPath())
-       if err != nil {
-               log.Error("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err)
-               ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
-                       "Err": fmt.Sprintf("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err),
-               })
-               return nil
-       }
-
-       ctx.Repo = &gitea_context.Repository{
-               Repository: repo,
-               GitRepo:    gitRepo,
-       }
-
-       // We opened it, we should close it
-       cancel := func() {
-               // If it's been set to nil then assume someone else has closed it.
-               if ctx.Repo.GitRepo != nil {
-                       ctx.Repo.GitRepo.Close()
-               }
-       }
-
-       return cancel
-}
diff --git a/routers/private/hook_post_receive.go b/routers/private/hook_post_receive.go
new file mode 100644 (file)
index 0000000..4dbe74a
--- /dev/null
@@ -0,0 +1,201 @@
+// Copyright 2021 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 private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead.
+package private
+
+import (
+       "fmt"
+       "net/http"
+       "strings"
+
+       "code.gitea.io/gitea/models"
+       gitea_context "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/git"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/private"
+       repo_module "code.gitea.io/gitea/modules/repository"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/util"
+       "code.gitea.io/gitea/modules/web"
+       repo_service "code.gitea.io/gitea/services/repository"
+)
+
+// HookPostReceive updates services and users
+func HookPostReceive(ctx *gitea_context.PrivateContext) {
+       opts := web.GetForm(ctx).(*private.HookOptions)
+
+       // We don't rely on RepoAssignment here because:
+       // a) we don't need the git repo in this function
+       // b) our update function will likely change the repository in the db so we will need to refresh it
+       // c) we don't always need the repo
+
+       ownerName := ctx.Params(":owner")
+       repoName := ctx.Params(":repo")
+
+       // defer getting the repository at this point - as we should only retrieve it if we're going to call update
+       var repo *models.Repository
+
+       updates := make([]*repo_module.PushUpdateOptions, 0, len(opts.OldCommitIDs))
+       wasEmpty := false
+
+       for i := range opts.OldCommitIDs {
+               refFullName := opts.RefFullNames[i]
+
+               // Only trigger activity updates for changes to branches or
+               // tags.  Updates to other refs (eg, refs/notes, refs/changes,
+               // or other less-standard refs spaces are ignored since there
+               // may be a very large number of them).
+               if strings.HasPrefix(refFullName, git.BranchPrefix) || strings.HasPrefix(refFullName, git.TagPrefix) {
+                       if repo == nil {
+                               repo = loadRepository(ctx, ownerName, repoName)
+                               if ctx.Written() {
+                                       // Error handled in loadRepository
+                                       return
+                               }
+                               wasEmpty = repo.IsEmpty
+                       }
+
+                       option := repo_module.PushUpdateOptions{
+                               RefFullName:  refFullName,
+                               OldCommitID:  opts.OldCommitIDs[i],
+                               NewCommitID:  opts.NewCommitIDs[i],
+                               PusherID:     opts.UserID,
+                               PusherName:   opts.UserName,
+                               RepoUserName: ownerName,
+                               RepoName:     repoName,
+                       }
+                       updates = append(updates, &option)
+                       if repo.IsEmpty && option.IsBranch() && (option.BranchName() == "master" || option.BranchName() == "main") {
+                               // put the master/main branch first
+                               copy(updates[1:], updates)
+                               updates[0] = &option
+                       }
+               }
+       }
+
+       if repo != nil && len(updates) > 0 {
+               if err := repo_service.PushUpdates(updates); err != nil {
+                       log.Error("Failed to Update: %s/%s Total Updates: %d", ownerName, repoName, len(updates))
+                       for i, update := range updates {
+                               log.Error("Failed to Update: %s/%s Update: %d/%d: Branch: %s", ownerName, repoName, i, len(updates), update.BranchName())
+                       }
+                       log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
+
+                       ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
+                               Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err),
+                       })
+                       return
+               }
+       }
+
+       // Handle Push Options
+       if len(opts.GitPushOptions) > 0 {
+               // load the repository
+               if repo == nil {
+                       repo = loadRepository(ctx, ownerName, repoName)
+                       if ctx.Written() {
+                               // Error handled in loadRepository
+                               return
+                       }
+                       wasEmpty = repo.IsEmpty
+               }
+
+               repo.IsPrivate = opts.GitPushOptions.Bool(private.GitPushOptionRepoPrivate, repo.IsPrivate)
+               repo.IsTemplate = opts.GitPushOptions.Bool(private.GitPushOptionRepoTemplate, repo.IsTemplate)
+               if err := models.UpdateRepositoryCols(repo, "is_private", "is_template"); err != nil {
+                       log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
+                       ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
+                               Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err),
+                       })
+               }
+       }
+
+       results := make([]private.HookPostReceiveBranchResult, 0, len(opts.OldCommitIDs))
+
+       // We have to reload the repo in case its state is changed above
+       repo = nil
+       var baseRepo *models.Repository
+
+       // Now handle the pull request notification trailers
+       for i := range opts.OldCommitIDs {
+               refFullName := opts.RefFullNames[i]
+               newCommitID := opts.NewCommitIDs[i]
+
+               branch := git.RefEndName(opts.RefFullNames[i])
+
+               // If we've pushed a branch (and not deleted it)
+               if newCommitID != git.EmptySHA && strings.HasPrefix(refFullName, git.BranchPrefix) {
+
+                       // First ensure we have the repository loaded, we're allowed pulls requests and we can get the base repo
+                       if repo == nil {
+                               repo = loadRepository(ctx, ownerName, repoName)
+                               if ctx.Written() {
+                                       return
+                               }
+
+                               if !repo.AllowsPulls() {
+                                       // We can stop there's no need to go any further
+                                       ctx.JSON(http.StatusOK, private.HookPostReceiveResult{
+                                               RepoWasEmpty: wasEmpty,
+                                       })
+                                       return
+                               }
+                               baseRepo = repo
+
+                               if repo.IsFork {
+                                       if err := repo.GetBaseRepo(); err != nil {
+                                               log.Error("Failed to get Base Repository of Forked repository: %-v Error: %v", repo, err)
+                                               ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
+                                                       Err:          fmt.Sprintf("Failed to get Base Repository of Forked repository: %-v Error: %v", repo, err),
+                                                       RepoWasEmpty: wasEmpty,
+                                               })
+                                               return
+                                       }
+                                       baseRepo = repo.BaseRepo
+                               }
+                       }
+
+                       // If our branch is the default branch of an unforked repo - there's no PR to create or refer to
+                       if !repo.IsFork && branch == baseRepo.DefaultBranch {
+                               results = append(results, private.HookPostReceiveBranchResult{})
+                               continue
+                       }
+
+                       pr, err := models.GetUnmergedPullRequest(repo.ID, baseRepo.ID, branch, baseRepo.DefaultBranch, models.PullRequestFlowGithub)
+                       if err != nil && !models.IsErrPullRequestNotExist(err) {
+                               log.Error("Failed to get active PR in: %-v Branch: %s to: %-v Branch: %s Error: %v", repo, branch, baseRepo, baseRepo.DefaultBranch, err)
+                               ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
+                                       Err: fmt.Sprintf(
+                                               "Failed to get active PR in: %-v Branch: %s to: %-v Branch: %s Error: %v", repo, branch, baseRepo, baseRepo.DefaultBranch, err),
+                                       RepoWasEmpty: wasEmpty,
+                               })
+                               return
+                       }
+
+                       if pr == nil {
+                               if repo.IsFork {
+                                       branch = fmt.Sprintf("%s:%s", repo.OwnerName, branch)
+                               }
+                               results = append(results, private.HookPostReceiveBranchResult{
+                                       Message: setting.Git.PullRequestPushMessage && repo.AllowsPulls(),
+                                       Create:  true,
+                                       Branch:  branch,
+                                       URL:     fmt.Sprintf("%s/compare/%s...%s", baseRepo.HTMLURL(), util.PathEscapeSegments(baseRepo.DefaultBranch), util.PathEscapeSegments(branch)),
+                               })
+                       } else {
+                               results = append(results, private.HookPostReceiveBranchResult{
+                                       Message: setting.Git.PullRequestPushMessage && repo.AllowsPulls(),
+                                       Create:  false,
+                                       Branch:  branch,
+                                       URL:     fmt.Sprintf("%s/pulls/%d", baseRepo.HTMLURL(), pr.Index),
+                               })
+                       }
+               }
+       }
+       ctx.JSON(http.StatusOK, private.HookPostReceiveResult{
+               Results:      results,
+               RepoWasEmpty: wasEmpty,
+       })
+}
diff --git a/routers/private/hook_pre_receive.go b/routers/private/hook_pre_receive.go
new file mode 100644 (file)
index 0000000..3e49953
--- /dev/null
@@ -0,0 +1,471 @@
+// 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 private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead.
+package private
+
+import (
+       "fmt"
+       "net/http"
+       "os"
+       "strings"
+
+       "code.gitea.io/gitea/models"
+       gitea_context "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/git"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/private"
+       "code.gitea.io/gitea/modules/web"
+       pull_service "code.gitea.io/gitea/services/pull"
+)
+
+type preReceiveContext struct {
+       *gitea_context.PrivateContext
+       user *models.User
+       perm models.Permission
+
+       canCreatePullRequest        bool
+       checkedCanCreatePullRequest bool
+
+       canWriteCode        bool
+       checkedCanWriteCode bool
+
+       protectedTags    []*models.ProtectedTag
+       gotProtectedTags bool
+
+       env []string
+
+       opts *private.HookOptions
+}
+
+// User gets or loads User
+func (ctx *preReceiveContext) User() *models.User {
+       if ctx.user == nil {
+               ctx.user, ctx.perm = loadUserAndPermission(ctx.PrivateContext, ctx.opts.UserID)
+       }
+       return ctx.user
+}
+
+// Perm gets or loads Perm
+func (ctx *preReceiveContext) Perm() *models.Permission {
+       if ctx.user == nil {
+               ctx.user, ctx.perm = loadUserAndPermission(ctx.PrivateContext, ctx.opts.UserID)
+       }
+       return &ctx.perm
+}
+
+// CanWriteCode returns true if can write code
+func (ctx *preReceiveContext) CanWriteCode() bool {
+       if !ctx.checkedCanWriteCode {
+               ctx.canWriteCode = ctx.Perm().CanWrite(models.UnitTypeCode)
+               ctx.checkedCanWriteCode = true
+       }
+       return ctx.canWriteCode
+}
+
+// AssertCanWriteCode returns true if can write code
+func (ctx *preReceiveContext) AssertCanWriteCode() bool {
+       if !ctx.CanWriteCode() {
+               if ctx.Written() {
+                       return false
+               }
+               ctx.JSON(http.StatusForbidden, map[string]interface{}{
+                       "err": "User permission denied.",
+               })
+               return false
+       }
+       return true
+}
+
+// CanCreatePullRequest returns true if can create pull requests
+func (ctx *preReceiveContext) CanCreatePullRequest() bool {
+       if !ctx.checkedCanCreatePullRequest {
+               ctx.canCreatePullRequest = ctx.Perm().CanRead(models.UnitTypePullRequests)
+               ctx.checkedCanCreatePullRequest = true
+       }
+       return ctx.canCreatePullRequest
+}
+
+// AssertCanCreatePullRequest returns true if can create pull requests
+func (ctx *preReceiveContext) AssertCreatePullRequest() bool {
+       if !ctx.CanCreatePullRequest() {
+               if ctx.Written() {
+                       return false
+               }
+               ctx.JSON(http.StatusForbidden, map[string]interface{}{
+                       "err": "User permission denied.",
+               })
+               return false
+       }
+       return true
+}
+
+// HookPreReceive checks whether a individual commit is acceptable
+func HookPreReceive(ctx *gitea_context.PrivateContext) {
+       opts := web.GetForm(ctx).(*private.HookOptions)
+
+       ourCtx := &preReceiveContext{
+               PrivateContext: ctx,
+               env:            generateGitEnv(opts), // Generate git environment for checking commits
+               opts:           opts,
+       }
+
+       // Iterate across the provided old commit IDs
+       for i := range opts.OldCommitIDs {
+               oldCommitID := opts.OldCommitIDs[i]
+               newCommitID := opts.NewCommitIDs[i]
+               refFullName := opts.RefFullNames[i]
+
+               switch {
+               case strings.HasPrefix(refFullName, git.BranchPrefix):
+                       preReceiveBranch(ourCtx, oldCommitID, newCommitID, refFullName)
+               case strings.HasPrefix(refFullName, git.TagPrefix):
+                       preReceiveTag(ourCtx, oldCommitID, newCommitID, refFullName)
+               case git.SupportProcReceive && strings.HasPrefix(refFullName, git.PullRequestPrefix):
+                       preReceivePullRequest(ourCtx, oldCommitID, newCommitID, refFullName)
+               default:
+                       ourCtx.AssertCanWriteCode()
+               }
+               if ctx.Written() {
+                       return
+               }
+       }
+
+       ctx.PlainText(http.StatusOK, []byte("ok"))
+}
+
+func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID, refFullName string) {
+       if !ctx.AssertCanWriteCode() {
+               return
+       }
+
+       repo := ctx.Repo.Repository
+       gitRepo := ctx.Repo.GitRepo
+       branchName := strings.TrimPrefix(refFullName, git.BranchPrefix)
+
+       if branchName == repo.DefaultBranch && newCommitID == git.EmptySHA {
+               log.Warn("Forbidden: Branch: %s is the default branch in %-v and cannot be deleted", branchName, repo)
+               ctx.JSON(http.StatusForbidden, private.Response{
+                       Err: fmt.Sprintf("branch %s is the default branch and cannot be deleted", branchName),
+               })
+               return
+       }
+
+       protectBranch, err := models.GetProtectedBranchBy(repo.ID, branchName)
+       if err != nil {
+               log.Error("Unable to get protected branch: %s in %-v Error: %v", branchName, repo, err)
+               ctx.JSON(http.StatusInternalServerError, private.Response{
+                       Err: err.Error(),
+               })
+               return
+       }
+
+       // Allow pushes to non-protected branches
+       if protectBranch == nil || !protectBranch.IsProtected() {
+               return
+       }
+
+       // This ref is a protected branch.
+       //
+       // First of all we need to enforce absolutely:
+       //
+       // 1. Detect and prevent deletion of the branch
+       if newCommitID == git.EmptySHA {
+               log.Warn("Forbidden: Branch: %s in %-v is protected from deletion", branchName, repo)
+               ctx.JSON(http.StatusForbidden, private.Response{
+                       Err: fmt.Sprintf("branch %s is protected from deletion", branchName),
+               })
+               return
+       }
+
+       // 2. Disallow force pushes to protected branches
+       if git.EmptySHA != oldCommitID {
+               output, err := git.NewCommand("rev-list", "--max-count=1", oldCommitID, "^"+newCommitID).RunInDirWithEnv(repo.RepoPath(), ctx.env)
+               if err != nil {
+                       log.Error("Unable to detect force push between: %s and %s in %-v Error: %v", oldCommitID, newCommitID, repo, err)
+                       ctx.JSON(http.StatusInternalServerError, private.Response{
+                               Err: fmt.Sprintf("Fail to detect force push: %v", err),
+                       })
+                       return
+               } else if len(output) > 0 {
+                       log.Warn("Forbidden: Branch: %s in %-v is protected from force push", branchName, repo)
+                       ctx.JSON(http.StatusForbidden, private.Response{
+                               Err: fmt.Sprintf("branch %s is protected from force push", branchName),
+                       })
+                       return
+
+               }
+       }
+
+       // 3. Enforce require signed commits
+       if protectBranch.RequireSignedCommits {
+               err := verifyCommits(oldCommitID, newCommitID, gitRepo, ctx.env)
+               if err != nil {
+                       if !isErrUnverifiedCommit(err) {
+                               log.Error("Unable to check commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err)
+                               ctx.JSON(http.StatusInternalServerError, private.Response{
+                                       Err: fmt.Sprintf("Unable to check commits from %s to %s: %v", oldCommitID, newCommitID, err),
+                               })
+                               return
+                       }
+                       unverifiedCommit := err.(*errUnverifiedCommit).sha
+                       log.Warn("Forbidden: Branch: %s in %-v is protected from unverified commit %s", branchName, repo, unverifiedCommit)
+                       ctx.JSON(http.StatusForbidden, private.Response{
+                               Err: fmt.Sprintf("branch %s is protected from unverified commit %s", branchName, unverifiedCommit),
+                       })
+                       return
+               }
+       }
+
+       // Now there are several tests which can be overridden:
+       //
+       // 4. Check protected file patterns - this is overridable from the UI
+       changedProtectedfiles := false
+       protectedFilePath := ""
+
+       globs := protectBranch.GetProtectedFilePatterns()
+       if len(globs) > 0 {
+               _, err := pull_service.CheckFileProtection(oldCommitID, newCommitID, globs, 1, ctx.env, gitRepo)
+               if err != nil {
+                       if !models.IsErrFilePathProtected(err) {
+                               log.Error("Unable to check file protection for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err)
+                               ctx.JSON(http.StatusInternalServerError, private.Response{
+                                       Err: fmt.Sprintf("Unable to check file protection for commits from %s to %s: %v", oldCommitID, newCommitID, err),
+                               })
+                               return
+
+                       }
+
+                       changedProtectedfiles = true
+                       protectedFilePath = err.(models.ErrFilePathProtected).Path
+               }
+       }
+
+       // 5. Check if the doer is allowed to push
+       canPush := false
+       if ctx.opts.IsDeployKey {
+               canPush = !changedProtectedfiles && protectBranch.CanPush && (!protectBranch.EnableWhitelist || protectBranch.WhitelistDeployKeys)
+       } else {
+               canPush = !changedProtectedfiles && protectBranch.CanUserPush(ctx.opts.UserID)
+       }
+
+       // 6. If we're not allowed to push directly
+       if !canPush {
+               // Is this is a merge from the UI/API?
+               if ctx.opts.PullRequestID == 0 {
+                       // 6a. If we're not merging from the UI/API then there are two ways we got here:
+                       //
+                       // We are changing a protected file and we're not allowed to do that
+                       if changedProtectedfiles {
+                               log.Warn("Forbidden: Branch: %s in %-v is protected from changing file %s", branchName, repo, protectedFilePath)
+                               ctx.JSON(http.StatusForbidden, private.Response{
+                                       Err: fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedFilePath),
+                               })
+                               return
+                       }
+
+                       // Allow commits that only touch unprotected files
+                       globs := protectBranch.GetUnprotectedFilePatterns()
+                       if len(globs) > 0 {
+                               unprotectedFilesOnly, err := pull_service.CheckUnprotectedFiles(oldCommitID, newCommitID, globs, ctx.env, gitRepo)
+                               if err != nil {
+                                       log.Error("Unable to check file protection for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err)
+                                       ctx.JSON(http.StatusInternalServerError, private.Response{
+                                               Err: fmt.Sprintf("Unable to check file protection for commits from %s to %s: %v", oldCommitID, newCommitID, err),
+                                       })
+                                       return
+                               }
+                               if unprotectedFilesOnly {
+                                       // Commit only touches unprotected files, this is allowed
+                                       return
+                               }
+                       }
+
+                       // Or we're simply not able to push to this protected branch
+                       log.Warn("Forbidden: User %d is not allowed to push to protected branch: %s in %-v", ctx.opts.UserID, branchName, repo)
+                       ctx.JSON(http.StatusForbidden, private.Response{
+                               Err: fmt.Sprintf("Not allowed to push to protected branch %s", branchName),
+                       })
+                       return
+               }
+               // 6b. Merge (from UI or API)
+
+               // Get the PR, user and permissions for the user in the repository
+               pr, err := models.GetPullRequestByID(ctx.opts.PullRequestID)
+               if err != nil {
+                       log.Error("Unable to get PullRequest %d Error: %v", ctx.opts.PullRequestID, err)
+                       ctx.JSON(http.StatusInternalServerError, private.Response{
+                               Err: fmt.Sprintf("Unable to get PullRequest %d Error: %v", ctx.opts.PullRequestID, err),
+                       })
+                       return
+               }
+
+               // Now check if the user is allowed to merge PRs for this repository
+               // Note: we can use ctx.perm and ctx.user directly as they will have been loaded above
+               allowedMerge, err := pull_service.IsUserAllowedToMerge(pr, ctx.perm, ctx.user)
+               if err != nil {
+                       log.Error("Error calculating if allowed to merge: %v", err)
+                       ctx.JSON(http.StatusInternalServerError, private.Response{
+                               Err: fmt.Sprintf("Error calculating if allowed to merge: %v", err),
+                       })
+                       return
+               }
+
+               if !allowedMerge {
+                       log.Warn("Forbidden: User %d is not allowed to push to protected branch: %s in %-v and is not allowed to merge pr #%d", ctx.opts.UserID, branchName, repo, pr.Index)
+                       ctx.JSON(http.StatusForbidden, private.Response{
+                               Err: fmt.Sprintf("Not allowed to push to protected branch %s", branchName),
+                       })
+                       return
+               }
+
+               // If we're an admin for the repository we can ignore status checks, reviews and override protected files
+               if ctx.perm.IsAdmin() {
+                       return
+               }
+
+               // Now if we're not an admin - we can't overwrite protected files so fail now
+               if changedProtectedfiles {
+                       log.Warn("Forbidden: Branch: %s in %-v is protected from changing file %s", branchName, repo, protectedFilePath)
+                       ctx.JSON(http.StatusForbidden, private.Response{
+                               Err: fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedFilePath),
+                       })
+                       return
+               }
+
+               // Check all status checks and reviews are ok
+               if err := pull_service.CheckPRReadyToMerge(pr, true); err != nil {
+                       if models.IsErrNotAllowedToMerge(err) {
+                               log.Warn("Forbidden: User %d is not allowed push to protected branch %s in %-v and pr #%d is not ready to be merged: %s", ctx.opts.UserID, branchName, repo, pr.Index, err.Error())
+                               ctx.JSON(http.StatusForbidden, private.Response{
+                                       Err: fmt.Sprintf("Not allowed to push to protected branch %s and pr #%d is not ready to be merged: %s", branchName, ctx.opts.PullRequestID, err.Error()),
+                               })
+                               return
+                       }
+                       log.Error("Unable to check if mergable: protected branch %s in %-v and pr #%d. Error: %v", ctx.opts.UserID, branchName, repo, pr.Index, err)
+                       ctx.JSON(http.StatusInternalServerError, private.Response{
+                               Err: fmt.Sprintf("Unable to get status of pull request %d. Error: %v", ctx.opts.PullRequestID, err),
+                       })
+                       return
+               }
+       }
+}
+
+func preReceiveTag(ctx *preReceiveContext, oldCommitID, newCommitID, refFullName string) {
+       if !ctx.AssertCanWriteCode() {
+               return
+       }
+
+       tagName := strings.TrimPrefix(refFullName, git.TagPrefix)
+
+       if !ctx.gotProtectedTags {
+               var err error
+               ctx.protectedTags, err = ctx.Repo.Repository.GetProtectedTags()
+               if err != nil {
+                       log.Error("Unable to get protected tags for %-v Error: %v", ctx.Repo.Repository, err)
+                       ctx.JSON(http.StatusInternalServerError, private.Response{
+                               Err: err.Error(),
+                       })
+                       return
+               }
+               ctx.gotProtectedTags = true
+       }
+
+       isAllowed, err := models.IsUserAllowedToControlTag(ctx.protectedTags, tagName, ctx.opts.UserID)
+       if err != nil {
+               ctx.JSON(http.StatusInternalServerError, private.Response{
+                       Err: err.Error(),
+               })
+               return
+       }
+       if !isAllowed {
+               log.Warn("Forbidden: Tag %s in %-v is protected", tagName, ctx.Repo.Repository)
+               ctx.JSON(http.StatusForbidden, private.Response{
+                       Err: fmt.Sprintf("Tag %s is protected", tagName),
+               })
+               return
+       }
+}
+
+func preReceivePullRequest(ctx *preReceiveContext, oldCommitID, newCommitID, refFullName string) {
+       if !ctx.AssertCreatePullRequest() {
+               return
+       }
+
+       if ctx.Repo.Repository.IsEmpty {
+               ctx.JSON(http.StatusForbidden, map[string]interface{}{
+                       "err": "Can't create pull request for an empty repository.",
+               })
+               return
+       }
+
+       if ctx.opts.IsWiki {
+               ctx.JSON(http.StatusForbidden, map[string]interface{}{
+                       "err": "Pull requests are not suppported on the wiki.",
+               })
+               return
+       }
+
+       baseBranchName := refFullName[len(git.PullRequestPrefix):]
+
+       baseBranchExist := false
+       if ctx.Repo.GitRepo.IsBranchExist(baseBranchName) {
+               baseBranchExist = true
+       }
+
+       if !baseBranchExist {
+               for p, v := range baseBranchName {
+                       if v == '/' && ctx.Repo.GitRepo.IsBranchExist(baseBranchName[:p]) && p != len(baseBranchName)-1 {
+                               baseBranchExist = true
+                               break
+                       }
+               }
+       }
+
+       if !baseBranchExist {
+               ctx.JSON(http.StatusForbidden, private.Response{
+                       Err: fmt.Sprintf("Unexpected ref: %s", refFullName),
+               })
+               return
+       }
+}
+
+func generateGitEnv(opts *private.HookOptions) (env []string) {
+       env = os.Environ()
+       if opts.GitAlternativeObjectDirectories != "" {
+               env = append(env,
+                       private.GitAlternativeObjectDirectories+"="+opts.GitAlternativeObjectDirectories)
+       }
+       if opts.GitObjectDirectory != "" {
+               env = append(env,
+                       private.GitObjectDirectory+"="+opts.GitObjectDirectory)
+       }
+       if opts.GitQuarantinePath != "" {
+               env = append(env,
+                       private.GitQuarantinePath+"="+opts.GitQuarantinePath)
+       }
+       return env
+}
+
+func loadUserAndPermission(ctx *gitea_context.PrivateContext, id int64) (user *models.User, perm models.Permission) {
+       user, err := models.GetUserByID(id)
+       if err != nil {
+               log.Error("Unable to get User id %d Error: %v", id, err)
+               ctx.JSON(http.StatusInternalServerError, private.Response{
+                       Err: fmt.Sprintf("Unable to get User id %d Error: %v", id, err),
+               })
+               return
+       }
+
+       perm, err = models.GetUserRepoPermission(ctx.Repo.Repository, user)
+       if err != nil {
+               log.Error("Unable to get Repo permission of repo %s/%s of User %s", ctx.Repo.Repository.OwnerName, ctx.Repo.Repository.Name, user.Name, err)
+               ctx.JSON(http.StatusInternalServerError, private.Response{
+                       Err: fmt.Sprintf("Unable to get Repo permission of repo %s/%s of User %s: %v", ctx.Repo.Repository.OwnerName, ctx.Repo.Repository.Name, user.Name, err),
+               })
+               return
+       }
+
+       return
+}
diff --git a/routers/private/hook_proc_receive.go b/routers/private/hook_proc_receive.go
new file mode 100644 (file)
index 0000000..e427a55
--- /dev/null
@@ -0,0 +1,34 @@
+// Copyright 2021 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 private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead.
+package private
+
+import (
+       "net/http"
+
+       gitea_context "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/git"
+       "code.gitea.io/gitea/modules/private"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/services/agit"
+)
+
+// HookProcReceive proc-receive hook - only handles agit Proc-Receive requests at present
+func HookProcReceive(ctx *gitea_context.PrivateContext) {
+       opts := web.GetForm(ctx).(*private.HookOptions)
+       if !git.SupportProcReceive {
+               ctx.Status(http.StatusNotFound)
+               return
+       }
+
+       results := agit.ProcRecive(ctx, opts)
+       if ctx.Written() {
+               return
+       }
+
+       ctx.JSON(http.StatusOK, private.HookProcReceiveResult{
+               Results: results,
+       })
+}
diff --git a/routers/private/hook_verification.go b/routers/private/hook_verification.go
new file mode 100644 (file)
index 0000000..8c7492e
--- /dev/null
@@ -0,0 +1,122 @@
+// Copyright 2021 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 private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead.
+package private
+
+import (
+       "bufio"
+       "context"
+       "fmt"
+       "io"
+       "os"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/git"
+       "code.gitea.io/gitea/modules/log"
+)
+
+// _________                        .__  __
+// \_   ___ \  ____   _____   _____ |__|/  |_
+// /    \  \/ /  _ \ /     \ /     \|  \   __\
+// \     \___(  <_> )  Y Y  \  Y Y  \  ||  |
+//  \______  /\____/|__|_|  /__|_|  /__||__|
+//         \/             \/      \/
+// ____   ____           .__  _____.__               __  .__
+// \   \ /   /___________|__|/ ____\__| ____ _____ _/  |_|__| ____   ____
+//  \   Y   // __ \_  __ \  \   __\|  |/ ___\\__  \\   __\  |/  _ \ /    \
+//   \     /\  ___/|  | \/  ||  |  |  \  \___ / __ \|  | |  (  <_> )   |  \
+//    \___/  \___  >__|  |__||__|  |__|\___  >____  /__| |__|\____/|___|  /
+//               \/                        \/     \/                    \/
+//
+// This file contains commit verification functions for refs passed across in hooks
+
+func verifyCommits(oldCommitID, newCommitID string, repo *git.Repository, env []string) error {
+       stdoutReader, stdoutWriter, err := os.Pipe()
+       if err != nil {
+               log.Error("Unable to create os.Pipe for %s", repo.Path)
+               return err
+       }
+       defer func() {
+               _ = stdoutReader.Close()
+               _ = stdoutWriter.Close()
+       }()
+
+       // This is safe as force pushes are already forbidden
+       err = git.NewCommand("rev-list", oldCommitID+"..."+newCommitID).
+               RunInDirTimeoutEnvFullPipelineFunc(env, -1, repo.Path,
+                       stdoutWriter, nil, nil,
+                       func(ctx context.Context, cancel context.CancelFunc) error {
+                               _ = stdoutWriter.Close()
+                               err := readAndVerifyCommitsFromShaReader(stdoutReader, repo, env)
+                               if err != nil {
+                                       log.Error("%v", err)
+                                       cancel()
+                               }
+                               _ = stdoutReader.Close()
+                               return err
+                       })
+       if err != nil && !isErrUnverifiedCommit(err) {
+               log.Error("Unable to check commits from %s to %s in %s: %v", oldCommitID, newCommitID, repo.Path, err)
+       }
+       return err
+}
+
+func readAndVerifyCommitsFromShaReader(input io.ReadCloser, repo *git.Repository, env []string) error {
+       scanner := bufio.NewScanner(input)
+       for scanner.Scan() {
+               line := scanner.Text()
+               err := readAndVerifyCommit(line, repo, env)
+               if err != nil {
+                       log.Error("%v", err)
+                       return err
+               }
+       }
+       return scanner.Err()
+}
+
+func readAndVerifyCommit(sha string, repo *git.Repository, env []string) error {
+       stdoutReader, stdoutWriter, err := os.Pipe()
+       if err != nil {
+               log.Error("Unable to create pipe for %s: %v", repo.Path, err)
+               return err
+       }
+       defer func() {
+               _ = stdoutReader.Close()
+               _ = stdoutWriter.Close()
+       }()
+       hash := git.MustIDFromString(sha)
+
+       return git.NewCommand("cat-file", "commit", sha).
+               RunInDirTimeoutEnvFullPipelineFunc(env, -1, repo.Path,
+                       stdoutWriter, nil, nil,
+                       func(ctx context.Context, cancel context.CancelFunc) error {
+                               _ = stdoutWriter.Close()
+                               commit, err := git.CommitFromReader(repo, hash, stdoutReader)
+                               if err != nil {
+                                       return err
+                               }
+                               verification := models.ParseCommitWithSignature(commit)
+                               if !verification.Verified {
+                                       cancel()
+                                       return &errUnverifiedCommit{
+                                               commit.ID.String(),
+                                       }
+                               }
+                               return nil
+                       })
+}
+
+type errUnverifiedCommit struct {
+       sha string
+}
+
+func (e *errUnverifiedCommit) Error() string {
+       return fmt.Sprintf("Unverified commit: %s", e.sha)
+}
+
+func isErrUnverifiedCommit(err error) bool {
+       _, ok := err.(*errUnverifiedCommit)
+       return ok
+}
index 155e8c036ba7b89f7df7b6f399db92e708536deb..183ab5e98ac2832cae5850747124e4b1878fdd19 100644 (file)
@@ -56,10 +56,10 @@ func Routes() *web.Route {
        r.Post("/ssh/authorized_keys", AuthorizedPublicKeyByContent)
        r.Post("/ssh/{id}/update/{repoid}", UpdatePublicKeyInRepo)
        r.Post("/ssh/log", bind(private.SSHLogOption{}), SSHLog)
-       r.Post("/hook/pre-receive/{owner}/{repo}", bind(private.HookOptions{}), HookPreReceive)
+       r.Post("/hook/pre-receive/{owner}/{repo}", RepoAssignment, bind(private.HookOptions{}), HookPreReceive)
        r.Post("/hook/post-receive/{owner}/{repo}", bind(private.HookOptions{}), HookPostReceive)
-       r.Post("/hook/proc-receive/{owner}/{repo}", bind(private.HookOptions{}), HookProcReceive)
-       r.Post("/hook/set-default-branch/{owner}/{repo}/{branch}", SetDefaultBranch)
+       r.Post("/hook/proc-receive/{owner}/{repo}", RepoAssignment, bind(private.HookOptions{}), HookProcReceive)
+       r.Post("/hook/set-default-branch/{owner}/{repo}/{branch}", RepoAssignment, SetDefaultBranch)
        r.Get("/serv/none/{keyid}", ServNoCommand)
        r.Get("/serv/command/{keyid}/{owner}/{repo}", ServCommand)
        r.Post("/manager/shutdown", Shutdown)
diff --git a/routers/private/internal_repo.go b/routers/private/internal_repo.go
new file mode 100644 (file)
index 0000000..60daa1d
--- /dev/null
@@ -0,0 +1,84 @@
+// Copyright 2021 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 private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead.
+package private
+
+import (
+       "context"
+       "fmt"
+       "net/http"
+
+       "code.gitea.io/gitea/models"
+       gitea_context "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/git"
+       "code.gitea.io/gitea/modules/log"
+)
+
+// __________
+// \______   \ ____ ______   ____
+//  |       _// __ \\____ \ /  _ \
+//  |    |   \  ___/|  |_> >  <_> )
+//  |____|_  /\___  >   __/ \____/
+//         \/     \/|__|
+//    _____                .__                                     __
+//   /  _  \   ______ _____|__| ____   ____   _____   ____   _____/  |_
+//  /  /_\  \ /  ___//  ___/  |/ ___\ /    \ /     \_/ __ \ /    \   __\
+// /    |    \\___ \ \___ \|  / /_/  >   |  \  Y Y  \  ___/|   |  \  |
+// \____|__  /____  >____  >__\___  /|___|  /__|_|  /\___  >___|  /__|
+//         \/     \/     \/  /_____/      \/      \/     \/     \/
+
+// This file contains common functions relating to setting the Repository for the
+// internal routes
+
+// RepoAssignment assigns the repository and gitrepository to the private context
+func RepoAssignment(ctx *gitea_context.PrivateContext) context.CancelFunc {
+       ownerName := ctx.Params(":owner")
+       repoName := ctx.Params(":repo")
+
+       repo := loadRepository(ctx, ownerName, repoName)
+       if ctx.Written() {
+               // Error handled in loadRepository
+               return nil
+       }
+
+       gitRepo, err := git.OpenRepository(repo.RepoPath())
+       if err != nil {
+               log.Error("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err)
+               ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
+                       "Err": fmt.Sprintf("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err),
+               })
+               return nil
+       }
+
+       ctx.Repo = &gitea_context.Repository{
+               Repository: repo,
+               GitRepo:    gitRepo,
+       }
+
+       // We opened it, we should close it
+       cancel := func() {
+               // If it's been set to nil then assume someone else has closed it.
+               if ctx.Repo.GitRepo != nil {
+                       ctx.Repo.GitRepo.Close()
+               }
+       }
+
+       return cancel
+}
+
+func loadRepository(ctx *gitea_context.PrivateContext, ownerName, repoName string) *models.Repository {
+       repo, err := models.GetRepositoryByOwnerAndName(ownerName, repoName)
+       if err != nil {
+               log.Error("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err)
+               ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
+                       "Err": fmt.Sprintf("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err),
+               })
+               return nil
+       }
+       if repo.OwnerName == "" {
+               repo.OwnerName = ownerName
+       }
+       return repo
+}