mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-05-13 06:20:24 +00:00
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 <gusted@noreply.codeberg.org>
This commit is contained in:
parent
f05ff7ec5b
commit
73b30acbd0
26 changed files with 418 additions and 439 deletions
6
Makefile
6
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
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@ func subcmdRegenerate() *cli.Command {
|
|||
Name: "regenerate",
|
||||
Usage: "Regenerate specific files",
|
||||
Commands: []*cli.Command{
|
||||
microcmdRegenHooks,
|
||||
microcmdRegenKeys,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
1
release-notes/10397.md
Normal file
1
release-notes/10397.md
Normal file
|
|
@ -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).
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
0
tests/install.ini.tmpl
Normal file
0
tests/install.ini.tmpl
Normal file
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
99
tests/integration/git_hooks_test.go
Normal file
99
tests/integration/git_hooks_test.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
}
|
||||
|
|
|
|||
2
tests/unittest.ini.tmpl
Normal file
2
tests/unittest.ini.tmpl
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[security]
|
||||
INSTALL_LOCK = true
|
||||
Loading…
Add table
Add a link
Reference in a new issue