2021-09-16 16:34:54 +03:00
// Copyright 2021 The Gitea Authors. All rights reserved.
2022-11-27 21:20:29 +03:00
// SPDX-License-Identifier: MIT
2021-09-16 16:34:54 +03:00
package private
import (
2024-05-07 10:36:48 +03:00
"context"
2021-09-16 16:34:54 +03:00
"fmt"
"net/http"
2024-05-07 10:36:48 +03:00
"code.gitea.io/gitea/models/db"
2024-03-06 11:47:52 +03:00
git_model "code.gitea.io/gitea/models/git"
2022-06-13 12:37:59 +03:00
issues_model "code.gitea.io/gitea/models/issues"
2024-04-09 06:43:17 +03:00
access_model "code.gitea.io/gitea/models/perm/access"
2024-05-07 10:36:48 +03:00
pull_model "code.gitea.io/gitea/models/pull"
2021-12-10 04:27:50 +03:00
repo_model "code.gitea.io/gitea/models/repo"
2024-04-09 06:43:17 +03:00
user_model "code.gitea.io/gitea/models/user"
2024-05-07 10:36:48 +03:00
"code.gitea.io/gitea/modules/cache"
2021-09-16 16:34:54 +03:00
"code.gitea.io/gitea/modules/git"
2024-03-06 11:47:52 +03:00
"code.gitea.io/gitea/modules/gitrepo"
2021-09-16 16:34:54 +03:00
"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"
2024-05-07 10:36:48 +03:00
timeutil "code.gitea.io/gitea/modules/timeutil"
2021-09-16 16:34:54 +03:00
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
2024-02-27 10:12:22 +03:00
gitea_context "code.gitea.io/gitea/services/context"
2024-03-27 05:34:10 +03:00
pull_service "code.gitea.io/gitea/services/pull"
2021-09-16 16:34:54 +03:00
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
2024-03-06 11:47:52 +03:00
// OUT OF DATE: we do need the git repo to sync the branch to the db now.
2021-09-16 16:34:54 +03:00
// 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
2024-06-19 01:32:45 +03:00
ownerName := ctx . PathParam ( ":owner" )
repoName := ctx . PathParam ( ":repo" )
2021-09-16 16:34:54 +03:00
// defer getting the repository at this point - as we should only retrieve it if we're going to call update
2024-03-06 11:47:52 +03:00
var (
repo * repo_model . Repository
gitRepo * git . Repository
)
defer gitRepo . Close ( ) // it's safe to call Close on a nil pointer
2021-09-16 16:34:54 +03:00
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).
2023-05-26 04:04:48 +03:00
if refFullName . IsBranch ( ) || refFullName . IsTag ( ) {
2021-09-16 16:34:54 +03:00
if repo == nil {
repo = loadRepository ( ctx , ownerName , repoName )
if ctx . Written ( ) {
// Error handled in loadRepository
return
}
wasEmpty = repo . IsEmpty
}
2021-11-24 12:08:13 +03:00
option := & repo_module . PushUpdateOptions {
2021-09-16 16:34:54 +03:00
RefFullName : refFullName ,
OldCommitID : opts . OldCommitIDs [ i ] ,
NewCommitID : opts . NewCommitIDs [ i ] ,
PusherID : opts . UserID ,
PusherName : opts . UserName ,
RepoUserName : ownerName ,
RepoName : repoName ,
}
2021-11-24 12:08:13 +03:00
updates = append ( updates , option )
2023-05-26 04:04:48 +03:00
if repo . IsEmpty && ( refFullName . BranchName ( ) == "master" || refFullName . BranchName ( ) == "main" ) {
2021-09-16 16:34:54 +03:00
// put the master/main branch first
2024-03-20 04:45:27 +03:00
// FIXME: It doesn't always work, since the master/main branch may not be the first batch of updates.
// If the user pushes many branches at once, the Git hook will call the internal API in batches, rather than all at once.
// See https://github.com/go-gitea/gitea/blob/cb52b17f92e2d2293f7c003649743464492bca48/cmd/hook.go#L27
// If the user executes `git push origin --all` and pushes more than 30 branches, the master/main may not be the default branch.
2021-09-16 16:34:54 +03:00
copy ( updates [ 1 : ] , updates )
2021-11-24 12:08:13 +03:00
updates [ 0 ] = option
2021-09-16 16:34:54 +03:00
}
}
}
if repo != nil && len ( updates ) > 0 {
2024-03-06 11:47:52 +03:00
branchesToSync := make ( [ ] * repo_module . PushUpdateOptions , 0 , len ( updates ) )
for _ , update := range updates {
if ! update . RefFullName . IsBranch ( ) {
continue
}
if repo == nil {
repo = loadRepository ( ctx , ownerName , repoName )
if ctx . Written ( ) {
return
}
wasEmpty = repo . IsEmpty
}
if update . IsDelRef ( ) {
if err := git_model . AddDeletedBranch ( ctx , repo . ID , update . RefFullName . BranchName ( ) , update . PusherID ) ; err != nil {
log . Error ( "Failed to add deleted branch: %s/%s Error: %v" , ownerName , repoName , err )
ctx . JSON ( http . StatusInternalServerError , private . HookPostReceiveResult {
Err : fmt . Sprintf ( "Failed to add deleted branch: %s/%s Error: %v" , ownerName , repoName , err ) ,
} )
return
}
} else {
branchesToSync = append ( branchesToSync , update )
2024-03-27 05:34:10 +03:00
// TODO: should we return the error and return the error when pushing? Currently it will log the error and not prevent the pushing
pull_service . UpdatePullsRefs ( ctx , repo , update )
2024-03-06 11:47:52 +03:00
}
}
if len ( branchesToSync ) > 0 {
2024-04-30 15:34:40 +03:00
var err error
gitRepo , err = gitrepo . OpenRepository ( ctx , repo )
if err != nil {
log . Error ( "Failed to open repository: %s/%s Error: %v" , ownerName , repoName , err )
ctx . JSON ( http . StatusInternalServerError , private . HookPostReceiveResult {
Err : fmt . Sprintf ( "Failed to open repository: %s/%s Error: %v" , ownerName , repoName , err ) ,
} )
return
2024-03-06 11:47:52 +03:00
}
var (
branchNames = make ( [ ] string , 0 , len ( branchesToSync ) )
commitIDs = make ( [ ] string , 0 , len ( branchesToSync ) )
)
for _ , update := range branchesToSync {
branchNames = append ( branchNames , update . RefFullName . BranchName ( ) )
commitIDs = append ( commitIDs , update . NewCommitID )
}
2024-03-20 04:45:27 +03:00
if err := repo_service . SyncBranchesToDB ( ctx , repo . ID , opts . UserID , branchNames , commitIDs , gitRepo . GetCommit ) ; err != nil {
2024-03-06 11:47:52 +03:00
ctx . JSON ( http . StatusInternalServerError , private . HookPostReceiveResult {
Err : fmt . Sprintf ( "Failed to sync branch to DB in repository: %s/%s Error: %v" , ownerName , repoName , err ) ,
} )
return
}
}
2024-03-11 09:42:50 +03:00
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 . RefFullName . 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
}
2021-09-16 16:34:54 +03:00
}
2024-05-07 10:36:48 +03:00
// handle pull request merging, a pull request action should push at least 1 commit
if opts . PushTrigger == repo_module . PushTriggerPRMergeToBase {
handlePullRequestMerging ( ctx , opts , ownerName , repoName , updates )
if ctx . Written ( ) {
return
}
}
2024-04-09 06:43:17 +03:00
isPrivate := opts . GitPushOptions . Bool ( private . GitPushOptionRepoPrivate )
isTemplate := opts . GitPushOptions . Bool ( private . GitPushOptionRepoTemplate )
2021-09-16 16:34:54 +03:00
// Handle Push Options
2024-04-09 06:43:17 +03:00
if isPrivate . Has ( ) || isTemplate . Has ( ) {
2021-09-16 16:34:54 +03:00
// load the repository
if repo == nil {
repo = loadRepository ( ctx , ownerName , repoName )
if ctx . Written ( ) {
// Error handled in loadRepository
return
}
wasEmpty = repo . IsEmpty
}
2024-05-07 10:36:48 +03:00
pusher , err := loadContextCacheUser ( ctx , opts . UserID )
2024-04-09 06:43:17 +03:00
if err != nil {
2021-09-16 16:34:54 +03:00
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 ) ,
} )
2024-04-09 06:43:17 +03:00
return
}
perm , err := access_model . GetUserRepoPermission ( ctx , repo , pusher )
if 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 ) ,
} )
return
}
if ! perm . IsOwner ( ) && ! perm . IsAdmin ( ) {
ctx . JSON ( http . StatusNotFound , private . HookPostReceiveResult {
Err : "Permissions denied" ,
} )
return
}
2024-10-12 08:42:10 +03:00
cols := make ( [ ] string , 0 , 2 )
2024-04-09 06:43:17 +03:00
if isPrivate . Has ( ) {
repo . IsPrivate = isPrivate . Value ( )
cols = append ( cols , "is_private" )
}
if isTemplate . Has ( ) {
repo . IsTemplate = isTemplate . Value ( )
cols = append ( cols , "is_template" )
}
if len ( cols ) > 0 {
if err := repo_model . UpdateRepositoryCols ( ctx , repo , cols ... ) ; 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 ) ,
} )
return
}
2021-09-16 16:34:54 +03:00
}
}
results := make ( [ ] private . HookPostReceiveBranchResult , 0 , len ( opts . OldCommitIDs ) )
// We have to reload the repo in case its state is changed above
repo = nil
2021-12-10 04:27:50 +03:00
var baseRepo * repo_model . Repository
2021-09-16 16:34:54 +03:00
// Now handle the pull request notification trailers
for i := range opts . OldCommitIDs {
refFullName := opts . RefFullNames [ i ]
newCommitID := opts . NewCommitIDs [ i ]
// If we've pushed a branch (and not deleted it)
2024-02-26 11:10:14 +03:00
if ! git . IsEmptyCommitID ( newCommitID ) && refFullName . IsBranch ( ) {
2021-09-16 16:34:54 +03:00
// 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
}
baseRepo = repo
if repo . IsFork {
2022-12-03 05:48:26 +03:00
if err := repo . GetBaseRepo ( ctx ) ; err != nil {
2021-09-16 16:34:54 +03:00
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
}
2023-10-11 07:24:07 +03:00
if repo . BaseRepo . AllowsPulls ( ctx ) {
2023-01-30 00:00:10 +03:00
baseRepo = repo . BaseRepo
}
}
2023-10-11 07:24:07 +03:00
if ! baseRepo . AllowsPulls ( ctx ) {
2023-01-30 00:00:10 +03:00
// We can stop there's no need to go any further
ctx . JSON ( http . StatusOK , private . HookPostReceiveResult {
RepoWasEmpty : wasEmpty ,
} )
return
2021-09-16 16:34:54 +03:00
}
}
2023-05-26 04:04:48 +03:00
branch := refFullName . BranchName ( )
2024-10-01 22:25:08 +03:00
if branch == baseRepo . DefaultBranch {
if err := repo_service . AddRepoToLicenseUpdaterQueue ( & repo_service . LicenseUpdaterOptions {
RepoID : repo . ID ,
} ) ; err != nil {
ctx . JSON ( http . StatusInternalServerError , private . Response { Err : err . Error ( ) } )
return
}
// If our branch is the default branch of an unforked repo - there's no PR to create or refer to
if ! repo . IsFork {
results = append ( results , private . HookPostReceiveBranchResult { } )
continue
}
2021-09-16 16:34:54 +03:00
}
2022-11-19 11:12:33 +03:00
pr , err := issues_model . GetUnmergedPullRequest ( ctx , repo . ID , baseRepo . ID , branch , baseRepo . DefaultBranch , issues_model . PullRequestFlowGithub )
2022-06-13 12:37:59 +03:00
if err != nil && ! issues_model . IsErrPullRequestNotExist ( err ) {
2021-09-16 16:34:54 +03:00
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 {
2023-10-11 07:24:07 +03:00
Message : setting . Git . PullRequestPushMessage && baseRepo . AllowsPulls ( ctx ) ,
2021-09-16 16:34:54 +03:00
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 {
2023-10-11 07:24:07 +03:00
Message : setting . Git . PullRequestPushMessage && baseRepo . AllowsPulls ( ctx ) ,
2021-09-16 16:34:54 +03:00
Create : false ,
Branch : branch ,
URL : fmt . Sprintf ( "%s/pulls/%d" , baseRepo . HTMLURL ( ) , pr . Index ) ,
} )
}
}
}
ctx . JSON ( http . StatusOK , private . HookPostReceiveResult {
Results : results ,
RepoWasEmpty : wasEmpty ,
} )
}
2024-05-07 10:36:48 +03:00
func loadContextCacheUser ( ctx context . Context , id int64 ) ( * user_model . User , error ) {
return cache . GetWithContextCache ( ctx , "hook_post_receive_user" , id , func ( ) ( * user_model . User , error ) {
return user_model . GetUserByID ( ctx , id )
} )
}
// handlePullRequestMerging handle pull request merging, a pull request action should push at least 1 commit
func handlePullRequestMerging ( ctx * gitea_context . PrivateContext , opts * private . HookOptions , ownerName , repoName string , updates [ ] * repo_module . PushUpdateOptions ) {
if len ( updates ) == 0 {
ctx . JSON ( http . StatusInternalServerError , private . HookPostReceiveResult {
Err : fmt . Sprintf ( "Pushing a merged PR (pr:%d) no commits pushed " , opts . PullRequestID ) ,
} )
return
}
pr , err := issues_model . GetPullRequestByID ( ctx , opts . PullRequestID )
if err != nil {
log . Error ( "GetPullRequestByID[%d]: %v" , opts . PullRequestID , err )
ctx . JSON ( http . StatusInternalServerError , private . HookPostReceiveResult { Err : "GetPullRequestByID failed" } )
return
}
pusher , err := loadContextCacheUser ( ctx , opts . UserID )
if err != nil {
log . Error ( "Failed to Update: %s/%s Error: %v" , ownerName , repoName , err )
ctx . JSON ( http . StatusInternalServerError , private . HookPostReceiveResult { Err : "Load pusher user failed" } )
return
}
pr . MergedCommitID = updates [ len ( updates ) - 1 ] . NewCommitID
pr . MergedUnix = timeutil . TimeStampNow ( )
pr . Merger = pusher
pr . MergerID = pusher . ID
err = db . WithTx ( ctx , func ( ctx context . Context ) error {
// Removing an auto merge pull and ignore if not exist
if err := pull_model . DeleteScheduledAutoMerge ( ctx , pr . ID ) ; err != nil && ! db . IsErrNotExist ( err ) {
return fmt . Errorf ( "DeleteScheduledAutoMerge[%d]: %v" , opts . PullRequestID , err )
}
if _ , err := pr . SetMerged ( ctx ) ; err != nil {
return fmt . Errorf ( "SetMerged failed: %s/%s Error: %v" , ownerName , repoName , err )
}
return nil
} )
if err != nil {
log . Error ( "Failed to update PR to merged: %v" , err )
ctx . JSON ( http . StatusInternalServerError , private . HookPostReceiveResult { Err : "Failed to update PR to merged" } )
}
}