1
1
mirror of https://github.com/go-gitea/gitea synced 2025-07-03 09:07:19 +00:00

Keeping consistent between UI and API about combined commit status state and fix some bugs (#34562)

Extract from #34531 

## Move Commit status state to a standalone package

Move the state from `structs` to `commitstatus` package. It also
introduce `CommitStatusStates` so that the combine function could be
used from UI and API logic.

## Combined commit status Changed

This PR will follow Github's combined commit status. Before this PR,
every commit status could be a combined one.
According to
https://docs.github.com/en/rest/commits/statuses?apiVersion=2022-11-28#get-the-combined-status-for-a-specific-reference
> Additionally, a combined state is returned. The state is one of:
> failure if any of the contexts report as error or failure
> pending if there are no statuses or a context is pending
> success if the latest status for all contexts is success

This PR will follow that rule and remove the `NoBetterThan` logic. This
also fixes the inconsistent between UI and API. In the API convert
package, it has implemented this which is different from the UI. It also
fixed the missing `URL` and `CommitURL` in the API.

## `CalcCommitStatus` return nil if there is no commit statuses

The behavior of `CalcCommitStatus` is changed. If the parameter commit
statuses is empty, it will return nil. The reference places should check
the returned value themselves.
This commit is contained in:
Lunny Xiao
2025-06-09 12:05:33 +08:00
committed by GitHub
parent f6041441ee
commit 6d0b24064a
21 changed files with 482 additions and 281 deletions

View File

@ -17,10 +17,10 @@ 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"
@ -30,17 +30,17 @@ import (
// CommitStatus holds a single 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"`
@ -230,28 +230,25 @@ 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
if len(statuses) == 0 {
return nil
}
states := make(commitstatus.CommitStatusStates, 0, len(statuses))
targetURL := ""
for _, status := range statuses {
if state == status.State || status.State.HasHigherPriorityThan(state) {
state = status.State
lastStatus = status
states = append(states, status.State)
if status.TargetURL != "" {
targetURL = status.TargetURL
}
}
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{}
}
return &CommitStatus{
RepoID: statuses[0].RepoID,
SHA: statuses[0].SHA,
State: states.Combine(),
TargetURL: targetURL,
}
return lastStatus
}
// CommitStatusOptions holds the options for query commit statuses

View File

@ -7,19 +7,19 @@ import (
"context"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/commitstatus"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"xorm.io/builder"
)
// CommitStatusSummary holds the latest commit Status of a single Commit
type CommitStatusSummary struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX UNIQUE(repo_id_sha)"`
SHA string `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_id_sha)"`
State api.CommitStatusState `xorm:"VARCHAR(7) NOT NULL"`
TargetURL string `xorm:"TEXT"`
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX UNIQUE(repo_id_sha)"`
SHA string `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_id_sha)"`
State commitstatus.CommitStatusState `xorm:"VARCHAR(7) NOT NULL"`
TargetURL string `xorm:"TEXT"`
}
func init() {

View File

@ -14,9 +14,9 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
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/gitrepo"
"code.gitea.io/gitea/modules/structs"
"github.com/stretchr/testify/assert"
)
@ -38,23 +38,23 @@ func TestGetCommitStatuses(t *testing.T) {
assert.Len(t, statuses, 5)
assert.Equal(t, "ci/awesomeness", statuses[0].Context)
assert.Equal(t, structs.CommitStatusPending, statuses[0].State)
assert.Equal(t, commitstatus.CommitStatusPending, statuses[0].State)
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[0].APIURL(db.DefaultContext))
assert.Equal(t, "cov/awesomeness", statuses[1].Context)
assert.Equal(t, structs.CommitStatusWarning, statuses[1].State)
assert.Equal(t, commitstatus.CommitStatusWarning, statuses[1].State)
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[1].APIURL(db.DefaultContext))
assert.Equal(t, "cov/awesomeness", statuses[2].Context)
assert.Equal(t, structs.CommitStatusSuccess, statuses[2].State)
assert.Equal(t, commitstatus.CommitStatusSuccess, statuses[2].State)
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[2].APIURL(db.DefaultContext))
assert.Equal(t, "ci/awesomeness", statuses[3].Context)
assert.Equal(t, structs.CommitStatusFailure, statuses[3].State)
assert.Equal(t, commitstatus.CommitStatusFailure, statuses[3].State)
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[3].APIURL(db.DefaultContext))
assert.Equal(t, "deploy/awesomeness", statuses[4].Context)
assert.Equal(t, structs.CommitStatusError, statuses[4].State)
assert.Equal(t, commitstatus.CommitStatusError, statuses[4].State)
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[4].APIURL(db.DefaultContext))
statuses, maxResults, err = db.FindAndCount[git_model.CommitStatus](db.DefaultContext, &git_model.CommitStatusOptions{
@ -75,110 +75,110 @@ func Test_CalcCommitStatus(t *testing.T) {
{
statuses: []*git_model.CommitStatus{
{
State: structs.CommitStatusPending,
State: commitstatus.CommitStatusPending,
},
},
expected: &git_model.CommitStatus{
State: structs.CommitStatusPending,
State: commitstatus.CommitStatusPending,
},
},
{
statuses: []*git_model.CommitStatus{
{
State: structs.CommitStatusSuccess,
State: commitstatus.CommitStatusSuccess,
},
{
State: structs.CommitStatusPending,
State: commitstatus.CommitStatusPending,
},
},
expected: &git_model.CommitStatus{
State: structs.CommitStatusPending,
State: commitstatus.CommitStatusPending,
},
},
{
statuses: []*git_model.CommitStatus{
{
State: structs.CommitStatusSuccess,
State: commitstatus.CommitStatusSuccess,
},
{
State: structs.CommitStatusPending,
State: commitstatus.CommitStatusPending,
},
{
State: structs.CommitStatusSuccess,
State: commitstatus.CommitStatusSuccess,
},
},
expected: &git_model.CommitStatus{
State: structs.CommitStatusPending,
State: commitstatus.CommitStatusPending,
},
},
{
statuses: []*git_model.CommitStatus{
{
State: structs.CommitStatusError,
State: commitstatus.CommitStatusError,
},
{
State: structs.CommitStatusPending,
State: commitstatus.CommitStatusPending,
},
{
State: structs.CommitStatusSuccess,
State: commitstatus.CommitStatusSuccess,
},
},
expected: &git_model.CommitStatus{
State: structs.CommitStatusError,
State: commitstatus.CommitStatusFailure,
},
},
{
statuses: []*git_model.CommitStatus{
{
State: structs.CommitStatusWarning,
State: commitstatus.CommitStatusWarning,
},
{
State: structs.CommitStatusPending,
State: commitstatus.CommitStatusPending,
},
{
State: structs.CommitStatusSuccess,
State: commitstatus.CommitStatusSuccess,
},
},
expected: &git_model.CommitStatus{
State: structs.CommitStatusWarning,
State: commitstatus.CommitStatusPending,
},
},
{
statuses: []*git_model.CommitStatus{
{
State: structs.CommitStatusSuccess,
State: commitstatus.CommitStatusSuccess,
},
{
State: structs.CommitStatusSuccess,
State: commitstatus.CommitStatusSuccess,
},
{
State: structs.CommitStatusSuccess,
State: commitstatus.CommitStatusSuccess,
},
},
expected: &git_model.CommitStatus{
State: structs.CommitStatusSuccess,
State: commitstatus.CommitStatusSuccess,
},
},
{
statuses: []*git_model.CommitStatus{
{
State: structs.CommitStatusFailure,
State: commitstatus.CommitStatusFailure,
},
{
State: structs.CommitStatusError,
State: commitstatus.CommitStatusError,
},
{
State: structs.CommitStatusWarning,
State: commitstatus.CommitStatusWarning,
},
},
expected: &git_model.CommitStatus{
State: structs.CommitStatusError,
State: commitstatus.CommitStatusFailure,
},
},
}
for _, kase := range kases {
assert.Equal(t, kase.expected, git_model.CalcCommitStatus(kase.statuses))
assert.Equal(t, kase.expected, git_model.CalcCommitStatus(kase.statuses), "statuses: %v", kase.statuses)
}
}
@ -208,7 +208,7 @@ func TestFindRepoRecentCommitStatusContexts(t *testing.T) {
Creator: user2,
SHA: commit.ID,
CommitStatus: &git_model.CommitStatus{
State: structs.CommitStatusFailure,
State: commitstatus.CommitStatusFailure,
TargetURL: "https://example.com/tests/",
Context: "compliance/lint-backend",
},
@ -220,7 +220,7 @@ func TestFindRepoRecentCommitStatusContexts(t *testing.T) {
Creator: user2,
SHA: commit.ID,
CommitStatus: &git_model.CommitStatus{
State: structs.CommitStatusSuccess,
State: commitstatus.CommitStatusSuccess,
TargetURL: "https://example.com/tests/",
Context: "compliance/lint-backend",
},
@ -270,9 +270,9 @@ func TestGetCountLatestCommitStatus(t *testing.T) {
})
assert.NoError(t, err)
assert.Len(t, commitStatuses, 2)
assert.Equal(t, structs.CommitStatusFailure, commitStatuses[0].State)
assert.Equal(t, commitstatus.CommitStatusFailure, commitStatuses[0].State)
assert.Equal(t, "ci/awesomeness", commitStatuses[0].Context)
assert.Equal(t, structs.CommitStatusError, commitStatuses[1].State)
assert.Equal(t, commitstatus.CommitStatusError, commitStatuses[1].State)
assert.Equal(t, "deploy/awesomeness", commitStatuses[1].Context)
count, err := git_model.CountLatestCommitStatus(db.DefaultContext, repo1.ID, sha1)