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 }