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 (
"fmt"
"net/http"
2022-01-17 08:56:43 +03:00
"strconv"
2021-09-16 16:34:54 +03:00
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"
2021-12-10 04:27:50 +03:00
repo_model "code.gitea.io/gitea/models/repo"
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"
"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
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
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 {
if gitRepo == nil {
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
}
}
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
}
// 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 )
2022-05-20 17:08:52 +03:00
if err := repo_model . UpdateRepositoryCols ( ctx , repo , "is_private" , "is_template" ) ; 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 ) ,
} )
}
}
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 ]
2022-01-17 08:56:43 +03:00
// post update for agit pull request
2023-05-26 04:04:48 +03:00
// FIXME: use pr.Flow to test whether it's an Agit PR or a GH PR
2024-02-25 08:35:47 +03:00
if git . DefaultFeatures . SupportProcReceive && refFullName . IsPull ( ) {
2022-01-17 08:56:43 +03:00
if repo == nil {
repo = loadRepository ( ctx , ownerName , repoName )
if ctx . Written ( ) {
return
}
}
2023-05-26 04:04:48 +03:00
pullIndex , _ := strconv . ParseInt ( refFullName . PullName ( ) , 10 , 64 )
2022-01-17 08:56:43 +03:00
if pullIndex <= 0 {
continue
}
2022-06-13 12:37:59 +03:00
pr , err := issues_model . GetPullRequestByIndex ( ctx , repo . ID , pullIndex )
if err != nil && ! issues_model . IsErrPullRequestNotExist ( err ) {
2022-01-17 08:56:43 +03:00
log . Error ( "Failed to get PR by index %v Error: %v" , pullIndex , err )
ctx . JSON ( http . StatusInternalServerError , private . Response {
Err : fmt . Sprintf ( "Failed to get PR by index %v Error: %v" , pullIndex , err ) ,
} )
return
}
if pr == nil {
continue
}
results = append ( results , private . HookPostReceiveBranchResult {
2023-10-11 07:24:07 +03:00
Message : setting . Git . PullRequestPushMessage && repo . AllowsPulls ( ctx ) ,
2022-01-17 08:56:43 +03:00
Create : false ,
Branch : "" ,
URL : fmt . Sprintf ( "%s/pulls/%d" , repo . HTMLURL ( ) , pr . Index ) ,
} )
continue
}
2021-09-16 16:34:54 +03:00
// 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 ( )
2021-09-16 16:34:54 +03:00
// 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
}
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 ,
} )
}