1
1
mirror of https://github.com/go-gitea/gitea synced 2025-07-28 05:08:37 +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

@@ -10,67 +10,56 @@ import (
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/commitstatus"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/structs"
"github.com/gobwas/glob"
"github.com/pkg/errors"
)
// MergeRequiredContextsCommitStatus returns a commit status state for given required contexts
func MergeRequiredContextsCommitStatus(commitStatuses []*git_model.CommitStatus, requiredContexts []string) structs.CommitStatusState {
// matchedCount is the number of `CommitStatus.Context` that match any context of `requiredContexts`
matchedCount := 0
returnedStatus := structs.CommitStatusSuccess
func MergeRequiredContextsCommitStatus(commitStatuses []*git_model.CommitStatus, requiredContexts []string) commitstatus.CommitStatusState {
if len(commitStatuses) == 0 {
return commitstatus.CommitStatusPending
}
if len(requiredContexts) > 0 {
requiredContextsGlob := make(map[string]glob.Glob, len(requiredContexts))
for _, ctx := range requiredContexts {
if gp, err := glob.Compile(ctx); err != nil {
log.Error("glob.Compile %s failed. Error: %v", ctx, err)
} else {
requiredContextsGlob[ctx] = gp
}
if len(requiredContexts) == 0 {
return git_model.CalcCommitStatus(commitStatuses).State
}
requiredContextsGlob := make(map[string]glob.Glob, len(requiredContexts))
for _, ctx := range requiredContexts {
if gp, err := glob.Compile(ctx); err != nil {
log.Error("glob.Compile %s failed. Error: %v", ctx, err)
} else {
requiredContextsGlob[ctx] = gp
}
}
for _, gp := range requiredContextsGlob {
var targetStatus structs.CommitStatusState
for _, commitStatus := range commitStatuses {
if gp.Match(commitStatus.Context) {
targetStatus = commitStatus.State
matchedCount++
break
}
}
// If required rule not match any action, then it is pending
if targetStatus == "" {
if structs.CommitStatusPending.HasHigherPriorityThan(returnedStatus) {
returnedStatus = structs.CommitStatusPending
}
requiredCommitStatuses := make([]*git_model.CommitStatus, 0, len(commitStatuses))
for _, gp := range requiredContextsGlob {
for _, commitStatus := range commitStatuses {
if gp.Match(commitStatus.Context) {
requiredCommitStatuses = append(requiredCommitStatuses, commitStatus)
break
}
if targetStatus.HasHigherPriorityThan(returnedStatus) {
returnedStatus = targetStatus
}
}
}
if matchedCount == 0 && returnedStatus == structs.CommitStatusSuccess {
if len(commitStatuses) == 0 {
// "no statuses" should mean "pending"
return structs.CommitStatusPending
}
status := git_model.CalcCommitStatus(commitStatuses)
if status.State == structs.CommitStatusSkipped {
return structs.CommitStatusSuccess // if all statuses are skipped, return success
}
return status.State
if len(requiredCommitStatuses) == 0 {
return commitstatus.CommitStatusPending
}
return returnedStatus
returnedStatus := git_model.CalcCommitStatus(requiredCommitStatuses).State
if len(requiredCommitStatuses) == len(requiredContexts) {
return returnedStatus
}
if returnedStatus == commitstatus.CommitStatusFailure {
return commitstatus.CommitStatusFailure
}
// even if part of success, return pending
return commitstatus.CommitStatusPending
}
// IsPullCommitStatusPass returns if all required status checks PASS
@@ -91,7 +80,7 @@ func IsPullCommitStatusPass(ctx context.Context, pr *issues_model.PullRequest) (
}
// GetPullRequestCommitStatusState returns pull request merged commit status state
func GetPullRequestCommitStatusState(ctx context.Context, pr *issues_model.PullRequest) (structs.CommitStatusState, error) {
func GetPullRequestCommitStatusState(ctx context.Context, pr *issues_model.PullRequest) (commitstatus.CommitStatusState, error) {
// Ensure HeadRepo is loaded
if err := pr.LoadHeadRepo(ctx); err != nil {
return "", errors.Wrap(err, "LoadHeadRepo")

View File

@@ -8,7 +8,7 @@ import (
"testing"
git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/commitstatus"
"github.com/stretchr/testify/assert"
)
@@ -17,64 +17,64 @@ func TestMergeRequiredContextsCommitStatus(t *testing.T) {
cases := []struct {
commitStatuses []*git_model.CommitStatus
requiredContexts []string
expected structs.CommitStatusState
expected commitstatus.CommitStatusState
}{
{
commitStatuses: []*git_model.CommitStatus{},
requiredContexts: []string{},
expected: structs.CommitStatusPending,
expected: commitstatus.CommitStatusPending,
},
{
commitStatuses: []*git_model.CommitStatus{
{Context: "Build xxx", State: structs.CommitStatusSkipped},
{Context: "Build xxx", State: commitstatus.CommitStatusSkipped},
},
requiredContexts: []string{"Build*"},
expected: structs.CommitStatusSuccess,
expected: commitstatus.CommitStatusSuccess,
},
{
commitStatuses: []*git_model.CommitStatus{
{Context: "Build 1", State: structs.CommitStatusSkipped},
{Context: "Build 2", State: structs.CommitStatusSuccess},
{Context: "Build 3", State: structs.CommitStatusSuccess},
{Context: "Build 1", State: commitstatus.CommitStatusSkipped},
{Context: "Build 2", State: commitstatus.CommitStatusSuccess},
{Context: "Build 3", State: commitstatus.CommitStatusSuccess},
},
requiredContexts: []string{"Build*"},
expected: structs.CommitStatusSuccess,
expected: commitstatus.CommitStatusSuccess,
},
{
commitStatuses: []*git_model.CommitStatus{
{Context: "Build 1", State: structs.CommitStatusSuccess},
{Context: "Build 2", State: structs.CommitStatusSuccess},
{Context: "Build 2t", State: structs.CommitStatusPending},
{Context: "Build 1", State: commitstatus.CommitStatusSuccess},
{Context: "Build 2", State: commitstatus.CommitStatusSuccess},
{Context: "Build 2t", State: commitstatus.CommitStatusPending},
},
requiredContexts: []string{"Build*", "Build 2t*"},
expected: structs.CommitStatusPending,
expected: commitstatus.CommitStatusPending,
},
{
commitStatuses: []*git_model.CommitStatus{
{Context: "Build 1", State: structs.CommitStatusSuccess},
{Context: "Build 2", State: structs.CommitStatusSuccess},
{Context: "Build 2t", State: structs.CommitStatusFailure},
{Context: "Build 1", State: commitstatus.CommitStatusSuccess},
{Context: "Build 2", State: commitstatus.CommitStatusSuccess},
{Context: "Build 2t", State: commitstatus.CommitStatusFailure},
},
requiredContexts: []string{"Build*", "Build 2t*"},
expected: structs.CommitStatusFailure,
expected: commitstatus.CommitStatusFailure,
},
{
commitStatuses: []*git_model.CommitStatus{
{Context: "Build 1", State: structs.CommitStatusSuccess},
{Context: "Build 2", State: structs.CommitStatusSuccess},
{Context: "Build 2t", State: structs.CommitStatusSuccess},
{Context: "Build 1", State: commitstatus.CommitStatusSuccess},
{Context: "Build 2", State: commitstatus.CommitStatusSuccess},
{Context: "Build 2t", State: commitstatus.CommitStatusSuccess},
},
requiredContexts: []string{"Build*", "Build 2t*", "Build 3*"},
expected: structs.CommitStatusPending,
expected: commitstatus.CommitStatusPending,
},
{
commitStatuses: []*git_model.CommitStatus{
{Context: "Build 1", State: structs.CommitStatusSuccess},
{Context: "Build 2", State: structs.CommitStatusSuccess},
{Context: "Build 2t", State: structs.CommitStatusSuccess},
{Context: "Build 1", State: commitstatus.CommitStatusSuccess},
{Context: "Build 2", State: commitstatus.CommitStatusSuccess},
{Context: "Build 2t", State: commitstatus.CommitStatusSuccess},
},
requiredContexts: []string{"Build*", "Build *", "Build 2t*", "Build 1*"},
expected: structs.CommitStatusSuccess,
expected: commitstatus.CommitStatusSuccess,
},
}
for i, c := range cases {