feat: Use receive.hideRefs (#10015)

In forgejo/forgejo!2834 and forgejo/forgejo!5307 it was made so it's no longer possible to modify and delete internal reference, not having this restriction lead to broken pull requests when people used something like `git push --mirror`. However it now still leads to problem with that command as the git client tries to delete such references. We can solve this by using git's `receive.hideRefs` to make this ref read-only and avoid advertising it when someone does `git push --mirror`.

Resolves forgejo/forgejo#9942

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/10015
Reviewed-by: Mathieu Fenniak <mfenniak@noreply.codeberg.org>
Co-authored-by: Gusted <postmaster@gusted.xyz>
Co-committed-by: Gusted <postmaster@gusted.xyz>
This commit is contained in:
Gusted
2025-11-10 14:36:15 +01:00
committed by Gusted
parent 5c9a909c03
commit 5b77ddb050
10 changed files with 178 additions and 120 deletions
+1 -29
View File
@@ -295,35 +295,7 @@ Forgejo or set your environment appropriately.`, "")
// runHookUpdate process the update hook: https://git-scm.com/docs/githooks#update
func runHookUpdate(ctx context.Context, c *cli.Command) error {
// Now if we're an internal don't do anything else
if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal {
return nil
}
ctx, cancel := installSignals(ctx)
defer cancel()
if c.NArg() != 3 {
return nil
}
args := c.Args().Slice()
// The arguments given to the hook are in order: reference name, old commit ID and new commit ID.
refFullName := git.RefName(args[0])
newCommitID := args[2]
// Only process pull references.
if !refFullName.IsPull() {
return nil
}
// Empty new commit ID means deletion.
if git.IsEmptyCommitID(newCommitID, nil) {
return fail(ctx, fmt.Sprintf("The deletion of %s is skipped as it's an internal reference.", refFullName), "")
}
// If the new comment isn't empty it means modification.
return fail(ctx, fmt.Sprintf("The modification of %s is skipped as it's an internal reference.", refFullName), "")
return nil
}
func runHookPostReceive(ctx context.Context, c *cli.Command) error {
-39
View File
@@ -161,42 +161,3 @@ func TestDelayWriter(t *testing.T) {
require.Empty(t, out)
})
}
func TestRunHookUpdate(t *testing.T) {
app := cli.Command{}
app.Commands = []*cli.Command{subcmdHookUpdate()}
t.Run("Removal of internal reference", func(t *testing.T) {
defer test.MockVariableValue(&cli.OsExiter, func(code int) {})()
defer test.MockVariableValue(&setting.IsProd, false)()
finish := captureOutput(t, os.Stderr)
err := app.Run(t.Context(), []string{"./forgejo", "update", "refs/pull/1/head", "0a51ae26bc73c47e2f754560c40904cf14ed51a9", "0000000000000000000000000000000000000000"})
out := finish()
require.Error(t, err)
assert.Contains(t, out, "The deletion of refs/pull/1/head is skipped as it's an internal reference.")
})
t.Run("Update of internal reference", func(t *testing.T) {
defer test.MockVariableValue(&cli.OsExiter, func(code int) {})()
defer test.MockVariableValue(&setting.IsProd, false)()
finish := captureOutput(t, os.Stderr)
err := app.Run(t.Context(), []string{"./forgejo", "update", "refs/pull/1/head", "0a51ae26bc73c47e2f754560c40904cf14ed51a9", "0000000000000000000000000000000000000001"})
out := finish()
require.Error(t, err)
assert.Contains(t, out, "The modification of refs/pull/1/head is skipped as it's an internal reference.")
})
t.Run("Removal of branch", func(t *testing.T) {
err := app.Run(t.Context(), []string{"./forgejo", "update", "refs/head/main", "0a51ae26bc73c47e2f754560c40904cf14ed51a9", "0000000000000000000000000000000000000000"})
require.NoError(t, err)
})
t.Run("Not enough arguments", func(t *testing.T) {
err := app.Run(t.Context(), []string{"./forgejo", "update"})
require.NoError(t, err)
})
}