* 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 | } | ||