From 73b30acbd0e96cd1afdd01bf2aebcb5153a27367 Mon Sep 17 00:00:00 2001 From: Gabor Pihaj Date: Mon, 27 Apr 2026 22:34:46 +0200 Subject: [PATCH] feat: replace repo based server-side hooks with centralised hooks (#10397) This PR is replacing repository based hooks hooks with centralised files, this way the files don't need to be copied into every repository, only one line of config need to be added in the repository. Closes: #3523 Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/10397 Reviewed-by: Gusted --- Makefile | 6 +- cmd/admin.go | 1 - cmd/admin_regenerate.go | 31 +-- cmd/main_test.go | 8 +- models/unittest/testdb.go | 38 ++- modules/git/git.go | 10 + .../hooks.go => git/hook_generate.go} | 122 +++------ modules/repository/init.go | 2 - release-notes/10397.md | 1 + routers/init.go | 2 - routers/install/routes_test.go | 2 +- services/cron/tasks_extended.go | 11 - services/doctor/misc.go | 33 --- services/repository/adopt.go | 4 - services/repository/fork.go | 4 - services/repository/hooks.go | 38 --- services/repository/migrate.go | 8 - services/wiki/wiki.go | 2 - tests/install.ini.tmpl | 0 tests/integration/api_admin_test.go | 4 +- tests/integration/api_issue_config_test.go | 245 +++++++++--------- tests/integration/empty_repo_test.go | 149 +++++------ tests/integration/git_hooks_test.go | 99 +++++++ tests/integration/git_test.go | 1 + .../repo_mergecommit_revert_test.go | 34 +-- tests/unittest.ini.tmpl | 2 + 26 files changed, 418 insertions(+), 439 deletions(-) rename modules/{repository/hooks.go => git/hook_generate.go} (60%) create mode 100644 release-notes/10397.md create mode 100644 tests/install.ini.tmpl create mode 100644 tests/integration/git_hooks_test.go create mode 100644 tests/unittest.ini.tmpl diff --git a/Makefile b/Makefile index 99aaaac8d8..a499443ddd 100644 --- a/Makefile +++ b/Makefile @@ -562,12 +562,12 @@ test: test-frontend test-backend .PHONY: test-backend test-backend: | compute-go-test-packages @echo "Running go test with $(GOTESTFLAGS) -tags '$(TEST_TAGS)'..." - @TZ=UTC $(GOTEST) $(GOTESTFLAGS) -tags='$(TEST_TAGS)' $(GO_TEST_PACKAGES) + @TZ=UTC GITEA_ROOT="$(CURDIR)" $(GOTEST) $(GOTESTFLAGS) -tags='$(TEST_TAGS)' $(GO_TEST_PACKAGES) .PHONY: test-remote-cacher test-remote-cacher: @echo "Running go test with $(GOTESTFLAGS) -tags '$(TEST_TAGS)'..." - @$(GOTEST) $(GOTESTFLAGS) -tags='$(TEST_TAGS)' $(GO_TEST_REMOTE_CACHER_PACKAGES) + GITEA_ROOT="$(CURDIR)" $(GOTEST) $(GOTESTFLAGS) -tags='$(TEST_TAGS)' $(GO_TEST_REMOTE_CACHER_PACKAGES) .PHONY: test-frontend test-frontend: node_modules @@ -592,7 +592,7 @@ test-check: .PHONY: test\#% test\#%: | compute-go-test-packages @echo "Running go test with $(GOTESTFLAGS) -tags '$(TEST_TAGS)'..." - @TZ=UTC $(GOTEST) $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -run $(subst .,/,$*) $(GO_TEST_PACKAGES) + @TZ=UTC GITEA_ROOT="$(CURDIR)" $(GOTEST) $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -run $(subst .,/,$*) $(GO_TEST_PACKAGES) coverage-merge: rm -fr coverage/merged ; mkdir -p coverage/merged diff --git a/cmd/admin.go b/cmd/admin.go index 60b25eb971..fe212cc388 100644 --- a/cmd/admin.go +++ b/cmd/admin.go @@ -47,7 +47,6 @@ func subcmdRegenerate() *cli.Command { Name: "regenerate", Usage: "Regenerate specific files", Commands: []*cli.Command{ - microcmdRegenHooks, microcmdRegenKeys, }, } diff --git a/cmd/admin_regenerate.go b/cmd/admin_regenerate.go index 4d14df317d..4620a7106d 100644 --- a/cmd/admin_regenerate.go +++ b/cmd/admin_regenerate.go @@ -7,36 +7,15 @@ import ( "context" asymkey_model "forgejo.org/models/asymkey" - "forgejo.org/modules/graceful" - repo_service "forgejo.org/services/repository" "github.com/urfave/cli/v3" ) -var ( - microcmdRegenHooks = &cli.Command{ - Name: "hooks", - Usage: "Regenerate git-hooks", - Before: noDanglingArgs, - Action: runRegenerateHooks, - } - - microcmdRegenKeys = &cli.Command{ - Name: "keys", - Usage: "Regenerate authorized_keys file", - Before: noDanglingArgs, - Action: runRegenerateKeys, - } -) - -func runRegenerateHooks(ctx context.Context, c *cli.Command) error { - ctx, cancel := installSignals(ctx) - defer cancel() - - if err := initDB(ctx); err != nil { - return err - } - return repo_service.SyncRepositoryHooks(graceful.GetManager().ShutdownContext()) +var microcmdRegenKeys = &cli.Command{ + Name: "keys", + Usage: "Regenerate authorized_keys file", + Before: noDanglingArgs, + Action: runRegenerateKeys, } func runRegenerateKeys(ctx context.Context, c *cli.Command) error { diff --git a/cmd/main_test.go b/cmd/main_test.go index 1ec3471343..ca6a56f4af 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "io" + "os" "path/filepath" "strings" "testing" @@ -62,7 +63,12 @@ func runTestApp(app *cli.Command, args ...string) (runResult, error) { } func TestCliCmd(t *testing.T) { - defaultWorkPath := filepath.Dir(setting.AppPath) + path, err := os.Executable() + if err != nil { + panic(err) + } + + defaultWorkPath := filepath.Dir(path) defaultCustomPath := filepath.Join(defaultWorkPath, "custom") defaultCustomConf := filepath.Join(defaultCustomPath, "conf/app.ini") diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go index 8e212bc0a3..c926a85df6 100644 --- a/models/unittest/testdb.go +++ b/models/unittest/testdb.go @@ -47,10 +47,29 @@ func fatalTestError(fmtStr string, args ...any) { // InitSettings initializes config provider and load common settings for tests func InitSettings() { - if setting.CustomConf == "" { - setting.CustomConf = filepath.Join(setting.CustomPath, "conf/app-unittest-tmp.ini") - _ = os.Remove(setting.CustomConf) + InitCustomSettings("unittest.ini") +} + +func InitCustomSettings(confFileName string) { + root := base.SetupGiteaRoot() + if root == "" { + fatalTestError("Environment variable $GITEA_ROOT not set") } + setting.AppPath = filepath.Join(root, "gitea") + if setting.CustomConf == "" { + templateFile := confFileName + ".tmpl" + content, err := os.ReadFile(filepath.Join(root, "tests", templateFile)) + if err != nil { + log.Fatalf("couldn't read config template: %s", templateFile) + } + err = os.WriteFile(filepath.Join(root, "tests", confFileName), content, 0o644) + if err != nil { + log.Fatalf("couldn't write config: %s", confFileName) + } + setting.CustomConf = filepath.Join(root, "tests", confFileName) + } + os.Setenv("GITEA_CONF", setting.CustomConf) + setting.InitCfgProvider(setting.CustomConf) setting.LoadCommonSettings() @@ -72,9 +91,10 @@ func InitSettings() { // TestOptions represents test options type TestOptions struct { - FixtureFiles []string - SetUp func() error // SetUp will be executed before all tests in this package - TearDown func() error // TearDown will be executed after all tests in this package + FixtureFiles []string + SetUp func() error // SetUp will be executed before all tests in this package + TearDown func() error // TearDown will be executed after all tests in this package + IniFileOverride string } // MainTest a reusable TestMain(..) function for unit tests that need to use a @@ -97,7 +117,11 @@ func MainTest(m *testing.M, testOpts ...*TestOptions) { giteaRoot = searchDir setting.CustomPath = filepath.Join(giteaRoot, "custom") - InitSettings() + if len(testOpts) == 0 || testOpts[0].IniFileOverride == "" { + InitSettings() + } else { + InitCustomSettings(testOpts[0].IniFileOverride) + } fixturesDir = filepath.Join(giteaRoot, "models", "fixtures") var opts FixturesOptions diff --git a/modules/git/git.go b/modules/git/git.go index 650fd3b5af..7f8674d099 100644 --- a/modules/git/git.go +++ b/modules/git/git.go @@ -10,6 +10,7 @@ import ( "fmt" "os" "os/exec" + "path" "path/filepath" "regexp" "runtime" @@ -200,6 +201,11 @@ func InitFull(ctx context.Context) (err error) { _, err = exec.LookPath("ssh") HasSSHExecutable = err == nil + err = InitDelegateHooks(HomeDir()) + if err != nil { + return nil + } + return syncGitConfig() } @@ -229,6 +235,10 @@ func syncGitConfig() (err error) { } } + if err := configSet("core.hooksPath", path.Join(HomeDir(), "hooks")); err != nil { + return err + } + // Set git some configurations - these must be set to these values for forgejo to work correctly if err := configSet("core.quotePath", "false"); err != nil { return err diff --git a/modules/repository/hooks.go b/modules/git/hook_generate.go similarity index 60% rename from modules/repository/hooks.go rename to modules/git/hook_generate.go index 0f5e3afc34..2af2487ab3 100644 --- a/modules/repository/hooks.go +++ b/modules/git/hook_generate.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package repository +package git import ( "fmt" @@ -21,14 +21,25 @@ func getHookTemplates() (hookNames, hookTpls, giteaHookTpls []string) { data=$(cat) exitcodes="" hookname=$(basename $0) -GIT_DIR=${GIT_DIR:-$(dirname $0)/..} -for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do +for hook in $(dirname $0)/${hookname}.d/*; do test -x "${hook}" && test -f "${hook}" || continue echo "${data}" | "${hook}" exitcodes="${exitcodes} $?" done +# Custom hooks +custom_hooks_dir="./hooks/${hookname}.d" +if [ -d "${custom_hooks_dir}" ]; then + for hook in ${custom_hooks_dir}/*; do + if [ $(basename "${hook}") != "gitea" ]; then + test -x "${hook}" && test -f "${hook}" || continue + echo "${data}" | "${hook}" + exitcodes="${exitcodes} $?" + fi + done +fi + for i in ${exitcodes}; do [ ${i} -eq 0 ] || exit ${i} done @@ -39,14 +50,25 @@ done # AUTO GENERATED BY GITEA, DO NOT MODIFY exitcodes="" hookname=$(basename $0) -GIT_DIR=${GIT_DIR:-$(dirname $0/..)} -for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do +for hook in $(dirname $0)/${hookname}.d/*; do test -x "${hook}" && test -f "${hook}" || continue "${hook}" $1 $2 $3 exitcodes="${exitcodes} $?" done +# Custom hooks +custom_hooks_dir="./hooks/${hookname}.d" +if [ -d "${custom_hooks_dir}" ]; then + for hook in ${custom_hooks_dir}/*; do + if [ $(basename "${hook}") != "gitea" ]; then + test -x "${hook}" && test -f "${hook}" || continue + "${hook}" $1 $2 $3 + exitcodes="${exitcodes} $?" + fi + done +fi + for i in ${exitcodes}; do [ ${i} -eq 0 ] || exit ${i} done @@ -58,14 +80,24 @@ done data=$(cat) exitcodes="" hookname=$(basename $0) -GIT_DIR=${GIT_DIR:-$(dirname $0)/..} -for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do +for hook in $(dirname $0)/${hookname}.d/*; do test -x "${hook}" && test -f "${hook}" || continue echo "${data}" | "${hook}" exitcodes="${exitcodes} $?" done +# Custom hooks +custom_hooks_dir="./hooks/${hookname}.d" +if [ -d "${custom_hooks_dir}" ]; then + for hook in ${custom_hooks_dir}/*; do + if [ $(basename "${hook}") != "gitea" ]; then + test -x "${hook}" && test -f "${hook}" || continue + echo "${data}" | "${hook}" + exitcodes="${exitcodes} $?" + fi + done +fi for i in ${exitcodes}; do [ ${i} -eq 0 ] || exit ${i} done @@ -104,10 +136,9 @@ done return hookNames, hookTpls, giteaHookTpls } -// CreateDelegateHooks creates all the hooks scripts for the repo -func CreateDelegateHooks(repoPath string) (err error) { +func InitDelegateHooks(path string) (err error) { hookNames, hookTpls, giteaHookTpls := getHookTemplates() - hookDir := filepath.Join(repoPath, "hooks") + hookDir := filepath.Join(path, "hooks") for i, hookName := range hookNames { oldHookPath := filepath.Join(hookDir, hookName) @@ -144,14 +175,6 @@ func CreateDelegateHooks(repoPath string) (err error) { return nil } -func checkExecutable(filename string) bool { - fileInfo, err := os.Stat(filename) - if err != nil { - return false - } - return (fileInfo.Mode() & 0o100) > 0 -} - func ensureExecutable(filename string) error { fileInfo, err := os.Stat(filename) if err != nil { @@ -163,66 +186,3 @@ func ensureExecutable(filename string) error { mode := fileInfo.Mode() | 0o100 return os.Chmod(filename, mode) } - -// CheckDelegateHooks checks the hooks scripts for the repo -func CheckDelegateHooks(repoPath string) ([]string, error) { - hookNames, hookTpls, giteaHookTpls := getHookTemplates() - - hookDir := filepath.Join(repoPath, "hooks") - results := make([]string, 0, 10) - - for i, hookName := range hookNames { - oldHookPath := filepath.Join(hookDir, hookName) - newHookPath := filepath.Join(hookDir, hookName+".d", "gitea") - - cont := false - isExist, err := util.IsExist(oldHookPath) - if err != nil { - results = append(results, fmt.Sprintf("unable to check if %s exists. Error: %v", oldHookPath, err)) - } - if err == nil && !isExist { - results = append(results, fmt.Sprintf("old hook file %s does not exist", oldHookPath)) - cont = true - } - isExist, err = util.IsExist(oldHookPath + ".d") - if err != nil { - results = append(results, fmt.Sprintf("unable to check if %s exists. Error: %v", oldHookPath+".d", err)) - } - if err == nil && !isExist { - results = append(results, fmt.Sprintf("hooks directory %s does not exist", oldHookPath+".d")) - cont = true - } - isExist, err = util.IsExist(newHookPath) - if err != nil { - results = append(results, fmt.Sprintf("unable to check if %s exists. Error: %v", newHookPath, err)) - } - if err == nil && !isExist { - results = append(results, fmt.Sprintf("new hook file %s does not exist", newHookPath)) - cont = true - } - if cont { - continue - } - contents, err := os.ReadFile(oldHookPath) - if err != nil { - return results, err - } - if string(contents) != hookTpls[i] { - results = append(results, fmt.Sprintf("old hook file %s is out of date", oldHookPath)) - } - if !checkExecutable(oldHookPath) { - results = append(results, fmt.Sprintf("old hook file %s is not executable", oldHookPath)) - } - contents, err = os.ReadFile(newHookPath) - if err != nil { - return results, err - } - if string(contents) != giteaHookTpls[i] { - results = append(results, fmt.Sprintf("new hook file %s is out of date", newHookPath)) - } - if !checkExecutable(newHookPath) { - results = append(results, fmt.Sprintf("new hook file %s is not executable", newHookPath)) - } - } - return results, nil -} diff --git a/modules/repository/init.go b/modules/repository/init.go index 66a65599a8..16d366c6f0 100644 --- a/modules/repository/init.go +++ b/modules/repository/init.go @@ -138,8 +138,6 @@ func CheckInitRepository(ctx context.Context, owner, name, objectFormatName stri // Init git bare new repository. if err = git.InitRepository(ctx, repoPath, true, objectFormatName); err != nil { return fmt.Errorf("git.InitRepository: %w", err) - } else if err = CreateDelegateHooks(repoPath); err != nil { - return fmt.Errorf("createDelegateHooks: %w", err) } return nil } diff --git a/release-notes/10397.md b/release-notes/10397.md new file mode 100644 index 0000000000..0560c173b9 --- /dev/null +++ b/release-notes/10397.md @@ -0,0 +1 @@ +feat: replace repository based server-side hooks with centralised hooks. Migration guide is available in [Admin docs](https://forgejo.org/docs/latest/admin/upgrade/#when-upgrading-from--known-problematic-versions-or-upgrade-paths). diff --git a/routers/init.go b/routers/init.go index 26cf0ace3e..f272d60d63 100644 --- a/routers/init.go +++ b/routers/init.go @@ -92,8 +92,6 @@ func syncAppConfForGit(ctx context.Context) error { } if updated { - log.Info("re-sync repository hooks ...") - mustInitCtx(ctx, repo_service.SyncRepositoryHooks) return system.AppState.Set(ctx, runtimeState) } return nil diff --git a/routers/install/routes_test.go b/routers/install/routes_test.go index 9b10f05b3b..d4409b36d0 100644 --- a/routers/install/routes_test.go +++ b/routers/install/routes_test.go @@ -34,5 +34,5 @@ func TestRoutes(t *testing.T) { } func TestMain(m *testing.M) { - unittest.MainTest(m) + unittest.MainTest(m, &unittest.TestOptions{IniFileOverride: "install.ini"}) } diff --git a/services/cron/tasks_extended.go b/services/cron/tasks_extended.go index 5cc5d4eb14..fc89fa020f 100644 --- a/services/cron/tasks_extended.go +++ b/services/cron/tasks_extended.go @@ -86,16 +86,6 @@ func registerRewriteAllPrincipalKeys() { }) } -func registerRepositoryUpdateHook() { - RegisterTaskFatal("resync_all_hooks", &BaseConfig{ - Enabled: false, - RunAtStart: false, - Schedule: "@every 72h", - }, func(ctx context.Context, _ *user_model.User, _ Config) error { - return repo_service.SyncRepositoryHooks(ctx) - }) -} - func registerReinitMissingRepositories() { RegisterTaskFatal("reinit_missing_repos", &BaseConfig{ Enabled: false, @@ -251,7 +241,6 @@ func initExtendedTasks() { registerGarbageCollectRepositories() registerRewriteAllPublicKeys() registerRewriteAllPrincipalKeys() - registerRepositoryUpdateHook() registerReinitMissingRepositories() registerDeleteMissingRepositories() registerRemoveRandomAvatars() diff --git a/services/doctor/misc.go b/services/doctor/misc.go index 9b9c96b52b..323606edf1 100644 --- a/services/doctor/misc.go +++ b/services/doctor/misc.go @@ -18,7 +18,6 @@ import ( "forgejo.org/modules/git" "forgejo.org/modules/gitrepo" "forgejo.org/modules/log" - "forgejo.org/modules/repository" "forgejo.org/modules/setting" "forgejo.org/modules/structs" "forgejo.org/modules/util" @@ -48,31 +47,6 @@ func checkScriptType(ctx context.Context, logger log.Logger, autofix bool) error return nil } -func checkHooks(ctx context.Context, logger log.Logger, autofix bool) error { - if err := iterateRepositories(ctx, func(repo *repo_model.Repository) error { - results, err := repository.CheckDelegateHooks(repo.RepoPath()) - if err != nil { - logger.Critical("Unable to check delegate hooks for repo %-v. ERROR: %v", repo, err) - return fmt.Errorf("Unable to check delegate hooks for repo %-v. ERROR: %w", repo, err) - } - if len(results) > 0 && autofix { - logger.Warn("Regenerated hooks for %s", repo.FullName()) - if err := repository.CreateDelegateHooks(repo.RepoPath()); err != nil { - logger.Critical("Unable to recreate delegate hooks for %-v. ERROR: %v", repo, err) - return fmt.Errorf("Unable to recreate delegate hooks for %-v. ERROR: %w", repo, err) - } - } - for _, result := range results { - logger.Warn(result) - } - return nil - }); err != nil { - logger.Critical("Errors noted whilst checking delegate hooks.") - return err - } - return nil -} - func checkUserStarNum(ctx context.Context, logger log.Logger, autofix bool) error { if autofix { if err := models.DoctorUserStarNum(ctx); err != nil { @@ -261,13 +235,6 @@ func init() { Run: checkScriptType, Priority: 5, }) - Register(&Check{ - Title: "Check if hook files are up-to-date and executable", - Name: "hooks", - IsDefault: false, - Run: checkHooks, - Priority: 6, - }) Register(&Check{ Title: "Recalculate Stars number for all user", Name: "recalculate-stars-number", diff --git a/services/repository/adopt.go b/services/repository/adopt.go index 3651b018e6..1949a62acf 100644 --- a/services/repository/adopt.go +++ b/services/repository/adopt.go @@ -117,10 +117,6 @@ func adoptRepository(ctx context.Context, repoPath string, repo *repo_model.Repo return fmt.Errorf("adoptRepository: path does not already exist: %s", repoPath) } - if err := repo_module.CreateDelegateHooks(repoPath); err != nil { - return fmt.Errorf("createDelegateHooks: %w", err) - } - repo.IsEmpty = false if len(defaultBranch) > 0 { diff --git a/services/repository/fork.go b/services/repository/fork.go index 9d15b6207d..a51a290c05 100644 --- a/services/repository/fork.go +++ b/services/repository/fork.go @@ -164,10 +164,6 @@ func ForkRepositoryIfNotExists(ctx context.Context, doer, owner *user_model.User return fmt.Errorf("git update-server-info: %w", err) } - if err = repo_module.CreateDelegateHooks(repoPath); err != nil { - return fmt.Errorf("createDelegateHooks: %w", err) - } - gitRepo, err := gitrepo.OpenRepository(txCtx, repo) if err != nil { return fmt.Errorf("OpenRepository: %w", err) diff --git a/services/repository/hooks.go b/services/repository/hooks.go index d3021414cf..808b49212e 100644 --- a/services/repository/hooks.go +++ b/services/repository/hooks.go @@ -5,51 +5,13 @@ package repository import ( "context" - "fmt" "forgejo.org/models/db" repo_model "forgejo.org/models/repo" "forgejo.org/models/webhook" "forgejo.org/modules/gitrepo" - "forgejo.org/modules/log" - repo_module "forgejo.org/modules/repository" - - "xorm.io/builder" ) -// 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 := db.Iterate( - ctx, - builder.Gt{"id": 0}, - func(ctx context.Context, repo *repo_model.Repository) error { - select { - case <-ctx.Done(): - return db.ErrCancelledf("before sync repository hooks for %s", repo.FullName()) - default: - } - - if err := repo_module.CreateDelegateHooks(repo.RepoPath()); err != nil { - return fmt.Errorf("SyncRepositoryHook: %w", err) - } - if repo.HasWiki() { - if err := repo_module.CreateDelegateHooks(repo.WikiPath()); err != nil { - return fmt.Errorf("SyncRepositoryHook: %w", err) - } - } - return nil - }, - ); err != nil { - return err - } - - log.Trace("Finished: SyncRepositoryHooks") - return nil -} - // GenerateGitHooks generates git hooks from a template repository func GenerateGitHooks(ctx context.Context, templateRepo, generateRepo *repo_model.Repository) error { generateGitRepo, err := gitrepo.OpenRepository(ctx, generateRepo) diff --git a/services/repository/migrate.go b/services/repository/migrate.go index 46b48d02d3..e4c73dde8c 100644 --- a/services/repository/migrate.go +++ b/services/repository/migrate.go @@ -258,14 +258,6 @@ func cleanUpMigrateGitConfig(ctx context.Context, repoPath string) error { // CleanUpMigrateInfo finishes migrating repository and/or wiki with things that don't need to be done for mirrors. func CleanUpMigrateInfo(ctx context.Context, repo *repo_model.Repository) (*repo_model.Repository, error) { repoPath := repo.RepoPath() - if err := repo_module.CreateDelegateHooks(repoPath); err != nil { - return repo, fmt.Errorf("createDelegateHooks: %w", err) - } - if repo.HasWiki() { - if err := repo_module.CreateDelegateHooks(repo.WikiPath()); err != nil { - return repo, fmt.Errorf("createDelegateHooks.(wiki): %w", err) - } - } _, _, err := git.NewCommand(ctx, "remote", "rm", "origin").RunStdString(&git.RunOpts{Dir: repoPath}) if err != nil && !git.IsRemoteNotExistError(err) { diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go index cf1477e72c..984c394f0e 100644 --- a/services/wiki/wiki.go +++ b/services/wiki/wiki.go @@ -42,8 +42,6 @@ func InitWiki(ctx context.Context, repo *repo_model.Repository) error { if err := git.InitRepository(ctx, repo.WikiPath(), true, repo.ObjectFormatName); err != nil { return fmt.Errorf("InitRepository: %w", err) - } else if err = repo_module.CreateDelegateHooks(repo.WikiPath()); err != nil { - return fmt.Errorf("createDelegateHooks: %w", err) } else if _, _, err = git.NewCommand(ctx, "symbolic-ref", "HEAD").AddDynamicArguments(git.BranchPrefix + branch).RunStdString(&git.RunOpts{Dir: repo.WikiPath()}); err != nil { return fmt.Errorf("unable to set default wiki branch to %s: %w", branch, err) } diff --git a/tests/install.ini.tmpl b/tests/install.ini.tmpl new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/api_admin_test.go b/tests/integration/api_admin_test.go index 513eac1155..e3cab38d9d 100644 --- a/tests/integration/api_admin_test.go +++ b/tests/integration/api_admin_test.go @@ -426,11 +426,11 @@ func TestAPICron(t *testing.T) { AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusOK) - assert.Equal(t, "31", resp.Header().Get("X-Total-Count")) + assert.Equal(t, "30", resp.Header().Get("X-Total-Count")) var crons []api.Cron DecodeJSON(t, resp, &crons) - assert.Len(t, crons, 31) + assert.Len(t, crons, 30) }) t.Run("Execute", func(t *testing.T) { diff --git a/tests/integration/api_issue_config_test.go b/tests/integration/api_issue_config_test.go index 99b2829fbf..eecf00e406 100644 --- a/tests/integration/api_issue_config_test.go +++ b/tests/integration/api_issue_config_test.go @@ -7,6 +7,7 @@ package integration import ( "fmt" "net/http" + "net/url" "testing" repo_model "forgejo.org/models/repo" @@ -44,169 +45,169 @@ func getIssueConfig(t *testing.T, owner, repo string) api.IssueConfig { } func TestAPIRepoGetIssueConfig(t *testing.T) { - defer tests.PrepareTestEnv(t)() + onApplicationRun(t, func(t *testing.T, _ *url.URL) { + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 49}) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 49}) - owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + t.Run("Default", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() - t.Run("Default", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() + issueConfig := getIssueConfig(t, owner.Name, repo.Name) - issueConfig := getIssueConfig(t, owner.Name, repo.Name) + assert.True(t, issueConfig.BlankIssuesEnabled) + assert.Empty(t, issueConfig.ContactLinks) + }) - assert.True(t, issueConfig.BlankIssuesEnabled) - assert.Empty(t, issueConfig.ContactLinks) - }) + t.Run("DisableBlankIssues", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() - t.Run("DisableBlankIssues", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() + config := make(map[string]any) + config["blank_issues_enabled"] = false - config := make(map[string]any) - config["blank_issues_enabled"] = false + createIssueConfig(t, owner, repo, config) - createIssueConfig(t, owner, repo, config) + issueConfig := getIssueConfig(t, owner.Name, repo.Name) - issueConfig := getIssueConfig(t, owner.Name, repo.Name) + assert.False(t, issueConfig.BlankIssuesEnabled) + assert.Empty(t, issueConfig.ContactLinks) + }) - assert.False(t, issueConfig.BlankIssuesEnabled) - assert.Empty(t, issueConfig.ContactLinks) - }) + t.Run("ContactLinks", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() - t.Run("ContactLinks", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() + contactLink := make(map[string]string) + contactLink["name"] = "TestName" + contactLink["url"] = "https://example.com" + contactLink["about"] = "TestAbout" - contactLink := make(map[string]string) - contactLink["name"] = "TestName" - contactLink["url"] = "https://example.com" - contactLink["about"] = "TestAbout" + config := make(map[string]any) + config["contact_links"] = []map[string]string{contactLink} - config := make(map[string]any) - config["contact_links"] = []map[string]string{contactLink} + createIssueConfig(t, owner, repo, config) - createIssueConfig(t, owner, repo, config) + issueConfig := getIssueConfig(t, owner.Name, repo.Name) - issueConfig := getIssueConfig(t, owner.Name, repo.Name) + assert.True(t, issueConfig.BlankIssuesEnabled) + assert.Len(t, issueConfig.ContactLinks, 1) - assert.True(t, issueConfig.BlankIssuesEnabled) - assert.Len(t, issueConfig.ContactLinks, 1) + assert.Equal(t, "TestName", issueConfig.ContactLinks[0].Name) + assert.Equal(t, "https://example.com", issueConfig.ContactLinks[0].URL) + assert.Equal(t, "TestAbout", issueConfig.ContactLinks[0].About) + }) - assert.Equal(t, "TestName", issueConfig.ContactLinks[0].Name) - assert.Equal(t, "https://example.com", issueConfig.ContactLinks[0].URL) - assert.Equal(t, "TestAbout", issueConfig.ContactLinks[0].About) - }) + t.Run("Full", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() - t.Run("Full", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() + contactLink := make(map[string]string) + contactLink["name"] = "TestName" + contactLink["url"] = "https://example.com" + contactLink["about"] = "TestAbout" - contactLink := make(map[string]string) - contactLink["name"] = "TestName" - contactLink["url"] = "https://example.com" - contactLink["about"] = "TestAbout" + config := make(map[string]any) + config["blank_issues_enabled"] = false + config["contact_links"] = []map[string]string{contactLink} - config := make(map[string]any) - config["blank_issues_enabled"] = false - config["contact_links"] = []map[string]string{contactLink} + createIssueConfig(t, owner, repo, config) - createIssueConfig(t, owner, repo, config) + issueConfig := getIssueConfig(t, owner.Name, repo.Name) - issueConfig := getIssueConfig(t, owner.Name, repo.Name) + assert.False(t, issueConfig.BlankIssuesEnabled) + assert.Len(t, issueConfig.ContactLinks, 1) - assert.False(t, issueConfig.BlankIssuesEnabled) - assert.Len(t, issueConfig.ContactLinks, 1) - - assert.Equal(t, "TestName", issueConfig.ContactLinks[0].Name) - assert.Equal(t, "https://example.com", issueConfig.ContactLinks[0].URL) - assert.Equal(t, "TestAbout", issueConfig.ContactLinks[0].About) + assert.Equal(t, "TestName", issueConfig.ContactLinks[0].Name) + assert.Equal(t, "https://example.com", issueConfig.ContactLinks[0].URL) + assert.Equal(t, "TestAbout", issueConfig.ContactLinks[0].About) + }) }) } func TestAPIRepoIssueConfigPaths(t *testing.T) { - defer tests.PrepareTestEnv(t)() + onApplicationRun(t, func(t *testing.T, _ *url.URL) { + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 49}) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 49}) - owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) - - templateConfigCandidates := []string{ - ".forgejo/ISSUE_TEMPLATE/config", - ".forgejo/issue_template/config", - ".gitea/ISSUE_TEMPLATE/config", - ".gitea/issue_template/config", - ".github/ISSUE_TEMPLATE/config", - ".github/issue_template/config", - "docs/issue_template/config", - } - - for _, candidate := range templateConfigCandidates { - for _, extension := range []string{".yaml", ".yml"} { - fullPath := candidate + extension - t.Run(fullPath, func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - - configMap := make(map[string]any) - configMap["blank_issues_enabled"] = false - - configData, err := yaml.Marshal(configMap) - require.NoError(t, err) - - _, err = createFileInBranch(owner, repo, fullPath, repo.DefaultBranch, string(configData)) - require.NoError(t, err) - - issueConfig := getIssueConfig(t, owner.Name, repo.Name) - - assert.False(t, issueConfig.BlankIssuesEnabled) - assert.Empty(t, issueConfig.ContactLinks) - - err = deleteFileInBranch(owner, repo, fullPath, repo.DefaultBranch) - require.NoError(t, err) - }) + templateConfigCandidates := []string{ + ".forgejo/ISSUE_TEMPLATE/config", + ".forgejo/issue_template/config", + ".gitea/ISSUE_TEMPLATE/config", + ".gitea/issue_template/config", + ".github/ISSUE_TEMPLATE/config", + ".github/issue_template/config", + "docs/issue_template/config", } - } + + for _, candidate := range templateConfigCandidates { + for _, extension := range []string{".yaml", ".yml"} { + fullPath := candidate + extension + t.Run(fullPath, func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + configMap := make(map[string]any) + configMap["blank_issues_enabled"] = false + + configData, err := yaml.Marshal(configMap) + require.NoError(t, err) + + _, err = createFileInBranch(owner, repo, fullPath, repo.DefaultBranch, string(configData)) + require.NoError(t, err) + + issueConfig := getIssueConfig(t, owner.Name, repo.Name) + + assert.False(t, issueConfig.BlankIssuesEnabled) + assert.Empty(t, issueConfig.ContactLinks) + + err = deleteFileInBranch(owner, repo, fullPath, repo.DefaultBranch) + require.NoError(t, err) + }) + } + } + }) } func TestAPIRepoValidateIssueConfig(t *testing.T) { - defer tests.PrepareTestEnv(t)() + onApplicationRun(t, func(t *testing.T, _ *url.URL) { + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 49}) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 49}) - owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issue_config/validate", owner.Name, repo.Name) - urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issue_config/validate", owner.Name, repo.Name) + t.Run("Valid", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() - t.Run("Valid", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() + req := NewRequest(t, "GET", urlStr) + resp := MakeRequest(t, req, http.StatusOK) - req := NewRequest(t, "GET", urlStr) - resp := MakeRequest(t, req, http.StatusOK) + var issueConfigValidation api.IssueConfigValidation + DecodeJSON(t, resp, &issueConfigValidation) - var issueConfigValidation api.IssueConfigValidation - DecodeJSON(t, resp, &issueConfigValidation) + assert.True(t, issueConfigValidation.Valid) + assert.Empty(t, issueConfigValidation.Message) + }) - assert.True(t, issueConfigValidation.Valid) - assert.Empty(t, issueConfigValidation.Message) - }) + t.Run("Invalid", func(t *testing.T) { + dirs := []string{".gitea", ".forgejo", "docs"} + for _, dir := range dirs { + t.Run(dir, func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer func() { + deleteFileInBranch(owner, repo, fmt.Sprintf("%s/ISSUE_TEMPLATE/config.yaml", dir), repo.DefaultBranch) + }() - t.Run("Invalid", func(t *testing.T) { - dirs := []string{".gitea", ".forgejo", "docs"} - for _, dir := range dirs { - t.Run(dir, func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - defer func() { - deleteFileInBranch(owner, repo, fmt.Sprintf("%s/ISSUE_TEMPLATE/config.yaml", dir), repo.DefaultBranch) - }() + config := make(map[string]any) + config["blank_issues_enabled"] = "Test" - config := make(map[string]any) - config["blank_issues_enabled"] = "Test" + createIssueConfigInDirectory(t, owner, repo, dir, config) - createIssueConfigInDirectory(t, owner, repo, dir, config) + req := NewRequest(t, "GET", urlStr) + resp := MakeRequest(t, req, http.StatusOK) - req := NewRequest(t, "GET", urlStr) - resp := MakeRequest(t, req, http.StatusOK) + var issueConfigValidation api.IssueConfigValidation + DecodeJSON(t, resp, &issueConfigValidation) - var issueConfigValidation api.IssueConfigValidation - DecodeJSON(t, resp, &issueConfigValidation) - - assert.False(t, issueConfigValidation.Valid) - assert.NotEmpty(t, issueConfigValidation.Message) - }) - } + assert.False(t, issueConfigValidation.Valid) + assert.NotEmpty(t, issueConfigValidation.Message) + }) + } + }) }) } diff --git a/tests/integration/empty_repo_test.go b/tests/integration/empty_repo_test.go index 67be6a29a5..fe7c131331 100644 --- a/tests/integration/empty_repo_test.go +++ b/tests/integration/empty_repo_test.go @@ -10,6 +10,7 @@ import ( "io" "mime/multipart" "net/http" + "net/url" "testing" auth_model "forgejo.org/models/auth" @@ -44,96 +45,96 @@ func TestEmptyRepo(t *testing.T) { } func TestEmptyRepoAddFile(t *testing.T) { - defer tests.PrepareTestEnv(t)() + onApplicationRun(t, func(t *testing.T, u *url.URL) { + session := loginUser(t, "user30") + req := NewRequest(t, "GET", "/user30/empty/_new/"+setting.Repository.DefaultBranch) + resp := session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body).Find(`input[name="commit_choice"]`) + assert.Empty(t, doc.AttrOr("checked", "_no_")) + req = NewRequestWithValues(t, "POST", "/user30/empty/_new/"+setting.Repository.DefaultBranch, map[string]string{ + "commit_choice": "direct", + "tree_path": "test-file.md", + "content": "newly-added-test-file", + "commit_mail_id": "32", + }) - session := loginUser(t, "user30") - req := NewRequest(t, "GET", "/user30/empty/_new/"+setting.Repository.DefaultBranch) - resp := session.MakeRequest(t, req, http.StatusOK) - doc := NewHTMLParser(t, resp.Body).Find(`input[name="commit_choice"]`) - assert.Empty(t, doc.AttrOr("checked", "_no_")) - req = NewRequestWithValues(t, "POST", "/user30/empty/_new/"+setting.Repository.DefaultBranch, map[string]string{ - "commit_choice": "direct", - "tree_path": "test-file.md", - "content": "newly-added-test-file", - "commit_mail_id": "32", + resp = session.MakeRequest(t, req, http.StatusSeeOther) + redirect := test.RedirectURL(resp) + assert.Equal(t, "/user30/empty/src/branch/"+setting.Repository.DefaultBranch+"/test-file.md", redirect) + + req = NewRequest(t, "GET", redirect) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.Contains(t, resp.Body.String(), "newly-added-test-file") }) - - resp = session.MakeRequest(t, req, http.StatusSeeOther) - redirect := test.RedirectURL(resp) - assert.Equal(t, "/user30/empty/src/branch/"+setting.Repository.DefaultBranch+"/test-file.md", redirect) - - req = NewRequest(t, "GET", redirect) - resp = session.MakeRequest(t, req, http.StatusOK) - assert.Contains(t, resp.Body.String(), "newly-added-test-file") } func TestEmptyRepoUploadFile(t *testing.T) { - defer tests.PrepareTestEnv(t)() + onApplicationRun(t, func(t *testing.T, u *url.URL) { + session := loginUser(t, "user30") + req := NewRequest(t, "GET", "/user30/empty/_new/"+setting.Repository.DefaultBranch) + resp := session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body).Find(`input[name="commit_choice"]`) + assert.Empty(t, doc.AttrOr("checked", "_no_")) - session := loginUser(t, "user30") - req := NewRequest(t, "GET", "/user30/empty/_new/"+setting.Repository.DefaultBranch) - resp := session.MakeRequest(t, req, http.StatusOK) - doc := NewHTMLParser(t, resp.Body).Find(`input[name="commit_choice"]`) - assert.Empty(t, doc.AttrOr("checked", "_no_")) + body := &bytes.Buffer{} + mpForm := multipart.NewWriter(body) + file, _ := mpForm.CreateFormFile("file", "uploaded-file.txt") + _, _ = io.Copy(file, bytes.NewBufferString("newly-uploaded-test-file")) + _ = mpForm.Close() - body := &bytes.Buffer{} - mpForm := multipart.NewWriter(body) - file, _ := mpForm.CreateFormFile("file", "uploaded-file.txt") - _, _ = io.Copy(file, bytes.NewBufferString("newly-uploaded-test-file")) - _ = mpForm.Close() + req = NewRequestWithBody(t, "POST", "/user30/empty/upload-file", body) + req.Header.Add("Content-Type", mpForm.FormDataContentType()) + resp = session.MakeRequest(t, req, http.StatusOK) + respMap := map[string]string{} + require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &respMap)) + filesFullpathKey := fmt.Sprintf("files_fullpath[%s]", respMap["uuid"]) + req = NewRequestWithValues(t, "POST", "/user30/empty/_upload/"+setting.Repository.DefaultBranch, map[string]string{ + "commit_choice": "direct", + "files": respMap["uuid"], + filesFullpathKey: "uploaded-file.txt", + "tree_path": "", + "commit_mail_id": "-1", + }) + resp = session.MakeRequest(t, req, http.StatusSeeOther) + redirect := test.RedirectURL(resp) + assert.Equal(t, "/user30/empty/src/branch/"+setting.Repository.DefaultBranch+"/", redirect) - req = NewRequestWithBody(t, "POST", "/user30/empty/upload-file", body) - req.Header.Add("Content-Type", mpForm.FormDataContentType()) - resp = session.MakeRequest(t, req, http.StatusOK) - respMap := map[string]string{} - require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &respMap)) - filesFullpathKey := fmt.Sprintf("files_fullpath[%s]", respMap["uuid"]) - req = NewRequestWithValues(t, "POST", "/user30/empty/_upload/"+setting.Repository.DefaultBranch, map[string]string{ - "commit_choice": "direct", - "files": respMap["uuid"], - filesFullpathKey: "uploaded-file.txt", - "tree_path": "", - "commit_mail_id": "-1", + req = NewRequest(t, "GET", redirect) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.Contains(t, resp.Body.String(), "uploaded-file.txt") }) - resp = session.MakeRequest(t, req, http.StatusSeeOther) - redirect := test.RedirectURL(resp) - assert.Equal(t, "/user30/empty/src/branch/"+setting.Repository.DefaultBranch+"/", redirect) - - req = NewRequest(t, "GET", redirect) - resp = session.MakeRequest(t, req, http.StatusOK) - assert.Contains(t, resp.Body.String(), "uploaded-file.txt") } func TestEmptyRepoAddFileByAPI(t *testing.T) { - defer tests.PrepareTestEnv(t)() + onApplicationRun(t, func(t *testing.T, _ *url.URL) { + session := loginUser(t, "user30") + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) - session := loginUser(t, "user30") - token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user30/empty/contents/new-file.txt", &api.CreateFileOptions{ + FileOptions: api.FileOptions{ + NewBranchName: "new_branch", + Message: "init", + }, + ContentBase64: base64.StdEncoding.EncodeToString([]byte("newly-added-api-file")), + }).AddTokenAuth(token) - req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user30/empty/contents/new-file.txt", &api.CreateFileOptions{ - FileOptions: api.FileOptions{ - NewBranchName: "new_branch", - Message: "init", - }, - ContentBase64: base64.StdEncoding.EncodeToString([]byte("newly-added-api-file")), - }).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusCreated) + var fileResponse api.FileResponse + DecodeJSON(t, resp, &fileResponse) + expectedHTMLURL := setting.AppURL + "user30/empty/src/branch/new_branch/new-file.txt" + assert.Equal(t, expectedHTMLURL, *fileResponse.Content.HTMLURL) - resp := MakeRequest(t, req, http.StatusCreated) - var fileResponse api.FileResponse - DecodeJSON(t, resp, &fileResponse) - expectedHTMLURL := setting.AppURL + "user30/empty/src/branch/new_branch/new-file.txt" - assert.Equal(t, expectedHTMLURL, *fileResponse.Content.HTMLURL) + req = NewRequest(t, "GET", "/user30/empty/src/branch/new_branch/new-file.txt") + resp = session.MakeRequest(t, req, http.StatusOK) + assert.Contains(t, resp.Body.String(), "newly-added-api-file") - req = NewRequest(t, "GET", "/user30/empty/src/branch/new_branch/new-file.txt") - resp = session.MakeRequest(t, req, http.StatusOK) - assert.Contains(t, resp.Body.String(), "newly-added-api-file") - - req = NewRequest(t, "GET", "/api/v1/repos/user30/empty"). - AddTokenAuth(token) - resp = session.MakeRequest(t, req, http.StatusOK) - var apiRepo api.Repository - DecodeJSON(t, resp, &apiRepo) - assert.Equal(t, "new_branch", apiRepo.DefaultBranch) + req = NewRequest(t, "GET", "/api/v1/repos/user30/empty"). + AddTokenAuth(token) + resp = session.MakeRequest(t, req, http.StatusOK) + var apiRepo api.Repository + DecodeJSON(t, resp, &apiRepo) + assert.Equal(t, "new_branch", apiRepo.DefaultBranch) + }) } func TestEmptyRepoAPIRequestsReturn404(t *testing.T) { diff --git a/tests/integration/git_hooks_test.go b/tests/integration/git_hooks_test.go new file mode 100644 index 0000000000..05d2223aee --- /dev/null +++ b/tests/integration/git_hooks_test.go @@ -0,0 +1,99 @@ +// Copyright 2026 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. +// SPDX-License-Identifier: MIT +package integration + +import ( + "fmt" + "net/url" + "os" + "path" + "testing" + + "forgejo.org/models/auth" + repo_model "forgejo.org/models/repo" + "forgejo.org/models/unittest" + user_model "forgejo.org/models/user" + "forgejo.org/modules/git" + + "github.com/stretchr/testify/require" +) + +func TestCustomGitHooks(t *testing.T) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + + httpContext := NewAPITestContext(t, owner.Name, repo.Name, auth.AccessTokenScopeReadRepository) + + dstPath := t.TempDir() + + u.Path = httpContext.GitPath() + u.User = url.UserPassword(owner.Name, userPassword) + + doGitClone(dstPath, u)(t) + + customHooksDir := path.Join(repo.RepoPath(), "hooks") + + hookNames := []string{"pre-receive", "update", "post-receive"} + + for _, hookName := range hookNames { + customPath := path.Join(customHooksDir, hookName+".d") + err := os.MkdirAll(customPath, 0x755) + require.NoError(t, err) + + err = os.WriteFile(path.Join(customPath, "append-proof"), customGitHookTpl(hookName), 0x755) + require.NoError(t, err) + + // The legacy, already existing gitea script might be there in the hooks directory in old installations, + // here it's ensured that these scripts filtered out when custom hooks run + err = os.WriteFile(path.Join(customPath, "gitea"), customGitHookGiteaTpl(), 0x755) + require.NoError(t, err) + } + + fd, err := os.Create(path.Join(dstPath, "hooks-test.txt")) + require.NoError(t, err) + + err = fd.Close() + require.NoError(t, err) + + _, _, err = git.NewCommand(git.DefaultContext, "checkout", "master").RunStdString(&git.RunOpts{Dir: dstPath}) + require.NoError(t, err) + + err = os.WriteFile(path.Join(dstPath, "hooks-test.txt"), []byte("test"), 0x644) + require.NoError(t, err) + + _, _, err = git.NewCommand(git.DefaultContext, "add", "hooks-test.txt").RunStdString(&git.RunOpts{Dir: dstPath}) + require.NoError(t, err) + + _, _, err = git.NewCommand(git.DefaultContext, "commit", "-m", "Add hooks-test.txt").RunStdString(&git.RunOpts{Dir: dstPath}) + require.NoError(t, err) + + _, _, err = git.NewCommand(git.DefaultContext, "push", "origin", "master").RunStdString(&git.RunOpts{Dir: dstPath}) + require.NoError(t, err) + + data, err := os.ReadFile(path.Join(customHooksDir, "hooks-proof.txt")) + require.NoError(t, err) + + require.Equal(t, `pre-receive +update +post-receive +`, string(data)) + }) +} + +func customGitHookTpl(hookName string) []byte { + hookStr := fmt.Sprintf(`#!/usr/bin/env sh +echo "%s" >> $(dirname $0)/../hooks-proof.txt +`, hookName) + + return []byte(hookStr) +} + +func customGitHookGiteaTpl() []byte { + hookStr := `#!/usr/bin/env sh +echo "legacy gitea script shouldn't be called!" +exit 1 +` + + return []byte(hookStr) +} diff --git a/tests/integration/git_test.go b/tests/integration/git_test.go index a5a242b912..dc86f4a4c2 100644 --- a/tests/integration/git_test.go +++ b/tests/integration/git_test.go @@ -201,6 +201,7 @@ func standardCommitAndPushTest(t *testing.T, dstPath string) (little, big string func lfsCommitAndPushTest(t *testing.T, dstPath string) (littleLFS, bigLFS string) { t.Run("LFS", func(t *testing.T) { defer tests.PrintCurrentTest(t)() + defer git.NewCommand(git.DefaultContext, "lfs").AddArguments("uninstall").Run(&git.RunOpts{Dir: dstPath}) prefix := "lfs-data-file-" err := git.NewCommand(git.DefaultContext, "lfs").AddArguments("install").Run(&git.RunOpts{Dir: dstPath}) require.NoError(t, err) diff --git a/tests/integration/repo_mergecommit_revert_test.go b/tests/integration/repo_mergecommit_revert_test.go index 9ddf39eaaa..8cbe8a8142 100644 --- a/tests/integration/repo_mergecommit_revert_test.go +++ b/tests/integration/repo_mergecommit_revert_test.go @@ -5,29 +5,29 @@ package integration import ( "net/http" + "net/url" "testing" - "forgejo.org/tests" - "github.com/stretchr/testify/assert" ) func TestRepoMergeCommitRevert(t *testing.T) { - defer tests.PrepareTestEnv(t)() - session := loginUser(t, "user2") + onApplicationRun(t, func(t *testing.T, _ *url.URL) { + session := loginUser(t, "user2") - req := NewRequestWithValues(t, "POST", "/user2/test_commit_revert/_cherrypick/deebcbc752e540bab4ce3ee713d3fc8fdc35b2f7/main", map[string]string{ - "last_commit": "deebcbc752e540bab4ce3ee713d3fc8fdc35b2f7", - "page_has_posted": "true", - "revert": "true", - "commit_summary": "reverting test commit", - "commit_message": "test message", - "commit_choice": "direct", - "new_branch_name": "test-revert-branch-1", - "commit_mail_id": "-1", + req := NewRequestWithValues(t, "POST", "/user2/test_commit_revert/_cherrypick/deebcbc752e540bab4ce3ee713d3fc8fdc35b2f7/main", map[string]string{ + "last_commit": "deebcbc752e540bab4ce3ee713d3fc8fdc35b2f7", + "page_has_posted": "true", + "revert": "true", + "commit_summary": "reverting test commit", + "commit_message": "test message", + "commit_choice": "direct", + "new_branch_name": "test-revert-branch-1", + "commit_mail_id": "-1", + }) + resp := session.MakeRequest(t, req, http.StatusSeeOther) + + // A successful revert redirects to the main branch + assert.Equal(t, "/user2/test_commit_revert/src/branch/main", resp.Header().Get("Location")) }) - resp := session.MakeRequest(t, req, http.StatusSeeOther) - - // A successful revert redirects to the main branch - assert.Equal(t, "/user2/test_commit_revert/src/branch/main", resp.Header().Get("Location")) } diff --git a/tests/unittest.ini.tmpl b/tests/unittest.ini.tmpl new file mode 100644 index 0000000000..a2925b42e7 --- /dev/null +++ b/tests/unittest.ini.tmpl @@ -0,0 +1,2 @@ +[security] +INSTALL_LOCK = true