code.gitea.io/gitea@v1.22.3/services/doctor/misc.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  	"fmt"
     9  	"os"
    10  	"os/exec"
    11  	"path"
    12  	"strings"
    13  
    14  	"code.gitea.io/gitea/models"
    15  	"code.gitea.io/gitea/models/db"
    16  	repo_model "code.gitea.io/gitea/models/repo"
    17  	user_model "code.gitea.io/gitea/models/user"
    18  	"code.gitea.io/gitea/modules/git"
    19  	"code.gitea.io/gitea/modules/gitrepo"
    20  	"code.gitea.io/gitea/modules/log"
    21  	"code.gitea.io/gitea/modules/repository"
    22  	"code.gitea.io/gitea/modules/setting"
    23  	"code.gitea.io/gitea/modules/structs"
    24  	"code.gitea.io/gitea/modules/util"
    25  
    26  	lru "github.com/hashicorp/golang-lru/v2"
    27  	"xorm.io/builder"
    28  )
    29  
    30  func iterateRepositories(ctx context.Context, each func(*repo_model.Repository) error) error {
    31  	err := db.Iterate(
    32  		ctx,
    33  		builder.Gt{"id": 0},
    34  		func(ctx context.Context, bean *repo_model.Repository) error {
    35  			return each(bean)
    36  		},
    37  	)
    38  	return err
    39  }
    40  
    41  func checkScriptType(ctx context.Context, logger log.Logger, autofix bool) error {
    42  	path, err := exec.LookPath(setting.ScriptType)
    43  	if err != nil {
    44  		logger.Critical("ScriptType \"%q\" is not on the current PATH. Error: %v", setting.ScriptType, err)
    45  		return fmt.Errorf("ScriptType \"%q\" is not on the current PATH. Error: %w", setting.ScriptType, err)
    46  	}
    47  	logger.Info("ScriptType %s is on the current PATH at %s", setting.ScriptType, path)
    48  	return nil
    49  }
    50  
    51  func checkHooks(ctx context.Context, logger log.Logger, autofix bool) error {
    52  	if err := iterateRepositories(ctx, func(repo *repo_model.Repository) error {
    53  		results, err := repository.CheckDelegateHooks(repo.RepoPath())
    54  		if err != nil {
    55  			logger.Critical("Unable to check delegate hooks for repo %-v. ERROR: %v", repo, err)
    56  			return fmt.Errorf("Unable to check delegate hooks for repo %-v. ERROR: %w", repo, err)
    57  		}
    58  		if len(results) > 0 && autofix {
    59  			logger.Warn("Regenerated hooks for %s", repo.FullName())
    60  			if err := repository.CreateDelegateHooks(repo.RepoPath()); err != nil {
    61  				logger.Critical("Unable to recreate delegate hooks for %-v. ERROR: %v", repo, err)
    62  				return fmt.Errorf("Unable to recreate delegate hooks for %-v. ERROR: %w", repo, err)
    63  			}
    64  		}
    65  		for _, result := range results {
    66  			logger.Warn(result)
    67  		}
    68  		return nil
    69  	}); err != nil {
    70  		logger.Critical("Errors noted whilst checking delegate hooks.")
    71  		return err
    72  	}
    73  	return nil
    74  }
    75  
    76  func checkUserStarNum(ctx context.Context, logger log.Logger, autofix bool) error {
    77  	if autofix {
    78  		if err := models.DoctorUserStarNum(ctx); err != nil {
    79  			logger.Critical("Unable update User Stars numbers")
    80  			return err
    81  		}
    82  		logger.Info("Updated User Stars numbers.")
    83  	} else {
    84  		logger.Info("No check available for User Stars numbers (skipped)")
    85  	}
    86  	return nil
    87  }
    88  
    89  func checkEnablePushOptions(ctx context.Context, logger log.Logger, autofix bool) error {
    90  	numRepos := 0
    91  	numNeedUpdate := 0
    92  
    93  	if err := iterateRepositories(ctx, func(repo *repo_model.Repository) error {
    94  		numRepos++
    95  		r, err := gitrepo.OpenRepository(ctx, repo)
    96  		if err != nil {
    97  			return err
    98  		}
    99  		defer r.Close()
   100  
   101  		if autofix {
   102  			_, _, err := git.NewCommand(ctx, "config", "receive.advertisePushOptions", "true").RunStdString(&git.RunOpts{Dir: r.Path})
   103  			return err
   104  		}
   105  
   106  		value, _, err := git.NewCommand(ctx, "config", "receive.advertisePushOptions").RunStdString(&git.RunOpts{Dir: r.Path})
   107  		if err != nil {
   108  			return err
   109  		}
   110  
   111  		result, valid := git.ParseBool(strings.TrimSpace(value))
   112  		if !result || !valid {
   113  			numNeedUpdate++
   114  			logger.Info("%s: does not have receive.advertisePushOptions set correctly: %q", repo.FullName(), value)
   115  		}
   116  		return nil
   117  	}); err != nil {
   118  		logger.Critical("Unable to EnablePushOptions: %v", err)
   119  		return err
   120  	}
   121  
   122  	if autofix {
   123  		logger.Info("Enabled push options for %d repositories.", numRepos)
   124  	} else {
   125  		logger.Info("Checked %d repositories, %d need updates.", numRepos, numNeedUpdate)
   126  	}
   127  
   128  	return nil
   129  }
   130  
   131  func checkDaemonExport(ctx context.Context, logger log.Logger, autofix bool) error {
   132  	numRepos := 0
   133  	numNeedUpdate := 0
   134  	cache, err := lru.New[int64, any](512)
   135  	if err != nil {
   136  		logger.Critical("Unable to create cache: %v", err)
   137  		return err
   138  	}
   139  	if err := iterateRepositories(ctx, func(repo *repo_model.Repository) error {
   140  		numRepos++
   141  
   142  		if owner, has := cache.Get(repo.OwnerID); has {
   143  			repo.Owner = owner.(*user_model.User)
   144  		} else {
   145  			if err := repo.LoadOwner(ctx); err != nil {
   146  				return err
   147  			}
   148  			cache.Add(repo.OwnerID, repo.Owner)
   149  		}
   150  
   151  		// Create/Remove git-daemon-export-ok for git-daemon...
   152  		daemonExportFile := path.Join(repo.RepoPath(), `git-daemon-export-ok`)
   153  		isExist, err := util.IsExist(daemonExportFile)
   154  		if err != nil {
   155  			log.Error("Unable to check if %s exists. Error: %v", daemonExportFile, err)
   156  			return err
   157  		}
   158  		isPublic := !repo.IsPrivate && repo.Owner.Visibility == structs.VisibleTypePublic
   159  
   160  		if isPublic != isExist {
   161  			numNeedUpdate++
   162  			if autofix {
   163  				if !isPublic && isExist {
   164  					if err = util.Remove(daemonExportFile); err != nil {
   165  						log.Error("Failed to remove %s: %v", daemonExportFile, err)
   166  					}
   167  				} else if isPublic && !isExist {
   168  					if f, err := os.Create(daemonExportFile); err != nil {
   169  						log.Error("Failed to create %s: %v", daemonExportFile, err)
   170  					} else {
   171  						f.Close()
   172  					}
   173  				}
   174  			}
   175  		}
   176  		return nil
   177  	}); err != nil {
   178  		logger.Critical("Unable to checkDaemonExport: %v", err)
   179  		return err
   180  	}
   181  
   182  	if autofix {
   183  		logger.Info("Updated git-daemon-export-ok files for %d of %d repositories.", numNeedUpdate, numRepos)
   184  	} else {
   185  		logger.Info("Checked %d repositories, %d need updates.", numRepos, numNeedUpdate)
   186  	}
   187  
   188  	return nil
   189  }
   190  
   191  func checkCommitGraph(ctx context.Context, logger log.Logger, autofix bool) error {
   192  	numRepos := 0
   193  	numNeedUpdate := 0
   194  	numWritten := 0
   195  	if err := iterateRepositories(ctx, func(repo *repo_model.Repository) error {
   196  		numRepos++
   197  
   198  		commitGraphExists := func() (bool, error) {
   199  			// Check commit-graph exists
   200  			commitGraphFile := path.Join(repo.RepoPath(), `objects/info/commit-graph`)
   201  			isExist, err := util.IsExist(commitGraphFile)
   202  			if err != nil {
   203  				logger.Error("Unable to check if %s exists. Error: %v", commitGraphFile, err)
   204  				return false, err
   205  			}
   206  
   207  			if !isExist {
   208  				commitGraphsDir := path.Join(repo.RepoPath(), `objects/info/commit-graphs`)
   209  				isExist, err = util.IsExist(commitGraphsDir)
   210  				if err != nil {
   211  					logger.Error("Unable to check if %s exists. Error: %v", commitGraphsDir, err)
   212  					return false, err
   213  				}
   214  			}
   215  			return isExist, nil
   216  		}
   217  
   218  		isExist, err := commitGraphExists()
   219  		if err != nil {
   220  			return err
   221  		}
   222  		if !isExist {
   223  			numNeedUpdate++
   224  			if autofix {
   225  				if err := git.WriteCommitGraph(ctx, repo.RepoPath()); err != nil {
   226  					logger.Error("Unable to write commit-graph in %s. Error: %v", repo.FullName(), err)
   227  					return err
   228  				}
   229  				isExist, err := commitGraphExists()
   230  				if err != nil {
   231  					return err
   232  				}
   233  				if isExist {
   234  					numWritten++
   235  					logger.Info("Commit-graph written:    %s", repo.FullName())
   236  				} else {
   237  					logger.Warn("No commit-graph written: %s", repo.FullName())
   238  				}
   239  			}
   240  		}
   241  		return nil
   242  	}); err != nil {
   243  		logger.Critical("Unable to checkCommitGraph: %v", err)
   244  		return err
   245  	}
   246  
   247  	if autofix {
   248  		logger.Info("Wrote commit-graph files for %d of %d repositories.", numWritten, numRepos)
   249  	} else {
   250  		logger.Info("Checked %d repositories, %d without commit-graphs.", numRepos, numNeedUpdate)
   251  	}
   252  
   253  	return nil
   254  }
   255  
   256  func init() {
   257  	Register(&Check{
   258  		Title:     "Check if SCRIPT_TYPE is available",
   259  		Name:      "script-type",
   260  		IsDefault: false,
   261  		Run:       checkScriptType,
   262  		Priority:  5,
   263  	})
   264  	Register(&Check{
   265  		Title:     "Check if hook files are up-to-date and executable",
   266  		Name:      "hooks",
   267  		IsDefault: false,
   268  		Run:       checkHooks,
   269  		Priority:  6,
   270  	})
   271  	Register(&Check{
   272  		Title:     "Recalculate Stars number for all user",
   273  		Name:      "recalculate-stars-number",
   274  		IsDefault: false,
   275  		Run:       checkUserStarNum,
   276  		Priority:  6,
   277  	})
   278  	Register(&Check{
   279  		Title:     "Check that all git repositories have receive.advertisePushOptions set to true",
   280  		Name:      "enable-push-options",
   281  		IsDefault: false,
   282  		Run:       checkEnablePushOptions,
   283  		Priority:  7,
   284  	})
   285  	Register(&Check{
   286  		Title:     "Check git-daemon-export-ok files",
   287  		Name:      "check-git-daemon-export-ok",
   288  		IsDefault: false,
   289  		Run:       checkDaemonExport,
   290  		Priority:  8,
   291  	})
   292  	Register(&Check{
   293  		Title:     "Check commit-graphs",
   294  		Name:      "check-commit-graphs",
   295  		IsDefault: false,
   296  		Run:       checkCommitGraph,
   297  		Priority:  9,
   298  	})
   299  }