You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

commitstatus.go 6.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. // Copyright 2024 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package commitstatus
  4. import (
  5. "context"
  6. "crypto/sha256"
  7. "fmt"
  8. "slices"
  9. "code.gitea.io/gitea/models/db"
  10. git_model "code.gitea.io/gitea/models/git"
  11. repo_model "code.gitea.io/gitea/models/repo"
  12. user_model "code.gitea.io/gitea/models/user"
  13. "code.gitea.io/gitea/modules/cache"
  14. "code.gitea.io/gitea/modules/git"
  15. "code.gitea.io/gitea/modules/gitrepo"
  16. "code.gitea.io/gitea/modules/json"
  17. "code.gitea.io/gitea/modules/log"
  18. api "code.gitea.io/gitea/modules/structs"
  19. "code.gitea.io/gitea/services/automerge"
  20. )
  21. func getCacheKey(repoID int64, brancheName string) string {
  22. hashBytes := sha256.Sum256([]byte(fmt.Sprintf("%d:%s", repoID, brancheName)))
  23. return fmt.Sprintf("commit_status:%x", hashBytes)
  24. }
  25. type commitStatusCacheValue struct {
  26. State string `json:"state"`
  27. TargetURL string `json:"target_url"`
  28. }
  29. func getCommitStatusCache(repoID int64, branchName string) *commitStatusCacheValue {
  30. c := cache.GetCache()
  31. statusStr, ok := c.Get(getCacheKey(repoID, branchName))
  32. if ok && statusStr != "" {
  33. var cv commitStatusCacheValue
  34. err := json.Unmarshal([]byte(statusStr), &cv)
  35. if err == nil && cv.State != "" {
  36. return &cv
  37. }
  38. if err != nil {
  39. log.Warn("getCommitStatusCache: json.Unmarshal failed: %v", err)
  40. }
  41. }
  42. return nil
  43. }
  44. func updateCommitStatusCache(repoID int64, branchName string, state api.CommitStatusState, targetURL string) error {
  45. c := cache.GetCache()
  46. bs, err := json.Marshal(commitStatusCacheValue{
  47. State: state.String(),
  48. TargetURL: targetURL,
  49. })
  50. if err != nil {
  51. log.Warn("updateCommitStatusCache: json.Marshal failed: %v", err)
  52. return nil
  53. }
  54. return c.Put(getCacheKey(repoID, branchName), string(bs), 3*24*60)
  55. }
  56. func deleteCommitStatusCache(repoID int64, branchName string) error {
  57. c := cache.GetCache()
  58. return c.Delete(getCacheKey(repoID, branchName))
  59. }
  60. // CreateCommitStatus creates a new CommitStatus given a bunch of parameters
  61. // NOTE: All text-values will be trimmed from whitespaces.
  62. // Requires: Repo, Creator, SHA
  63. func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creator *user_model.User, sha string, status *git_model.CommitStatus) error {
  64. repoPath := repo.RepoPath()
  65. // confirm that commit is exist
  66. gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo)
  67. if err != nil {
  68. return fmt.Errorf("OpenRepository[%s]: %w", repoPath, err)
  69. }
  70. defer closer.Close()
  71. objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
  72. commit, err := gitRepo.GetCommit(sha)
  73. if err != nil {
  74. return fmt.Errorf("GetCommit[%s]: %w", sha, err)
  75. }
  76. if len(sha) != objectFormat.FullLength() {
  77. // use complete commit sha
  78. sha = commit.ID.String()
  79. }
  80. if err := db.WithTx(ctx, func(ctx context.Context) error {
  81. if err := git_model.NewCommitStatus(ctx, git_model.NewCommitStatusOptions{
  82. Repo: repo,
  83. Creator: creator,
  84. SHA: commit.ID,
  85. CommitStatus: status,
  86. }); err != nil {
  87. return fmt.Errorf("NewCommitStatus[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, creator.ID, sha, err)
  88. }
  89. return git_model.UpdateCommitStatusSummary(ctx, repo.ID, commit.ID.String())
  90. }); err != nil {
  91. return err
  92. }
  93. defaultBranchCommit, err := gitRepo.GetBranchCommit(repo.DefaultBranch)
  94. if err != nil {
  95. return fmt.Errorf("GetBranchCommit[%s]: %w", repo.DefaultBranch, err)
  96. }
  97. if commit.ID.String() == defaultBranchCommit.ID.String() { // since one commit status updated, the combined commit status should be invalid
  98. if err := deleteCommitStatusCache(repo.ID, repo.DefaultBranch); err != nil {
  99. log.Error("deleteCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err)
  100. }
  101. }
  102. if status.State.IsSuccess() {
  103. if err := automerge.MergeScheduledPullRequest(ctx, sha, repo); err != nil {
  104. return fmt.Errorf("MergeScheduledPullRequest[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, creator.ID, sha, err)
  105. }
  106. }
  107. return nil
  108. }
  109. // FindReposLastestCommitStatuses loading repository default branch latest combinded commit status with cache
  110. func FindReposLastestCommitStatuses(ctx context.Context, repos []*repo_model.Repository) ([]*git_model.CommitStatus, error) {
  111. results := make([]*git_model.CommitStatus, len(repos))
  112. for i, repo := range repos {
  113. if cv := getCommitStatusCache(repo.ID, repo.DefaultBranch); cv != nil {
  114. results[i] = &git_model.CommitStatus{
  115. State: api.CommitStatusState(cv.State),
  116. TargetURL: cv.TargetURL,
  117. }
  118. }
  119. }
  120. // collect the latest commit of each repo
  121. // at most there are dozens of repos (limited by MaxResponseItems), so it's not a big problem at the moment
  122. repoBranchNames := make(map[int64]string, len(repos))
  123. for i, repo := range repos {
  124. if results[i] == nil {
  125. repoBranchNames[repo.ID] = repo.DefaultBranch
  126. }
  127. }
  128. repoIDsToLatestCommitSHAs, err := git_model.FindBranchesByRepoAndBranchName(ctx, repoBranchNames)
  129. if err != nil {
  130. return nil, fmt.Errorf("FindBranchesByRepoAndBranchName: %v", err)
  131. }
  132. var repoSHAs []git_model.RepoSHA
  133. for id, sha := range repoIDsToLatestCommitSHAs {
  134. repoSHAs = append(repoSHAs, git_model.RepoSHA{RepoID: id, SHA: sha})
  135. }
  136. summaryResults, err := git_model.GetLatestCommitStatusForRepoAndSHAs(ctx, repoSHAs)
  137. if err != nil {
  138. return nil, fmt.Errorf("GetLatestCommitStatusForRepoAndSHAs: %v", err)
  139. }
  140. for _, summary := range summaryResults {
  141. for i, repo := range repos {
  142. if repo.ID == summary.RepoID {
  143. results[i] = summary
  144. _ = slices.DeleteFunc(repoSHAs, func(repoSHA git_model.RepoSHA) bool {
  145. return repoSHA.RepoID == repo.ID
  146. })
  147. if results[i].State != "" {
  148. if err := updateCommitStatusCache(repo.ID, repo.DefaultBranch, results[i].State, results[i].TargetURL); err != nil {
  149. log.Error("updateCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err)
  150. }
  151. }
  152. break
  153. }
  154. }
  155. }
  156. // call the database O(1) times to get the commit statuses for all repos
  157. repoToItsLatestCommitStatuses, err := git_model.GetLatestCommitStatusForPairs(ctx, repoSHAs)
  158. if err != nil {
  159. return nil, fmt.Errorf("GetLatestCommitStatusForPairs: %v", err)
  160. }
  161. for i, repo := range repos {
  162. if results[i] == nil {
  163. results[i] = git_model.CalcCommitStatus(repoToItsLatestCommitStatuses[repo.ID])
  164. if results[i].State != "" {
  165. if err := updateCommitStatusCache(repo.ID, repo.DefaultBranch, results[i].State, results[i].TargetURL); err != nil {
  166. log.Error("updateCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err)
  167. }
  168. }
  169. }
  170. }
  171. return results, nil
  172. }