Skip to content

Introduce combined status #34531

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions models/git/combined_status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// Copyright 2024 Gitea. All rights reserved.
// SPDX-License-Identifier: MIT

package git

import (
"context"
"strings"

"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/commitstatus"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/translation"

"xorm.io/builder"
)

// CombinedStatus holds the latest combined Status of a single Commit
type CombinedStatus struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX UNIQUE(repo_id_sha)"`
Repo *repo_model.Repository `xorm:"-"`
SHA string `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_id_sha)"`
State commitstatus.CombinedStatusState `xorm:"VARCHAR(7) NOT NULL"`
TargetURL string `xorm:"TEXT"`
}

func (CombinedStatus) TableName() string {
return "commit_status_summary" // legacy name for compatibility
}

func init() {
db.RegisterModel(new(CombinedStatus))
}

func (status *CombinedStatus) loadRepository(ctx context.Context) error {
if status.RepoID == 0 || status.Repo != nil {
return nil
}

repo, err := repo_model.GetRepositoryByID(ctx, status.RepoID)
if err != nil {
return err
}
status.Repo = repo

return nil
}

// LocaleString returns the locale string name of the Status
func (status *CombinedStatus) LocaleString(lang translation.Locale) string {
return lang.TrString("repo.commitstatus." + status.State.String())
}

// HideActionsURL set `TargetURL` to an empty string if the status comes from Gitea Actions
func (status *CombinedStatus) HideActionsURL(ctx context.Context) {
if status.RepoID == 0 {
return
}

if err := status.loadRepository(ctx); err != nil {
log.Error("loadRepository: %v", err)
return
}

prefix := status.Repo.Link() + "/actions"
if strings.HasPrefix(status.TargetURL, prefix) {
status.TargetURL = ""
}
}

type RepoSHA struct {
RepoID int64
SHA string
}

func GetLatestCombinedStatusForRepoAndSHAs(ctx context.Context, repoSHAs []RepoSHA) ([]*CombinedStatus, error) {
cond := builder.NewCond()
for _, rs := range repoSHAs {
cond = cond.Or(builder.Eq{"repo_id": rs.RepoID, "sha": rs.SHA})
}

combinedStatuses := make([]*CombinedStatus, 0, len(repoSHAs))
if err := db.GetEngine(ctx).Where(cond).Find(&combinedStatuses); err != nil {
return nil, err
}
return combinedStatuses, nil
}

func InsertOrUpdateCombinedStatus(ctx context.Context, repoID int64, sha string) error {
commitStatuses, err := GetLatestCommitStatus(ctx, repoID, sha, db.ListOptionsAll)
if err != nil {
return err
}
// it guarantees that commitStatuses is not empty because this function is always called after a commit status is created
if len(commitStatuses) == 0 {
setting.PanicInDevOrTesting("no commit statuses found for repo %d and sha %s", repoID, sha)
}
combinedStatus := CalcCombinedStatus(commitStatuses)

// mysql will return 0 when update a record which state hasn't been changed which behaviour is different from other database,
// so we need to use insert in on duplicate
if setting.Database.Type.IsMySQL() {
_, err := db.GetEngine(ctx).Exec("INSERT INTO commit_status_summary (repo_id,sha,state,target_url) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE state=?",
repoID, sha, combinedStatus.State, combinedStatus.TargetURL, combinedStatus.State)
return err
}

if cnt, err := db.GetEngine(ctx).Where("repo_id=? AND sha=?", repoID, sha).
Cols("state, target_url").
Update(combinedStatus); err != nil {
return err
} else if cnt == 0 {
_, err = db.GetEngine(ctx).Insert(combinedStatus)
return err
}
return nil
}

func CombinedStatusesHideActionsURL(ctx context.Context, statuses []*CombinedStatus) {
idToRepos := make(map[int64]*repo_model.Repository)
for _, status := range statuses {
if status == nil {
continue
}

if status.Repo == nil {
status.Repo = idToRepos[status.RepoID]
}
status.HideActionsURL(ctx)
idToRepos[status.RepoID] = status.Repo
}
}
82 changes: 43 additions & 39 deletions models/git/commit_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,30 +17,30 @@ import (
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/commitstatus"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/translation"

"xorm.io/builder"
"xorm.io/xorm"
)

// CommitStatus holds a single Status of a single Commit
// CommitStatus holds a single commit status of a single Commit
type CommitStatus struct {
ID int64 `xorm:"pk autoincr"`
Index int64 `xorm:"INDEX UNIQUE(repo_sha_index)"`
RepoID int64 `xorm:"INDEX UNIQUE(repo_sha_index)"`
Repo *repo_model.Repository `xorm:"-"`
State api.CommitStatusState `xorm:"VARCHAR(7) NOT NULL"`
SHA string `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_sha_index)"`
TargetURL string `xorm:"TEXT"`
Description string `xorm:"TEXT"`
ContextHash string `xorm:"VARCHAR(64) index"`
Context string `xorm:"TEXT"`
Creator *user_model.User `xorm:"-"`
ID int64 `xorm:"pk autoincr"`
Index int64 `xorm:"INDEX UNIQUE(repo_sha_index)"`
RepoID int64 `xorm:"INDEX UNIQUE(repo_sha_index)"`
Repo *repo_model.Repository `xorm:"-"`
State commitstatus.CommitStatusState `xorm:"VARCHAR(7) NOT NULL"`
SHA string `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_sha_index)"`
TargetURL string `xorm:"TEXT"`
Description string `xorm:"TEXT"`
ContextHash string `xorm:"VARCHAR(64) index"`
Context string `xorm:"TEXT"`
Creator *user_model.User `xorm:"-"`
CreatorID int64

CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
Expand Down Expand Up @@ -215,11 +215,9 @@ func (status *CommitStatus) HideActionsURL(ctx context.Context) {
return
}

if status.Repo == nil {
if err := status.loadRepository(ctx); err != nil {
log.Error("loadRepository: %v", err)
return
}
if err := status.loadRepository(ctx); err != nil {
log.Error("loadRepository: %v", err)
return
}

prefix := status.Repo.Link() + "/actions"
Expand All @@ -228,30 +226,35 @@ func (status *CommitStatus) HideActionsURL(ctx context.Context) {
}
}

// CalcCommitStatus returns commit status state via some status, the commit statues should order by id desc
func CalcCommitStatus(statuses []*CommitStatus) *CommitStatus {
// This function is widely used, but it is not quite right.
// Ideally it should return something like "CommitStatusSummary" with properly aggregated state.
// GitHub's behavior: if all statuses are "skipped", GitHub will return "success" as the combined status.
var lastStatus *CommitStatus
state := api.CommitStatusSuccess
// CalcCombinedStatusState returns a combined status state, the commit statuses should order by id desc
func CalcCombinedStatusState(statuses []*CommitStatus) commitstatus.CombinedStatusState {
states := make(commitstatus.CommitStatusStates, 0, len(statuses))
for _, status := range statuses {
if state == status.State || status.State.HasHigherPriorityThan(state) {
state = status.State
lastStatus = status
}
states = append(states, status.State)
}
return states.Combine()
}

// CalcCombinedStatus returns combined status struct, the commit statuses should order by id desc
func CalcCombinedStatus(statuses []*CommitStatus) *CombinedStatus {
if len(statuses) == 0 {
return nil
}
if lastStatus == nil {
if len(statuses) > 0 {
// FIXME: a bad case: Gitea just returns the first commit status, its status is "skipped" in this case.
lastStatus = statuses[0]
} else {
// FIXME: another bad case: if the "statuses" slice is empty, the returned value is an invalid CommitStatus, all its fields are empty.
// Frontend code (tmpl&vue) sometimes depend on the empty fields to skip rendering commit status elements (need to double check in the future)
lastStatus = &CommitStatus{}

states := make(commitstatus.CommitStatusStates, 0, len(statuses))
targetURL := ""
for _, status := range statuses {
states = append(states, status.State)
if status.TargetURL != "" {
targetURL = status.TargetURL
}
}
return lastStatus
return &CombinedStatus{
RepoID: statuses[0].RepoID,
SHA: statuses[0].SHA,
State: states.Combine(),
TargetURL: targetURL,
}
}

// CommitStatusOptions holds the options for query commit statuses
Expand Down Expand Up @@ -322,6 +325,7 @@ func GetLatestCommitStatus(ctx context.Context, repoID int64, sha string, listOp
if err := sess.Find(&indices); err != nil {
return nil, err
}

statuses := make([]*CommitStatus, 0, len(indices))
if len(indices) == 0 {
return statuses, nil
Expand Down Expand Up @@ -506,7 +510,7 @@ func NewCommitStatus(ctx context.Context, opts NewCommitStatusOptions) error {

// SignCommitWithStatuses represents a commit with validation of signature and status state.
type SignCommitWithStatuses struct {
Status *CommitStatus
Status *CombinedStatus
Statuses []*CommitStatus
*asymkey_model.SignCommit
}
Expand Down
92 changes: 0 additions & 92 deletions models/git/commit_status_summary.go

This file was deleted.

Loading
Loading