code.gitea.io/gitea@v1.22.3/services/repository/push.go (about) 1 // Copyright 2020 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package repository 5 6 import ( 7 "context" 8 "errors" 9 "fmt" 10 "strings" 11 "time" 12 13 "code.gitea.io/gitea/models/db" 14 repo_model "code.gitea.io/gitea/models/repo" 15 user_model "code.gitea.io/gitea/models/user" 16 "code.gitea.io/gitea/modules/cache" 17 "code.gitea.io/gitea/modules/git" 18 "code.gitea.io/gitea/modules/gitrepo" 19 "code.gitea.io/gitea/modules/graceful" 20 "code.gitea.io/gitea/modules/log" 21 "code.gitea.io/gitea/modules/process" 22 "code.gitea.io/gitea/modules/queue" 23 repo_module "code.gitea.io/gitea/modules/repository" 24 "code.gitea.io/gitea/modules/setting" 25 "code.gitea.io/gitea/modules/timeutil" 26 issue_service "code.gitea.io/gitea/services/issue" 27 notify_service "code.gitea.io/gitea/services/notify" 28 pull_service "code.gitea.io/gitea/services/pull" 29 ) 30 31 // pushQueue represents a queue to handle update pull request tests 32 var pushQueue *queue.WorkerPoolQueue[[]*repo_module.PushUpdateOptions] 33 34 // handle passed PR IDs and test the PRs 35 func handler(items ...[]*repo_module.PushUpdateOptions) [][]*repo_module.PushUpdateOptions { 36 for _, opts := range items { 37 if err := pushUpdates(opts); err != nil { 38 // Username and repository stays the same between items in opts. 39 pushUpdate := opts[0] 40 log.Error("pushUpdate[%s/%s] failed: %v", pushUpdate.RepoUserName, pushUpdate.RepoName, err) 41 } 42 } 43 return nil 44 } 45 46 func initPushQueue() error { 47 pushQueue = queue.CreateSimpleQueue(graceful.GetManager().ShutdownContext(), "push_update", handler) 48 if pushQueue == nil { 49 return errors.New("unable to create push_update queue") 50 } 51 go graceful.GetManager().RunWithCancel(pushQueue) 52 return nil 53 } 54 55 // PushUpdate is an alias of PushUpdates for single push update options 56 func PushUpdate(opts *repo_module.PushUpdateOptions) error { 57 return PushUpdates([]*repo_module.PushUpdateOptions{opts}) 58 } 59 60 // PushUpdates adds a push update to push queue 61 func PushUpdates(opts []*repo_module.PushUpdateOptions) error { 62 if len(opts) == 0 { 63 return nil 64 } 65 66 for _, opt := range opts { 67 if opt.IsNewRef() && opt.IsDelRef() { 68 return fmt.Errorf("Old and new revisions are both NULL") 69 } 70 } 71 72 return pushQueue.Push(opts) 73 } 74 75 // pushUpdates generates push action history feeds for push updating multiple refs 76 func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { 77 if len(optsList) == 0 { 78 return nil 79 } 80 81 ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("PushUpdates: %s/%s", optsList[0].RepoUserName, optsList[0].RepoName)) 82 defer finished() 83 84 repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, optsList[0].RepoUserName, optsList[0].RepoName) 85 if err != nil { 86 return fmt.Errorf("GetRepositoryByOwnerAndName failed: %w", err) 87 } 88 89 gitRepo, err := gitrepo.OpenRepository(ctx, repo) 90 if err != nil { 91 return fmt.Errorf("OpenRepository[%s]: %w", repo.FullName(), err) 92 } 93 defer gitRepo.Close() 94 95 if err = repo_module.UpdateRepoSize(ctx, repo); err != nil { 96 return fmt.Errorf("Failed to update size for repository: %v", err) 97 } 98 99 addTags := make([]string, 0, len(optsList)) 100 delTags := make([]string, 0, len(optsList)) 101 var pusher *user_model.User 102 objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName) 103 104 for _, opts := range optsList { 105 log.Trace("pushUpdates: %-v %s %s %s", repo, opts.OldCommitID, opts.NewCommitID, opts.RefFullName) 106 107 if opts.IsNewRef() && opts.IsDelRef() { 108 return fmt.Errorf("old and new revisions are both %s", objectFormat.EmptyObjectID()) 109 } 110 if opts.RefFullName.IsTag() { 111 if pusher == nil || pusher.ID != opts.PusherID { 112 if opts.PusherID == user_model.ActionsUserID { 113 pusher = user_model.NewActionsUser() 114 } else { 115 var err error 116 if pusher, err = user_model.GetUserByID(ctx, opts.PusherID); err != nil { 117 return err 118 } 119 } 120 } 121 tagName := opts.RefFullName.TagName() 122 if opts.IsDelRef() { 123 notify_service.PushCommits( 124 ctx, pusher, repo, 125 &repo_module.PushUpdateOptions{ 126 RefFullName: git.RefNameFromTag(tagName), 127 OldCommitID: opts.OldCommitID, 128 NewCommitID: objectFormat.EmptyObjectID().String(), 129 }, repo_module.NewPushCommits()) 130 131 delTags = append(delTags, tagName) 132 notify_service.DeleteRef(ctx, pusher, repo, opts.RefFullName) 133 } else { // is new tag 134 newCommit, err := gitRepo.GetCommit(opts.NewCommitID) 135 if err != nil { 136 return fmt.Errorf("gitRepo.GetCommit(%s) in %s/%s[%d]: %w", opts.NewCommitID, repo.OwnerName, repo.Name, repo.ID, err) 137 } 138 139 commits := repo_module.NewPushCommits() 140 commits.HeadCommit = repo_module.CommitToPushCommit(newCommit) 141 commits.CompareURL = repo.ComposeCompareURL(objectFormat.EmptyObjectID().String(), opts.NewCommitID) 142 143 notify_service.PushCommits( 144 ctx, pusher, repo, 145 &repo_module.PushUpdateOptions{ 146 RefFullName: opts.RefFullName, 147 OldCommitID: objectFormat.EmptyObjectID().String(), 148 NewCommitID: opts.NewCommitID, 149 }, commits) 150 151 addTags = append(addTags, tagName) 152 notify_service.CreateRef(ctx, pusher, repo, opts.RefFullName, opts.NewCommitID) 153 } 154 } else if opts.RefFullName.IsBranch() { 155 if pusher == nil || pusher.ID != opts.PusherID { 156 if opts.PusherID == user_model.ActionsUserID { 157 pusher = user_model.NewActionsUser() 158 } else { 159 var err error 160 if pusher, err = user_model.GetUserByID(ctx, opts.PusherID); err != nil { 161 return err 162 } 163 } 164 } 165 166 branch := opts.RefFullName.BranchName() 167 if !opts.IsDelRef() { 168 log.Trace("TriggerTask '%s/%s' by %s", repo.Name, branch, pusher.Name) 169 go pull_service.AddTestPullRequestTask(pusher, repo.ID, branch, true, opts.OldCommitID, opts.NewCommitID) 170 171 newCommit, err := gitRepo.GetCommit(opts.NewCommitID) 172 if err != nil { 173 return fmt.Errorf("gitRepo.GetCommit(%s) in %s/%s[%d]: %w", opts.NewCommitID, repo.OwnerName, repo.Name, repo.ID, err) 174 } 175 176 refName := opts.RefName() 177 178 // Push new branch. 179 var l []*git.Commit 180 if opts.IsNewRef() { 181 if repo.IsEmpty { // Change default branch and empty status only if pushed ref is non-empty branch. 182 repo.DefaultBranch = refName 183 repo.IsEmpty = false 184 if repo.DefaultBranch != setting.Repository.DefaultBranch { 185 if err := gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil { 186 if !git.IsErrUnsupportedVersion(err) { 187 return err 188 } 189 } 190 } 191 // Update the is empty and default_branch columns 192 if err := repo_model.UpdateRepositoryCols(ctx, repo, "default_branch", "is_empty"); err != nil { 193 return fmt.Errorf("UpdateRepositoryCols: %w", err) 194 } 195 } 196 197 l, err = newCommit.CommitsBeforeLimit(10) 198 if err != nil { 199 return fmt.Errorf("newCommit.CommitsBeforeLimit: %w", err) 200 } 201 notify_service.CreateRef(ctx, pusher, repo, opts.RefFullName, opts.NewCommitID) 202 } else { 203 l, err = newCommit.CommitsBeforeUntil(opts.OldCommitID) 204 if err != nil { 205 return fmt.Errorf("newCommit.CommitsBeforeUntil: %w", err) 206 } 207 208 isForcePush, err := newCommit.IsForcePush(opts.OldCommitID) 209 if err != nil { 210 log.Error("IsForcePush %s:%s failed: %v", repo.FullName(), branch, err) 211 } 212 213 if isForcePush { 214 log.Trace("Push %s is a force push", opts.NewCommitID) 215 216 cache.Remove(repo.GetCommitsCountCacheKey(opts.RefName(), true)) 217 } else { 218 // TODO: increment update the commit count cache but not remove 219 cache.Remove(repo.GetCommitsCountCacheKey(opts.RefName(), true)) 220 } 221 } 222 223 // delete cache for divergence 224 if branch == repo.DefaultBranch { 225 if err := DelRepoDivergenceFromCache(ctx, repo.ID); err != nil { 226 log.Error("DelRepoDivergenceFromCache: %v", err) 227 } 228 } else { 229 if err := DelDivergenceFromCache(repo.ID, branch); err != nil { 230 log.Error("DelDivergenceFromCache: %v", err) 231 } 232 } 233 234 commits := repo_module.GitToPushCommits(l) 235 commits.HeadCommit = repo_module.CommitToPushCommit(newCommit) 236 237 if err := issue_service.UpdateIssuesCommit(ctx, pusher, repo, commits.Commits, refName); err != nil { 238 log.Error("updateIssuesCommit: %v", err) 239 } 240 241 oldCommitID := opts.OldCommitID 242 if oldCommitID == objectFormat.EmptyObjectID().String() && len(commits.Commits) > 0 { 243 oldCommit, err := gitRepo.GetCommit(commits.Commits[len(commits.Commits)-1].Sha1) 244 if err != nil && !git.IsErrNotExist(err) { 245 log.Error("unable to GetCommit %s from %-v: %v", oldCommitID, repo, err) 246 } 247 if oldCommit != nil { 248 for i := 0; i < oldCommit.ParentCount(); i++ { 249 commitID, _ := oldCommit.ParentID(i) 250 if !commitID.IsZero() { 251 oldCommitID = commitID.String() 252 break 253 } 254 } 255 } 256 } 257 258 if oldCommitID == objectFormat.EmptyObjectID().String() && repo.DefaultBranch != branch { 259 oldCommitID = repo.DefaultBranch 260 } 261 262 if oldCommitID != objectFormat.EmptyObjectID().String() { 263 commits.CompareURL = repo.ComposeCompareURL(oldCommitID, opts.NewCommitID) 264 } else { 265 commits.CompareURL = "" 266 } 267 268 if len(commits.Commits) > setting.UI.FeedMaxCommitNum { 269 commits.Commits = commits.Commits[:setting.UI.FeedMaxCommitNum] 270 } 271 272 notify_service.PushCommits(ctx, pusher, repo, opts, commits) 273 274 // Cache for big repository 275 if err := CacheRef(graceful.GetManager().HammerContext(), repo, gitRepo, opts.RefFullName); err != nil { 276 log.Error("repo_module.CacheRef %s/%s failed: %v", repo.ID, branch, err) 277 } 278 } else { 279 notify_service.DeleteRef(ctx, pusher, repo, opts.RefFullName) 280 if err = pull_service.CloseBranchPulls(ctx, pusher, repo.ID, branch); err != nil { 281 // close all related pulls 282 log.Error("close related pull request failed: %v", err) 283 } 284 } 285 286 // Even if user delete a branch on a repository which he didn't watch, he will be watch that. 287 if err = repo_model.WatchIfAuto(ctx, opts.PusherID, repo.ID, true); err != nil { 288 log.Warn("Fail to perform auto watch on user %v for repo %v: %v", opts.PusherID, repo.ID, err) 289 } 290 } else { 291 log.Trace("Non-tag and non-branch commits pushed.") 292 } 293 } 294 if err := PushUpdateAddDeleteTags(ctx, repo, gitRepo, addTags, delTags); err != nil { 295 return fmt.Errorf("PushUpdateAddDeleteTags: %w", err) 296 } 297 298 // Change repository last updated time. 299 if err := repo_model.UpdateRepositoryUpdatedTime(ctx, repo.ID, time.Now()); err != nil { 300 return fmt.Errorf("UpdateRepositoryUpdatedTime: %w", err) 301 } 302 303 return nil 304 } 305 306 // PushUpdateAddDeleteTags updates a number of added and delete tags 307 func PushUpdateAddDeleteTags(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, addTags, delTags []string) error { 308 return db.WithTx(ctx, func(ctx context.Context) error { 309 if err := repo_model.PushUpdateDeleteTagsContext(ctx, repo, delTags); err != nil { 310 return err 311 } 312 return pushUpdateAddTags(ctx, repo, gitRepo, addTags) 313 }) 314 } 315 316 // pushUpdateAddTags updates a number of add tags 317 func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, tags []string) error { 318 if len(tags) == 0 { 319 return nil 320 } 321 322 releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{ 323 RepoID: repo.ID, 324 TagNames: tags, 325 IncludeTags: true, 326 }) 327 if err != nil { 328 return fmt.Errorf("db.Find[repo_model.Release]: %w", err) 329 } 330 relMap := make(map[string]*repo_model.Release) 331 for _, rel := range releases { 332 relMap[rel.LowerTagName] = rel 333 } 334 335 lowerTags := make([]string, 0, len(tags)) 336 for _, tag := range tags { 337 lowerTags = append(lowerTags, strings.ToLower(tag)) 338 } 339 340 newReleases := make([]*repo_model.Release, 0, len(lowerTags)-len(relMap)) 341 342 emailToUser := make(map[string]*user_model.User) 343 344 for i, lowerTag := range lowerTags { 345 tag, err := gitRepo.GetTag(tags[i]) 346 if err != nil { 347 return fmt.Errorf("GetTag: %w", err) 348 } 349 commit, err := tag.Commit(gitRepo) 350 if err != nil { 351 return fmt.Errorf("Commit: %w", err) 352 } 353 354 sig := tag.Tagger 355 if sig == nil { 356 sig = commit.Author 357 } 358 if sig == nil { 359 sig = commit.Committer 360 } 361 var author *user_model.User 362 createdAt := time.Unix(1, 0) 363 364 if sig != nil { 365 var ok bool 366 author, ok = emailToUser[sig.Email] 367 if !ok { 368 author, err = user_model.GetUserByEmail(ctx, sig.Email) 369 if err != nil && !user_model.IsErrUserNotExist(err) { 370 return fmt.Errorf("GetUserByEmail: %w", err) 371 } 372 if author != nil { 373 emailToUser[sig.Email] = author 374 } 375 } 376 createdAt = sig.When 377 } 378 379 commitsCount, err := commit.CommitsCount() 380 if err != nil { 381 return fmt.Errorf("CommitsCount: %w", err) 382 } 383 384 rel, has := relMap[lowerTag] 385 386 parts := strings.SplitN(tag.Message, "\n", 2) 387 note := "" 388 if len(parts) > 1 { 389 note = parts[1] 390 } 391 if !has { 392 rel = &repo_model.Release{ 393 RepoID: repo.ID, 394 Title: parts[0], 395 TagName: tags[i], 396 LowerTagName: lowerTag, 397 Target: "", 398 Sha1: commit.ID.String(), 399 NumCommits: commitsCount, 400 Note: note, 401 IsDraft: false, 402 IsPrerelease: false, 403 IsTag: true, 404 CreatedUnix: timeutil.TimeStamp(createdAt.Unix()), 405 } 406 if author != nil { 407 rel.PublisherID = author.ID 408 } 409 410 newReleases = append(newReleases, rel) 411 } else { 412 rel.Title = parts[0] 413 rel.Note = note 414 rel.Sha1 = commit.ID.String() 415 rel.CreatedUnix = timeutil.TimeStamp(createdAt.Unix()) 416 rel.NumCommits = commitsCount 417 if rel.IsTag && author != nil { 418 rel.PublisherID = author.ID 419 } 420 if err = repo_model.UpdateRelease(ctx, rel); err != nil { 421 return fmt.Errorf("Update: %w", err) 422 } 423 } 424 } 425 426 if len(newReleases) > 0 { 427 if err = db.Insert(ctx, newReleases); err != nil { 428 return fmt.Errorf("Insert: %w", err) 429 } 430 } 431 432 return nil 433 }