code.gitea.io/gitea@v1.21.7/routers/private/hook_post_receive.go (about)

     1  // Copyright 2021 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package private
     5  
     6  import (
     7  	"fmt"
     8  	"net/http"
     9  	"strconv"
    10  
    11  	issues_model "code.gitea.io/gitea/models/issues"
    12  	repo_model "code.gitea.io/gitea/models/repo"
    13  	gitea_context "code.gitea.io/gitea/modules/context"
    14  	"code.gitea.io/gitea/modules/git"
    15  	"code.gitea.io/gitea/modules/log"
    16  	"code.gitea.io/gitea/modules/private"
    17  	repo_module "code.gitea.io/gitea/modules/repository"
    18  	"code.gitea.io/gitea/modules/setting"
    19  	"code.gitea.io/gitea/modules/util"
    20  	"code.gitea.io/gitea/modules/web"
    21  	repo_service "code.gitea.io/gitea/services/repository"
    22  )
    23  
    24  // HookPostReceive updates services and users
    25  func HookPostReceive(ctx *gitea_context.PrivateContext) {
    26  	opts := web.GetForm(ctx).(*private.HookOptions)
    27  
    28  	// We don't rely on RepoAssignment here because:
    29  	// a) we don't need the git repo in this function
    30  	// b) our update function will likely change the repository in the db so we will need to refresh it
    31  	// c) we don't always need the repo
    32  
    33  	ownerName := ctx.Params(":owner")
    34  	repoName := ctx.Params(":repo")
    35  
    36  	// defer getting the repository at this point - as we should only retrieve it if we're going to call update
    37  	var repo *repo_model.Repository
    38  
    39  	updates := make([]*repo_module.PushUpdateOptions, 0, len(opts.OldCommitIDs))
    40  	wasEmpty := false
    41  
    42  	for i := range opts.OldCommitIDs {
    43  		refFullName := opts.RefFullNames[i]
    44  
    45  		// Only trigger activity updates for changes to branches or
    46  		// tags.  Updates to other refs (eg, refs/notes, refs/changes,
    47  		// or other less-standard refs spaces are ignored since there
    48  		// may be a very large number of them).
    49  		if refFullName.IsBranch() || refFullName.IsTag() {
    50  			if repo == nil {
    51  				repo = loadRepository(ctx, ownerName, repoName)
    52  				if ctx.Written() {
    53  					// Error handled in loadRepository
    54  					return
    55  				}
    56  				wasEmpty = repo.IsEmpty
    57  			}
    58  
    59  			option := &repo_module.PushUpdateOptions{
    60  				RefFullName:  refFullName,
    61  				OldCommitID:  opts.OldCommitIDs[i],
    62  				NewCommitID:  opts.NewCommitIDs[i],
    63  				PusherID:     opts.UserID,
    64  				PusherName:   opts.UserName,
    65  				RepoUserName: ownerName,
    66  				RepoName:     repoName,
    67  			}
    68  			updates = append(updates, option)
    69  			if repo.IsEmpty && (refFullName.BranchName() == "master" || refFullName.BranchName() == "main") {
    70  				// put the master/main branch first
    71  				copy(updates[1:], updates)
    72  				updates[0] = option
    73  			}
    74  		}
    75  	}
    76  
    77  	if repo != nil && len(updates) > 0 {
    78  		if err := repo_service.PushUpdates(updates); err != nil {
    79  			log.Error("Failed to Update: %s/%s Total Updates: %d", ownerName, repoName, len(updates))
    80  			for i, update := range updates {
    81  				log.Error("Failed to Update: %s/%s Update: %d/%d: Branch: %s", ownerName, repoName, i, len(updates), update.RefFullName.BranchName())
    82  			}
    83  			log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
    84  
    85  			ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
    86  				Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err),
    87  			})
    88  			return
    89  		}
    90  	}
    91  
    92  	// Handle Push Options
    93  	if len(opts.GitPushOptions) > 0 {
    94  		// load the repository
    95  		if repo == nil {
    96  			repo = loadRepository(ctx, ownerName, repoName)
    97  			if ctx.Written() {
    98  				// Error handled in loadRepository
    99  				return
   100  			}
   101  			wasEmpty = repo.IsEmpty
   102  		}
   103  
   104  		repo.IsPrivate = opts.GitPushOptions.Bool(private.GitPushOptionRepoPrivate, repo.IsPrivate)
   105  		repo.IsTemplate = opts.GitPushOptions.Bool(private.GitPushOptionRepoTemplate, repo.IsTemplate)
   106  		if err := repo_model.UpdateRepositoryCols(ctx, repo, "is_private", "is_template"); err != nil {
   107  			log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
   108  			ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
   109  				Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err),
   110  			})
   111  		}
   112  	}
   113  
   114  	results := make([]private.HookPostReceiveBranchResult, 0, len(opts.OldCommitIDs))
   115  
   116  	// We have to reload the repo in case its state is changed above
   117  	repo = nil
   118  	var baseRepo *repo_model.Repository
   119  
   120  	// Now handle the pull request notification trailers
   121  	for i := range opts.OldCommitIDs {
   122  		refFullName := opts.RefFullNames[i]
   123  		newCommitID := opts.NewCommitIDs[i]
   124  
   125  		// post update for agit pull request
   126  		// FIXME: use pr.Flow to test whether it's an Agit PR or a GH PR
   127  		if git.SupportProcReceive && refFullName.IsPull() {
   128  			if repo == nil {
   129  				repo = loadRepository(ctx, ownerName, repoName)
   130  				if ctx.Written() {
   131  					return
   132  				}
   133  			}
   134  
   135  			pullIndex, _ := strconv.ParseInt(refFullName.PullName(), 10, 64)
   136  			if pullIndex <= 0 {
   137  				continue
   138  			}
   139  
   140  			pr, err := issues_model.GetPullRequestByIndex(ctx, repo.ID, pullIndex)
   141  			if err != nil && !issues_model.IsErrPullRequestNotExist(err) {
   142  				log.Error("Failed to get PR by index %v Error: %v", pullIndex, err)
   143  				ctx.JSON(http.StatusInternalServerError, private.Response{
   144  					Err: fmt.Sprintf("Failed to get PR by index %v Error: %v", pullIndex, err),
   145  				})
   146  				return
   147  			}
   148  			if pr == nil {
   149  				continue
   150  			}
   151  
   152  			results = append(results, private.HookPostReceiveBranchResult{
   153  				Message: setting.Git.PullRequestPushMessage && repo.AllowsPulls(),
   154  				Create:  false,
   155  				Branch:  "",
   156  				URL:     fmt.Sprintf("%s/pulls/%d", repo.HTMLURL(), pr.Index),
   157  			})
   158  			continue
   159  		}
   160  
   161  		// If we've pushed a branch (and not deleted it)
   162  		if newCommitID != git.EmptySHA && refFullName.IsBranch() {
   163  
   164  			// First ensure we have the repository loaded, we're allowed pulls requests and we can get the base repo
   165  			if repo == nil {
   166  				repo = loadRepository(ctx, ownerName, repoName)
   167  				if ctx.Written() {
   168  					return
   169  				}
   170  
   171  				baseRepo = repo
   172  
   173  				if repo.IsFork {
   174  					if err := repo.GetBaseRepo(ctx); err != nil {
   175  						log.Error("Failed to get Base Repository of Forked repository: %-v Error: %v", repo, err)
   176  						ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
   177  							Err:          fmt.Sprintf("Failed to get Base Repository of Forked repository: %-v Error: %v", repo, err),
   178  							RepoWasEmpty: wasEmpty,
   179  						})
   180  						return
   181  					}
   182  					if repo.BaseRepo.AllowsPulls() {
   183  						baseRepo = repo.BaseRepo
   184  					}
   185  				}
   186  
   187  				if !baseRepo.AllowsPulls() {
   188  					// We can stop there's no need to go any further
   189  					ctx.JSON(http.StatusOK, private.HookPostReceiveResult{
   190  						RepoWasEmpty: wasEmpty,
   191  					})
   192  					return
   193  				}
   194  			}
   195  
   196  			branch := refFullName.BranchName()
   197  
   198  			// If our branch is the default branch of an unforked repo - there's no PR to create or refer to
   199  			if !repo.IsFork && branch == baseRepo.DefaultBranch {
   200  				results = append(results, private.HookPostReceiveBranchResult{})
   201  				continue
   202  			}
   203  
   204  			pr, err := issues_model.GetUnmergedPullRequest(ctx, repo.ID, baseRepo.ID, branch, baseRepo.DefaultBranch, issues_model.PullRequestFlowGithub)
   205  			if err != nil && !issues_model.IsErrPullRequestNotExist(err) {
   206  				log.Error("Failed to get active PR in: %-v Branch: %s to: %-v Branch: %s Error: %v", repo, branch, baseRepo, baseRepo.DefaultBranch, err)
   207  				ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
   208  					Err: fmt.Sprintf(
   209  						"Failed to get active PR in: %-v Branch: %s to: %-v Branch: %s Error: %v", repo, branch, baseRepo, baseRepo.DefaultBranch, err),
   210  					RepoWasEmpty: wasEmpty,
   211  				})
   212  				return
   213  			}
   214  
   215  			if pr == nil {
   216  				if repo.IsFork {
   217  					branch = fmt.Sprintf("%s:%s", repo.OwnerName, branch)
   218  				}
   219  				results = append(results, private.HookPostReceiveBranchResult{
   220  					Message: setting.Git.PullRequestPushMessage && baseRepo.AllowsPulls(),
   221  					Create:  true,
   222  					Branch:  branch,
   223  					URL:     fmt.Sprintf("%s/compare/%s...%s", baseRepo.HTMLURL(), util.PathEscapeSegments(baseRepo.DefaultBranch), util.PathEscapeSegments(branch)),
   224  				})
   225  			} else {
   226  				results = append(results, private.HookPostReceiveBranchResult{
   227  					Message: setting.Git.PullRequestPushMessage && baseRepo.AllowsPulls(),
   228  					Create:  false,
   229  					Branch:  branch,
   230  					URL:     fmt.Sprintf("%s/pulls/%d", baseRepo.HTMLURL(), pr.Index),
   231  				})
   232  			}
   233  		}
   234  	}
   235  	ctx.JSON(http.StatusOK, private.HookPostReceiveResult{
   236  		Results:      results,
   237  		RepoWasEmpty: wasEmpty,
   238  	})
   239  }