2019-06-01 23:00:21 +08:00
// 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 (
2020-01-15 16:32:57 +08:00
"bufio"
"context"
2019-06-01 23:00:21 +08:00
"fmt"
2020-01-15 16:32:57 +08:00
"io"
2019-06-01 23:00:21 +08:00
"net/http"
"os"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/private"
2019-06-10 19:35:13 +08:00
"code.gitea.io/gitea/modules/repofiles"
2019-06-01 23:00:21 +08:00
"code.gitea.io/gitea/modules/util"
2020-01-11 15:29:34 +08:00
pull_service "code.gitea.io/gitea/services/pull"
2020-01-15 16:32:57 +08:00
"gopkg.in/src-d/go-git.v4/plumbing"
2019-06-01 23:00:21 +08:00
2019-08-24 00:40:30 +08:00
"gitea.com/macaron/macaron"
2019-06-01 23:00:21 +08:00
)
2020-01-15 16:32:57 +08:00
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 ( )
} ( )
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 := plumbing . NewHash ( 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
}
log . Info ( "have commit %s" , commit . ID . String ( ) )
verification := models . ParseCommitWithSignature ( commit )
if ! verification . Verified {
log . Info ( "unverified commit %s" , commit . ID . String ( ) )
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
}
2019-06-01 23:00:21 +08:00
// HookPreReceive checks whether a individual commit is acceptable
2019-12-26 19:29:45 +08:00
func HookPreReceive ( ctx * macaron . Context , opts private . HookOptions ) {
2019-06-01 23:00:21 +08:00
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 , map [ string ] interface { } {
"err" : err . Error ( ) ,
} )
return
}
repo . OwnerName = ownerName
2020-01-15 16:32:57 +08:00
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 , map [ string ] interface { } {
"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 )
}
2019-12-26 19:29:45 +08:00
for i := range opts . OldCommitIDs {
oldCommitID := opts . OldCommitIDs [ i ]
newCommitID := opts . NewCommitIDs [ i ]
refFullName := opts . RefFullNames [ i ]
branchName := strings . TrimPrefix ( refFullName , git . BranchPrefix )
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 ( 500 , map [ string ] interface { } {
"err" : err . Error ( ) ,
2019-06-01 23:00:21 +08:00
} )
return
}
2019-12-26 19:29:45 +08:00
if protectBranch != nil && protectBranch . IsProtected ( ) {
2020-01-15 16:32:57 +08:00
// detect and prevent deletion
2019-12-26 19:29:45 +08:00
if newCommitID == git . EmptySHA {
log . Warn ( "Forbidden: Branch: %s in %-v is protected from deletion" , branchName , repo )
2019-06-01 23:00:21 +08:00
ctx . JSON ( http . StatusForbidden , map [ string ] interface { } {
2019-12-26 19:29:45 +08:00
"err" : fmt . Sprintf ( "branch %s is protected from deletion" , branchName ) ,
2019-06-01 23:00:21 +08:00
} )
return
}
2019-12-26 19:29:45 +08:00
// detect force push
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 , map [ string ] interface { } {
"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 , map [ string ] interface { } {
"err" : fmt . Sprintf ( "branch %s is protected from force push" , branchName ) ,
} )
return
}
}
2020-01-15 16:32:57 +08:00
// 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 , map [ string ] interface { } {
"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 , map [ string ] interface { } {
"err" : fmt . Sprintf ( "branch %s is protected from unverified commit %s" , branchName , unverifiedCommit ) ,
} )
return
}
}
2019-12-26 19:29:45 +08:00
canPush := false
if opts . IsDeployKey {
canPush = protectBranch . CanPush && ( ! protectBranch . EnableWhitelist || protectBranch . WhitelistDeployKeys )
} else {
canPush = protectBranch . CanUserPush ( opts . UserID )
2019-07-01 09:18:13 +08:00
}
2019-12-26 19:29:45 +08:00
if ! canPush && opts . ProtectedBranchID > 0 {
2020-01-17 05:01:22 +08:00
// Merge (from UI or API)
2019-12-26 19:29:45 +08:00
pr , err := models . GetPullRequestByID ( opts . ProtectedBranchID )
if err != nil {
log . Error ( "Unable to get PullRequest %d Error: %v" , opts . ProtectedBranchID , err )
ctx . JSON ( http . StatusInternalServerError , map [ string ] interface { } {
"err" : fmt . Sprintf ( "Unable to get PullRequest %d Error: %v" , opts . ProtectedBranchID , err ) ,
} )
return
}
2020-01-11 15:29:34 +08:00
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 , map [ string ] interface { } {
"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 , map [ string ] interface { } {
"err" : fmt . Sprintf ( "Unable to get Repo permission of repo %s/%s of User %s: %v" , repo . OwnerName , repo . Name , user . Name , err ) ,
2019-12-26 19:29:45 +08:00
} )
return
}
2020-01-11 15:29:34 +08:00
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 , map [ string ] interface { } {
"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 )
2020-01-04 01:47:10 +08:00
ctx . JSON ( http . StatusForbidden , map [ string ] interface { } {
2020-01-11 15:29:34 +08:00
"err" : fmt . Sprintf ( "Not allowed to push to protected branch %s" , branchName ) ,
2020-01-04 01:47:10 +08:00
} )
return
}
2020-01-17 05:01:22 +08:00
// Check all status checks and reviews is ok, unless repo admin which can bypass this.
if ! perm . IsAdmin ( ) {
if err := pull_service . CheckPRReadyToMerge ( pr ) ; 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 , map [ string ] interface { } {
"err" : fmt . Sprintf ( "Not allowed to push to protected branch %s and pr #%d is not ready to be merged: %s" , branchName , opts . ProtectedBranchID , 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 , map [ string ] interface { } {
"err" : fmt . Sprintf ( "Unable to get status of pull request %d. Error: %v" , opts . ProtectedBranchID , err ) ,
2020-01-11 15:29:34 +08:00
} )
}
}
2019-12-26 19:29:45 +08:00
} else if ! canPush {
2020-01-11 15:29:34 +08:00
log . Warn ( "Forbidden: User %d is not allowed to push to protected branch: %s in %-v" , opts . UserID , branchName , repo )
2019-07-01 09:18:13 +08:00
ctx . JSON ( http . StatusForbidden , map [ string ] interface { } {
2020-01-11 15:29:34 +08:00
"err" : fmt . Sprintf ( "Not allowed to push to protected branch %s" , branchName ) ,
2019-07-01 09:18:13 +08:00
} )
return
}
2019-06-01 23:00:21 +08:00
}
}
2019-12-26 19:29:45 +08:00
2019-06-01 23:00:21 +08:00
ctx . PlainText ( http . StatusOK , [ ] byte ( "ok" ) )
}
// HookPostReceive updates services and users
2019-12-26 19:29:45 +08:00
func HookPostReceive ( ctx * macaron . Context , opts private . HookOptions ) {
2019-06-01 23:00:21 +08:00
ownerName := ctx . Params ( ":owner" )
repoName := ctx . Params ( ":repo" )
2019-12-26 19:29:45 +08:00
var repo * models . Repository
updates := make ( [ ] * repofiles . PushUpdateOptions , 0 , len ( opts . OldCommitIDs ) )
wasEmpty := false
for i := range opts . OldCommitIDs {
refFullName := opts . RefFullNames [ i ]
branch := opts . RefFullNames [ i ]
if strings . HasPrefix ( branch , git . BranchPrefix ) {
branch = strings . TrimPrefix ( branch , git . BranchPrefix )
} else {
branch = strings . TrimPrefix ( branch , git . TagPrefix )
2019-06-10 19:35:13 +08:00
}
2019-12-26 19:29:45 +08:00
// 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 := repofiles . PushUpdateOptions {
RefFullName : refFullName ,
OldCommitID : opts . OldCommitIDs [ i ] ,
NewCommitID : opts . NewCommitIDs [ i ] ,
Branch : branch ,
PusherID : opts . UserID ,
PusherName : opts . UserName ,
RepoUserName : ownerName ,
RepoName : repoName ,
}
updates = append ( updates , & option )
if repo . IsEmpty && branch == "master" && strings . HasPrefix ( refFullName , git . BranchPrefix ) {
// put the master branch first
copy ( updates [ 1 : ] , updates )
updates [ 0 ] = & option
}
2019-06-01 23:00:21 +08:00
}
}
2019-12-26 19:29:45 +08:00
if repo != nil && len ( updates ) > 0 {
if err := repofiles . PushUpdates ( repo , 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 . Branch )
}
log . Error ( "Failed to Update: %s/%s Error: %v" , ownerName , repoName , err )
2019-06-01 23:00:21 +08:00
2019-12-26 19:29:45 +08:00
ctx . JSON ( http . StatusInternalServerError , private . HookPostReceiveResult {
Err : fmt . Sprintf ( "Failed to Update: %s/%s Error: %v" , ownerName , repoName , err ) ,
2019-06-01 23:00:21 +08:00
} )
return
}
2019-12-26 19:29:45 +08:00
}
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
}
2019-06-01 23:00:21 +08:00
2019-12-26 19:29:45 +08:00
pr , err := models . GetUnmergedPullRequest ( repo . ID , baseRepo . ID , branch , baseRepo . DefaultBranch )
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 ,
2019-06-01 23:00:21 +08:00
} )
return
}
2019-12-26 19:29:45 +08:00
if pr == nil {
if repo . IsFork {
branch = fmt . Sprintf ( "%s:%s" , repo . OwnerName , branch )
}
results = append ( results , private . HookPostReceiveBranchResult {
Message : true ,
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 : true ,
Create : false ,
Branch : branch ,
URL : fmt . Sprintf ( "%s/pulls/%d" , baseRepo . HTMLURL ( ) , pr . Index ) ,
} )
}
2019-06-01 23:00:21 +08:00
}
2019-12-26 19:29:45 +08:00
}
ctx . JSON ( http . StatusOK , private . HookPostReceiveResult {
Results : results ,
RepoWasEmpty : wasEmpty ,
} )
}
2019-06-01 23:00:21 +08:00
2019-12-26 19:29:45 +08:00
// SetDefaultBranch updates the default branch
func SetDefaultBranch ( ctx * macaron . Context ) {
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 , map [ string ] interface { } {
"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 , map [ string ] interface { } {
"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 ( )
2019-06-01 23:00:21 +08:00
ctx . JSON ( http . StatusInternalServerError , map [ string ] interface { } {
2019-12-26 19:29:45 +08:00
"Err" : fmt . Sprintf ( "Unable to set default branch onrepository: %s/%s Error: %v" , ownerName , repoName , err ) ,
2019-06-01 23:00:21 +08:00
} )
return
}
2019-12-26 19:29:45 +08:00
}
gitRepo . Close ( )
2019-06-01 23:00:21 +08:00
2019-12-26 19:29:45 +08:00
if err := repo . UpdateDefaultBranch ( ) ; err != nil {
ctx . JSON ( http . StatusInternalServerError , map [ string ] interface { } {
"Err" : fmt . Sprintf ( "Unable to set default branch onrepository: %s/%s Error: %v" , ownerName , repoName , err ) ,
} )
2019-06-01 23:00:21 +08:00
return
}
2019-12-26 19:29:45 +08:00
ctx . PlainText ( 200 , [ ] byte ( "success" ) )
2019-06-01 23:00:21 +08:00
}