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.6KB

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