From d92781bf941972761177ac9e07441f8893758fd3 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Tue, 21 Jan 2020 04:01:19 +0800
Subject: [PATCH] Refactor repository check and sync functions (#9854)

Move more general repository functions out of models/repo.go
---
 cmd/admin.go                |   7 +-
 models/context.go           |  13 +++
 models/migrations/v36.go    |   5 +-
 models/repo.go              | 201 ++----------------------------------
 modules/cron/cron.go        |  13 ++-
 modules/repository/check.go | 156 ++++++++++++++++++++++++++++
 modules/repository/fork.go  |   2 +-
 modules/repository/hooks.go | 104 +++++++++++++++++++
 modules/repository/init.go  |   2 +-
 modules/repository/repo.go  |   5 +-
 routers/admin/admin.go      |  12 ++-
 services/wiki/wiki.go       |   3 +-
 12 files changed, 313 insertions(+), 210 deletions(-)
 create mode 100644 modules/repository/check.go
 create mode 100644 modules/repository/hooks.go

diff --git a/cmd/admin.go b/cmd/admin.go
index 0b9f6eac44..f6f3e22b97 100644
--- a/cmd/admin.go
+++ b/cmd/admin.go
@@ -14,9 +14,10 @@ import (
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/modules/auth/oauth2"
 	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/graceful"
 	"code.gitea.io/gitea/modules/log"
 	pwd "code.gitea.io/gitea/modules/password"
-	"code.gitea.io/gitea/modules/repository"
+	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/setting"
 
 	"github.com/urfave/cli"
@@ -375,7 +376,7 @@ func runRepoSyncReleases(c *cli.Context) error {
 			}
 			log.Trace(" currentNumReleases is %d, running SyncReleasesWithTags", oldnum)
 
-			if err = repository.SyncReleasesWithTags(repo, gitRepo); err != nil {
+			if err = repo_module.SyncReleasesWithTags(repo, gitRepo); err != nil {
 				log.Warn(" SyncReleasesWithTags: %v", err)
 				gitRepo.Close()
 				continue
@@ -410,7 +411,7 @@ func runRegenerateHooks(c *cli.Context) error {
 	if err := initDB(); err != nil {
 		return err
 	}
-	return models.SyncRepositoryHooks()
+	return repo_module.SyncRepositoryHooks(graceful.GetManager().ShutdownContext())
 }
 
 func runRegenerateKeys(c *cli.Context) error {
diff --git a/models/context.go b/models/context.go
index 5f47c595a2..6b8b9af570 100644
--- a/models/context.go
+++ b/models/context.go
@@ -4,6 +4,12 @@
 
 package models
 
+import (
+	"code.gitea.io/gitea/modules/setting"
+
+	"xorm.io/builder"
+)
+
 // DBContext represents a db context
 type DBContext struct {
 	e Engine
@@ -53,3 +59,10 @@ func WithTx(f func(ctx DBContext) error) error {
 	sess.Close()
 	return err
 }
+
+// Iterate iterates the databases and doing something
+func Iterate(ctx DBContext, tableBean interface{}, cond builder.Cond, fun func(idx int, bean interface{}) error) error {
+	return ctx.e.Where(cond).
+		BufferSize(setting.Database.IterateBufferSize).
+		Iterate(tableBean, fun)
+}
diff --git a/models/migrations/v36.go b/models/migrations/v36.go
index 729019925e..8027ed2103 100644
--- a/models/migrations/v36.go
+++ b/models/migrations/v36.go
@@ -5,11 +5,12 @@
 package migrations
 
 import (
-	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/graceful"
+	repo_module "code.gitea.io/gitea/modules/repository"
 
 	"xorm.io/xorm"
 )
 
 func regenerateGitHooks36(x *xorm.Engine) (err error) {
-	return models.SyncRepositoryHooks()
+	return repo_module.SyncRepositoryHooks(graceful.GetManager().ShutdownContext())
 }
diff --git a/models/repo.go b/models/repo.go
index 6c89dbcbbb..ecff8482a7 100644
--- a/models/repo.go
+++ b/models/repo.go
@@ -26,12 +26,10 @@ import (
 	"time"
 
 	"code.gitea.io/gitea/modules/avatar"
-	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/options"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/structs"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
@@ -147,13 +145,13 @@ type Repository struct {
 	ID                  int64 `xorm:"pk autoincr"`
 	OwnerID             int64 `xorm:"UNIQUE(s) index"`
 	OwnerName           string
-	Owner               *User                  `xorm:"-"`
-	LowerName           string                 `xorm:"UNIQUE(s) INDEX NOT NULL"`
-	Name                string                 `xorm:"INDEX NOT NULL"`
-	Description         string                 `xorm:"TEXT"`
-	Website             string                 `xorm:"VARCHAR(2048)"`
-	OriginalServiceType structs.GitServiceType `xorm:"index"`
-	OriginalURL         string                 `xorm:"VARCHAR(2048)"`
+	Owner               *User              `xorm:"-"`
+	LowerName           string             `xorm:"UNIQUE(s) INDEX NOT NULL"`
+	Name                string             `xorm:"INDEX NOT NULL"`
+	Description         string             `xorm:"TEXT"`
+	Website             string             `xorm:"VARCHAR(2048)"`
+	OriginalServiceType api.GitServiceType `xorm:"index"`
+	OriginalURL         string             `xorm:"VARCHAR(2048)"`
 	DefaultBranch       string
 
 	NumWatches          int
@@ -911,63 +909,12 @@ func CheckCreateRepository(doer, u *User, name string) error {
 	return nil
 }
 
-// CreateDelegateHooks creates all the hooks scripts for the repo
-func CreateDelegateHooks(repoPath string) error {
-	return createDelegateHooks(repoPath)
-}
-
-// createDelegateHooks creates all the hooks scripts for the repo
-func createDelegateHooks(repoPath string) (err error) {
-
-	var (
-		hookNames = []string{"pre-receive", "update", "post-receive"}
-		hookTpls  = []string{
-			fmt.Sprintf("#!/usr/bin/env %s\ndata=$(cat)\nexitcodes=\"\"\nhookname=$(basename $0)\nGIT_DIR=${GIT_DIR:-$(dirname $0)}\n\nfor hook in ${GIT_DIR}/hooks/${hookname}.d/*; do\ntest -x \"${hook}\" || continue\necho \"${data}\" | \"${hook}\"\nexitcodes=\"${exitcodes} $?\"\ndone\n\nfor i in ${exitcodes}; do\n[ ${i} -eq 0 ] || exit ${i}\ndone\n", setting.ScriptType),
-			fmt.Sprintf("#!/usr/bin/env %s\nexitcodes=\"\"\nhookname=$(basename $0)\nGIT_DIR=${GIT_DIR:-$(dirname $0)}\n\nfor hook in ${GIT_DIR}/hooks/${hookname}.d/*; do\ntest -x \"${hook}\" || continue\n\"${hook}\" $1 $2 $3\nexitcodes=\"${exitcodes} $?\"\ndone\n\nfor i in ${exitcodes}; do\n[ ${i} -eq 0 ] || exit ${i}\ndone\n", setting.ScriptType),
-			fmt.Sprintf("#!/usr/bin/env %s\ndata=$(cat)\nexitcodes=\"\"\nhookname=$(basename $0)\nGIT_DIR=${GIT_DIR:-$(dirname $0)}\n\nfor hook in ${GIT_DIR}/hooks/${hookname}.d/*; do\ntest -x \"${hook}\" || continue\necho \"${data}\" | \"${hook}\"\nexitcodes=\"${exitcodes} $?\"\ndone\n\nfor i in ${exitcodes}; do\n[ ${i} -eq 0 ] || exit ${i}\ndone\n", setting.ScriptType),
-		}
-		giteaHookTpls = []string{
-			fmt.Sprintf("#!/usr/bin/env %s\n\"%s\" hook --config='%s' pre-receive\n", setting.ScriptType, setting.AppPath, setting.CustomConf),
-			fmt.Sprintf("#!/usr/bin/env %s\n\"%s\" hook --config='%s' update $1 $2 $3\n", setting.ScriptType, setting.AppPath, setting.CustomConf),
-			fmt.Sprintf("#!/usr/bin/env %s\n\"%s\" hook --config='%s' post-receive\n", setting.ScriptType, setting.AppPath, setting.CustomConf),
-		}
-	)
-
-	hookDir := filepath.Join(repoPath, "hooks")
-
-	for i, hookName := range hookNames {
-		oldHookPath := filepath.Join(hookDir, hookName)
-		newHookPath := filepath.Join(hookDir, hookName+".d", "gitea")
-
-		if err := os.MkdirAll(filepath.Join(hookDir, hookName+".d"), os.ModePerm); err != nil {
-			return fmt.Errorf("create hooks dir '%s': %v", filepath.Join(hookDir, hookName+".d"), err)
-		}
-
-		// WARNING: This will override all old server-side hooks
-		if err = os.Remove(oldHookPath); err != nil && !os.IsNotExist(err) {
-			return fmt.Errorf("unable to pre-remove old hook file '%s' prior to rewriting: %v ", oldHookPath, err)
-		}
-		if err = ioutil.WriteFile(oldHookPath, []byte(hookTpls[i]), 0777); err != nil {
-			return fmt.Errorf("write old hook file '%s': %v", oldHookPath, err)
-		}
-
-		if err = os.Remove(newHookPath); err != nil && !os.IsNotExist(err) {
-			return fmt.Errorf("unable to pre-remove new hook file '%s' prior to rewriting: %v", newHookPath, err)
-		}
-		if err = ioutil.WriteFile(newHookPath, []byte(giteaHookTpls[i]), 0777); err != nil {
-			return fmt.Errorf("write new hook file '%s': %v", newHookPath, err)
-		}
-	}
-
-	return nil
-}
-
 // CreateRepoOptions contains the create repository options
 type CreateRepoOptions struct {
 	Name           string
 	Description    string
 	OriginalURL    string
-	GitServiceType structs.GitServiceType
+	GitServiceType api.GitServiceType
 	Gitignores     string
 	IssueLabels    string
 	License        string
@@ -1883,138 +1830,6 @@ func deleteOldRepositoryArchives(ctx context.Context, idx int, bean interface{})
 	return nil
 }
 
-func gatherMissingRepoRecords() ([]*Repository, error) {
-	repos := make([]*Repository, 0, 10)
-	if err := x.
-		Where("id > 0").
-		Iterate(new(Repository),
-			func(idx int, bean interface{}) error {
-				repo := bean.(*Repository)
-				if !com.IsDir(repo.RepoPath()) {
-					repos = append(repos, repo)
-				}
-				return nil
-			}); err != nil {
-		if err2 := CreateRepositoryNotice(fmt.Sprintf("gatherMissingRepoRecords: %v", err)); err2 != nil {
-			return nil, fmt.Errorf("CreateRepositoryNotice: %v", err)
-		}
-	}
-	return repos, nil
-}
-
-// DeleteMissingRepositories deletes all repository records that lost Git files.
-func DeleteMissingRepositories(doer *User) error {
-	repos, err := gatherMissingRepoRecords()
-	if err != nil {
-		return fmt.Errorf("gatherMissingRepoRecords: %v", err)
-	}
-
-	if len(repos) == 0 {
-		return nil
-	}
-
-	for _, repo := range repos {
-		log.Trace("Deleting %d/%d...", repo.OwnerID, repo.ID)
-		if err := DeleteRepository(doer, repo.OwnerID, repo.ID); err != nil {
-			if err2 := CreateRepositoryNotice(fmt.Sprintf("DeleteRepository [%d]: %v", repo.ID, err)); err2 != nil {
-				return fmt.Errorf("CreateRepositoryNotice: %v", err)
-			}
-		}
-	}
-	return nil
-}
-
-// ReinitMissingRepositories reinitializes all repository records that lost Git files.
-func ReinitMissingRepositories() error {
-	repos, err := gatherMissingRepoRecords()
-	if err != nil {
-		return fmt.Errorf("gatherMissingRepoRecords: %v", err)
-	}
-
-	if len(repos) == 0 {
-		return nil
-	}
-
-	for _, repo := range repos {
-		log.Trace("Initializing %d/%d...", repo.OwnerID, repo.ID)
-		if err := git.InitRepository(repo.RepoPath(), true); err != nil {
-			if err2 := CreateRepositoryNotice(fmt.Sprintf("InitRepository [%d]: %v", repo.ID, err)); err2 != nil {
-				return fmt.Errorf("CreateRepositoryNotice: %v", err)
-			}
-		}
-	}
-	return nil
-}
-
-// SyncRepositoryHooks rewrites all repositories' pre-receive, update and post-receive hooks
-// to make sure the binary and custom conf path are up-to-date.
-func SyncRepositoryHooks() error {
-	return x.Cols("owner_id", "name").Where("id > 0").Iterate(new(Repository),
-		func(idx int, bean interface{}) error {
-			if err := createDelegateHooks(bean.(*Repository).RepoPath()); err != nil {
-				return fmt.Errorf("SyncRepositoryHook: %v", err)
-			}
-			if bean.(*Repository).HasWiki() {
-				if err := createDelegateHooks(bean.(*Repository).WikiPath()); err != nil {
-					return fmt.Errorf("SyncRepositoryHook: %v", err)
-				}
-			}
-			return nil
-		})
-}
-
-// GitFsck calls 'git fsck' to check repository health.
-func GitFsck(ctx context.Context) {
-	log.Trace("Doing: GitFsck")
-	if err := x.
-		Where("id>0 AND is_fsck_enabled=?", true).BufferSize(setting.Database.IterateBufferSize).
-		Iterate(new(Repository),
-			func(idx int, bean interface{}) error {
-				select {
-				case <-ctx.Done():
-					return fmt.Errorf("Aborted due to shutdown")
-				default:
-				}
-				repo := bean.(*Repository)
-				repoPath := repo.RepoPath()
-				log.Trace("Running health check on repository %s", repoPath)
-				if err := git.Fsck(repoPath, setting.Cron.RepoHealthCheck.Timeout, setting.Cron.RepoHealthCheck.Args...); err != nil {
-					desc := fmt.Sprintf("Failed to health check repository (%s): %v", repoPath, err)
-					log.Warn(desc)
-					if err = CreateRepositoryNotice(desc); err != nil {
-						log.Error("CreateRepositoryNotice: %v", err)
-					}
-				}
-				return nil
-			}); err != nil {
-		log.Error("GitFsck: %v", err)
-	}
-	log.Trace("Finished: GitFsck")
-}
-
-// GitGcRepos calls 'git gc' to remove unnecessary files and optimize the local repository
-func GitGcRepos() error {
-	args := append([]string{"gc"}, setting.Git.GCArgs...)
-	return x.
-		Where("id > 0").BufferSize(setting.Database.IterateBufferSize).
-		Iterate(new(Repository),
-			func(idx int, bean interface{}) error {
-				repo := bean.(*Repository)
-				if err := repo.GetOwner(); err != nil {
-					return err
-				}
-				if stdout, err := git.NewCommand(args...).
-					SetDescription(fmt.Sprintf("Repository Garbage Collection: %s", repo.FullName())).
-					RunInDirTimeout(
-						time.Duration(setting.Git.Timeout.GC)*time.Second,
-						repo.RepoPath()); err != nil {
-					log.Error("Repository garbage collection failed for %v. Stdout: %s\nError: %v", repo, stdout, err)
-					return fmt.Errorf("Repository garbage collection failed: Error: %v", err)
-				}
-				return nil
-			})
-}
-
 type repoChecker struct {
 	querySQL, correctSQL string
 	desc                 string
diff --git a/modules/cron/cron.go b/modules/cron/cron.go
index f4511a8e79..692642e4ce 100644
--- a/modules/cron/cron.go
+++ b/modules/cron/cron.go
@@ -13,6 +13,7 @@ import (
 	"code.gitea.io/gitea/modules/graceful"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/migrations"
+	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/sync"
 	mirror_service "code.gitea.io/gitea/services/mirror"
@@ -69,14 +70,22 @@ func NewContext() {
 		}
 	}
 	if setting.Cron.RepoHealthCheck.Enabled {
-		entry, err = c.AddFunc("Repository health check", setting.Cron.RepoHealthCheck.Schedule, WithUnique(gitFsck, models.GitFsck))
+		entry, err = c.AddFunc("Repository health check", setting.Cron.RepoHealthCheck.Schedule, WithUnique(gitFsck, func(ctx context.Context) {
+			if err := repo_module.GitFsck(ctx); err != nil {
+				log.Error("GitFsck: %s", err)
+			}
+		}))
 		if err != nil {
 			log.Fatal("Cron[Repository health check]: %v", err)
 		}
 		if setting.Cron.RepoHealthCheck.RunAtStart {
 			entry.Prev = time.Now()
 			entry.ExecTimes++
-			go WithUnique(gitFsck, models.GitFsck)()
+			go WithUnique(gitFsck, func(ctx context.Context) {
+				if err := repo_module.GitFsck(ctx); err != nil {
+					log.Error("GitFsck: %s", err)
+				}
+			})()
 		}
 	}
 	if setting.Cron.CheckRepoStats.Enabled {
diff --git a/modules/repository/check.go b/modules/repository/check.go
new file mode 100644
index 0000000000..fcaf76308f
--- /dev/null
+++ b/modules/repository/check.go
@@ -0,0 +1,156 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package repository
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/setting"
+
+	"github.com/unknwon/com"
+	"xorm.io/builder"
+)
+
+// GitFsck calls 'git fsck' to check repository health.
+func GitFsck(ctx context.Context) error {
+	log.Trace("Doing: GitFsck")
+
+	if err := models.Iterate(
+		models.DefaultDBContext(),
+		new(models.Repository),
+		builder.Expr("id>0 AND is_fsck_enabled=?", true),
+		func(idx int, bean interface{}) error {
+			select {
+			case <-ctx.Done():
+				return fmt.Errorf("Aborted due to shutdown")
+			default:
+			}
+			repo := bean.(*models.Repository)
+			repoPath := repo.RepoPath()
+			log.Trace("Running health check on repository %s", repoPath)
+			if err := git.Fsck(repoPath, setting.Cron.RepoHealthCheck.Timeout, setting.Cron.RepoHealthCheck.Args...); err != nil {
+				desc := fmt.Sprintf("Failed to health check repository (%s): %v", repoPath, err)
+				log.Warn(desc)
+				if err = models.CreateRepositoryNotice(desc); err != nil {
+					log.Error("CreateRepositoryNotice: %v", err)
+				}
+			}
+			return nil
+		},
+	); err != nil {
+		return err
+	}
+
+	log.Trace("Finished: GitFsck")
+	return nil
+}
+
+// GitGcRepos calls 'git gc' to remove unnecessary files and optimize the local repository
+func GitGcRepos(ctx context.Context) error {
+	log.Trace("Doing: GitGcRepos")
+	args := append([]string{"gc"}, setting.Git.GCArgs...)
+
+	if err := models.Iterate(
+		models.DefaultDBContext(),
+		new(models.Repository),
+		builder.Gt{"id": 0},
+		func(idx int, bean interface{}) error {
+			select {
+			case <-ctx.Done():
+				return fmt.Errorf("Aborted due to shutdown")
+			default:
+			}
+
+			repo := bean.(*models.Repository)
+			if err := repo.GetOwner(); err != nil {
+				return err
+			}
+			if stdout, err := git.NewCommand(args...).
+				SetDescription(fmt.Sprintf("Repository Garbage Collection: %s", repo.FullName())).
+				RunInDirTimeout(
+					time.Duration(setting.Git.Timeout.GC)*time.Second,
+					repo.RepoPath()); err != nil {
+				log.Error("Repository garbage collection failed for %v. Stdout: %s\nError: %v", repo, stdout, err)
+				return fmt.Errorf("Repository garbage collection failed: Error: %v", err)
+			}
+			return nil
+		},
+	); err != nil {
+		return err
+	}
+
+	log.Trace("Finished: GitGcRepos")
+	return nil
+}
+
+func gatherMissingRepoRecords() ([]*models.Repository, error) {
+	repos := make([]*models.Repository, 0, 10)
+	if err := models.Iterate(
+		models.DefaultDBContext(),
+		new(models.Repository),
+		builder.Gt{"id": 0},
+		func(idx int, bean interface{}) error {
+			repo := bean.(*models.Repository)
+			if !com.IsDir(repo.RepoPath()) {
+				repos = append(repos, repo)
+			}
+			return nil
+		},
+	); err != nil {
+		if err2 := models.CreateRepositoryNotice(fmt.Sprintf("gatherMissingRepoRecords: %v", err)); err2 != nil {
+			return nil, fmt.Errorf("CreateRepositoryNotice: %v", err)
+		}
+	}
+	return repos, nil
+}
+
+// DeleteMissingRepositories deletes all repository records that lost Git files.
+func DeleteMissingRepositories(doer *models.User) error {
+	repos, err := gatherMissingRepoRecords()
+	if err != nil {
+		return fmt.Errorf("gatherMissingRepoRecords: %v", err)
+	}
+
+	if len(repos) == 0 {
+		return nil
+	}
+
+	for _, repo := range repos {
+		log.Trace("Deleting %d/%d...", repo.OwnerID, repo.ID)
+		if err := models.DeleteRepository(doer, repo.OwnerID, repo.ID); err != nil {
+			if err2 := models.CreateRepositoryNotice(fmt.Sprintf("DeleteRepository [%d]: %v", repo.ID, err)); err2 != nil {
+				return fmt.Errorf("CreateRepositoryNotice: %v", err)
+			}
+		}
+	}
+	return nil
+}
+
+// ReinitMissingRepositories reinitializes all repository records that lost Git files.
+func ReinitMissingRepositories() error {
+	repos, err := gatherMissingRepoRecords()
+	if err != nil {
+		return fmt.Errorf("gatherMissingRepoRecords: %v", err)
+	}
+
+	if len(repos) == 0 {
+		return nil
+	}
+
+	for _, repo := range repos {
+		log.Trace("Initializing %d/%d...", repo.OwnerID, repo.ID)
+		if err := git.InitRepository(repo.RepoPath(), true); err != nil {
+			if err2 := models.CreateRepositoryNotice(fmt.Sprintf("InitRepository [%d]: %v", repo.ID, err)); err2 != nil {
+				return fmt.Errorf("CreateRepositoryNotice: %v", err)
+			}
+		}
+	}
+	return nil
+}
diff --git a/modules/repository/fork.go b/modules/repository/fork.go
index 8953ce9ba4..638d3588ec 100644
--- a/modules/repository/fork.go
+++ b/modules/repository/fork.go
@@ -69,7 +69,7 @@ func ForkRepository(doer, owner *models.User, oldRepo *models.Repository, name,
 			return fmt.Errorf("git update-server-info: %v", err)
 		}
 
-		if err = models.CreateDelegateHooks(repoPath); err != nil {
+		if err = createDelegateHooks(repoPath); err != nil {
 			return fmt.Errorf("createDelegateHooks: %v", err)
 		}
 		return nil
diff --git a/modules/repository/hooks.go b/modules/repository/hooks.go
new file mode 100644
index 0000000000..60e3418571
--- /dev/null
+++ b/modules/repository/hooks.go
@@ -0,0 +1,104 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package repository
+
+import (
+	"context"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/setting"
+
+	"xorm.io/builder"
+)
+
+// CreateDelegateHooks creates all the hooks scripts for the repo
+func CreateDelegateHooks(repoPath string) error {
+	return createDelegateHooks(repoPath)
+}
+
+// createDelegateHooks creates all the hooks scripts for the repo
+func createDelegateHooks(repoPath string) (err error) {
+
+	var (
+		hookNames = []string{"pre-receive", "update", "post-receive"}
+		hookTpls  = []string{
+			fmt.Sprintf("#!/usr/bin/env %s\ndata=$(cat)\nexitcodes=\"\"\nhookname=$(basename $0)\nGIT_DIR=${GIT_DIR:-$(dirname $0)}\n\nfor hook in ${GIT_DIR}/hooks/${hookname}.d/*; do\ntest -x \"${hook}\" || continue\necho \"${data}\" | \"${hook}\"\nexitcodes=\"${exitcodes} $?\"\ndone\n\nfor i in ${exitcodes}; do\n[ ${i} -eq 0 ] || exit ${i}\ndone\n", setting.ScriptType),
+			fmt.Sprintf("#!/usr/bin/env %s\nexitcodes=\"\"\nhookname=$(basename $0)\nGIT_DIR=${GIT_DIR:-$(dirname $0)}\n\nfor hook in ${GIT_DIR}/hooks/${hookname}.d/*; do\ntest -x \"${hook}\" || continue\n\"${hook}\" $1 $2 $3\nexitcodes=\"${exitcodes} $?\"\ndone\n\nfor i in ${exitcodes}; do\n[ ${i} -eq 0 ] || exit ${i}\ndone\n", setting.ScriptType),
+			fmt.Sprintf("#!/usr/bin/env %s\ndata=$(cat)\nexitcodes=\"\"\nhookname=$(basename $0)\nGIT_DIR=${GIT_DIR:-$(dirname $0)}\n\nfor hook in ${GIT_DIR}/hooks/${hookname}.d/*; do\ntest -x \"${hook}\" || continue\necho \"${data}\" | \"${hook}\"\nexitcodes=\"${exitcodes} $?\"\ndone\n\nfor i in ${exitcodes}; do\n[ ${i} -eq 0 ] || exit ${i}\ndone\n", setting.ScriptType),
+		}
+		giteaHookTpls = []string{
+			fmt.Sprintf("#!/usr/bin/env %s\n\"%s\" hook --config='%s' pre-receive\n", setting.ScriptType, setting.AppPath, setting.CustomConf),
+			fmt.Sprintf("#!/usr/bin/env %s\n\"%s\" hook --config='%s' update $1 $2 $3\n", setting.ScriptType, setting.AppPath, setting.CustomConf),
+			fmt.Sprintf("#!/usr/bin/env %s\n\"%s\" hook --config='%s' post-receive\n", setting.ScriptType, setting.AppPath, setting.CustomConf),
+		}
+	)
+
+	hookDir := filepath.Join(repoPath, "hooks")
+
+	for i, hookName := range hookNames {
+		oldHookPath := filepath.Join(hookDir, hookName)
+		newHookPath := filepath.Join(hookDir, hookName+".d", "gitea")
+
+		if err := os.MkdirAll(filepath.Join(hookDir, hookName+".d"), os.ModePerm); err != nil {
+			return fmt.Errorf("create hooks dir '%s': %v", filepath.Join(hookDir, hookName+".d"), err)
+		}
+
+		// WARNING: This will override all old server-side hooks
+		if err = os.Remove(oldHookPath); err != nil && !os.IsNotExist(err) {
+			return fmt.Errorf("unable to pre-remove old hook file '%s' prior to rewriting: %v ", oldHookPath, err)
+		}
+		if err = ioutil.WriteFile(oldHookPath, []byte(hookTpls[i]), 0777); err != nil {
+			return fmt.Errorf("write old hook file '%s': %v", oldHookPath, err)
+		}
+
+		if err = os.Remove(newHookPath); err != nil && !os.IsNotExist(err) {
+			return fmt.Errorf("unable to pre-remove new hook file '%s' prior to rewriting: %v", newHookPath, err)
+		}
+		if err = ioutil.WriteFile(newHookPath, []byte(giteaHookTpls[i]), 0777); err != nil {
+			return fmt.Errorf("write new hook file '%s': %v", newHookPath, err)
+		}
+	}
+
+	return nil
+}
+
+// SyncRepositoryHooks rewrites all repositories' pre-receive, update and post-receive hooks
+// to make sure the binary and custom conf path are up-to-date.
+func SyncRepositoryHooks(ctx context.Context) error {
+	log.Trace("Doing: SyncRepositoryHooks")
+
+	if err := models.Iterate(
+		models.DefaultDBContext(),
+		new(models.Repository),
+		builder.Gt{"id": 0},
+		func(idx int, bean interface{}) error {
+			select {
+			case <-ctx.Done():
+				return fmt.Errorf("Aborted due to shutdown")
+			default:
+			}
+
+			if err := createDelegateHooks(bean.(*models.Repository).RepoPath()); err != nil {
+				return fmt.Errorf("SyncRepositoryHook: %v", err)
+			}
+			if bean.(*models.Repository).HasWiki() {
+				if err := createDelegateHooks(bean.(*models.Repository).WikiPath()); err != nil {
+					return fmt.Errorf("SyncRepositoryHook: %v", err)
+				}
+			}
+			return nil
+		},
+	); err != nil {
+		return err
+	}
+
+	log.Trace("Finished: SyncRepositoryHooks")
+	return nil
+}
diff --git a/modules/repository/init.go b/modules/repository/init.go
index 9d0beb1138..7b7d07f43e 100644
--- a/modules/repository/init.go
+++ b/modules/repository/init.go
@@ -164,7 +164,7 @@ func checkInitRepository(repoPath string) (err error) {
 	// Init git bare new repository.
 	if err = git.InitRepository(repoPath, true); err != nil {
 		return fmt.Errorf("git.InitRepository: %v", err)
-	} else if err = models.CreateDelegateHooks(repoPath); err != nil {
+	} else if err = createDelegateHooks(repoPath); err != nil {
 		return fmt.Errorf("createDelegateHooks: %v", err)
 	}
 	return nil
diff --git a/modules/repository/repo.go b/modules/repository/repo.go
index bb8cceeadc..4ecb9f660a 100644
--- a/modules/repository/repo.go
+++ b/modules/repository/repo.go
@@ -17,6 +17,7 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
+
 	"gopkg.in/ini.v1"
 )
 
@@ -156,11 +157,11 @@ func cleanUpMigrateGitConfig(configPath string) error {
 // CleanUpMigrateInfo finishes migrating repository and/or wiki with things that don't need to be done for mirrors.
 func CleanUpMigrateInfo(repo *models.Repository) (*models.Repository, error) {
 	repoPath := repo.RepoPath()
-	if err := models.CreateDelegateHooks(repoPath); err != nil {
+	if err := createDelegateHooks(repoPath); err != nil {
 		return repo, fmt.Errorf("createDelegateHooks: %v", err)
 	}
 	if repo.HasWiki() {
-		if err := models.CreateDelegateHooks(repo.WikiPath()); err != nil {
+		if err := createDelegateHooks(repo.WikiPath()); err != nil {
 			return repo, fmt.Errorf("createDelegateHooks.(wiki): %v", err)
 		}
 	}
diff --git a/routers/admin/admin.go b/routers/admin/admin.go
index 055b8f5a5e..71a22e1f9e 100644
--- a/routers/admin/admin.go
+++ b/routers/admin/admin.go
@@ -24,6 +24,7 @@ import (
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/process"
 	"code.gitea.io/gitea/modules/queue"
+	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/services/mailer"
@@ -150,6 +151,7 @@ func Dashboard(ctx *context.Context) {
 	if op > 0 {
 		var err error
 		var success string
+		shutdownCtx := graceful.GetManager().ShutdownContext()
 
 		switch Operation(op) {
 		case cleanInactivateUser:
@@ -160,25 +162,25 @@ func Dashboard(ctx *context.Context) {
 			err = models.DeleteRepositoryArchives()
 		case cleanMissingRepos:
 			success = ctx.Tr("admin.dashboard.delete_missing_repos_success")
-			err = models.DeleteMissingRepositories(ctx.User)
+			err = repo_module.DeleteMissingRepositories(ctx.User)
 		case gitGCRepos:
 			success = ctx.Tr("admin.dashboard.git_gc_repos_success")
-			err = models.GitGcRepos()
+			err = repo_module.GitGcRepos(shutdownCtx)
 		case syncSSHAuthorizedKey:
 			success = ctx.Tr("admin.dashboard.resync_all_sshkeys_success")
 			err = models.RewriteAllPublicKeys()
 		case syncRepositoryUpdateHook:
 			success = ctx.Tr("admin.dashboard.resync_all_hooks_success")
-			err = models.SyncRepositoryHooks()
+			err = repo_module.SyncRepositoryHooks(shutdownCtx)
 		case reinitMissingRepository:
 			success = ctx.Tr("admin.dashboard.reinit_missing_repos_success")
-			err = models.ReinitMissingRepositories()
+			err = repo_module.ReinitMissingRepositories()
 		case syncExternalUsers:
 			success = ctx.Tr("admin.dashboard.sync_external_users_started")
 			go graceful.GetManager().RunWithShutdownContext(models.SyncExternalUsers)
 		case gitFsck:
 			success = ctx.Tr("admin.dashboard.git_fsck_started")
-			go graceful.GetManager().RunWithShutdownContext(models.GitFsck)
+			err = repo_module.GitFsck(shutdownCtx)
 		case deleteGeneratedRepositoryAvatars:
 			success = ctx.Tr("admin.dashboard.delete_generated_repository_avatars_success")
 			err = models.RemoveRandomAvatars()
diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go
index e2b04ade77..2ace1d7a08 100644
--- a/services/wiki/wiki.go
+++ b/services/wiki/wiki.go
@@ -14,6 +14,7 @@ import (
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
+	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/sync"
 	"code.gitea.io/gitea/modules/util"
 
@@ -74,7 +75,7 @@ func InitWiki(repo *models.Repository) error {
 
 	if err := git.InitRepository(repo.WikiPath(), true); err != nil {
 		return fmt.Errorf("InitRepository: %v", err)
-	} else if err = models.CreateDelegateHooks(repo.WikiPath()); err != nil {
+	} else if err = repo_module.CreateDelegateHooks(repo.WikiPath()); err != nil {
 		return fmt.Errorf("createDelegateHooks: %v", err)
 	}
 	return nil