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