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  }