fix(git-gate): scope new-branch scan to incoming commits
A new ref made the pre-receive hook scan the full ancestry (`log_opts="$new"`), so historical test-fixture findings rejected every new-branch push (#106). Scope it to `$new --not --all` — only commits new to the gate, which (since the bare repo is populated solely by upstream mirror-fetch and gitleaks-gated pushes) loses no coverage on what a push actually brings to the upstream. Also add BatchMode=yes + ConnectTimeout=10 to both the forward and access-hook ssh so an unreachable upstream fails fast instead of hanging. Refs #106 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+10
-3
@@ -280,7 +280,14 @@ while IFS=' ' read -r old new ref; do
|
||||
[ -z "$ref" ] && continue
|
||||
[ "$new" = "$zero" ] && continue
|
||||
if [ "$old" = "$zero" ]; then
|
||||
log_opts="$new"
|
||||
# New ref: scan only the commits this push introduces — those
|
||||
# reachable from $new but not from any ref the gate already has.
|
||||
# Everything already on the gate arrived via upstream mirror-fetch
|
||||
# or a previously gitleaks-scanned push, so it's already-upstream
|
||||
# or already-scanned; re-scanning it (the old `$new` full-ancestry
|
||||
# range) only resurfaces historical findings and blocks every new
|
||||
# branch. See PRD 0028 / issue #106.
|
||||
log_opts="$new --not --all"
|
||||
else
|
||||
log_opts="$old..$new"
|
||||
fi
|
||||
@@ -300,7 +307,7 @@ if [ ! -f "$hostsfile" ]; then
|
||||
echo "git-gate: add KnownHostKey to the bottle.git entry and restart the bottle" >&2
|
||||
exit 1
|
||||
fi
|
||||
ssh_cmd="ssh -i $keyfile -o UserKnownHostsFile=$hostsfile -o StrictHostKeyChecking=yes -o IdentitiesOnly=yes"
|
||||
ssh_cmd="ssh -i $keyfile -o UserKnownHostsFile=$hostsfile -o StrictHostKeyChecking=yes -o IdentitiesOnly=yes -o BatchMode=yes -o ConnectTimeout=10"
|
||||
|
||||
while IFS=' ' read -r old new ref; do
|
||||
[ -z "$ref" ] && continue
|
||||
@@ -355,7 +362,7 @@ if [ -z "$keyfile" ] || [ ! -f "$hostsfile" ]; then
|
||||
echo "git-gate: missing credentials for $repo_dir; refusing fetch" >&2
|
||||
exit 1
|
||||
fi
|
||||
ssh_cmd="ssh -i $keyfile -o UserKnownHostsFile=$hostsfile -o StrictHostKeyChecking=yes -o IdentitiesOnly=yes"
|
||||
ssh_cmd="ssh -i $keyfile -o UserKnownHostsFile=$hostsfile -o StrictHostKeyChecking=yes -o IdentitiesOnly=yes -o BatchMode=yes -o ConnectTimeout=10"
|
||||
|
||||
echo "git-gate: refreshing $repo_dir from upstream" >&2
|
||||
if ! GIT_SSH_COMMAND="$ssh_cmd" git -C "$repo_dir" fetch origin --prune >&2; then
|
||||
|
||||
@@ -197,6 +197,24 @@ class TestHookRender(unittest.TestCase):
|
||||
# Stdin is buffered to a tempfile so both phases can re-read.
|
||||
self.assertIn("refs_file=$(mktemp)", hook)
|
||||
|
||||
def test_new_ref_scan_scoped_to_incoming_commits(self):
|
||||
# A new branch (old=all-zeros) must scan only commits new to the
|
||||
# gate, not the full ancestry — otherwise historical findings
|
||||
# block every new-branch push (PRD 0028 / issue #106).
|
||||
hook = git_gate_render_hook()
|
||||
self.assertIn('log_opts="$new --not --all"', hook)
|
||||
# The old over-broad full-ancestry range must be gone.
|
||||
self.assertNotIn('log_opts="$new"', hook)
|
||||
# Existing-branch delta scan is unchanged.
|
||||
self.assertIn('log_opts="$old..$new"', hook)
|
||||
|
||||
def test_forward_ssh_is_non_interactive_and_bounded(self):
|
||||
# No prompt (BatchMode) and a connect timeout, so an unreachable
|
||||
# upstream fails fast instead of hanging the receive-pack.
|
||||
hook = git_gate_render_hook()
|
||||
self.assertIn("BatchMode=yes", hook)
|
||||
self.assertIn("ConnectTimeout=", hook)
|
||||
|
||||
|
||||
class TestAccessHookRender(unittest.TestCase):
|
||||
def test_access_hook_refreshes_origin_on_upload_pack(self):
|
||||
@@ -216,6 +234,13 @@ class TestAccessHookRender(unittest.TestCase):
|
||||
self.assertIn("refusing to serve stale data", hook)
|
||||
self.assertIn("exit 1", hook)
|
||||
|
||||
def test_access_hook_ssh_is_non_interactive_and_bounded(self):
|
||||
# Same hardening as the forward path: the fetch ssh must not
|
||||
# prompt and must time out rather than hang upload-pack.
|
||||
hook = git_gate_render_access_hook()
|
||||
self.assertIn("BatchMode=yes", hook)
|
||||
self.assertIn("ConnectTimeout=", hook)
|
||||
|
||||
|
||||
class TestPrepare(unittest.TestCase):
|
||||
def setUp(self):
|
||||
|
||||
Reference in New Issue
Block a user