gitea源码

commitstatus.go 6.4KB

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