code.gitea.io/gitea@v1.21.7/models/git/commit_status.go (about) 1 // Copyright 2017 Gitea. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package git 5 6 import ( 7 "context" 8 "crypto/sha1" 9 "errors" 10 "fmt" 11 "net/url" 12 "strconv" 13 "strings" 14 "time" 15 16 asymkey_model "code.gitea.io/gitea/models/asymkey" 17 "code.gitea.io/gitea/models/db" 18 repo_model "code.gitea.io/gitea/models/repo" 19 user_model "code.gitea.io/gitea/models/user" 20 "code.gitea.io/gitea/modules/git" 21 "code.gitea.io/gitea/modules/log" 22 "code.gitea.io/gitea/modules/setting" 23 api "code.gitea.io/gitea/modules/structs" 24 "code.gitea.io/gitea/modules/timeutil" 25 "code.gitea.io/gitea/modules/translation" 26 27 "xorm.io/builder" 28 ) 29 30 // CommitStatus holds a single Status of a single Commit 31 type CommitStatus struct { 32 ID int64 `xorm:"pk autoincr"` 33 Index int64 `xorm:"INDEX UNIQUE(repo_sha_index)"` 34 RepoID int64 `xorm:"INDEX UNIQUE(repo_sha_index)"` 35 Repo *repo_model.Repository `xorm:"-"` 36 State api.CommitStatusState `xorm:"VARCHAR(7) NOT NULL"` 37 SHA string `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_sha_index)"` 38 TargetURL string `xorm:"TEXT"` 39 Description string `xorm:"TEXT"` 40 ContextHash string `xorm:"char(40) index"` 41 Context string `xorm:"TEXT"` 42 Creator *user_model.User `xorm:"-"` 43 CreatorID int64 44 45 CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` 46 UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` 47 } 48 49 func init() { 50 db.RegisterModel(new(CommitStatus)) 51 db.RegisterModel(new(CommitStatusIndex)) 52 } 53 54 func postgresGetCommitStatusIndex(ctx context.Context, repoID int64, sha string) (int64, error) { 55 res, err := db.GetEngine(ctx).Query("INSERT INTO `commit_status_index` (repo_id, sha, max_index) "+ 56 "VALUES (?,?,1) ON CONFLICT (repo_id, sha) DO UPDATE SET max_index = `commit_status_index`.max_index+1 RETURNING max_index", 57 repoID, sha) 58 if err != nil { 59 return 0, err 60 } 61 if len(res) == 0 { 62 return 0, db.ErrGetResourceIndexFailed 63 } 64 return strconv.ParseInt(string(res[0]["max_index"]), 10, 64) 65 } 66 67 func mysqlGetCommitStatusIndex(ctx context.Context, repoID int64, sha string) (int64, error) { 68 if _, err := db.GetEngine(ctx).Exec("INSERT INTO `commit_status_index` (repo_id, sha, max_index) "+ 69 "VALUES (?,?,1) ON DUPLICATE KEY UPDATE max_index = max_index+1", 70 repoID, sha); err != nil { 71 return 0, err 72 } 73 74 var idx int64 75 _, err := db.GetEngine(ctx).SQL("SELECT max_index FROM `commit_status_index` WHERE repo_id = ? AND sha = ?", 76 repoID, sha).Get(&idx) 77 if err != nil { 78 return 0, err 79 } 80 if idx == 0 { 81 return 0, errors.New("cannot get the correct index") 82 } 83 return idx, nil 84 } 85 86 func mssqlGetCommitStatusIndex(ctx context.Context, repoID int64, sha string) (int64, error) { 87 if _, err := db.GetEngine(ctx).Exec(` 88 MERGE INTO commit_status_index WITH (HOLDLOCK) AS target 89 USING (SELECT ? AS repo_id, ? AS sha) AS source 90 (repo_id, sha) 91 ON target.repo_id = source.repo_id AND target.sha = source.sha 92 WHEN MATCHED 93 THEN UPDATE 94 SET max_index = max_index + 1 95 WHEN NOT MATCHED 96 THEN INSERT (repo_id, sha, max_index) 97 VALUES (?, ?, 1); 98 `, repoID, sha, repoID, sha); err != nil { 99 return 0, err 100 } 101 102 var idx int64 103 _, err := db.GetEngine(ctx).SQL("SELECT max_index FROM `commit_status_index` WHERE repo_id = ? AND sha = ?", 104 repoID, sha).Get(&idx) 105 if err != nil { 106 return 0, err 107 } 108 if idx == 0 { 109 return 0, errors.New("cannot get the correct index") 110 } 111 return idx, nil 112 } 113 114 // GetNextCommitStatusIndex retried 3 times to generate a resource index 115 func GetNextCommitStatusIndex(ctx context.Context, repoID int64, sha string) (int64, error) { 116 if !git.IsValidSHAPattern(sha) { 117 return 0, git.ErrInvalidSHA{SHA: sha} 118 } 119 120 switch { 121 case setting.Database.Type.IsPostgreSQL(): 122 return postgresGetCommitStatusIndex(ctx, repoID, sha) 123 case setting.Database.Type.IsMySQL(): 124 return mysqlGetCommitStatusIndex(ctx, repoID, sha) 125 case setting.Database.Type.IsMSSQL(): 126 return mssqlGetCommitStatusIndex(ctx, repoID, sha) 127 } 128 129 e := db.GetEngine(ctx) 130 131 // try to update the max_index to next value, and acquire the write-lock for the record 132 res, err := e.Exec("UPDATE `commit_status_index` SET max_index=max_index+1 WHERE repo_id=? AND sha=?", repoID, sha) 133 if err != nil { 134 return 0, fmt.Errorf("update failed: %w", err) 135 } 136 affected, err := res.RowsAffected() 137 if err != nil { 138 return 0, err 139 } 140 if affected == 0 { 141 // this slow path is only for the first time of creating a resource index 142 _, errIns := e.Exec("INSERT INTO `commit_status_index` (repo_id, sha, max_index) VALUES (?, ?, 0)", repoID, sha) 143 res, err = e.Exec("UPDATE `commit_status_index` SET max_index=max_index+1 WHERE repo_id=? AND sha=?", repoID, sha) 144 if err != nil { 145 return 0, fmt.Errorf("update2 failed: %w", err) 146 } 147 affected, err = res.RowsAffected() 148 if err != nil { 149 return 0, fmt.Errorf("RowsAffected failed: %w", err) 150 } 151 // if the update still can not update any records, the record must not exist and there must be some errors (insert error) 152 if affected == 0 { 153 if errIns == nil { 154 return 0, errors.New("impossible error when GetNextCommitStatusIndex, insert and update both succeeded but no record is updated") 155 } 156 return 0, fmt.Errorf("insert failed: %w", errIns) 157 } 158 } 159 160 // now, the new index is in database (protected by the transaction and write-lock) 161 var newIdx int64 162 has, err := e.SQL("SELECT max_index FROM `commit_status_index` WHERE repo_id=? AND sha=?", repoID, sha).Get(&newIdx) 163 if err != nil { 164 return 0, fmt.Errorf("select failed: %w", err) 165 } 166 if !has { 167 return 0, errors.New("impossible error when GetNextCommitStatusIndex, upsert succeeded but no record can be selected") 168 } 169 return newIdx, nil 170 } 171 172 func (status *CommitStatus) loadAttributes(ctx context.Context) (err error) { 173 if status.Repo == nil { 174 status.Repo, err = repo_model.GetRepositoryByID(ctx, status.RepoID) 175 if err != nil { 176 return fmt.Errorf("getRepositoryByID [%d]: %w", status.RepoID, err) 177 } 178 } 179 if status.Creator == nil && status.CreatorID > 0 { 180 status.Creator, err = user_model.GetUserByID(ctx, status.CreatorID) 181 if err != nil { 182 return fmt.Errorf("getUserByID [%d]: %w", status.CreatorID, err) 183 } 184 } 185 return nil 186 } 187 188 // APIURL returns the absolute APIURL to this commit-status. 189 func (status *CommitStatus) APIURL(ctx context.Context) string { 190 _ = status.loadAttributes(ctx) 191 return status.Repo.APIURL() + "/statuses/" + url.PathEscape(status.SHA) 192 } 193 194 // LocaleString returns the locale string name of the Status 195 func (status *CommitStatus) LocaleString(lang translation.Locale) string { 196 return lang.Tr("repo.commitstatus." + status.State.String()) 197 } 198 199 // CalcCommitStatus returns commit status state via some status, the commit statues should order by id desc 200 func CalcCommitStatus(statuses []*CommitStatus) *CommitStatus { 201 var lastStatus *CommitStatus 202 state := api.CommitStatusSuccess 203 for _, status := range statuses { 204 if status.State.NoBetterThan(state) { 205 state = status.State 206 lastStatus = status 207 } 208 } 209 if lastStatus == nil { 210 if len(statuses) > 0 { 211 lastStatus = statuses[0] 212 } else { 213 lastStatus = &CommitStatus{} 214 } 215 } 216 return lastStatus 217 } 218 219 // CommitStatusOptions holds the options for query commit statuses 220 type CommitStatusOptions struct { 221 db.ListOptions 222 RepoID int64 223 SHA string 224 State string 225 SortType string 226 } 227 228 func (opts *CommitStatusOptions) ToConds() builder.Cond { 229 var cond builder.Cond = builder.Eq{ 230 "repo_id": opts.RepoID, 231 "sha": opts.SHA, 232 } 233 234 switch opts.State { 235 case "pending", "success", "error", "failure", "warning": 236 cond = cond.And(builder.Eq{ 237 "state": opts.State, 238 }) 239 } 240 241 return cond 242 } 243 244 func (opts *CommitStatusOptions) ToOrders() string { 245 switch opts.SortType { 246 case "oldest": 247 return "created_unix ASC" 248 case "recentupdate": 249 return "updated_unix DESC" 250 case "leastupdate": 251 return "updated_unix ASC" 252 case "leastindex": 253 return "`index` DESC" 254 case "highestindex": 255 return "`index` ASC" 256 default: 257 return "created_unix DESC" 258 } 259 } 260 261 // GetCommitStatuses returns all statuses for a given commit. 262 func GetCommitStatuses(ctx context.Context, opts *CommitStatusOptions) ([]*CommitStatus, int64, error) { 263 sess := db.GetEngine(ctx). 264 Where(opts.ToConds()). 265 OrderBy(opts.ToOrders()) 266 267 db.SetSessionPagination(sess, opts) 268 269 statuses := make([]*CommitStatus, 0, opts.PageSize) 270 count, err := sess.FindAndCount(&statuses) 271 return statuses, count, err 272 } 273 274 // CommitStatusIndex represents a table for commit status index 275 type CommitStatusIndex struct { 276 ID int64 277 RepoID int64 `xorm:"unique(repo_sha)"` 278 SHA string `xorm:"unique(repo_sha)"` 279 MaxIndex int64 `xorm:"index"` 280 } 281 282 // GetLatestCommitStatus returns all statuses with a unique context for a given commit. 283 func GetLatestCommitStatus(ctx context.Context, repoID int64, sha string, listOptions db.ListOptions) ([]*CommitStatus, int64, error) { 284 ids := make([]int64, 0, 10) 285 sess := db.GetEngine(ctx).Table(&CommitStatus{}). 286 Where("repo_id = ?", repoID).And("sha = ?", sha). 287 Select("max( id ) as id"). 288 GroupBy("context_hash").OrderBy("max( id ) desc") 289 if !listOptions.IsListAll() { 290 sess = db.SetSessionPagination(sess, &listOptions) 291 } 292 count, err := sess.FindAndCount(&ids) 293 if err != nil { 294 return nil, count, err 295 } 296 statuses := make([]*CommitStatus, 0, len(ids)) 297 if len(ids) == 0 { 298 return statuses, count, nil 299 } 300 return statuses, count, db.GetEngine(ctx).In("id", ids).Find(&statuses) 301 } 302 303 // GetLatestCommitStatusForPairs returns all statuses with a unique context for a given list of repo-sha pairs 304 func GetLatestCommitStatusForPairs(ctx context.Context, repoIDsToLatestCommitSHAs map[int64]string, listOptions db.ListOptions) (map[int64][]*CommitStatus, error) { 305 type result struct { 306 ID int64 307 RepoID int64 308 } 309 310 results := make([]result, 0, len(repoIDsToLatestCommitSHAs)) 311 312 sess := db.GetEngine(ctx).Table(&CommitStatus{}) 313 314 // Create a disjunction of conditions for each repoID and SHA pair 315 conds := make([]builder.Cond, 0, len(repoIDsToLatestCommitSHAs)) 316 for repoID, sha := range repoIDsToLatestCommitSHAs { 317 conds = append(conds, builder.Eq{"repo_id": repoID, "sha": sha}) 318 } 319 sess = sess.Where(builder.Or(conds...)). 320 Select("max( id ) as id, repo_id"). 321 GroupBy("context_hash, repo_id").OrderBy("max( id ) desc") 322 323 sess = db.SetSessionPagination(sess, &listOptions) 324 325 err := sess.Find(&results) 326 if err != nil { 327 return nil, err 328 } 329 330 ids := make([]int64, 0, len(results)) 331 repoStatuses := make(map[int64][]*CommitStatus) 332 for _, result := range results { 333 ids = append(ids, result.ID) 334 } 335 336 statuses := make([]*CommitStatus, 0, len(ids)) 337 if len(ids) > 0 { 338 err = db.GetEngine(ctx).In("id", ids).Find(&statuses) 339 if err != nil { 340 return nil, err 341 } 342 343 // Group the statuses by repo ID 344 for _, status := range statuses { 345 repoStatuses[status.RepoID] = append(repoStatuses[status.RepoID], status) 346 } 347 } 348 349 return repoStatuses, nil 350 } 351 352 // GetLatestCommitStatusForRepoCommitIDs returns all statuses with a unique context for a given list of repo-sha pairs 353 func GetLatestCommitStatusForRepoCommitIDs(ctx context.Context, repoID int64, commitIDs []string) (map[string][]*CommitStatus, error) { 354 type result struct { 355 ID int64 356 Sha string 357 } 358 359 results := make([]result, 0, len(commitIDs)) 360 361 sess := db.GetEngine(ctx).Table(&CommitStatus{}) 362 363 // Create a disjunction of conditions for each repoID and SHA pair 364 conds := make([]builder.Cond, 0, len(commitIDs)) 365 for _, sha := range commitIDs { 366 conds = append(conds, builder.Eq{"sha": sha}) 367 } 368 sess = sess.Where(builder.Eq{"repo_id": repoID}.And(builder.Or(conds...))). 369 Select("max( id ) as id, sha"). 370 GroupBy("context_hash, sha").OrderBy("max( id ) desc") 371 372 err := sess.Find(&results) 373 if err != nil { 374 return nil, err 375 } 376 377 ids := make([]int64, 0, len(results)) 378 repoStatuses := make(map[string][]*CommitStatus) 379 for _, result := range results { 380 ids = append(ids, result.ID) 381 } 382 383 statuses := make([]*CommitStatus, 0, len(ids)) 384 if len(ids) > 0 { 385 err = db.GetEngine(ctx).In("id", ids).Find(&statuses) 386 if err != nil { 387 return nil, err 388 } 389 390 // Group the statuses by repo ID 391 for _, status := range statuses { 392 repoStatuses[status.SHA] = append(repoStatuses[status.SHA], status) 393 } 394 } 395 396 return repoStatuses, nil 397 } 398 399 // FindRepoRecentCommitStatusContexts returns repository's recent commit status contexts 400 func FindRepoRecentCommitStatusContexts(ctx context.Context, repoID int64, before time.Duration) ([]string, error) { 401 start := timeutil.TimeStampNow().AddDuration(-before) 402 ids := make([]int64, 0, 10) 403 if err := db.GetEngine(ctx).Table("commit_status"). 404 Where("repo_id = ?", repoID). 405 And("updated_unix >= ?", start). 406 Select("max( id ) as id"). 407 GroupBy("context_hash").OrderBy("max( id ) desc"). 408 Find(&ids); err != nil { 409 return nil, err 410 } 411 412 contexts := make([]string, 0, len(ids)) 413 if len(ids) == 0 { 414 return contexts, nil 415 } 416 return contexts, db.GetEngine(ctx).Select("context").Table("commit_status").In("id", ids).Find(&contexts) 417 } 418 419 // NewCommitStatusOptions holds options for creating a CommitStatus 420 type NewCommitStatusOptions struct { 421 Repo *repo_model.Repository 422 Creator *user_model.User 423 SHA string 424 CommitStatus *CommitStatus 425 } 426 427 // NewCommitStatus save commit statuses into database 428 func NewCommitStatus(ctx context.Context, opts NewCommitStatusOptions) error { 429 if opts.Repo == nil { 430 return fmt.Errorf("NewCommitStatus[nil, %s]: no repository specified", opts.SHA) 431 } 432 433 repoPath := opts.Repo.RepoPath() 434 if opts.Creator == nil { 435 return fmt.Errorf("NewCommitStatus[%s, %s]: no user specified", repoPath, opts.SHA) 436 } 437 438 if _, err := git.NewIDFromString(opts.SHA); err != nil { 439 return fmt.Errorf("NewCommitStatus[%s, %s]: invalid sha: %w", repoPath, opts.SHA, err) 440 } 441 442 ctx, committer, err := db.TxContext(ctx) 443 if err != nil { 444 return fmt.Errorf("NewCommitStatus[repo_id: %d, user_id: %d, sha: %s]: %w", opts.Repo.ID, opts.Creator.ID, opts.SHA, err) 445 } 446 defer committer.Close() 447 448 // Get the next Status Index 449 idx, err := GetNextCommitStatusIndex(ctx, opts.Repo.ID, opts.SHA) 450 if err != nil { 451 return fmt.Errorf("generate commit status index failed: %w", err) 452 } 453 454 opts.CommitStatus.Description = strings.TrimSpace(opts.CommitStatus.Description) 455 opts.CommitStatus.Context = strings.TrimSpace(opts.CommitStatus.Context) 456 opts.CommitStatus.TargetURL = strings.TrimSpace(opts.CommitStatus.TargetURL) 457 opts.CommitStatus.SHA = opts.SHA 458 opts.CommitStatus.CreatorID = opts.Creator.ID 459 opts.CommitStatus.RepoID = opts.Repo.ID 460 opts.CommitStatus.Index = idx 461 log.Debug("NewCommitStatus[%s, %s]: %d", repoPath, opts.SHA, opts.CommitStatus.Index) 462 463 opts.CommitStatus.ContextHash = hashCommitStatusContext(opts.CommitStatus.Context) 464 465 // Insert new CommitStatus 466 if _, err = db.GetEngine(ctx).Insert(opts.CommitStatus); err != nil { 467 return fmt.Errorf("insert CommitStatus[%s, %s]: %w", repoPath, opts.SHA, err) 468 } 469 470 return committer.Commit() 471 } 472 473 // SignCommitWithStatuses represents a commit with validation of signature and status state. 474 type SignCommitWithStatuses struct { 475 Status *CommitStatus 476 Statuses []*CommitStatus 477 *asymkey_model.SignCommit 478 } 479 480 // ParseCommitsWithStatus checks commits latest statuses and calculates its worst status state 481 func ParseCommitsWithStatus(ctx context.Context, oldCommits []*asymkey_model.SignCommit, repo *repo_model.Repository) []*SignCommitWithStatuses { 482 newCommits := make([]*SignCommitWithStatuses, 0, len(oldCommits)) 483 484 for _, c := range oldCommits { 485 commit := &SignCommitWithStatuses{ 486 SignCommit: c, 487 } 488 statuses, _, err := GetLatestCommitStatus(ctx, repo.ID, commit.ID.String(), db.ListOptions{}) 489 if err != nil { 490 log.Error("GetLatestCommitStatus: %v", err) 491 } else { 492 commit.Statuses = statuses 493 commit.Status = CalcCommitStatus(statuses) 494 } 495 496 newCommits = append(newCommits, commit) 497 } 498 return newCommits 499 } 500 501 // hashCommitStatusContext hash context 502 func hashCommitStatusContext(context string) string { 503 return fmt.Sprintf("%x", sha1.Sum([]byte(context))) 504 } 505 506 // ConvertFromGitCommit converts git commits into SignCommitWithStatuses 507 func ConvertFromGitCommit(ctx context.Context, commits []*git.Commit, repo *repo_model.Repository) []*SignCommitWithStatuses { 508 return ParseCommitsWithStatus(ctx, 509 asymkey_model.ParseCommitsWithSignature( 510 ctx, 511 user_model.ValidateCommitsWithEmails(ctx, commits), 512 repo.GetTrustModel(), 513 func(user *user_model.User) (bool, error) { 514 return repo_model.IsOwnerMemberCollaborator(ctx, repo, user.ID) 515 }, 516 ), 517 repo, 518 ) 519 }