mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
286 lines
9.5 KiB
Go
286 lines
9.5 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package gitrepo
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
|
|
"github.com/hanzoai/git/modules/setting"
|
|
"github.com/hanzoai/git/modules/util"
|
|
)
|
|
|
|
// delegateHookName is the file we write into "<hook>.d/". It is named after the
|
|
// shipped binary (gitd) and is the script that re-execs us as "gitd hook ...".
|
|
//
|
|
// legacyDelegateHookNames are the names earlier releases used. They MUST be
|
|
// removed, because the outer "<hook>" script runs EVERY executable in
|
|
// "<hook>.d/" and rejects the push if any of them exits non-zero: a delegate
|
|
// left behind by the old binary name would invoke a path that no longer exists,
|
|
// exit 127, and break every push to that repository — permanently.
|
|
//
|
|
// Renaming delegateHookName is therefore only safe in a release that ALSO moves
|
|
// setting.AppPath, because that (and only that) is what makes
|
|
// routers.syncAppConfForGit re-run SyncRepositoryHooks over every repository on
|
|
// the first boot after the upgrade — before the web listener opens. The
|
|
// gitea->gitd rename moved both at once.
|
|
const delegateHookName = "gitd"
|
|
|
|
var legacyDelegateHookNames = []string{"gitea"}
|
|
|
|
func getHookTemplates() (hookNames, hookTpls, delegateHookTpls []string) {
|
|
hookNames = []string{"pre-receive", "update", "post-receive"}
|
|
hookTpls = []string{
|
|
// for pre-receive
|
|
fmt.Sprintf(`#!/usr/bin/env %s
|
|
# AUTO GENERATED BY HANZO GIT, DO NOT MODIFY
|
|
data=$(cat)
|
|
exitcodes=""
|
|
hookname=$(basename $0)
|
|
GIT_DIR=${GIT_DIR:-$(dirname $0)/..}
|
|
|
|
for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do
|
|
test -x "${hook}" && test -f "${hook}" || continue
|
|
echo "${data}" | "${hook}"
|
|
exitcodes="${exitcodes} $?"
|
|
done
|
|
|
|
for i in ${exitcodes}; do
|
|
[ ${i} -eq 0 ] || exit ${i}
|
|
done
|
|
`, setting.ScriptType),
|
|
|
|
// for update
|
|
fmt.Sprintf(`#!/usr/bin/env %s
|
|
# AUTO GENERATED BY HANZO GIT, DO NOT MODIFY
|
|
exitcodes=""
|
|
hookname=$(basename $0)
|
|
GIT_DIR=${GIT_DIR:-$(dirname $0/..)}
|
|
|
|
for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do
|
|
test -x "${hook}" && test -f "${hook}" || continue
|
|
"${hook}" $1 $2 $3
|
|
exitcodes="${exitcodes} $?"
|
|
done
|
|
|
|
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 HANZO GIT, DO NOT MODIFY
|
|
data=$(cat)
|
|
exitcodes=""
|
|
hookname=$(basename $0)
|
|
GIT_DIR=${GIT_DIR:-$(dirname $0)/..}
|
|
|
|
for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do
|
|
test -x "${hook}" && test -f "${hook}" || continue
|
|
echo "${data}" | "${hook}"
|
|
exitcodes="${exitcodes} $?"
|
|
done
|
|
|
|
for i in ${exitcodes}; do
|
|
[ ${i} -eq 0 ] || exit ${i}
|
|
done
|
|
`, setting.ScriptType),
|
|
}
|
|
|
|
delegateHookTpls = []string{
|
|
// for pre-receive
|
|
fmt.Sprintf(`#!/usr/bin/env %s
|
|
# AUTO GENERATED BY HANZO GIT, 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 HANZO GIT, 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 HANZO GIT, 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 HANZO GIT, DO NOT MODIFY
|
|
%s hook --config=%s proc-receive
|
|
`, setting.ScriptType, util.ShellEscape(setting.AppPath), util.ShellEscape(setting.CustomConf)))
|
|
delegateHookTpls = append(delegateHookTpls, "")
|
|
|
|
return hookNames, hookTpls, delegateHookTpls
|
|
}
|
|
|
|
// CreateDelegateHooks creates all the hooks scripts for the repo
|
|
func CreateDelegateHooks(_ context.Context, repo Repository) (err error) {
|
|
return createDelegateHooks(filepath.Join(repoPath(repo), "hooks"))
|
|
}
|
|
|
|
func createDelegateHooks(hookDir string) (err error) {
|
|
hookNames, hookTpls, delegateHookTpls := getHookTemplates()
|
|
|
|
for i, hookName := range hookNames {
|
|
oldHookPath := filepath.Join(hookDir, hookName)
|
|
newHookPath := filepath.Join(hookDir, hookName+".d", delegateHookName)
|
|
|
|
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(delegateHookTpls[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)
|
|
}
|
|
|
|
// Drop delegates written under an earlier binary name, LAST: if we die
|
|
// midway the repo is left with two delegates and pushes fail loudly,
|
|
// which is the safe direction. Removing first and dying would leave a
|
|
// repo with no delegate at all — pushes would silently bypass every
|
|
// server-side check.
|
|
for _, legacy := range legacyDelegateHookNames {
|
|
legacyHookPath := filepath.Join(hookDir, hookName+".d", legacy)
|
|
if err = util.Remove(legacyHookPath); err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("unable to remove legacy hook file '%s': %w", legacyHookPath, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func checkExecutable(filename string) bool {
|
|
// windows has no concept of a executable bit
|
|
if runtime.GOOS == "windows" {
|
|
return true
|
|
}
|
|
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 {
|
|
return err
|
|
}
|
|
if (fileInfo.Mode() & 0o100) > 0 {
|
|
return nil
|
|
}
|
|
mode := fileInfo.Mode() | 0o100
|
|
return os.Chmod(filename, mode)
|
|
}
|
|
|
|
// CheckDelegateHooks checks the hooks scripts for the repo
|
|
func CheckDelegateHooks(_ context.Context, repo Repository) ([]string, error) {
|
|
return checkDelegateHooks(filepath.Join(repoPath(repo), "hooks"))
|
|
}
|
|
|
|
func checkDelegateHooks(hookDir string) ([]string, error) {
|
|
hookNames, hookTpls, delegateHookTpls := getHookTemplates()
|
|
|
|
results := make([]string, 0, 10)
|
|
|
|
for i, hookName := range hookNames {
|
|
oldHookPath := filepath.Join(hookDir, hookName)
|
|
newHookPath := filepath.Join(hookDir, hookName+".d", delegateHookName)
|
|
|
|
cont := false
|
|
// Reported before anything else, and never short-circuited by `cont`: a
|
|
// surviving legacy delegate is the one defect here that rejects pushes
|
|
// outright, and reporting it is what makes `doctor --fix` (and the repo
|
|
// settings page) call CreateDelegateHooks, which deletes it.
|
|
for _, legacy := range legacyDelegateHookNames {
|
|
legacyHookPath := filepath.Join(hookDir, hookName+".d", legacy)
|
|
isExist, err := util.IsExist(legacyHookPath)
|
|
if err != nil {
|
|
results = append(results, fmt.Sprintf("unable to check if %s exists. Error: %v", legacyHookPath, err))
|
|
}
|
|
if err == nil && isExist {
|
|
results = append(results, fmt.Sprintf("legacy hook file %s still exists and would reject every push", legacyHookPath))
|
|
}
|
|
}
|
|
|
|
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) != delegateHookTpls[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
|
|
}
|