* 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>tags/v1.16.0-rc1
@@ -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) |
@@ -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")) | |||
} |
@@ -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 | |||
} |
@@ -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, | |||
}) | |||
} |
@@ -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 | |||
} |
@@ -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, | |||
}) | |||
} |
@@ -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 | |||
} |
@@ -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) |
@@ -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 | |||
} |