// Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package private import ( "fmt" "net/http" "strconv" issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" gitea_context "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/git" "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" 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 // 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 var repo *repo_model.Repository 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). if refFullName.IsBranch() || refFullName.IsTag() { if repo == nil { repo = loadRepository(ctx, ownerName, repoName) if ctx.Written() { // Error handled in loadRepository return } wasEmpty = repo.IsEmpty } option := &repo_module.PushUpdateOptions{ RefFullName: refFullName, OldCommitID: opts.OldCommitIDs[i], NewCommitID: opts.NewCommitIDs[i], PusherID: opts.UserID, PusherName: opts.UserName, RepoUserName: ownerName, RepoName: repoName, } updates = append(updates, option) if repo.IsEmpty && (refFullName.BranchName() == "master" || refFullName.BranchName() == "main") { // put the master/main branch first copy(updates[1:], updates) updates[0] = option } } } if repo != nil && len(updates) > 0 { 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 } } // 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) if err := repo_model.UpdateRepositoryCols(ctx, repo, "is_private", "is_template"); 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), }) } } 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 *repo_model.Repository // Now handle the pull request notification trailers for i := range opts.OldCommitIDs { refFullName := opts.RefFullNames[i] newCommitID := opts.NewCommitIDs[i] // post update for agit pull request // FIXME: use pr.Flow to test whether it's an Agit PR or a GH PR if git.SupportProcReceive && refFullName.IsPull() { if repo == nil { repo = loadRepository(ctx, ownerName, repoName) if ctx.Written() { return } } pullIndex, _ := strconv.ParseInt(refFullName.PullName(), 10, 64) if pullIndex <= 0 { continue } pr, err := issues_model.GetPullRequestByIndex(ctx, repo.ID, pullIndex) if err != nil && !issues_model.IsErrPullRequestNotExist(err) { 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{ Message: setting.Git.PullRequestPushMessage && repo.AllowsPulls(), Create: false, Branch: "", URL: fmt.Sprintf("%s/pulls/%d", repo.HTMLURL(), pr.Index), }) continue } // If we've pushed a branch (and not deleted it) if newCommitID != git.EmptySHA && refFullName.IsBranch() { // 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 { if err := repo.GetBaseRepo(ctx); 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 } if repo.BaseRepo.AllowsPulls() { baseRepo = repo.BaseRepo } } if !baseRepo.AllowsPulls() { // We can stop there's no need to go any further ctx.JSON(http.StatusOK, private.HookPostReceiveResult{ RepoWasEmpty: wasEmpty, }) return } } branch := refFullName.BranchName() // 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 } pr, err := issues_model.GetUnmergedPullRequest(ctx, repo.ID, baseRepo.ID, branch, baseRepo.DefaultBranch, issues_model.PullRequestFlowGithub) if err != nil && !issues_model.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, }) return } if pr == nil { if repo.IsFork { branch = fmt.Sprintf("%s:%s", repo.OwnerName, branch) } results = append(results, private.HookPostReceiveBranchResult{ Message: setting.Git.PullRequestPushMessage && baseRepo.AllowsPulls(), 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: setting.Git.PullRequestPushMessage && baseRepo.AllowsPulls(), Create: false, Branch: branch, URL: fmt.Sprintf("%s/pulls/%d", baseRepo.HTMLURL(), pr.Index), }) } } } ctx.JSON(http.StatusOK, private.HookPostReceiveResult{ Results: results, RepoWasEmpty: wasEmpty, }) }