code.gitea.io/gitea@v1.22.3/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  	"context"
     8  	"fmt"
     9  	"net/http"
    10  
    11  	"code.gitea.io/gitea/models/db"
    12  	git_model "code.gitea.io/gitea/models/git"
    13  	issues_model "code.gitea.io/gitea/models/issues"
    14  	access_model "code.gitea.io/gitea/models/perm/access"
    15  	pull_model "code.gitea.io/gitea/models/pull"
    16  	repo_model "code.gitea.io/gitea/models/repo"
    17  	user_model "code.gitea.io/gitea/models/user"
    18  	"code.gitea.io/gitea/modules/cache"
    19  	"code.gitea.io/gitea/modules/git"
    20  	"code.gitea.io/gitea/modules/gitrepo"
    21  	"code.gitea.io/gitea/modules/log"
    22  	"code.gitea.io/gitea/modules/private"
    23  	repo_module "code.gitea.io/gitea/modules/repository"
    24  	"code.gitea.io/gitea/modules/setting"
    25  	timeutil "code.gitea.io/gitea/modules/timeutil"
    26  	"code.gitea.io/gitea/modules/util"
    27  	"code.gitea.io/gitea/modules/web"
    28  	gitea_context "code.gitea.io/gitea/services/context"
    29  	pull_service "code.gitea.io/gitea/services/pull"
    30  	repo_service "code.gitea.io/gitea/services/repository"
    31  )
    32  
    33  // HookPostReceive updates services and users
    34  func HookPostReceive(ctx *gitea_context.PrivateContext) {
    35  	opts := web.GetForm(ctx).(*private.HookOptions)
    36  
    37  	// We don't rely on RepoAssignment here because:
    38  	// a) we don't need the git repo in this function
    39  	//    OUT OF DATE: we do need the git repo to sync the branch to the db now.
    40  	// b) our update function will likely change the repository in the db so we will need to refresh it
    41  	// c) we don't always need the repo
    42  
    43  	ownerName := ctx.Params(":owner")
    44  	repoName := ctx.Params(":repo")
    45  
    46  	// defer getting the repository at this point - as we should only retrieve it if we're going to call update
    47  	var (
    48  		repo    *repo_model.Repository
    49  		gitRepo *git.Repository
    50  	)
    51  	defer gitRepo.Close() // it's safe to call Close on a nil pointer
    52  
    53  	updates := make([]*repo_module.PushUpdateOptions, 0, len(opts.OldCommitIDs))
    54  	wasEmpty := false
    55  
    56  	for i := range opts.OldCommitIDs {
    57  		refFullName := opts.RefFullNames[i]
    58  
    59  		// Only trigger activity updates for changes to branches or
    60  		// tags.  Updates to other refs (eg, refs/notes, refs/changes,
    61  		// or other less-standard refs spaces are ignored since there
    62  		// may be a very large number of them).
    63  		if refFullName.IsBranch() || refFullName.IsTag() {
    64  			if repo == nil {
    65  				repo = loadRepository(ctx, ownerName, repoName)
    66  				if ctx.Written() {
    67  					// Error handled in loadRepository
    68  					return
    69  				}
    70  				wasEmpty = repo.IsEmpty
    71  			}
    72  
    73  			option := &repo_module.PushUpdateOptions{
    74  				RefFullName:  refFullName,
    75  				OldCommitID:  opts.OldCommitIDs[i],
    76  				NewCommitID:  opts.NewCommitIDs[i],
    77  				PusherID:     opts.UserID,
    78  				PusherName:   opts.UserName,
    79  				RepoUserName: ownerName,
    80  				RepoName:     repoName,
    81  			}
    82  			updates = append(updates, option)
    83  			if repo.IsEmpty && (refFullName.BranchName() == "master" || refFullName.BranchName() == "main") {
    84  				// put the master/main branch first
    85  				// FIXME: It doesn't always work, since the master/main branch may not be the first batch of updates.
    86  				//        If the user pushes many branches at once, the Git hook will call the internal API in batches, rather than all at once.
    87  				//        See https://github.com/go-gitea/gitea/blob/cb52b17f92e2d2293f7c003649743464492bca48/cmd/hook.go#L27
    88  				//        If the user executes `git push origin --all` and pushes more than 30 branches, the master/main may not be the default branch.
    89  				copy(updates[1:], updates)
    90  				updates[0] = option
    91  			}
    92  		}
    93  	}
    94  
    95  	if repo != nil && len(updates) > 0 {
    96  		branchesToSync := make([]*repo_module.PushUpdateOptions, 0, len(updates))
    97  		for _, update := range updates {
    98  			if !update.RefFullName.IsBranch() {
    99  				continue
   100  			}
   101  			if repo == nil {
   102  				repo = loadRepository(ctx, ownerName, repoName)
   103  				if ctx.Written() {
   104  					return
   105  				}
   106  				wasEmpty = repo.IsEmpty
   107  			}
   108  
   109  			if update.IsDelRef() {
   110  				if err := git_model.AddDeletedBranch(ctx, repo.ID, update.RefFullName.BranchName(), update.PusherID); err != nil {
   111  					log.Error("Failed to add deleted branch: %s/%s Error: %v", ownerName, repoName, err)
   112  					ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
   113  						Err: fmt.Sprintf("Failed to add deleted branch: %s/%s Error: %v", ownerName, repoName, err),
   114  					})
   115  					return
   116  				}
   117  			} else {
   118  				branchesToSync = append(branchesToSync, update)
   119  
   120  				// TODO: should we return the error and return the error when pushing? Currently it will log the error and not prevent the pushing
   121  				pull_service.UpdatePullsRefs(ctx, repo, update)
   122  			}
   123  		}
   124  		if len(branchesToSync) > 0 {
   125  			if gitRepo == nil {
   126  				var err error
   127  				gitRepo, err = gitrepo.OpenRepository(ctx, repo)
   128  				if err != nil {
   129  					log.Error("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err)
   130  					ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
   131  						Err: fmt.Sprintf("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err),
   132  					})
   133  					return
   134  				}
   135  			}
   136  
   137  			var (
   138  				branchNames = make([]string, 0, len(branchesToSync))
   139  				commitIDs   = make([]string, 0, len(branchesToSync))
   140  			)
   141  			for _, update := range branchesToSync {
   142  				branchNames = append(branchNames, update.RefFullName.BranchName())
   143  				commitIDs = append(commitIDs, update.NewCommitID)
   144  			}
   145  
   146  			if err := repo_service.SyncBranchesToDB(ctx, repo.ID, opts.UserID, branchNames, commitIDs, gitRepo.GetCommit); err != nil {
   147  				ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
   148  					Err: fmt.Sprintf("Failed to sync branch to DB in repository: %s/%s Error: %v", ownerName, repoName, err),
   149  				})
   150  				return
   151  			}
   152  		}
   153  
   154  		if err := repo_service.PushUpdates(updates); err != nil {
   155  			log.Error("Failed to Update: %s/%s Total Updates: %d", ownerName, repoName, len(updates))
   156  			for i, update := range updates {
   157  				log.Error("Failed to Update: %s/%s Update: %d/%d: Branch: %s", ownerName, repoName, i, len(updates), update.RefFullName.BranchName())
   158  			}
   159  			log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
   160  
   161  			ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
   162  				Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err),
   163  			})
   164  			return
   165  		}
   166  	}
   167  
   168  	// handle pull request merging, a pull request action should push at least 1 commit
   169  	if opts.PushTrigger == repo_module.PushTriggerPRMergeToBase {
   170  		handlePullRequestMerging(ctx, opts, ownerName, repoName, updates)
   171  		if ctx.Written() {
   172  			return
   173  		}
   174  	}
   175  
   176  	isPrivate := opts.GitPushOptions.Bool(private.GitPushOptionRepoPrivate)
   177  	isTemplate := opts.GitPushOptions.Bool(private.GitPushOptionRepoTemplate)
   178  	// Handle Push Options
   179  	if isPrivate.Has() || isTemplate.Has() {
   180  		// load the repository
   181  		if repo == nil {
   182  			repo = loadRepository(ctx, ownerName, repoName)
   183  			if ctx.Written() {
   184  				// Error handled in loadRepository
   185  				return
   186  			}
   187  			wasEmpty = repo.IsEmpty
   188  		}
   189  
   190  		pusher, err := loadContextCacheUser(ctx, opts.UserID)
   191  		if err != nil {
   192  			log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
   193  			ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
   194  				Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err),
   195  			})
   196  			return
   197  		}
   198  		perm, err := access_model.GetUserRepoPermission(ctx, repo, pusher)
   199  		if err != nil {
   200  			log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
   201  			ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
   202  				Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err),
   203  			})
   204  			return
   205  		}
   206  		if !perm.IsOwner() && !perm.IsAdmin() {
   207  			ctx.JSON(http.StatusNotFound, private.HookPostReceiveResult{
   208  				Err: "Permissions denied",
   209  			})
   210  			return
   211  		}
   212  
   213  		cols := make([]string, 0, len(opts.GitPushOptions))
   214  
   215  		if isPrivate.Has() {
   216  			repo.IsPrivate = isPrivate.Value()
   217  			cols = append(cols, "is_private")
   218  		}
   219  
   220  		if isTemplate.Has() {
   221  			repo.IsTemplate = isTemplate.Value()
   222  			cols = append(cols, "is_template")
   223  		}
   224  
   225  		if len(cols) > 0 {
   226  			if err := repo_model.UpdateRepositoryCols(ctx, repo, cols...); err != nil {
   227  				log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
   228  				ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
   229  					Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err),
   230  				})
   231  				return
   232  			}
   233  		}
   234  	}
   235  
   236  	results := make([]private.HookPostReceiveBranchResult, 0, len(opts.OldCommitIDs))
   237  
   238  	// We have to reload the repo in case its state is changed above
   239  	repo = nil
   240  	var baseRepo *repo_model.Repository
   241  
   242  	// Now handle the pull request notification trailers
   243  	for i := range opts.OldCommitIDs {
   244  		refFullName := opts.RefFullNames[i]
   245  		newCommitID := opts.NewCommitIDs[i]
   246  
   247  		// If we've pushed a branch (and not deleted it)
   248  		if !git.IsEmptyCommitID(newCommitID) && refFullName.IsBranch() {
   249  			// First ensure we have the repository loaded, we're allowed pulls requests and we can get the base repo
   250  			if repo == nil {
   251  				repo = loadRepository(ctx, ownerName, repoName)
   252  				if ctx.Written() {
   253  					return
   254  				}
   255  
   256  				baseRepo = repo
   257  
   258  				if repo.IsFork {
   259  					if err := repo.GetBaseRepo(ctx); err != nil {
   260  						log.Error("Failed to get Base Repository of Forked repository: %-v Error: %v", repo, err)
   261  						ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
   262  							Err:          fmt.Sprintf("Failed to get Base Repository of Forked repository: %-v Error: %v", repo, err),
   263  							RepoWasEmpty: wasEmpty,
   264  						})
   265  						return
   266  					}
   267  					if repo.BaseRepo.AllowsPulls(ctx) {
   268  						baseRepo = repo.BaseRepo
   269  					}
   270  				}
   271  
   272  				if !baseRepo.AllowsPulls(ctx) {
   273  					// We can stop there's no need to go any further
   274  					ctx.JSON(http.StatusOK, private.HookPostReceiveResult{
   275  						RepoWasEmpty: wasEmpty,
   276  					})
   277  					return
   278  				}
   279  			}
   280  
   281  			branch := refFullName.BranchName()
   282  
   283  			// If our branch is the default branch of an unforked repo - there's no PR to create or refer to
   284  			if !repo.IsFork && branch == baseRepo.DefaultBranch {
   285  				results = append(results, private.HookPostReceiveBranchResult{})
   286  				continue
   287  			}
   288  
   289  			pr, err := issues_model.GetUnmergedPullRequest(ctx, repo.ID, baseRepo.ID, branch, baseRepo.DefaultBranch, issues_model.PullRequestFlowGithub)
   290  			if err != nil && !issues_model.IsErrPullRequestNotExist(err) {
   291  				log.Error("Failed to get active PR in: %-v Branch: %s to: %-v Branch: %s Error: %v", repo, branch, baseRepo, baseRepo.DefaultBranch, err)
   292  				ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
   293  					Err: fmt.Sprintf(
   294  						"Failed to get active PR in: %-v Branch: %s to: %-v Branch: %s Error: %v", repo, branch, baseRepo, baseRepo.DefaultBranch, err),
   295  					RepoWasEmpty: wasEmpty,
   296  				})
   297  				return
   298  			}
   299  
   300  			if pr == nil {
   301  				if repo.IsFork {
   302  					branch = fmt.Sprintf("%s:%s", repo.OwnerName, branch)
   303  				}
   304  				results = append(results, private.HookPostReceiveBranchResult{
   305  					Message: setting.Git.PullRequestPushMessage && baseRepo.AllowsPulls(ctx),
   306  					Create:  true,
   307  					Branch:  branch,
   308  					URL:     fmt.Sprintf("%s/compare/%s...%s", baseRepo.HTMLURL(), util.PathEscapeSegments(baseRepo.DefaultBranch), util.PathEscapeSegments(branch)),
   309  				})
   310  			} else {
   311  				results = append(results, private.HookPostReceiveBranchResult{
   312  					Message: setting.Git.PullRequestPushMessage && baseRepo.AllowsPulls(ctx),
   313  					Create:  false,
   314  					Branch:  branch,
   315  					URL:     fmt.Sprintf("%s/pulls/%d", baseRepo.HTMLURL(), pr.Index),
   316  				})
   317  			}
   318  		}
   319  	}
   320  	ctx.JSON(http.StatusOK, private.HookPostReceiveResult{
   321  		Results:      results,
   322  		RepoWasEmpty: wasEmpty,
   323  	})
   324  }
   325  
   326  func loadContextCacheUser(ctx context.Context, id int64) (*user_model.User, error) {
   327  	return cache.GetWithContextCache(ctx, "hook_post_receive_user", id, func() (*user_model.User, error) {
   328  		return user_model.GetUserByID(ctx, id)
   329  	})
   330  }
   331  
   332  // handlePullRequestMerging handle pull request merging, a pull request action should push at least 1 commit
   333  func handlePullRequestMerging(ctx *gitea_context.PrivateContext, opts *private.HookOptions, ownerName, repoName string, updates []*repo_module.PushUpdateOptions) {
   334  	if len(updates) == 0 {
   335  		ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
   336  			Err: fmt.Sprintf("Pushing a merged PR (pr:%d) no commits pushed ", opts.PullRequestID),
   337  		})
   338  		return
   339  	}
   340  
   341  	pr, err := issues_model.GetPullRequestByID(ctx, opts.PullRequestID)
   342  	if err != nil {
   343  		log.Error("GetPullRequestByID[%d]: %v", opts.PullRequestID, err)
   344  		ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "GetPullRequestByID failed"})
   345  		return
   346  	}
   347  
   348  	pusher, err := loadContextCacheUser(ctx, opts.UserID)
   349  	if err != nil {
   350  		log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
   351  		ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "Load pusher user failed"})
   352  		return
   353  	}
   354  
   355  	pr.MergedCommitID = updates[len(updates)-1].NewCommitID
   356  	pr.MergedUnix = timeutil.TimeStampNow()
   357  	pr.Merger = pusher
   358  	pr.MergerID = pusher.ID
   359  	err = db.WithTx(ctx, func(ctx context.Context) error {
   360  		// Removing an auto merge pull and ignore if not exist
   361  		if err := pull_model.DeleteScheduledAutoMerge(ctx, pr.ID); err != nil && !db.IsErrNotExist(err) {
   362  			return fmt.Errorf("DeleteScheduledAutoMerge[%d]: %v", opts.PullRequestID, err)
   363  		}
   364  		if _, err := pr.SetMerged(ctx); err != nil {
   365  			return fmt.Errorf("SetMerged failed: %s/%s Error: %v", ownerName, repoName, err)
   366  		}
   367  		return nil
   368  	})
   369  	if err != nil {
   370  		log.Error("Failed to update PR to merged: %v", err)
   371  		ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "Failed to update PR to merged"})
   372  	}
   373  }