You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

dbconsistency.go 8.4KB

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