Clean-up HookPreReceive and restore functionality for pushing non-standard refs (#16705)
* 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>
This commit is contained in:
		
							parent
							
								
									a959ed99c2
								
							
						
					
					
						commit
						8de44d1995
					
				| @ -31,6 +31,7 @@ func Wrap(handlers ...interface{}) http.HandlerFunc { | |||||||
| 			func(ctx *context.Context) goctx.CancelFunc, | 			func(ctx *context.Context) goctx.CancelFunc, | ||||||
| 			func(*context.APIContext), | 			func(*context.APIContext), | ||||||
| 			func(*context.PrivateContext), | 			func(*context.PrivateContext), | ||||||
|  | 			func(*context.PrivateContext) goctx.CancelFunc, | ||||||
| 			func(http.Handler) http.Handler: | 			func(http.Handler) http.Handler: | ||||||
| 		default: | 		default: | ||||||
| 			panic(fmt.Sprintf("Unsupported handler type: %#v", t)) | 			panic(fmt.Sprintf("Unsupported handler type: %#v", t)) | ||||||
| @ -59,6 +60,15 @@ func Wrap(handlers ...interface{}) http.HandlerFunc { | |||||||
| 				if ctx.Written() { | 				if ctx.Written() { | ||||||
| 					return | 					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): | 			case func(ctx *context.Context): | ||||||
| 				ctx := context.GetContext(req) | 				ctx := context.GetContext(req) | ||||||
| 				t(ctx) | 				t(ctx) | ||||||
|  | |||||||
							
								
								
									
										75
									
								
								routers/private/default_branch.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								routers/private/default_branch.go
									
									
									
									
									
										Normal file
									
								
							| @ -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 |  | ||||||
| } |  | ||||||
							
								
								
									
										201
									
								
								routers/private/hook_post_receive.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										201
									
								
								routers/private/hook_post_receive.go
									
									
									
									
									
										Normal file
									
								
							| @ -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, | ||||||
|  | 	}) | ||||||
|  | } | ||||||
							
								
								
									
										471
									
								
								routers/private/hook_pre_receive.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										471
									
								
								routers/private/hook_pre_receive.go
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||||
|  | } | ||||||
							
								
								
									
										34
									
								
								routers/private/hook_proc_receive.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								routers/private/hook_proc_receive.go
									
									
									
									
									
										Normal file
									
								
							| @ -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, | ||||||
|  | 	}) | ||||||
|  | } | ||||||
							
								
								
									
										122
									
								
								routers/private/hook_verification.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								routers/private/hook_verification.go
									
									
									
									
									
										Normal file
									
								
							| @ -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/authorized_keys", AuthorizedPublicKeyByContent) | ||||||
| 	r.Post("/ssh/{id}/update/{repoid}", UpdatePublicKeyInRepo) | 	r.Post("/ssh/{id}/update/{repoid}", UpdatePublicKeyInRepo) | ||||||
| 	r.Post("/ssh/log", bind(private.SSHLogOption{}), SSHLog) | 	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/post-receive/{owner}/{repo}", bind(private.HookOptions{}), HookPostReceive) | ||||||
| 	r.Post("/hook/proc-receive/{owner}/{repo}", bind(private.HookOptions{}), HookProcReceive) | 	r.Post("/hook/proc-receive/{owner}/{repo}", RepoAssignment, bind(private.HookOptions{}), HookProcReceive) | ||||||
| 	r.Post("/hook/set-default-branch/{owner}/{repo}/{branch}", SetDefaultBranch) | 	r.Post("/hook/set-default-branch/{owner}/{repo}/{branch}", RepoAssignment, SetDefaultBranch) | ||||||
| 	r.Get("/serv/none/{keyid}", ServNoCommand) | 	r.Get("/serv/none/{keyid}", ServNoCommand) | ||||||
| 	r.Get("/serv/command/{keyid}/{owner}/{repo}", ServCommand) | 	r.Get("/serv/command/{keyid}/{owner}/{repo}", ServCommand) | ||||||
| 	r.Post("/manager/shutdown", Shutdown) | 	r.Post("/manager/shutdown", Shutdown) | ||||||
|  | |||||||
							
								
								
									
										84
									
								
								routers/private/internal_repo.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								routers/private/internal_repo.go
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||||
|  | } | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user