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:
Gabor Pihaj 2026-04-27 22:34:46 +02:00 committed by Gusted
parent f05ff7ec5b
commit 73b30acbd0
26 changed files with 418 additions and 439 deletions

View file

@ -0,0 +1,188 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"fmt"
"os"
"path/filepath"
"forgejo.org/modules/setting"
"forgejo.org/modules/util"
)
func getHookTemplates() (hookNames, hookTpls, giteaHookTpls []string) {
hookNames = []string{"pre-receive", "update", "post-receive"}
hookTpls = []string{
// for pre-receive
fmt.Sprintf(`#!/usr/bin/env %s
# AUTO GENERATED BY GITEA, DO NOT MODIFY
data=$(cat)
exitcodes=""
hookname=$(basename $0)
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
`, setting.ScriptType),
// for update
fmt.Sprintf(`#!/usr/bin/env %s
# AUTO GENERATED BY GITEA, DO NOT MODIFY
exitcodes=""
hookname=$(basename $0)
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
`, setting.ScriptType),
// for post-receive
fmt.Sprintf(`#!/usr/bin/env %s
# AUTO GENERATED BY GITEA, DO NOT MODIFY
data=$(cat)
exitcodes=""
hookname=$(basename $0)
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
`, setting.ScriptType),
}
giteaHookTpls = []string{
// for pre-receive
fmt.Sprintf(`#!/usr/bin/env %s
# AUTO GENERATED BY GITEA, DO NOT MODIFY
%s hook --config=%s pre-receive
`, setting.ScriptType, util.ShellEscape(setting.AppPath), util.ShellEscape(setting.CustomConf)),
// for update
fmt.Sprintf(`#!/usr/bin/env %s
# AUTO GENERATED BY GITEA, DO NOT MODIFY
%s hook --config=%s update $1 $2 $3
`, setting.ScriptType, util.ShellEscape(setting.AppPath), util.ShellEscape(setting.CustomConf)),
// for post-receive
fmt.Sprintf(`#!/usr/bin/env %s
# AUTO GENERATED BY GITEA, DO NOT MODIFY
%s hook --config=%s post-receive
`, setting.ScriptType, util.ShellEscape(setting.AppPath), util.ShellEscape(setting.CustomConf)),
}
// although only new git (>=2.29) supports proc-receive, it's still good to create its hook, in case the user upgrades git
hookNames = append(hookNames, "proc-receive")
hookTpls = append(hookTpls,
fmt.Sprintf(`#!/usr/bin/env %s
# AUTO GENERATED BY GITEA, DO NOT MODIFY
%s hook --config=%s proc-receive
`, setting.ScriptType, util.ShellEscape(setting.AppPath), util.ShellEscape(setting.CustomConf)))
giteaHookTpls = append(giteaHookTpls, "")
return hookNames, hookTpls, giteaHookTpls
}
func InitDelegateHooks(path string) (err error) {
hookNames, hookTpls, giteaHookTpls := getHookTemplates()
hookDir := filepath.Join(path, "hooks")
for i, hookName := range hookNames {
oldHookPath := filepath.Join(hookDir, hookName)
newHookPath := filepath.Join(hookDir, hookName+".d", "gitea")
if err := os.MkdirAll(filepath.Join(hookDir, hookName+".d"), os.ModePerm); err != nil {
return fmt.Errorf("create hooks dir '%s': %w", filepath.Join(hookDir, hookName+".d"), err)
}
// WARNING: This will override all old server-side hooks
if err = util.Remove(oldHookPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("unable to pre-remove old hook file '%s' prior to rewriting: %w ", oldHookPath, err)
}
if err = os.WriteFile(oldHookPath, []byte(hookTpls[i]), 0o777); err != nil {
return fmt.Errorf("write old hook file '%s': %w", oldHookPath, err)
}
if err = ensureExecutable(oldHookPath); err != nil {
return fmt.Errorf("Unable to set %s executable. Error %w", oldHookPath, err)
}
if err = util.Remove(newHookPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("unable to pre-remove new hook file '%s' prior to rewriting: %w", newHookPath, err)
}
if err = os.WriteFile(newHookPath, []byte(giteaHookTpls[i]), 0o777); err != nil {
return fmt.Errorf("write new hook file '%s': %w", newHookPath, err)
}
if err = ensureExecutable(newHookPath); err != nil {
return fmt.Errorf("Unable to set %s executable. Error %w", oldHookPath, err)
}
}
return nil
}
func ensureExecutable(filename string) error {
fileInfo, err := os.Stat(filename)
if err != nil {
return err
}
if (fileInfo.Mode() & 0o100) > 0 {
return nil
}
mode := fileInfo.Mode() | 0o100
return os.Chmod(filename, mode)
}