2017-04-21 07:32:31 -04:00
// Copyright 2017 Gitea. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
2022-06-12 11:51:54 -04:00
package git
2017-04-21 07:32:31 -04:00
import (
2021-12-09 20:27:50 -05:00
"context"
2019-06-30 03:57:59 -04:00
"crypto/sha1"
2017-04-21 07:32:31 -04:00
"fmt"
2021-11-16 13:18:25 -05:00
"net/url"
2017-04-21 07:32:31 -04:00
"strings"
2019-09-18 01:39:45 -04:00
"time"
2017-04-21 07:32:31 -04:00
2021-12-10 03:14:24 -05:00
asymkey_model "code.gitea.io/gitea/models/asymkey"
2021-09-19 07:49:59 -04:00
"code.gitea.io/gitea/models/db"
2021-12-09 20:27:50 -05:00
repo_model "code.gitea.io/gitea/models/repo"
2021-11-24 04:49:20 -05:00
user_model "code.gitea.io/gitea/models/user"
2022-06-12 11:51:54 -04:00
"code.gitea.io/gitea/modules/git"
2017-04-21 07:32:31 -04:00
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
2019-05-11 06:21:34 -04:00
api "code.gitea.io/gitea/modules/structs"
2019-08-15 10:46:21 -04:00
"code.gitea.io/gitea/modules/timeutil"
2019-07-25 06:55:06 -04:00
2019-10-17 05:26:49 -04:00
"xorm.io/xorm"
2017-04-21 07:32:31 -04:00
)
// CommitStatus holds a single Status of a single Commit
type CommitStatus struct {
2021-12-09 20:27:50 -05:00
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:"char(40) index" `
Context string ` xorm:"TEXT" `
Creator * user_model . User ` xorm:"-" `
2017-04-21 07:32:31 -04:00
CreatorID int64
2019-08-15 10:46:21 -04:00
CreatedUnix timeutil . TimeStamp ` xorm:"INDEX created" `
UpdatedUnix timeutil . TimeStamp ` xorm:"INDEX updated" `
2017-04-21 07:32:31 -04:00
}
2021-09-19 07:49:59 -04:00
func init ( ) {
db . RegisterModel ( new ( CommitStatus ) )
2021-09-23 06:50:06 -04:00
db . RegisterModel ( new ( CommitStatusIndex ) )
}
// upsertCommitStatusIndex the function will not return until it acquires the lock or receives an error.
2022-05-20 10:08:52 -04:00
func upsertCommitStatusIndex ( ctx context . Context , repoID int64 , sha string ) ( err error ) {
2021-09-23 06:50:06 -04:00
// An atomic UPSERT operation (INSERT/UPDATE) is the only operation
// that ensures that the key is actually locked.
switch {
case setting . Database . UseSQLite3 || setting . Database . UsePostgreSQL :
2022-05-20 10:08:52 -04:00
_ , err = db . Exec ( ctx , "INSERT INTO `commit_status_index` (repo_id, sha, max_index) " +
2021-09-23 06:50:06 -04:00
"VALUES (?,?,1) ON CONFLICT (repo_id,sha) DO UPDATE SET max_index = `commit_status_index`.max_index+1" ,
repoID , sha )
case setting . Database . UseMySQL :
2022-05-20 10:08:52 -04:00
_ , err = db . Exec ( ctx , "INSERT INTO `commit_status_index` (repo_id, sha, max_index) " +
2021-09-23 06:50:06 -04:00
"VALUES (?,?,1) ON DUPLICATE KEY UPDATE max_index = max_index+1" ,
repoID , sha )
case setting . Database . UseMSSQL :
// https://weblogs.sqlteam.com/dang/2009/01/31/upsert-race-condition-with-merge/
2022-05-20 10:08:52 -04:00
_ , err = db . Exec ( ctx , "MERGE `commit_status_index` WITH (HOLDLOCK) as target " +
2021-09-23 06:50:06 -04:00
"USING (SELECT ? AS repo_id, ? AS sha) AS src " +
"ON src.repo_id = target.repo_id AND src.sha = target.sha " +
"WHEN MATCHED THEN UPDATE SET target.max_index = target.max_index+1 " +
"WHEN NOT MATCHED THEN INSERT (repo_id, sha, max_index) " +
"VALUES (src.repo_id, src.sha, 1);" ,
repoID , sha )
default :
return fmt . Errorf ( "database type not supported" )
}
return
}
// GetNextCommitStatusIndex retried 3 times to generate a resource index
func GetNextCommitStatusIndex ( repoID int64 , sha string ) ( int64 , error ) {
for i := 0 ; i < db . MaxDupIndexAttempts ; i ++ {
idx , err := getNextCommitStatusIndex ( repoID , sha )
if err == db . ErrResouceOutdated {
continue
}
if err != nil {
return 0 , err
}
return idx , nil
}
return 0 , db . ErrGetResourceIndexFailed
}
// getNextCommitStatusIndex return the next index
func getNextCommitStatusIndex ( repoID int64 , sha string ) ( int64 , error ) {
ctx , commiter , err := db . TxContext ( )
if err != nil {
return 0 , err
}
defer commiter . Close ( )
var preIdx int64
2022-05-20 10:08:52 -04:00
_ , err = db . GetEngine ( ctx ) . SQL ( "SELECT max_index FROM `commit_status_index` WHERE repo_id = ? AND sha = ?" , repoID , sha ) . Get ( & preIdx )
2021-09-23 06:50:06 -04:00
if err != nil {
return 0 , err
}
2022-05-20 10:08:52 -04:00
if err := upsertCommitStatusIndex ( ctx , repoID , sha ) ; err != nil {
2021-09-23 06:50:06 -04:00
return 0 , err
}
var curIdx int64
2022-05-20 10:08:52 -04:00
has , err := db . GetEngine ( ctx ) . SQL ( "SELECT max_index FROM `commit_status_index` WHERE repo_id = ? AND sha = ? AND max_index=?" , repoID , sha , preIdx + 1 ) . Get ( & curIdx )
2021-09-23 06:50:06 -04:00
if err != nil {
return 0 , err
}
if ! has {
return 0 , db . ErrResouceOutdated
}
if err := commiter . Commit ( ) ; err != nil {
return 0 , err
}
return curIdx , nil
2021-09-19 07:49:59 -04:00
}
2021-12-09 20:27:50 -05:00
func ( status * CommitStatus ) loadAttributes ( ctx context . Context ) ( err error ) {
2017-04-21 07:32:31 -04:00
if status . Repo == nil {
2021-12-09 20:27:50 -05:00
status . Repo , err = repo_model . GetRepositoryByIDCtx ( ctx , status . RepoID )
2017-04-21 07:32:31 -04:00
if err != nil {
return fmt . Errorf ( "getRepositoryByID [%d]: %v" , status . RepoID , err )
}
}
if status . Creator == nil && status . CreatorID > 0 {
2022-05-20 10:08:52 -04:00
status . Creator , err = user_model . GetUserByIDCtx ( ctx , status . CreatorID )
2017-04-21 07:32:31 -04:00
if err != nil {
return fmt . Errorf ( "getUserByID [%d]: %v" , status . CreatorID , err )
}
}
return nil
}
// APIURL returns the absolute APIURL to this commit-status.
func ( status * CommitStatus ) APIURL ( ) string {
2021-12-09 20:27:50 -05:00
_ = status . loadAttributes ( db . DefaultContext )
2021-11-16 13:18:25 -05:00
return status . Repo . APIURL ( ) + "/statuses/" + url . PathEscape ( status . SHA )
2017-04-21 07:32:31 -04:00
}
2017-09-14 02:51:32 -04:00
// CalcCommitStatus returns commit status state via some status, the commit statues should order by id desc
func CalcCommitStatus ( statuses [ ] * CommitStatus ) * CommitStatus {
var lastStatus * CommitStatus
2020-01-21 22:46:04 -05:00
var state api . CommitStatusState
2017-09-14 02:51:32 -04:00
for _ , status := range statuses {
2020-01-21 22:46:04 -05:00
if status . State . NoBetterThan ( state ) {
2017-09-14 02:51:32 -04:00
state = status . State
lastStatus = status
}
}
if lastStatus == nil {
if len ( statuses ) > 0 {
lastStatus = statuses [ 0 ]
} else {
lastStatus = & CommitStatus { }
}
}
return lastStatus
}
2019-07-25 06:55:06 -04:00
// CommitStatusOptions holds the options for query commit statuses
type CommitStatusOptions struct {
2021-09-24 07:32:56 -04:00
db . ListOptions
2019-07-25 06:55:06 -04:00
State string
SortType string
}
2017-04-21 07:32:31 -04:00
// GetCommitStatuses returns all statuses for a given commit.
2021-12-09 20:27:50 -05:00
func GetCommitStatuses ( repo * repo_model . Repository , sha string , opts * CommitStatusOptions ) ( [ ] * CommitStatus , int64 , error ) {
2019-07-25 06:55:06 -04:00
if opts . Page <= 0 {
opts . Page = 1
}
2020-01-24 14:00:29 -05:00
if opts . PageSize <= 0 {
2022-06-12 11:51:54 -04:00
opts . Page = setting . ItemsPerPage
2020-01-24 14:00:29 -05:00
}
2019-07-25 06:55:06 -04:00
countSession := listCommitStatusesStatement ( repo , sha , opts )
2021-09-24 07:32:56 -04:00
countSession = db . SetSessionPagination ( countSession , opts )
2019-07-25 06:55:06 -04:00
maxResults , err := countSession . Count ( new ( CommitStatus ) )
if err != nil {
log . Error ( "Count PRs: %v" , err )
return nil , maxResults , err
}
2020-01-24 14:00:29 -05:00
statuses := make ( [ ] * CommitStatus , 0 , opts . PageSize )
2019-07-25 06:55:06 -04:00
findSession := listCommitStatusesStatement ( repo , sha , opts )
2021-09-24 07:32:56 -04:00
findSession = db . SetSessionPagination ( findSession , opts )
2019-07-25 06:55:06 -04:00
sortCommitStatusesSession ( findSession , opts . SortType )
return statuses , maxResults , findSession . Find ( & statuses )
}
2021-12-09 20:27:50 -05:00
func listCommitStatusesStatement ( repo * repo_model . Repository , sha string , opts * CommitStatusOptions ) * xorm . Session {
2021-09-23 11:45:36 -04:00
sess := db . GetEngine ( db . DefaultContext ) . Where ( "repo_id = ?" , repo . ID ) . And ( "sha = ?" , sha )
2019-07-25 06:55:06 -04:00
switch opts . State {
case "pending" , "success" , "error" , "failure" , "warning" :
sess . And ( "state = ?" , opts . State )
}
return sess
}
func sortCommitStatusesSession ( sess * xorm . Session , sortType string ) {
switch sortType {
case "oldest" :
sess . Asc ( "created_unix" )
case "recentupdate" :
sess . Desc ( "updated_unix" )
case "leastupdate" :
sess . Asc ( "updated_unix" )
case "leastindex" :
sess . Desc ( "index" )
case "highestindex" :
sess . Asc ( "index" )
default :
sess . Desc ( "created_unix" )
}
2017-04-21 07:32:31 -04:00
}
2021-09-23 06:50:06 -04:00
// CommitStatusIndex represents a table for commit status index
type CommitStatusIndex struct {
ID int64
RepoID int64 ` xorm:"unique(repo_sha)" `
SHA string ` xorm:"unique(repo_sha)" `
MaxIndex int64 ` xorm:"index" `
}
2017-04-21 07:32:31 -04:00
// GetLatestCommitStatus returns all statuses with a unique context for a given commit.
2022-05-20 10:08:52 -04:00
func GetLatestCommitStatus ( ctx context . Context , repoID int64 , sha string , listOptions db . ListOptions ) ( [ ] * CommitStatus , int64 , error ) {
2017-05-07 10:40:31 -04:00
ids := make ( [ ] int64 , 0 , 10 )
2022-04-28 07:48:48 -04:00
sess := db . GetEngine ( ctx ) . Table ( & CommitStatus { } ) .
2020-12-17 22:33:32 -05:00
Where ( "repo_id = ?" , repoID ) . And ( "sha = ?" , sha ) .
2017-05-07 10:40:31 -04:00
Select ( "max( id ) as id" ) .
2020-12-17 22:33:32 -05:00
GroupBy ( "context_hash" ) . OrderBy ( "max( id ) desc" )
2021-09-24 07:32:56 -04:00
sess = db . SetSessionPagination ( sess , & listOptions )
2020-12-17 22:33:32 -05:00
2021-12-15 00:39:34 -05:00
count , err := sess . FindAndCount ( & ids )
2017-05-07 10:40:31 -04:00
if err != nil {
2021-12-15 00:39:34 -05:00
return nil , count , err
2017-05-07 10:40:31 -04:00
}
statuses := make ( [ ] * CommitStatus , 0 , len ( ids ) )
if len ( ids ) == 0 {
2021-12-15 00:39:34 -05:00
return statuses , count , nil
2017-05-07 10:40:31 -04:00
}
2022-04-28 07:48:48 -04:00
return statuses , count , db . GetEngine ( ctx ) . In ( "id" , ids ) . Find ( & statuses )
2017-04-21 07:32:31 -04:00
}
2019-09-18 01:39:45 -04:00
// FindRepoRecentCommitStatusContexts returns repository's recent commit status contexts
func FindRepoRecentCommitStatusContexts ( repoID int64 , before time . Duration ) ( [ ] string , error ) {
start := timeutil . TimeStampNow ( ) . AddDuration ( - before )
ids := make ( [ ] int64 , 0 , 10 )
2021-09-23 11:45:36 -04:00
if err := db . GetEngine ( db . DefaultContext ) . Table ( "commit_status" ) .
2019-09-18 01:39:45 -04:00
Where ( "repo_id = ?" , repoID ) .
And ( "updated_unix >= ?" , start ) .
Select ( "max( id ) as id" ) .
GroupBy ( "context_hash" ) . OrderBy ( "max( id ) desc" ) .
Find ( & ids ) ; err != nil {
return nil , err
}
2021-03-14 14:52:12 -04:00
contexts := make ( [ ] string , 0 , len ( ids ) )
2019-09-18 01:39:45 -04:00
if len ( ids ) == 0 {
return contexts , nil
}
2021-09-23 11:45:36 -04:00
return contexts , db . GetEngine ( db . DefaultContext ) . Select ( "context" ) . Table ( "commit_status" ) . In ( "id" , ids ) . Find ( & contexts )
2019-09-18 01:39:45 -04:00
}
2017-04-21 07:32:31 -04:00
// NewCommitStatusOptions holds options for creating a CommitStatus
type NewCommitStatusOptions struct {
2021-12-09 20:27:50 -05:00
Repo * repo_model . Repository
2021-11-24 04:49:20 -05:00
Creator * user_model . User
2017-04-21 07:32:31 -04:00
SHA string
CommitStatus * CommitStatus
}
2019-06-30 03:57:59 -04:00
// NewCommitStatus save commit statuses into database
func NewCommitStatus ( opts NewCommitStatusOptions ) error {
2017-04-21 07:32:31 -04:00
if opts . Repo == nil {
2019-06-30 03:57:59 -04:00
return fmt . Errorf ( "NewCommitStatus[nil, %s]: no repository specified" , opts . SHA )
2017-04-21 07:32:31 -04:00
}
2019-06-30 03:57:59 -04:00
repoPath := opts . Repo . RepoPath ( )
2017-04-21 07:32:31 -04:00
if opts . Creator == nil {
2019-06-30 03:57:59 -04:00
return fmt . Errorf ( "NewCommitStatus[%s, %s]: no user specified" , repoPath , opts . SHA )
2017-04-21 07:32:31 -04:00
}
2021-09-23 06:50:06 -04:00
// Get the next Status Index
idx , err := GetNextCommitStatusIndex ( opts . Repo . ID , opts . SHA )
if err != nil {
return fmt . Errorf ( "generate commit status index failed: %v" , err )
}
2021-09-19 07:49:59 -04:00
ctx , committer , err := db . TxContext ( )
if err != nil {
2019-06-30 03:57:59 -04:00
return fmt . Errorf ( "NewCommitStatus[repo_id: %d, user_id: %d, sha: %s]: %v" , opts . Repo . ID , opts . Creator . ID , opts . SHA , err )
2017-04-21 07:32:31 -04:00
}
2021-09-19 07:49:59 -04:00
defer committer . Close ( )
2017-04-21 07:32:31 -04:00
2019-06-30 03:57:59 -04:00
opts . CommitStatus . Description = strings . TrimSpace ( opts . CommitStatus . Description )
opts . CommitStatus . Context = strings . TrimSpace ( opts . CommitStatus . Context )
opts . CommitStatus . TargetURL = strings . TrimSpace ( opts . CommitStatus . TargetURL )
opts . CommitStatus . SHA = opts . SHA
opts . CommitStatus . CreatorID = opts . Creator . ID
opts . CommitStatus . RepoID = opts . Repo . ID
2021-09-23 06:50:06 -04:00
opts . CommitStatus . Index = idx
2019-06-30 03:57:59 -04:00
log . Debug ( "NewCommitStatus[%s, %s]: %d" , repoPath , opts . SHA , opts . CommitStatus . Index )
opts . CommitStatus . ContextHash = hashCommitStatusContext ( opts . CommitStatus . Context )
2017-04-21 07:32:31 -04:00
// Insert new CommitStatus
2021-09-23 11:45:36 -04:00
if _ , err = db . GetEngine ( ctx ) . Insert ( opts . CommitStatus ) ; err != nil {
2019-06-30 03:57:59 -04:00
return fmt . Errorf ( "Insert CommitStatus[%s, %s]: %v" , repoPath , opts . SHA , err )
2017-04-21 07:32:31 -04:00
}
2021-09-19 07:49:59 -04:00
return committer . Commit ( )
2017-04-21 07:32:31 -04:00
}
2017-05-07 10:40:31 -04:00
// SignCommitWithStatuses represents a commit with validation of signature and status state.
type SignCommitWithStatuses struct {
2020-12-19 22:13:12 -05:00
Status * CommitStatus
Statuses [ ] * CommitStatus
2021-12-10 03:14:24 -05:00
* asymkey_model . SignCommit
2017-05-07 10:40:31 -04:00
}
// ParseCommitsWithStatus checks commits latest statuses and calculates its worst status state
2021-12-10 03:14:24 -05:00
func ParseCommitsWithStatus ( oldCommits [ ] * asymkey_model . SignCommit , repo * repo_model . Repository ) [ ] * SignCommitWithStatuses {
2021-08-09 14:08:51 -04:00
newCommits := make ( [ ] * SignCommitWithStatuses , 0 , len ( oldCommits ) )
for _ , c := range oldCommits {
commit := & SignCommitWithStatuses {
SignCommit : c ,
2017-05-07 10:40:31 -04:00
}
2022-05-20 10:08:52 -04:00
statuses , _ , err := GetLatestCommitStatus ( db . DefaultContext , repo . ID , commit . ID . String ( ) , db . ListOptions { } )
2017-05-07 10:40:31 -04:00
if err != nil {
2019-04-02 03:48:31 -04:00
log . Error ( "GetLatestCommitStatus: %v" , err )
2017-05-07 10:40:31 -04:00
} else {
2020-12-19 22:13:12 -05:00
commit . Statuses = statuses
2017-09-14 02:51:32 -04:00
commit . Status = CalcCommitStatus ( statuses )
2017-05-07 10:40:31 -04:00
}
2021-08-09 14:08:51 -04:00
newCommits = append ( newCommits , commit )
2017-05-07 10:40:31 -04:00
}
return newCommits
}
2019-06-30 03:57:59 -04:00
// hashCommitStatusContext hash context
func hashCommitStatusContext ( context string ) string {
return fmt . Sprintf ( "%x" , sha1 . Sum ( [ ] byte ( context ) ) )
}
2022-06-12 11:51:54 -04:00
// ConvertFromGitCommit converts git commits into SignCommitWithStatuses
func ConvertFromGitCommit ( commits [ ] * git . Commit , repo * repo_model . Repository ) [ ] * SignCommitWithStatuses {
return ParseCommitsWithStatus (
asymkey_model . ParseCommitsWithSignature (
user_model . ValidateCommitsWithEmails ( commits ) ,
repo . GetTrustModel ( ) ,
func ( user * user_model . User ) ( bool , error ) {
return repo_model . IsOwnerMemberCollaborator ( repo , user . ID )
} ,
) ,
repo ,
)
}