From b00b0ba4aa5088988627e5be398596debc4f651c Mon Sep 17 00:00:00 2001 From: claude Date: Thu, 11 Jun 2026 02:17:27 +0000 Subject: [PATCH] fix(git-gate): forward force push as +refspec to upstream When $old != zero and $new is not a descendant of $old (detected via git merge-base --is-ancestor), the hook now forwards +$new:$ref so the upstream accepts the force push instead of rejecting it as a non-fast-forward. Closes #233 --- bot_bottle/git_gate.py | 2 ++ tests/unit/test_git_gate.py | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/bot_bottle/git_gate.py b/bot_bottle/git_gate.py index 0c3b09f..1338b80 100644 --- a/bot_bottle/git_gate.py +++ b/bot_bottle/git_gate.py @@ -300,6 +300,8 @@ while IFS=' ' read -r old new ref; do [ -z "$ref" ] && continue if [ "$new" = "$zero" ]; then refspec=":$ref" + elif [ "$old" != "$zero" ] && ! git merge-base --is-ancestor "$old" "$new" 2>/dev/null; then + refspec="+$new:$ref" else refspec="$new:$ref" fi diff --git a/tests/unit/test_git_gate.py b/tests/unit/test_git_gate.py index 11e96ea..2e1de27 100644 --- a/tests/unit/test_git_gate.py +++ b/tests/unit/test_git_gate.py @@ -181,6 +181,13 @@ class TestHookRender(unittest.TestCase): self.assertIn("BatchMode=yes", hook) self.assertIn("ConnectTimeout=", hook) + def test_force_push_uses_plus_refspec(self): + # A non-fast-forward push (old != zero, new not a descendant of old) + # must forward +$new:$ref so the upstream accepts the force push. + hook = git_gate_render_hook() + self.assertIn('git merge-base --is-ancestor "$old" "$new"', hook) + self.assertIn('refspec="+$new:$ref"', hook) + def test_forward_preserves_push_options(self): # Git exposes push options to pre-receive hooks as # GIT_PUSH_OPTION_COUNT + indexed GIT_PUSH_OPTION_N variables.