code.gitea.io/gitea@v1.22.3/services/doctor/dbconsistency.go (about) 1 // Copyright 2020 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package doctor 5 6 import ( 7 "context" 8 9 actions_model "code.gitea.io/gitea/models/actions" 10 activities_model "code.gitea.io/gitea/models/activities" 11 "code.gitea.io/gitea/models/db" 12 issues_model "code.gitea.io/gitea/models/issues" 13 "code.gitea.io/gitea/models/migrations" 14 repo_model "code.gitea.io/gitea/models/repo" 15 "code.gitea.io/gitea/modules/log" 16 "code.gitea.io/gitea/modules/setting" 17 ) 18 19 type consistencyCheck struct { 20 Name string 21 Counter func(context.Context) (int64, error) 22 Fixer func(context.Context) (int64, error) 23 FixedMessage string 24 } 25 26 func (c *consistencyCheck) Run(ctx context.Context, logger log.Logger, autofix bool) error { 27 count, err := c.Counter(ctx) 28 if err != nil { 29 logger.Critical("Error: %v whilst counting %s", err, c.Name) 30 return err 31 } 32 if count > 0 { 33 if autofix { 34 var fixed int64 35 if fixed, err = c.Fixer(ctx); err != nil { 36 logger.Critical("Error: %v whilst fixing %s", err, c.Name) 37 return err 38 } 39 40 prompt := "Deleted" 41 if c.FixedMessage != "" { 42 prompt = c.FixedMessage 43 } 44 45 if fixed < 0 { 46 logger.Info(prompt+" %d %s", count, c.Name) 47 } else { 48 logger.Info(prompt+" %d/%d %s", fixed, count, c.Name) 49 } 50 } else { 51 logger.Warn("Found %d %s", count, c.Name) 52 } 53 } 54 return nil 55 } 56 57 func asFixer(fn func(ctx context.Context) error) func(ctx context.Context) (int64, error) { 58 return func(ctx context.Context) (int64, error) { 59 err := fn(ctx) 60 return -1, err 61 } 62 } 63 64 func genericOrphanCheck(name, subject, refObject, joinCond string) consistencyCheck { 65 return consistencyCheck{ 66 Name: name, 67 Counter: func(ctx context.Context) (int64, error) { 68 return db.CountOrphanedObjects(ctx, subject, refObject, joinCond) 69 }, 70 Fixer: func(ctx context.Context) (int64, error) { 71 err := db.DeleteOrphanedObjects(ctx, subject, refObject, joinCond) 72 return -1, err 73 }, 74 } 75 } 76 77 func prepareDBConsistencyChecks() []consistencyCheck { 78 consistencyChecks := []consistencyCheck{ 79 { 80 // find labels without existing repo or org 81 Name: "Orphaned Labels without existing repository or organisation", 82 Counter: issues_model.CountOrphanedLabels, 83 Fixer: asFixer(issues_model.DeleteOrphanedLabels), 84 }, 85 { 86 // find IssueLabels without existing label 87 Name: "Orphaned Issue Labels without existing label", 88 Counter: issues_model.CountOrphanedIssueLabels, 89 Fixer: asFixer(issues_model.DeleteOrphanedIssueLabels), 90 }, 91 { 92 // find issues without existing repository 93 Name: "Orphaned Issues without existing repository", 94 Counter: issues_model.CountOrphanedIssues, 95 Fixer: asFixer(issues_model.DeleteOrphanedIssues), 96 }, 97 // find releases without existing repository 98 genericOrphanCheck("Orphaned Releases without existing repository", 99 "release", "repository", "`release`.repo_id=repository.id"), 100 // find pulls without existing issues 101 genericOrphanCheck("Orphaned PullRequests without existing issue", 102 "pull_request", "issue", "pull_request.issue_id=issue.id"), 103 // find pull requests without base repository 104 genericOrphanCheck("Pull request entries without existing base repository", 105 "pull_request", "repository", "pull_request.base_repo_id=repository.id"), 106 // find tracked times without existing issues/pulls 107 genericOrphanCheck("Orphaned TrackedTimes without existing issue", 108 "tracked_time", "issue", "tracked_time.issue_id=issue.id"), 109 // find attachments without existing issues or releases 110 { 111 Name: "Orphaned Attachments without existing issues or releases", 112 Counter: repo_model.CountOrphanedAttachments, 113 Fixer: asFixer(repo_model.DeleteOrphanedAttachments), 114 }, 115 // find null archived repositories 116 { 117 Name: "Repositories with is_archived IS NULL", 118 Counter: repo_model.CountNullArchivedRepository, 119 Fixer: repo_model.FixNullArchivedRepository, 120 FixedMessage: "Fixed", 121 }, 122 // find label comments with empty labels 123 { 124 Name: "Label comments with empty labels", 125 Counter: issues_model.CountCommentTypeLabelWithEmptyLabel, 126 Fixer: issues_model.FixCommentTypeLabelWithEmptyLabel, 127 FixedMessage: "Fixed", 128 }, 129 // find label comments with labels from outside the repository 130 { 131 Name: "Label comments with labels from outside the repository", 132 Counter: issues_model.CountCommentTypeLabelWithOutsideLabels, 133 Fixer: issues_model.FixCommentTypeLabelWithOutsideLabels, 134 FixedMessage: "Removed", 135 }, 136 // find issue_label with labels from outside the repository 137 { 138 Name: "IssueLabels with Labels from outside the repository", 139 Counter: issues_model.CountIssueLabelWithOutsideLabels, 140 Fixer: issues_model.FixIssueLabelWithOutsideLabels, 141 FixedMessage: "Removed", 142 }, 143 { 144 Name: "Action with created_unix set as an empty string", 145 Counter: activities_model.CountActionCreatedUnixString, 146 Fixer: activities_model.FixActionCreatedUnixString, 147 FixedMessage: "Set to zero", 148 }, 149 { 150 Name: "Action Runners without existing owner", 151 Counter: actions_model.CountRunnersWithoutBelongingOwner, 152 Fixer: actions_model.FixRunnersWithoutBelongingOwner, 153 FixedMessage: "Removed", 154 }, 155 { 156 Name: "Action Runners without existing repository", 157 Counter: actions_model.CountRunnersWithoutBelongingRepo, 158 Fixer: actions_model.FixRunnersWithoutBelongingRepo, 159 FixedMessage: "Removed", 160 }, 161 { 162 Name: "Topics with empty repository count", 163 Counter: repo_model.CountOrphanedTopics, 164 Fixer: repo_model.DeleteOrphanedTopics, 165 FixedMessage: "Removed", 166 }, 167 } 168 169 // TODO: function to recalc all counters 170 171 if setting.Database.Type.IsPostgreSQL() { 172 consistencyChecks = append(consistencyChecks, consistencyCheck{ 173 Name: "Sequence values", 174 Counter: db.CountBadSequences, 175 Fixer: asFixer(db.FixBadSequences), 176 FixedMessage: "Updated", 177 }) 178 } 179 180 consistencyChecks = append(consistencyChecks, 181 // find protected branches without existing repository 182 genericOrphanCheck("Protected Branches without existing repository", 183 "protected_branch", "repository", "protected_branch.repo_id=repository.id"), 184 // find branches without existing repository 185 genericOrphanCheck("Branches without existing repository", 186 "branch", "repository", "branch.repo_id=repository.id"), 187 // find LFS locks without existing repository 188 genericOrphanCheck("LFS locks without existing repository", 189 "lfs_lock", "repository", "lfs_lock.repo_id=repository.id"), 190 // find collaborations without users 191 genericOrphanCheck("Collaborations without existing user", 192 "collaboration", "user", "collaboration.user_id=`user`.id"), 193 // find collaborations without repository 194 genericOrphanCheck("Collaborations without existing repository", 195 "collaboration", "repository", "collaboration.repo_id=repository.id"), 196 // find access without users 197 genericOrphanCheck("Access entries without existing user", 198 "access", "user", "access.user_id=`user`.id"), 199 // find access without repository 200 genericOrphanCheck("Access entries without existing repository", 201 "access", "repository", "access.repo_id=repository.id"), 202 // find action without repository 203 genericOrphanCheck("Action entries without existing repository", 204 "action", "repository", "action.repo_id=repository.id"), 205 // find action without user 206 genericOrphanCheck("Action entries without existing user", 207 "action", "user", "action.act_user_id=`user`.id"), 208 // find OAuth2Grant without existing user 209 genericOrphanCheck("Orphaned OAuth2Grant without existing User", 210 "oauth2_grant", "user", "oauth2_grant.user_id=`user`.id"), 211 // find OAuth2Application without existing user 212 genericOrphanCheck("Orphaned OAuth2Application without existing User", 213 "oauth2_application", "user", "oauth2_application.uid=0 OR oauth2_application.uid=`user`.id"), 214 // find OAuth2AuthorizationCode without existing OAuth2Grant 215 genericOrphanCheck("Orphaned OAuth2AuthorizationCode without existing OAuth2Grant", 216 "oauth2_authorization_code", "oauth2_grant", "oauth2_authorization_code.grant_id=oauth2_grant.id"), 217 // find stopwatches without existing user 218 genericOrphanCheck("Orphaned Stopwatches without existing User", 219 "stopwatch", "user", "stopwatch.user_id=`user`.id"), 220 // find stopwatches without existing issue 221 genericOrphanCheck("Orphaned Stopwatches without existing Issue", 222 "stopwatch", "issue", "stopwatch.issue_id=`issue`.id"), 223 // find redirects without existing user. 224 genericOrphanCheck("Orphaned Redirects without existing redirect user", 225 "user_redirect", "user", "user_redirect.redirect_user_id=`user`.id"), 226 ) 227 return consistencyChecks 228 } 229 230 func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) error { 231 // make sure DB version is uptodate 232 if err := db.InitEngineWithMigration(ctx, migrations.EnsureUpToDate); err != nil { 233 logger.Critical("Model version on the database does not match the current Gitea version. Model consistency will not be checked until the database is upgraded") 234 return err 235 } 236 consistencyChecks := prepareDBConsistencyChecks() 237 for _, c := range consistencyChecks { 238 if err := c.Run(ctx, logger, autofix); err != nil { 239 return err 240 } 241 } 242 243 return nil 244 } 245 246 func init() { 247 Register(&Check{ 248 Title: "Check consistency of database", 249 Name: "check-db-consistency", 250 IsDefault: false, 251 Run: checkDBConsistency, 252 Priority: 3, 253 }) 254 }