feat(git-gate): provision ~/.gitconfig pushInsteadOf in the bottle
provision_git now does two things: copy the host cwd's .git (when --cwd is set, existing behavior) and write ~/.gitconfig with pushInsteadOf rules for each bottle.git entry. A 'git push <real upstream URL>' from inside the agent transparently rewrites to 'git://<gate>/<name>.git' so the gate gets first crack at the incoming refs. pushInsteadOf (not insteadOf) keeps fetch on the original URL — v1 of the git-gate is push-only scope per PRD 0008. The render helper is exposed for testing without docker.
This commit is contained in:
@@ -1,19 +1,38 @@
|
||||
"""Copy the host cwd's .git directory into a running Docker bottle.
|
||||
"""Git provisioning inside a running Docker bottle.
|
||||
|
||||
Only fires when `--cwd` was passed AND the host cwd actually has a
|
||||
.git. The container-side path is fixed at /home/node/workspace/.git;
|
||||
ownership is reset to node so the agent can run git commands."""
|
||||
Two concerns, both about git in the agent:
|
||||
|
||||
1. If --cwd was passed AND the host cwd has a .git, copy that .git
|
||||
into /home/node/workspace/.git so the agent operates on the
|
||||
user's repo.
|
||||
2. If the bottle declares `git` entries (PRD 0008), write a
|
||||
~/.gitconfig with pushInsteadOf rules so a `git push <upstream>`
|
||||
from inside the agent transparently hits the per-agent git-gate
|
||||
instead of the real remote. Fetch keeps the original URL — v1
|
||||
gates push only.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from ....log import info
|
||||
from ....manifest import GitEntry
|
||||
from .. import util as docker_mod
|
||||
from ..bottle_plan import DockerBottlePlan
|
||||
from ..git_gate import git_gate_host
|
||||
|
||||
|
||||
def provision_git(plan: DockerBottlePlan, target: str) -> None:
|
||||
"""Set up git inside the bottle. Runs both subcases; each no-ops
|
||||
when its condition isn't met."""
|
||||
_provision_cwd_git(plan, target)
|
||||
_provision_git_gate_config(plan, target)
|
||||
|
||||
|
||||
def _provision_cwd_git(plan: DockerBottlePlan, target: str) -> None:
|
||||
"""If --cwd was set and the host cwd has a .git directory, copy
|
||||
it into /home/node/workspace/.git and fix ownership. No-op
|
||||
otherwise."""
|
||||
@@ -34,3 +53,48 @@ def provision_git(plan: DockerBottlePlan, target: str) -> None:
|
||||
stdout=subprocess.DEVNULL,
|
||||
check=True,
|
||||
)
|
||||
|
||||
|
||||
def render_git_gate_gitconfig(slug: str, entries: tuple[GitEntry, ...]) -> str:
|
||||
"""Render the ~/.gitconfig content for git-gate `pushInsteadOf`
|
||||
rewrites. Pure host-side, no docker; exposed for tests.
|
||||
|
||||
Empty `entries` returns an empty string so callers can no-op
|
||||
cleanly without conditional formatting at the call site."""
|
||||
if not entries:
|
||||
return ""
|
||||
gate = git_gate_host(slug)
|
||||
out = [
|
||||
"# claude-bottle git-gate (PRD 0008): pushes to declared upstreams\n",
|
||||
"# transparently route through the gitleaks-scanning git-gate.\n",
|
||||
"# Fetch keeps the original URL (v1 gates push only).\n",
|
||||
]
|
||||
for entry in entries:
|
||||
out.append(f'[url "git://{gate}/{entry.Name}.git"]\n')
|
||||
out.append(f"\tpushInsteadOf = {entry.Upstream}\n")
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def _provision_git_gate_config(plan: DockerBottlePlan, target: str) -> None:
|
||||
"""Write ~/.gitconfig in the bottle with the git-gate
|
||||
pushInsteadOf rules. No-op when the bottle has no `git` entries."""
|
||||
bottle = plan.spec.manifest.bottle_for(plan.spec.agent_name)
|
||||
if not bottle.git:
|
||||
return
|
||||
container = target
|
||||
container_home = os.environ.get("CLAUDE_BOTTLE_CONTAINER_HOME", "/home/node")
|
||||
container_gitconfig = f"{container_home}/.gitconfig"
|
||||
|
||||
content = render_git_gate_gitconfig(plan.slug, bottle.git)
|
||||
config_file = plan.stage_dir / "agent_gitconfig"
|
||||
config_file.write_text(content)
|
||||
config_file.chmod(0o600)
|
||||
|
||||
info(f"writing {container_gitconfig} with {len(bottle.git)} pushInsteadOf rule(s)")
|
||||
subprocess.run(
|
||||
["docker", "cp", str(config_file), f"{container}:{container_gitconfig}"],
|
||||
stdout=subprocess.DEVNULL,
|
||||
check=True,
|
||||
)
|
||||
docker_mod.docker_exec_root(container, ["chown", "node:node", container_gitconfig])
|
||||
docker_mod.docker_exec_root(container, ["chmod", "644", container_gitconfig])
|
||||
|
||||
Reference in New Issue
Block a user