summaryrefslogtreecommitdiffstats
path: root/services/doctor/dbconsistency.go
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--services/doctor/dbconsistency.go265
1 files changed, 265 insertions, 0 deletions
diff --git a/services/doctor/dbconsistency.go b/services/doctor/dbconsistency.go
new file mode 100644
index 0000000..7fce505
--- /dev/null
+++ b/services/doctor/dbconsistency.go
@@ -0,0 +1,265 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package doctor
+
+import (
+ "context"
+
+ actions_model "code.gitea.io/gitea/models/actions"
+ activities_model "code.gitea.io/gitea/models/activities"
+ auth_model "code.gitea.io/gitea/models/auth"
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+ "code.gitea.io/gitea/models/migrations"
+ org_model "code.gitea.io/gitea/models/organization"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+)
+
+type consistencyCheck struct {
+ Name string
+ Counter func(context.Context) (int64, error)
+ Fixer func(context.Context) (int64, error)
+ FixedMessage string
+}
+
+func (c *consistencyCheck) Run(ctx context.Context, logger log.Logger, autofix bool) error {
+ count, err := c.Counter(ctx)
+ if err != nil {
+ logger.Critical("Error: %v whilst counting %s", err, c.Name)
+ return err
+ }
+ if count > 0 {
+ if autofix {
+ var fixed int64
+ if fixed, err = c.Fixer(ctx); err != nil {
+ logger.Critical("Error: %v whilst fixing %s", err, c.Name)
+ return err
+ }
+
+ prompt := "Deleted"
+ if c.FixedMessage != "" {
+ prompt = c.FixedMessage
+ }
+
+ if fixed < 0 {
+ logger.Info(prompt+" %d %s", count, c.Name)
+ } else {
+ logger.Info(prompt+" %d/%d %s", fixed, count, c.Name)
+ }
+ } else {
+ logger.Warn("Found %d %s", count, c.Name)
+ }
+ }
+ return nil
+}
+
+func asFixer(fn func(ctx context.Context) error) func(ctx context.Context) (int64, error) {
+ return func(ctx context.Context) (int64, error) {
+ err := fn(ctx)
+ return -1, err
+ }
+}
+
+func genericOrphanCheck(name, subject, refobject, joincond string) consistencyCheck {
+ return consistencyCheck{
+ Name: name,
+ Counter: func(ctx context.Context) (int64, error) {
+ return db.CountOrphanedObjects(ctx, subject, refobject, joincond)
+ },
+ Fixer: func(ctx context.Context) (int64, error) {
+ err := db.DeleteOrphanedObjects(ctx, subject, refobject, joincond)
+ return -1, err
+ },
+ }
+}
+
+func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) error {
+ // make sure DB version is up-to-date
+ if err := db.InitEngineWithMigration(ctx, migrations.EnsureUpToDate); err != nil {
+ 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")
+ return err
+ }
+
+ consistencyChecks := []consistencyCheck{
+ {
+ // find labels without existing repo or org
+ Name: "Orphaned Labels without existing repository or organisation",
+ Counter: issues_model.CountOrphanedLabels,
+ Fixer: asFixer(issues_model.DeleteOrphanedLabels),
+ },
+ {
+ // find IssueLabels without existing label
+ Name: "Orphaned Issue Labels without existing label",
+ Counter: issues_model.CountOrphanedIssueLabels,
+ Fixer: asFixer(issues_model.DeleteOrphanedIssueLabels),
+ },
+ {
+ // find issues without existing repository
+ Name: "Orphaned Issues without existing repository",
+ Counter: issues_model.CountOrphanedIssues,
+ Fixer: asFixer(issues_model.DeleteOrphanedIssues),
+ },
+ // find releases without existing repository
+ genericOrphanCheck("Orphaned Releases without existing repository",
+ "release", "repository", "`release`.repo_id=repository.id"),
+ // find pulls without existing issues
+ genericOrphanCheck("Orphaned PullRequests without existing issue",
+ "pull_request", "issue", "pull_request.issue_id=issue.id"),
+ // find pull requests without base repository
+ genericOrphanCheck("Pull request entries without existing base repository",
+ "pull_request", "repository", "pull_request.base_repo_id=repository.id"),
+ // find tracked times without existing issues/pulls
+ genericOrphanCheck("Orphaned TrackedTimes without existing issue",
+ "tracked_time", "issue", "tracked_time.issue_id=issue.id"),
+ // find attachments without existing issues or releases
+ {
+ Name: "Orphaned Attachments without existing issues or releases",
+ Counter: repo_model.CountOrphanedAttachments,
+ Fixer: asFixer(repo_model.DeleteOrphanedAttachments),
+ },
+ // find null archived repositories
+ {
+ Name: "Repositories with is_archived IS NULL",
+ Counter: repo_model.CountNullArchivedRepository,
+ Fixer: repo_model.FixNullArchivedRepository,
+ FixedMessage: "Fixed",
+ },
+ // find label comments with empty labels
+ {
+ Name: "Label comments with empty labels",
+ Counter: issues_model.CountCommentTypeLabelWithEmptyLabel,
+ Fixer: issues_model.FixCommentTypeLabelWithEmptyLabel,
+ FixedMessage: "Fixed",
+ },
+ // find label comments with labels from outside the repository
+ {
+ Name: "Label comments with labels from outside the repository",
+ Counter: issues_model.CountCommentTypeLabelWithOutsideLabels,
+ Fixer: issues_model.FixCommentTypeLabelWithOutsideLabels,
+ FixedMessage: "Removed",
+ },
+ // find issue_label with labels from outside the repository
+ {
+ Name: "IssueLabels with Labels from outside the repository",
+ Counter: issues_model.CountIssueLabelWithOutsideLabels,
+ Fixer: issues_model.FixIssueLabelWithOutsideLabels,
+ FixedMessage: "Removed",
+ },
+ {
+ Name: "Action with created_unix set as an empty string",
+ Counter: activities_model.CountActionCreatedUnixString,
+ Fixer: activities_model.FixActionCreatedUnixString,
+ FixedMessage: "Set to zero",
+ },
+ {
+ Name: "Action Runners without existing owner",
+ Counter: actions_model.CountRunnersWithoutBelongingOwner,
+ Fixer: actions_model.FixRunnersWithoutBelongingOwner,
+ FixedMessage: "Removed",
+ },
+ {
+ Name: "Action Runners without existing repository",
+ Counter: actions_model.CountRunnersWithoutBelongingRepo,
+ Fixer: actions_model.FixRunnersWithoutBelongingRepo,
+ FixedMessage: "Removed",
+ },
+ {
+ Name: "Topics with empty repository count",
+ Counter: repo_model.CountOrphanedTopics,
+ Fixer: repo_model.DeleteOrphanedTopics,
+ FixedMessage: "Removed",
+ },
+ {
+ Name: "Orphaned OAuth2Application without existing User",
+ Counter: auth_model.CountOrphanedOAuth2Applications,
+ Fixer: auth_model.DeleteOrphanedOAuth2Applications,
+ FixedMessage: "Removed",
+ },
+ {
+ Name: "Owner teams with no admin access",
+ Counter: org_model.CountInconsistentOwnerTeams,
+ Fixer: org_model.FixInconsistentOwnerTeams,
+ FixedMessage: "Fixed",
+ },
+ }
+
+ // TODO: function to recalc all counters
+
+ if setting.Database.Type.IsPostgreSQL() {
+ consistencyChecks = append(consistencyChecks, consistencyCheck{
+ Name: "Sequence values",
+ Counter: db.CountBadSequences,
+ Fixer: asFixer(db.FixBadSequences),
+ FixedMessage: "Updated",
+ })
+ }
+
+ consistencyChecks = append(consistencyChecks,
+ // find protected branches without existing repository
+ genericOrphanCheck("Protected Branches without existing repository",
+ "protected_branch", "repository", "protected_branch.repo_id=repository.id"),
+ // find branches without existing repository
+ genericOrphanCheck("Branches without existing repository",
+ "branch", "repository", "branch.repo_id=repository.id"),
+ // find LFS locks without existing repository
+ genericOrphanCheck("LFS locks without existing repository",
+ "lfs_lock", "repository", "lfs_lock.repo_id=repository.id"),
+ // find collaborations without users
+ genericOrphanCheck("Collaborations without existing user",
+ "collaboration", "user", "collaboration.user_id=`user`.id"),
+ // find collaborations without repository
+ genericOrphanCheck("Collaborations without existing repository",
+ "collaboration", "repository", "collaboration.repo_id=repository.id"),
+ // find access without users
+ genericOrphanCheck("Access entries without existing user",
+ "access", "user", "access.user_id=`user`.id"),
+ // find access without repository
+ genericOrphanCheck("Access entries without existing repository",
+ "access", "repository", "access.repo_id=repository.id"),
+ // find action without repository
+ genericOrphanCheck("Action entries without existing repository",
+ "action", "repository", "action.repo_id=repository.id"),
+ // find action without user
+ genericOrphanCheck("Action entries without existing user",
+ "action", "user", "action.act_user_id=`user`.id"),
+ // find OAuth2Grant without existing user
+ genericOrphanCheck("Orphaned OAuth2Grant without existing User",
+ "oauth2_grant", "user", "oauth2_grant.user_id=`user`.id"),
+ // find OAuth2AuthorizationCode without existing OAuth2Grant
+ genericOrphanCheck("Orphaned OAuth2AuthorizationCode without existing OAuth2Grant",
+ "oauth2_authorization_code", "oauth2_grant", "oauth2_authorization_code.grant_id=oauth2_grant.id"),
+ // find stopwatches without existing user
+ genericOrphanCheck("Orphaned Stopwatches without existing User",
+ "stopwatch", "user", "stopwatch.user_id=`user`.id"),
+ // find stopwatches without existing issue
+ genericOrphanCheck("Orphaned Stopwatches without existing Issue",
+ "stopwatch", "issue", "stopwatch.issue_id=`issue`.id"),
+ // find redirects without existing user.
+ genericOrphanCheck("Orphaned Redirects without existing redirect user",
+ "user_redirect", "user", "user_redirect.redirect_user_id=`user`.id"),
+ // find archive download count without existing release
+ genericOrphanCheck("Archive download count without existing Release",
+ "repo_archive_download_count", "release", "repo_archive_download_count.release_id=release.id"),
+ )
+
+ for _, c := range consistencyChecks {
+ if err := c.Run(ctx, logger, autofix); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func init() {
+ Register(&Check{
+ Title: "Check consistency of database",
+ Name: "check-db-consistency",
+ IsDefault: false,
+ Run: checkDBConsistency,
+ Priority: 3,
+ })
+}