* Move access and repo permission to models/perm/access * fix test * Move some git related files into sub package models/git * Fix build * fix git test * move lfs to sub package * move more git related functions to models/git * Move functions sequence * Some improvements per @KN4CK3R and @delvh
Showing the changes to models/git/commit_status.go (1 of 67 files). Show all
Unified · Split
| @@ -0,0 +1,370 @@ | ||
| 1 | +// Copyright 2017 Gitea. All rights reserved. | |
| 2 | +// Use of this source code is governed by a MIT-style | |
| 3 | +// license that can be found in the LICENSE file. | |
| 4 | + | |
| 5 | +package git | |
| 6 | + | |
| 7 | +import ( | |
| 8 | + "context" | |
| 9 | + "crypto/sha1" | |
| 10 | + "fmt" | |
| 11 | + "net/url" | |
| 12 | + "strings" | |
| 13 | + "time" | |
| 14 | + | |
| 15 | + asymkey_model "code.gitea.io/gitea/models/asymkey" | |
| 16 | + "code.gitea.io/gitea/models/db" | |
| 17 | + repo_model "code.gitea.io/gitea/models/repo" | |
| 18 | + user_model "code.gitea.io/gitea/models/user" | |
| 19 | + "code.gitea.io/gitea/modules/git" | |
| 20 | + "code.gitea.io/gitea/modules/log" | |
| 21 | + "code.gitea.io/gitea/modules/setting" | |
| 22 | + api "code.gitea.io/gitea/modules/structs" | |
| 23 | + "code.gitea.io/gitea/modules/timeutil" | |
| 24 | + | |
| 25 | + "xorm.io/xorm" | |
| 26 | +) | |
| 27 | + | |
| 28 | +// CommitStatus holds a single Status of a single Commit | |
| 29 | +type CommitStatus struct { | |
| 30 | + ID int64 `xorm:"pk autoincr"` | |
| 31 | + Index int64 `xorm:"INDEX UNIQUE(repo_sha_index)"` | |
| 32 | + RepoID int64 `xorm:"INDEX UNIQUE(repo_sha_index)"` | |
| 33 | + Repo *repo_model.Repository `xorm:"-"` | |
| 34 | + State api.CommitStatusState `xorm:"VARCHAR(7) NOT NULL"` | |
| 35 | + SHA string `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_sha_index)"` | |
| 36 | + TargetURL string `xorm:"TEXT"` | |
| 37 | + Description string `xorm:"TEXT"` | |
| 38 | + ContextHash string `xorm:"char(40) index"` | |
| 39 | + Context string `xorm:"TEXT"` | |
| 40 | + Creator *user_model.User `xorm:"-"` | |
| 41 | + CreatorID int64 | |
| 42 | + | |
| 43 | + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` | |
| 44 | + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` | |
| 45 | +} | |
| 46 | + | |
| 47 | +func init() { | |
| 48 | + db.RegisterModel(new(CommitStatus)) | |
| 49 | + db.RegisterModel(new(CommitStatusIndex)) | |
| 50 | +} | |
| 51 | + | |
| 52 | +// upsertCommitStatusIndex the function will not return until it acquires the lock or receives an error. | |
| 53 | +func upsertCommitStatusIndex(ctx context.Context, repoID int64, sha string) (err error) { | |
| 54 | + // An atomic UPSERT operation (INSERT/UPDATE) is the only operation | |
| 55 | + // that ensures that the key is actually locked. | |
| 56 | + switch { | |
| 57 | + case setting.Database.UseSQLite3 || setting.Database.UsePostgreSQL: | |
| 58 | + _, err = db.Exec(ctx, "INSERT INTO `commit_status_index` (repo_id, sha, max_index) "+ | |
| 59 | + "VALUES (?,?,1) ON CONFLICT (repo_id,sha) DO UPDATE SET max_index = `commit_status_index`.max_index+1", | |
| 60 | + repoID, sha) | |
| 61 | + case setting.Database.UseMySQL: | |
| 62 | + _, err = db.Exec(ctx, "INSERT INTO `commit_status_index` (repo_id, sha, max_index) "+ | |
| 63 | + "VALUES (?,?,1) ON DUPLICATE KEY UPDATE max_index = max_index+1", | |
| 64 | + repoID, sha) | |
| 65 | + case setting.Database.UseMSSQL: | |
| 66 | + // https://weblogs.sqlteam.com/dang/2009/01/31/upsert-race-condition-with-merge/ | |
| 67 | + _, err = db.Exec(ctx, "MERGE `commit_status_index` WITH (HOLDLOCK) as target "+ | |
| 68 | + "USING (SELECT ? AS repo_id, ? AS sha) AS src "+ | |
| 69 | + "ON src.repo_id = target.repo_id AND src.sha = target.sha "+ | |
| 70 | + "WHEN MATCHED THEN UPDATE SET target.max_index = target.max_index+1 "+ | |
| 71 | + "WHEN NOT MATCHED THEN INSERT (repo_id, sha, max_index) "+ | |
| 72 | + "VALUES (src.repo_id, src.sha, 1);", | |
| 73 | + repoID, sha) | |
| 74 | + default: | |
| 75 | + return fmt.Errorf("database type not supported") | |
| 76 | + } | |
| 77 | + return | |
| 78 | +} | |
| 79 | + | |
| 80 | +// GetNextCommitStatusIndex retried 3 times to generate a resource index | |
| 81 | +func GetNextCommitStatusIndex(repoID int64, sha string) (int64, error) { | |
| 82 | + for i := 0; i < db.MaxDupIndexAttempts; i++ { | |
| 83 | + idx, err := getNextCommitStatusIndex(repoID, sha) | |
| 84 | + if err == db.ErrResouceOutdated { | |
| 85 | + continue | |
| 86 | + } | |
| 87 | + if err != nil { | |
| 88 | + return 0, err | |
| 89 | + } | |
| 90 | + return idx, nil | |
| 91 | + } | |
| 92 | + return 0, db.ErrGetResourceIndexFailed | |
| 93 | +} | |
| 94 | + | |
| 95 | +// getNextCommitStatusIndex return the next index | |
| 96 | +func getNextCommitStatusIndex(repoID int64, sha string) (int64, error) { | |
| 97 | + ctx, commiter, err := db.TxContext() | |
| 98 | + if err != nil { | |
| 99 | + return 0, err | |
| 100 | + } | |
| 101 | + defer commiter.Close() | |
| 102 | + | |
| 103 | + var preIdx int64 | |
| 104 | + _, err = db.GetEngine(ctx).SQL("SELECT max_index FROM `commit_status_index` WHERE repo_id = ? AND sha = ?", repoID, sha).Get(&preIdx) | |
| 105 | + if err != nil { | |
| 106 | + return 0, err | |
| 107 | + } | |
| 108 | + | |
| 109 | + if err := upsertCommitStatusIndex(ctx, repoID, sha); err != nil { | |
| 110 | + return 0, err | |
| 111 | + } | |
| 112 | + | |
| 113 | + var curIdx int64 | |
| 114 | + 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) | |
| 115 | + if err != nil { | |
| 116 | + return 0, err | |
| 117 | + } | |
| 118 | + if !has { | |
| 119 | + return 0, db.ErrResouceOutdated | |
| 120 | + } | |
| 121 | + if err := commiter.Commit(); err != nil { | |
| 122 | + return 0, err | |
| 123 | + } | |
| 124 | + return curIdx, nil | |
| 125 | +} | |
| 126 | + | |
| 127 | +func (status *CommitStatus) loadAttributes(ctx context.Context) (err error) { | |
| 128 | + if status.Repo == nil { | |
| 129 | + status.Repo, err = repo_model.GetRepositoryByIDCtx(ctx, status.RepoID) | |
| 130 | + if err != nil { | |
| 131 | + return fmt.Errorf("getRepositoryByID [%d]: %v", status.RepoID, err) | |
| 132 | + } | |
| 133 | + } | |
| 134 | + if status.Creator == nil && status.CreatorID > 0 { | |
| 135 | + status.Creator, err = user_model.GetUserByIDCtx(ctx, status.CreatorID) | |
| 136 | + if err != nil { | |
| 137 | + return fmt.Errorf("getUserByID [%d]: %v", status.CreatorID, err) | |
| 138 | + } | |
| 139 | + } | |
| 140 | + return nil | |
| 141 | +} | |
| 142 | + | |
| 143 | +// APIURL returns the absolute APIURL to this commit-status. | |
| 144 | +func (status *CommitStatus) APIURL() string { | |
| 145 | + _ = status.loadAttributes(db.DefaultContext) | |
| 146 | + return status.Repo.APIURL() + "/statuses/" + url.PathEscape(status.SHA) | |
| 147 | +} | |
| 148 | + | |
| 149 | +// CalcCommitStatus returns commit status state via some status, the commit statues should order by id desc | |
| 150 | +func CalcCommitStatus(statuses []*CommitStatus) *CommitStatus { | |
| 151 | + var lastStatus *CommitStatus | |
| 152 | + var state api.CommitStatusState | |
| 153 | + for _, status := range statuses { | |
| 154 | + if status.State.NoBetterThan(state) { | |
| 155 | + state = status.State | |
| 156 | + lastStatus = status | |
| 157 | + } | |
| 158 | + } | |
| 159 | + if lastStatus == nil { | |
| 160 | + if len(statuses) > 0 { | |
| 161 | + lastStatus = statuses[0] | |
| 162 | + } else { | |
| 163 | + lastStatus = &CommitStatus{} | |
| 164 | + } | |
| 165 | + } | |
| 166 | + return lastStatus | |
| 167 | +} | |
| 168 | + | |
| 169 | +// CommitStatusOptions holds the options for query commit statuses | |
| 170 | +type CommitStatusOptions struct { | |
| 171 | + db.ListOptions | |
| 172 | + State string | |
| 173 | + SortType string | |
| 174 | +} | |
| 175 | + | |
| 176 | +// GetCommitStatuses returns all statuses for a given commit. | |
| 177 | +func GetCommitStatuses(repo *repo_model.Repository, sha string, opts *CommitStatusOptions) ([]*CommitStatus, int64, error) { | |
| 178 | + if opts.Page <= 0 { | |
| 179 | + opts.Page = 1 | |
| 180 | + } | |
| 181 | + if opts.PageSize <= 0 { | |
| 182 | + opts.Page = setting.ItemsPerPage | |
| 183 | + } | |
| 184 | + | |
| 185 | + countSession := listCommitStatusesStatement(repo, sha, opts) | |
| 186 | + countSession = db.SetSessionPagination(countSession, opts) | |
| 187 | + maxResults, err := countSession.Count(new(CommitStatus)) | |
| 188 | + if err != nil { | |
| 189 | + log.Error("Count PRs: %v", err) | |
| 190 | + return nil, maxResults, err | |
| 191 | + } | |
| 192 | + | |
| 193 | + statuses := make([]*CommitStatus, 0, opts.PageSize) | |
| 194 | + findSession := listCommitStatusesStatement(repo, sha, opts) | |
| 195 | + findSession = db.SetSessionPagination(findSession, opts) | |
| 196 | + sortCommitStatusesSession(findSession, opts.SortType) | |
| 197 | + return statuses, maxResults, findSession.Find(&statuses) | |
| 198 | +} | |
| 199 | + | |
| 200 | +func listCommitStatusesStatement(repo *repo_model.Repository, sha string, opts *CommitStatusOptions) *xorm.Session { | |
| 201 | + sess := db.GetEngine(db.DefaultContext).Where("repo_id = ?", repo.ID).And("sha = ?", sha) | |
| 202 | + switch opts.State { | |
| 203 | + case "pending", "success", "error", "failure", "warning": | |
| 204 | + sess.And("state = ?", opts.State) | |
| 205 | + } | |
| 206 | + return sess | |
| 207 | +} | |
| 208 | + | |
| 209 | +func sortCommitStatusesSession(sess *xorm.Session, sortType string) { | |
| 210 | + switch sortType { | |
| 211 | + case "oldest": | |
| 212 | + sess.Asc("created_unix") | |
| 213 | + case "recentupdate": | |
| 214 | + sess.Desc("updated_unix") | |
| 215 | + case "leastupdate": | |
| 216 | + sess.Asc("updated_unix") | |
| 217 | + case "leastindex": | |
| 218 | + sess.Desc("index") | |
| 219 | + case "highestindex": | |
| 220 | + sess.Asc("index") | |
| 221 | + default: | |
| 222 | + sess.Desc("created_unix") | |
| 223 | + } | |
| 224 | +} | |
| 225 | + | |
| 226 | +// CommitStatusIndex represents a table for commit status index | |
| 227 | +type CommitStatusIndex struct { | |
| 228 | + ID int64 | |
| 229 | + RepoID int64 `xorm:"unique(repo_sha)"` | |
| 230 | + SHA string `xorm:"unique(repo_sha)"` | |
| 231 | + MaxIndex int64 `xorm:"index"` | |
| 232 | +} | |
| 233 | + | |
| 234 | +// GetLatestCommitStatus returns all statuses with a unique context for a given commit. | |
| 235 | +func GetLatestCommitStatus(ctx context.Context, repoID int64, sha string, listOptions db.ListOptions) ([]*CommitStatus, int64, error) { | |
| 236 | + ids := make([]int64, 0, 10) | |
| 237 | + sess := db.GetEngine(ctx).Table(&CommitStatus{}). | |
| 238 | + Where("repo_id = ?", repoID).And("sha = ?", sha). | |
| 239 | + Select("max( id ) as id"). | |
| 240 | + GroupBy("context_hash").OrderBy("max( id ) desc") | |
| 241 | + | |
| 242 | + sess = db.SetSessionPagination(sess, &listOptions) | |
| 243 | + | |
| 244 | + count, err := sess.FindAndCount(&ids) | |
| 245 | + if err != nil { | |
| 246 | + return nil, count, err | |
| 247 | + } | |
| 248 | + statuses := make([]*CommitStatus, 0, len(ids)) | |
| 249 | + if len(ids) == 0 { | |
| 250 | + return statuses, count, nil | |
| 251 | + } | |
| 252 | + return statuses, count, db.GetEngine(ctx).In("id", ids).Find(&statuses) | |
| 253 | +} | |
| 254 | + | |
| 255 | +// FindRepoRecentCommitStatusContexts returns repository's recent commit status contexts | |
| 256 | +func FindRepoRecentCommitStatusContexts(repoID int64, before time.Duration) ([]string, error) { | |
| 257 | + start := timeutil.TimeStampNow().AddDuration(-before) | |
| 258 | + ids := make([]int64, 0, 10) | |
| 259 | + if err := db.GetEngine(db.DefaultContext).Table("commit_status"). | |
| 260 | + Where("repo_id = ?", repoID). | |
| 261 | + And("updated_unix >= ?", start). | |
| 262 | + Select("max( id ) as id"). | |
| 263 | + GroupBy("context_hash").OrderBy("max( id ) desc"). | |
| 264 | + Find(&ids); err != nil { | |
| 265 | + return nil, err | |
| 266 | + } | |
| 267 | + | |
| 268 | + contexts := make([]string, 0, len(ids)) | |
| 269 | + if len(ids) == 0 { | |
| 270 | + return contexts, nil | |
| 271 | + } | |
| 272 | + return contexts, db.GetEngine(db.DefaultContext).Select("context").Table("commit_status").In("id", ids).Find(&contexts) | |
| 273 | +} | |
| 274 | + | |
| 275 | +// NewCommitStatusOptions holds options for creating a CommitStatus | |
| 276 | +type NewCommitStatusOptions struct { | |
| 277 | + Repo *repo_model.Repository | |
| 278 | + Creator *user_model.User | |
| 279 | + SHA string | |
| 280 | + CommitStatus *CommitStatus | |
| 281 | +} | |
| 282 | + | |
| 283 | +// NewCommitStatus save commit statuses into database | |
| 284 | +func NewCommitStatus(opts NewCommitStatusOptions) error { | |
| 285 | + if opts.Repo == nil { | |
| 286 | + return fmt.Errorf("NewCommitStatus[nil, %s]: no repository specified", opts.SHA) | |
| 287 | + } | |
| 288 | + | |
| 289 | + repoPath := opts.Repo.RepoPath() | |
| 290 | + if opts.Creator == nil { | |
| 291 | + return fmt.Errorf("NewCommitStatus[%s, %s]: no user specified", repoPath, opts.SHA) | |
| 292 | + } | |
| 293 | + | |
| 294 | + // Get the next Status Index | |
| 295 | + idx, err := GetNextCommitStatusIndex(opts.Repo.ID, opts.SHA) | |
| 296 | + if err != nil { | |
| 297 | + return fmt.Errorf("generate commit status index failed: %v", err) | |
| 298 | + } | |
| 299 | + | |
| 300 | + ctx, committer, err := db.TxContext() | |
| 301 | + if err != nil { | |
| 302 | + return fmt.Errorf("NewCommitStatus[repo_id: %d, user_id: %d, sha: %s]: %v", opts.Repo.ID, opts.Creator.ID, opts.SHA, err) | |
| 303 | + } | |
| 304 | + defer committer.Close() | |
| 305 | + | |
| 306 | + opts.CommitStatus.Description = strings.TrimSpace(opts.CommitStatus.Description) | |
| 307 | + opts.CommitStatus.Context = strings.TrimSpace(opts.CommitStatus.Context) | |
| 308 | + opts.CommitStatus.TargetURL = strings.TrimSpace(opts.CommitStatus.TargetURL) | |
| 309 | + opts.CommitStatus.SHA = opts.SHA | |
| 310 | + opts.CommitStatus.CreatorID = opts.Creator.ID | |
| 311 | + opts.CommitStatus.RepoID = opts.Repo.ID | |
| 312 | + opts.CommitStatus.Index = idx | |
| 313 | + log.Debug("NewCommitStatus[%s, %s]: %d", repoPath, opts.SHA, opts.CommitStatus.Index) | |
| 314 | + | |
| 315 | + opts.CommitStatus.ContextHash = hashCommitStatusContext(opts.CommitStatus.Context) | |
| 316 | + | |
| 317 | + // Insert new CommitStatus | |
| 318 | + if _, err = db.GetEngine(ctx).Insert(opts.CommitStatus); err != nil { | |
| 319 | + return fmt.Errorf("Insert CommitStatus[%s, %s]: %v", repoPath, opts.SHA, err) | |
| 320 | + } | |
| 321 | + | |
| 322 | + return committer.Commit() | |
| 323 | +} | |
| 324 | + | |
| 325 | +// SignCommitWithStatuses represents a commit with validation of signature and status state. | |
| 326 | +type SignCommitWithStatuses struct { | |
| 327 | + Status *CommitStatus | |
| 328 | + Statuses []*CommitStatus | |
| 329 | + *asymkey_model.SignCommit | |
| 330 | +} | |
| 331 | + | |
| 332 | +// ParseCommitsWithStatus checks commits latest statuses and calculates its worst status state | |
| 333 | +func ParseCommitsWithStatus(oldCommits []*asymkey_model.SignCommit, repo *repo_model.Repository) []*SignCommitWithStatuses { | |
| 334 | + newCommits := make([]*SignCommitWithStatuses, 0, len(oldCommits)) | |
| 335 | + | |
| 336 | + for _, c := range oldCommits { | |
| 337 | + commit := &SignCommitWithStatuses{ | |
| 338 | + SignCommit: c, | |
| 339 | + } | |
| 340 | + statuses, _, err := GetLatestCommitStatus(db.DefaultContext, repo.ID, commit.ID.String(), db.ListOptions{}) | |
| 341 | + if err != nil { | |
| 342 | + log.Error("GetLatestCommitStatus: %v", err) | |
| 343 | + } else { | |
| 344 | + commit.Statuses = statuses | |
| 345 | + commit.Status = CalcCommitStatus(statuses) | |
| 346 | + } | |
| 347 | + | |
| 348 | + newCommits = append(newCommits, commit) | |
| 349 | + } | |
| 350 | + return newCommits | |
| 351 | +} | |
| 352 | + | |
| 353 | +// hashCommitStatusContext hash context | |
| 354 | +func hashCommitStatusContext(context string) string { | |
| 355 | + return fmt.Sprintf("%x", sha1.Sum([]byte(context))) | |
| 356 | +} | |
| 357 | + | |
| 358 | +// ConvertFromGitCommit converts git commits into SignCommitWithStatuses | |
| 359 | +func ConvertFromGitCommit(commits []*git.Commit, repo *repo_model.Repository) []*SignCommitWithStatuses { | |
| 360 | + return ParseCommitsWithStatus( | |
| 361 | + asymkey_model.ParseCommitsWithSignature( | |
| 362 | + user_model.ValidateCommitsWithEmails(commits), | |
| 363 | + repo.GetTrustModel(), | |
| 364 | + func(user *user_model.User) (bool, error) { | |
| 365 | + return repo_model.IsOwnerMemberCollaborator(repo, user.ID) | |
| 366 | + }, | |
| 367 | + ), | |
| 368 | + repo, | |
| 369 | + ) | |
| 370 | +} | |