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