mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
203 lines
6.4 KiB
Go
203 lines
6.4 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package commitstatus
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"slices"
|
|
|
|
"github.com/hanzoai/git/models/db"
|
|
git_model "github.com/hanzoai/git/models/git"
|
|
repo_model "github.com/hanzoai/git/models/repo"
|
|
user_model "github.com/hanzoai/git/models/user"
|
|
"github.com/hanzoai/git/modules/cache"
|
|
"github.com/hanzoai/git/modules/commitstatus"
|
|
"github.com/hanzoai/git/modules/git"
|
|
"github.com/hanzoai/git/modules/gitrepo"
|
|
"github.com/hanzoai/git/modules/json"
|
|
"github.com/hanzoai/git/modules/log"
|
|
repo_module "github.com/hanzoai/git/modules/repository"
|
|
"github.com/hanzoai/git/services/notify"
|
|
)
|
|
|
|
func getCacheKey(repoID int64, brancheName string) string {
|
|
hashBytes := sha256.Sum256(fmt.Appendf(nil, "%d:%s", repoID, brancheName))
|
|
return fmt.Sprintf("commit_status:%x", hashBytes)
|
|
}
|
|
|
|
type commitStatusCacheValue struct {
|
|
State string `json:"state"`
|
|
TargetURL string `json:"target_url"`
|
|
}
|
|
|
|
func getCommitStatusCache(repoID int64, branchName string) *commitStatusCacheValue {
|
|
c := cache.GetCache()
|
|
statusStr, ok := c.Get(getCacheKey(repoID, branchName))
|
|
if ok && statusStr != "" {
|
|
var cv commitStatusCacheValue
|
|
err := json.Unmarshal([]byte(statusStr), &cv)
|
|
if err == nil {
|
|
return &cv
|
|
}
|
|
log.Warn("getCommitStatusCache: json.Unmarshal failed: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func updateCommitStatusCache(repoID int64, branchName string, state commitstatus.CommitStatusState, targetURL string) error {
|
|
c := cache.GetCache()
|
|
bs, err := json.Marshal(commitStatusCacheValue{
|
|
State: state.String(),
|
|
TargetURL: targetURL,
|
|
})
|
|
if err != nil {
|
|
log.Warn("updateCommitStatusCache: json.Marshal failed: %v", err)
|
|
return nil
|
|
}
|
|
return c.Put(getCacheKey(repoID, branchName), string(bs), 3*24*60)
|
|
}
|
|
|
|
func deleteCommitStatusCache(repoID int64, branchName string) error {
|
|
c := cache.GetCache()
|
|
return c.Delete(getCacheKey(repoID, branchName))
|
|
}
|
|
|
|
// CreateCommitStatus creates a new CommitStatus given a bunch of parameters
|
|
// NOTE: All text-values will be trimmed from whitespaces.
|
|
// Requires: Repo, Creator, SHA
|
|
func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creator *user_model.User, sha string, status *git_model.CommitStatus) error {
|
|
// confirm that commit is exist
|
|
gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo)
|
|
if err != nil {
|
|
return fmt.Errorf("OpenRepository[%s]: %w", repo.RelativePath(), err)
|
|
}
|
|
defer closer.Close()
|
|
|
|
objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
|
|
|
|
commit, err := gitRepo.GetCommit(sha)
|
|
if err != nil {
|
|
return fmt.Errorf("GetCommit[%s]: %w", sha, err)
|
|
}
|
|
if len(sha) != objectFormat.FullLength() {
|
|
// use complete commit sha
|
|
sha = commit.ID.String()
|
|
}
|
|
|
|
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
|
if err := git_model.NewCommitStatus(ctx, git_model.NewCommitStatusOptions{
|
|
Repo: repo,
|
|
Creator: creator,
|
|
SHA: commit.ID,
|
|
CommitStatus: status,
|
|
}); err != nil {
|
|
return fmt.Errorf("NewCommitStatus[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, creator.ID, sha, err)
|
|
}
|
|
|
|
return git_model.UpdateCommitStatusSummary(ctx, repo.ID, commit.ID.String())
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
notify.CreateCommitStatus(ctx, repo, repo_module.CommitToPushCommit(commit), creator, status)
|
|
|
|
defaultBranchCommit, err := gitRepo.GetBranchCommit(repo.DefaultBranch)
|
|
if err != nil {
|
|
return fmt.Errorf("GetBranchCommit[%s]: %w", repo.DefaultBranch, err)
|
|
}
|
|
|
|
if commit.ID.String() == defaultBranchCommit.ID.String() { // since one commit status updated, the combined commit status should be invalid
|
|
if err := deleteCommitStatusCache(repo.ID, repo.DefaultBranch); err != nil {
|
|
log.Error("deleteCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// FindReposLatestCommitStatuses loading repository default branch latest combined commit status with cache
|
|
func FindReposLatestCommitStatuses(ctx context.Context, repos []*repo_model.Repository) ([]*git_model.CommitStatus, error) {
|
|
results := make([]*git_model.CommitStatus, len(repos))
|
|
allCached := true
|
|
for i, repo := range repos {
|
|
if cv := getCommitStatusCache(repo.ID, repo.DefaultBranch); cv != nil {
|
|
results[i] = &git_model.CommitStatus{
|
|
State: commitstatus.CommitStatusState(cv.State),
|
|
TargetURL: cv.TargetURL,
|
|
}
|
|
} else {
|
|
allCached = false
|
|
}
|
|
}
|
|
|
|
if allCached {
|
|
return results, nil
|
|
}
|
|
|
|
// collect the latest commit of each repo
|
|
// at most there are dozens of repos (limited by MaxResponseItems), so it's not a big problem at the moment
|
|
repoBranchNames := make(map[int64]string, len(repos))
|
|
for i, repo := range repos {
|
|
if results[i] == nil {
|
|
repoBranchNames[repo.ID] = repo.DefaultBranch
|
|
}
|
|
}
|
|
|
|
repoIDsToLatestCommitSHAs, err := git_model.FindBranchesByRepoAndBranchName(ctx, repoBranchNames)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("FindBranchesByRepoAndBranchName: %v", err)
|
|
}
|
|
|
|
var repoSHAs []git_model.RepoSHA
|
|
for id, sha := range repoIDsToLatestCommitSHAs {
|
|
repoSHAs = append(repoSHAs, git_model.RepoSHA{RepoID: id, SHA: sha})
|
|
}
|
|
|
|
summaryResults, err := git_model.GetLatestCommitStatusForRepoAndSHAs(ctx, repoSHAs)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("GetLatestCommitStatusForRepoAndSHAs: %v", err)
|
|
}
|
|
|
|
for _, summary := range summaryResults {
|
|
for i, repo := range repos {
|
|
if repo.ID == summary.RepoID {
|
|
results[i] = summary
|
|
repoSHAs = slices.DeleteFunc(repoSHAs, func(repoSHA git_model.RepoSHA) bool {
|
|
return repoSHA.RepoID == repo.ID
|
|
})
|
|
if results[i] != nil {
|
|
if err := updateCommitStatusCache(repo.ID, repo.DefaultBranch, results[i].State, results[i].TargetURL); err != nil {
|
|
log.Error("updateCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err)
|
|
}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if len(repoSHAs) == 0 {
|
|
return results, nil
|
|
}
|
|
|
|
// call the database O(1) times to get the commit statuses for all repos
|
|
repoToItsLatestCommitStatuses, err := git_model.GetLatestCommitStatusForPairs(ctx, repoSHAs)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("GetLatestCommitStatusForPairs: %v", err)
|
|
}
|
|
|
|
for i, repo := range repos {
|
|
if results[i] == nil {
|
|
results[i] = git_model.CalcCommitStatus(repoToItsLatestCommitStatuses[repo.ID])
|
|
if results[i] != nil {
|
|
if err := updateCommitStatusCache(repo.ID, repo.DefaultBranch, results[i].State, results[i].TargetURL); err != nil {
|
|
log.Error("updateCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return results, nil
|
|
}
|