feat(git-gate): mirror fetch through access-hook (bidirectional)
test / unit (pull_request) Successful in 11s
test / integration (pull_request) Successful in 14s

The gate is now a transparent mirror, not push-only. Per-repo
init now runs `git remote add --mirror=fetch origin <url>` so a
later `git fetch origin` mirrors the upstream's full ref graph at
canonical paths. The pre-receive hook forwards accepted refs via
`git push origin` (renamed from upstream).

New: an access-hook script wired via `git daemon --access-hook`
runs `git fetch origin --prune` against the real upstream before
every upload-pack request (clone, fetch, pull, ls-remote). On
upstream error the hook exits non-zero — the agent's fetch fails
rather than the gate serving stale data.

The pre-existing smoke test (ls-remote against unreachable
upstream returns refs) had to invert: under the bidirectional
design any ls-remote success is necessarily a success against
the upstream, so the unreachable-upstream case now correctly
fails closed.
This commit is contained in:
2026-05-12 21:37:04 -04:00
parent ae7e22065f
commit fdd06c54d2
4 changed files with 180 additions and 47 deletions
+7
View File
@@ -23,6 +23,7 @@ GIT_GATE_DOCKERFILE = "Dockerfile.git-gate"
GIT_GATE_ENTRYPOINT_IN_CONTAINER = "/git-gate-entrypoint.sh"
GIT_GATE_HOOK_IN_CONTAINER = "/etc/git-gate/pre-receive"
GIT_GATE_ACCESS_HOOK_IN_CONTAINER = "/etc/git-gate/access-hook"
GIT_GATE_CREDS_DIR_IN_CONTAINER = "/git-gate/creds"
# git daemon's default listening port. Surfaced as a constant because
@@ -85,6 +86,11 @@ class DockerGitGate(GitGate):
f"git-gate hook missing at {plan.hook_script}; "
f"GitGate.prepare must run first"
)
if not plan.access_hook_script.is_file():
die(
f"git-gate access-hook missing at {plan.access_hook_script}; "
f"GitGate.prepare must run first"
)
build_git_gate_image()
@@ -111,6 +117,7 @@ class DockerGitGate(GitGate):
cps: list[tuple[str, str, str]] = [
(str(plan.entrypoint_script), GIT_GATE_ENTRYPOINT_IN_CONTAINER, "entrypoint"),
(str(plan.hook_script), GIT_GATE_HOOK_IN_CONTAINER, "pre-receive hook"),
(str(plan.access_hook_script), GIT_GATE_ACCESS_HOOK_IN_CONTAINER, "access-hook"),
]
for u in plan.upstreams:
keypath = expand_tilde(u.identity_file)