code.gitea.io/gitea@v1.22.3/models/repo.go (about) 1 // Copyright 2014 The Gogs Authors. All rights reserved. 2 // Copyright 2017 The Gitea Authors. All rights reserved. 3 // SPDX-License-Identifier: MIT 4 5 package models 6 7 import ( 8 "context" 9 "fmt" 10 "strconv" 11 12 _ "image/jpeg" // Needed for jpeg support 13 14 asymkey_model "code.gitea.io/gitea/models/asymkey" 15 "code.gitea.io/gitea/models/db" 16 issues_model "code.gitea.io/gitea/models/issues" 17 access_model "code.gitea.io/gitea/models/perm/access" 18 repo_model "code.gitea.io/gitea/models/repo" 19 "code.gitea.io/gitea/models/unit" 20 user_model "code.gitea.io/gitea/models/user" 21 "code.gitea.io/gitea/modules/log" 22 ) 23 24 // Init initialize model 25 func Init(ctx context.Context) error { 26 return unit.LoadUnitConfig() 27 } 28 29 type repoChecker struct { 30 querySQL func(ctx context.Context) ([]map[string][]byte, error) 31 correctSQL func(ctx context.Context, id int64) error 32 desc string 33 } 34 35 func repoStatsCheck(ctx context.Context, checker *repoChecker) { 36 results, err := checker.querySQL(ctx) 37 if err != nil { 38 log.Error("Select %s: %v", checker.desc, err) 39 return 40 } 41 for _, result := range results { 42 id, _ := strconv.ParseInt(string(result["id"]), 10, 64) 43 select { 44 case <-ctx.Done(): 45 log.Warn("CheckRepoStats: Cancelled before checking %s for with id=%d", checker.desc, id) 46 return 47 default: 48 } 49 log.Trace("Updating %s: %d", checker.desc, id) 50 err = checker.correctSQL(ctx, id) 51 if err != nil { 52 log.Error("Update %s[%d]: %v", checker.desc, id, err) 53 } 54 } 55 } 56 57 func StatsCorrectSQL(ctx context.Context, sql string, id int64) error { 58 _, err := db.GetEngine(ctx).Exec(sql, id, id) 59 return err 60 } 61 62 func repoStatsCorrectNumWatches(ctx context.Context, id int64) error { 63 return StatsCorrectSQL(ctx, "UPDATE `repository` SET num_watches=(SELECT COUNT(*) FROM `watch` WHERE repo_id=? AND mode<>2) WHERE id=?", id) 64 } 65 66 func repoStatsCorrectNumStars(ctx context.Context, id int64) error { 67 return StatsCorrectSQL(ctx, "UPDATE `repository` SET num_stars=(SELECT COUNT(*) FROM `star` WHERE repo_id=?) WHERE id=?", id) 68 } 69 70 func labelStatsCorrectNumIssues(ctx context.Context, id int64) error { 71 return StatsCorrectSQL(ctx, "UPDATE `label` SET num_issues=(SELECT COUNT(*) FROM `issue_label` WHERE label_id=?) WHERE id=?", id) 72 } 73 74 func labelStatsCorrectNumIssuesRepo(ctx context.Context, id int64) error { 75 _, err := db.GetEngine(ctx).Exec("UPDATE `label` SET num_issues=(SELECT COUNT(*) FROM `issue_label` WHERE label_id=id) WHERE repo_id=?", id) 76 return err 77 } 78 79 func labelStatsCorrectNumClosedIssues(ctx context.Context, id int64) error { 80 _, err := db.GetEngine(ctx).Exec("UPDATE `label` SET num_closed_issues=(SELECT COUNT(*) FROM `issue_label`,`issue` WHERE `issue_label`.label_id=`label`.id AND `issue_label`.issue_id=`issue`.id AND `issue`.is_closed=?) WHERE `label`.id=?", true, id) 81 return err 82 } 83 84 func labelStatsCorrectNumClosedIssuesRepo(ctx context.Context, id int64) error { 85 _, err := db.GetEngine(ctx).Exec("UPDATE `label` SET num_closed_issues=(SELECT COUNT(*) FROM `issue_label`,`issue` WHERE `issue_label`.label_id=`label`.id AND `issue_label`.issue_id=`issue`.id AND `issue`.is_closed=?) WHERE `label`.repo_id=?", true, id) 86 return err 87 } 88 89 var milestoneStatsQueryNumIssues = "SELECT `milestone`.id FROM `milestone` WHERE `milestone`.num_closed_issues!=(SELECT COUNT(*) FROM `issue` WHERE `issue`.milestone_id=`milestone`.id AND `issue`.is_closed=?) OR `milestone`.num_issues!=(SELECT COUNT(*) FROM `issue` WHERE `issue`.milestone_id=`milestone`.id)" 90 91 func milestoneStatsCorrectNumIssuesRepo(ctx context.Context, id int64) error { 92 e := db.GetEngine(ctx) 93 results, err := e.Query(milestoneStatsQueryNumIssues+" AND `milestone`.repo_id = ?", true, id) 94 if err != nil { 95 return err 96 } 97 for _, result := range results { 98 id, _ := strconv.ParseInt(string(result["id"]), 10, 64) 99 err = issues_model.UpdateMilestoneCounters(ctx, id) 100 if err != nil { 101 return err 102 } 103 } 104 return nil 105 } 106 107 func userStatsCorrectNumRepos(ctx context.Context, id int64) error { 108 return StatsCorrectSQL(ctx, "UPDATE `user` SET num_repos=(SELECT COUNT(*) FROM `repository` WHERE owner_id=?) WHERE id=?", id) 109 } 110 111 func repoStatsCorrectIssueNumComments(ctx context.Context, id int64) error { 112 return StatsCorrectSQL(ctx, "UPDATE `issue` SET num_comments=(SELECT COUNT(*) FROM `comment` WHERE issue_id=? AND type=0) WHERE id=?", id) 113 } 114 115 func repoStatsCorrectNumIssues(ctx context.Context, id int64) error { 116 return repo_model.UpdateRepoIssueNumbers(ctx, id, false, false) 117 } 118 119 func repoStatsCorrectNumPulls(ctx context.Context, id int64) error { 120 return repo_model.UpdateRepoIssueNumbers(ctx, id, true, false) 121 } 122 123 func repoStatsCorrectNumClosedIssues(ctx context.Context, id int64) error { 124 return repo_model.UpdateRepoIssueNumbers(ctx, id, false, true) 125 } 126 127 func repoStatsCorrectNumClosedPulls(ctx context.Context, id int64) error { 128 return repo_model.UpdateRepoIssueNumbers(ctx, id, true, true) 129 } 130 131 func statsQuery(args ...any) func(context.Context) ([]map[string][]byte, error) { 132 return func(ctx context.Context) ([]map[string][]byte, error) { 133 return db.GetEngine(ctx).Query(args...) 134 } 135 } 136 137 // CheckRepoStats checks the repository stats 138 func CheckRepoStats(ctx context.Context) error { 139 log.Trace("Doing: CheckRepoStats") 140 141 checkers := []*repoChecker{ 142 // Repository.NumWatches 143 { 144 statsQuery("SELECT repo.id FROM `repository` repo WHERE repo.num_watches!=(SELECT COUNT(*) FROM `watch` WHERE repo_id=repo.id AND mode<>2)"), 145 repoStatsCorrectNumWatches, 146 "repository count 'num_watches'", 147 }, 148 // Repository.NumStars 149 { 150 statsQuery("SELECT repo.id FROM `repository` repo WHERE repo.num_stars!=(SELECT COUNT(*) FROM `star` WHERE repo_id=repo.id)"), 151 repoStatsCorrectNumStars, 152 "repository count 'num_stars'", 153 }, 154 // Repository.NumIssues 155 { 156 statsQuery("SELECT repo.id FROM `repository` repo WHERE repo.num_issues!=(SELECT COUNT(*) FROM `issue` WHERE repo_id=repo.id AND is_pull=?)", false), 157 repoStatsCorrectNumIssues, 158 "repository count 'num_issues'", 159 }, 160 // Repository.NumClosedIssues 161 { 162 statsQuery("SELECT repo.id FROM `repository` repo WHERE repo.num_closed_issues!=(SELECT COUNT(*) FROM `issue` WHERE repo_id=repo.id AND is_closed=? AND is_pull=?)", true, false), 163 repoStatsCorrectNumClosedIssues, 164 "repository count 'num_closed_issues'", 165 }, 166 // Repository.NumPulls 167 { 168 statsQuery("SELECT repo.id FROM `repository` repo WHERE repo.num_pulls!=(SELECT COUNT(*) FROM `issue` WHERE repo_id=repo.id AND is_pull=?)", true), 169 repoStatsCorrectNumPulls, 170 "repository count 'num_pulls'", 171 }, 172 // Repository.NumClosedPulls 173 { 174 statsQuery("SELECT repo.id FROM `repository` repo WHERE repo.num_closed_pulls!=(SELECT COUNT(*) FROM `issue` WHERE repo_id=repo.id AND is_closed=? AND is_pull=?)", true, true), 175 repoStatsCorrectNumClosedPulls, 176 "repository count 'num_closed_pulls'", 177 }, 178 // Label.NumIssues 179 { 180 statsQuery("SELECT label.id FROM `label` WHERE label.num_issues!=(SELECT COUNT(*) FROM `issue_label` WHERE label_id=label.id)"), 181 labelStatsCorrectNumIssues, 182 "label count 'num_issues'", 183 }, 184 // Label.NumClosedIssues 185 { 186 statsQuery("SELECT `label`.id FROM `label` WHERE `label`.num_closed_issues!=(SELECT COUNT(*) FROM `issue_label`,`issue` WHERE `issue_label`.label_id=`label`.id AND `issue_label`.issue_id=`issue`.id AND `issue`.is_closed=?)", true), 187 labelStatsCorrectNumClosedIssues, 188 "label count 'num_closed_issues'", 189 }, 190 // Milestone.Num{,Closed}Issues 191 { 192 statsQuery(milestoneStatsQueryNumIssues, true), 193 issues_model.UpdateMilestoneCounters, 194 "milestone count 'num_closed_issues' and 'num_issues'", 195 }, 196 // User.NumRepos 197 { 198 statsQuery("SELECT `user`.id FROM `user` WHERE `user`.num_repos!=(SELECT COUNT(*) FROM `repository` WHERE owner_id=`user`.id)"), 199 userStatsCorrectNumRepos, 200 "user count 'num_repos'", 201 }, 202 // Issue.NumComments 203 { 204 statsQuery("SELECT `issue`.id FROM `issue` WHERE `issue`.num_comments!=(SELECT COUNT(*) FROM `comment` WHERE issue_id=`issue`.id AND type=0)"), 205 repoStatsCorrectIssueNumComments, 206 "issue count 'num_comments'", 207 }, 208 } 209 for _, checker := range checkers { 210 select { 211 case <-ctx.Done(): 212 log.Warn("CheckRepoStats: Cancelled before %s", checker.desc) 213 return db.ErrCancelledf("before checking %s", checker.desc) 214 default: 215 repoStatsCheck(ctx, checker) 216 } 217 } 218 219 // FIXME: use checker when stop supporting old fork repo format. 220 // ***** START: Repository.NumForks ***** 221 e := db.GetEngine(ctx) 222 results, err := e.Query("SELECT repo.id FROM `repository` repo WHERE repo.num_forks!=(SELECT COUNT(*) FROM `repository` WHERE fork_id=repo.id)") 223 if err != nil { 224 log.Error("Select repository count 'num_forks': %v", err) 225 } else { 226 for _, result := range results { 227 id, _ := strconv.ParseInt(string(result["id"]), 10, 64) 228 select { 229 case <-ctx.Done(): 230 log.Warn("CheckRepoStats: Cancelled") 231 return db.ErrCancelledf("during repository count 'num_fork' for repo ID %d", id) 232 default: 233 } 234 log.Trace("Updating repository count 'num_forks': %d", id) 235 236 repo, err := repo_model.GetRepositoryByID(ctx, id) 237 if err != nil { 238 log.Error("repo_model.GetRepositoryByID[%d]: %v", id, err) 239 continue 240 } 241 242 _, err = e.SQL("SELECT COUNT(*) FROM `repository` WHERE fork_id=?", repo.ID).Get(&repo.NumForks) 243 if err != nil { 244 log.Error("Select count of forks[%d]: %v", repo.ID, err) 245 continue 246 } 247 248 if _, err = e.ID(repo.ID).Cols("num_forks").Update(repo); err != nil { 249 log.Error("UpdateRepository[%d]: %v", id, err) 250 continue 251 } 252 } 253 } 254 // ***** END: Repository.NumForks ***** 255 return nil 256 } 257 258 func UpdateRepoStats(ctx context.Context, id int64) error { 259 var err error 260 261 for _, f := range []func(ctx context.Context, id int64) error{ 262 repoStatsCorrectNumWatches, 263 repoStatsCorrectNumStars, 264 repoStatsCorrectNumIssues, 265 repoStatsCorrectNumPulls, 266 repoStatsCorrectNumClosedIssues, 267 repoStatsCorrectNumClosedPulls, 268 labelStatsCorrectNumIssuesRepo, 269 labelStatsCorrectNumClosedIssuesRepo, 270 milestoneStatsCorrectNumIssuesRepo, 271 } { 272 err = f(ctx, id) 273 if err != nil { 274 return err 275 } 276 } 277 return nil 278 } 279 280 func updateUserStarNumbers(ctx context.Context, users []user_model.User) error { 281 ctx, committer, err := db.TxContext(ctx) 282 if err != nil { 283 return err 284 } 285 defer committer.Close() 286 287 for _, user := range users { 288 if _, err = db.Exec(ctx, "UPDATE `user` SET num_stars=(SELECT COUNT(*) FROM `star` WHERE uid=?) WHERE id=?", user.ID, user.ID); err != nil { 289 return err 290 } 291 } 292 293 return committer.Commit() 294 } 295 296 // DoctorUserStarNum recalculate Stars number for all user 297 func DoctorUserStarNum(ctx context.Context) (err error) { 298 const batchSize = 100 299 300 for start := 0; ; start += batchSize { 301 users := make([]user_model.User, 0, batchSize) 302 if err = db.GetEngine(ctx).Limit(batchSize, start).Where("type = ?", 0).Cols("id").Find(&users); err != nil { 303 return err 304 } 305 if len(users) == 0 { 306 break 307 } 308 309 if err = updateUserStarNumbers(ctx, users); err != nil { 310 return err 311 } 312 } 313 314 log.Debug("recalculate Stars number for all user finished") 315 316 return err 317 } 318 319 // DeleteDeployKey delete deploy keys 320 func DeleteDeployKey(ctx context.Context, doer *user_model.User, id int64) error { 321 key, err := asymkey_model.GetDeployKeyByID(ctx, id) 322 if err != nil { 323 if asymkey_model.IsErrDeployKeyNotExist(err) { 324 return nil 325 } 326 return fmt.Errorf("GetDeployKeyByID: %w", err) 327 } 328 329 // Check if user has access to delete this key. 330 if !doer.IsAdmin { 331 repo, err := repo_model.GetRepositoryByID(ctx, key.RepoID) 332 if err != nil { 333 return fmt.Errorf("GetRepositoryByID: %w", err) 334 } 335 has, err := access_model.IsUserRepoAdmin(ctx, repo, doer) 336 if err != nil { 337 return fmt.Errorf("GetUserRepoPermission: %w", err) 338 } else if !has { 339 return asymkey_model.ErrKeyAccessDenied{ 340 UserID: doer.ID, 341 KeyID: key.ID, 342 Note: "deploy", 343 } 344 } 345 } 346 347 if _, err := db.DeleteByID[asymkey_model.DeployKey](ctx, key.ID); err != nil { 348 return fmt.Errorf("delete deploy key [%d]: %w", key.ID, err) 349 } 350 351 // Check if this is the last reference to same key content. 352 has, err := asymkey_model.IsDeployKeyExistByKeyID(ctx, key.KeyID) 353 if err != nil { 354 return err 355 } else if !has { 356 if _, err = db.DeleteByID[asymkey_model.PublicKey](ctx, key.KeyID); err != nil { 357 return err 358 } 359 } 360 361 return nil 362 }