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