Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions .github/workflows/git-transport-boundaries.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: Git transport boundary contracts
on:
push:
branches: [fix/git-transport-boundaries-20260929]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: git-transport-boundaries-${{ github.ref }}
cancel-in-progress: false
jobs:
contracts:
runs-on: ubuntu-latest
timeout-minutes: 20
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: msgtest
POSTGRES_PASSWORD: msgtest
POSTGRES_DB: postgres
ports: ['5432:5432']
options: >-
--health-cmd "pg_isready -U msgtest"
--health-interval 5s --health-timeout 5s --health-retries 10
valkey:
image: valkey/valkey:9.0.6
ports: ['6379:6379']
options: >-
--health-cmd "valkey-cli ping"
--health-interval 5s --health-timeout 5s --health-retries 10
env:
MSG_TEST_POSTGRES_URL_TEMPLATE: postgresql://msgtest:msgtest@127.0.0.1:5432/{database}
MSG_TEST_VALKEY_URL: redis://127.0.0.1:6379/0
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-python@v7
with:
python-version: '3.15'
allow-prereleases: true
cache: pip
- name: Install actual Git, LFS and cryptographic test tools
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq age git-lfs
git --version
git lfs version
python -m pip install -e '.[dev]'
- name: Record exact source
env:
SOURCE_SHA: ${{ github.sha }}
run: |
mkdir -p artifacts
test "$(git rev-parse HEAD)" = "$SOURCE_SHA"
python - <<'PY'
import json,os,subprocess
from pathlib import Path
git=lambda *args:subprocess.check_output(['git',*args],text=True).strip()
Path('artifacts/source.json').write_text(json.dumps({'commit':git('rev-parse','HEAD'),
'tree':git('rev-parse','HEAD^{tree}'),'event_sha':os.environ['SOURCE_SHA'],
'dirty':bool(git('status','--porcelain'))},indent=2)+'\n')
PY
- name: Verify ownership and unchanged real Git reads
run: python -m pytest tests/test_git_transport_boundaries.py --junitxml=artifacts/git-boundaries.xml
- name: Preserve real HTTP, SSH, bundle, LFS and publication fences
run: |
python -m pytest tests/test_git_http_push.py tests/test_native_git_read_only.py tests/test_git_lfs.py tests/test_lfs_shared_gc_quota.py tests/test_ssh_rss.py tests/test_extensions.py tests/test_storage_layout.py tests/test_effect_completion_fencing.py tests/issue_78_80/test_lfs_integrity.py tests/issue_78_80/test_lfs_migration_contract.py --junitxml=artifacts/git-regressions.xml
- uses: actions/upload-artifact@v4
if: always()
with:
name: git-transport-boundaries-${{ github.sha }}
path: artifacts/
if-no-files-found: error
56 changes: 56 additions & 0 deletions docs/GIT_BOUNDARY_PROGRESS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Git transport boundary progress

## Scope and coordination

Issue #163. Branch `fix/git-transport-boundaries-20260929`. Base main
`08c58928ee3f6c6cb350d9e9fba75d072bd78f08`, tree
`8a62150d8273a2c75775872694749cb529835966` includes merged #165.
Coordination: #163 comment5878830664 and #83 comment5878871933.
No changes to parallel #166/#167/#169 executor/storage or #155 recovery branches.

Main was compared with the inspected source baseline476e64f: only #165's eight
files changed, none of the repository/SSH/HTTP modules under review. The source
blobs remain repositories940a1a1bd987a2d97273734a8c0646097abf3c83,
ssh_gitd6465ce37fd7e4743773942144e91e10a7c847af, and
http_routes04a7de0431f1211893111e887ef87661ef877c0f.

## Regression-first stage

Nine new tests cover shared import isolation, actual store/adapter ownership,
one shared publication implementation, shared HTTP-helper ownership, explicit
composition and two existing-behavior characterizations (real Git public reads
with unchanged database/refs, and bounded reference batch parsing).
Production source is unchanged in the first commit. Obtain actual red cloud
JUnit/source evidence before implementation; do not claim a result from intent.

## Observed and intended boundaries

NativeGitStore currently owns physical repositories and HTTP/LFS Request/Response
handling; HTTP receive imports guarded_command from ssh_git, which imports the
store back. The planned separation is:

- NativeGitStore keeps repository/LFS paths, bounded store operations and physical
publication primitives, taking a snapshot of required settings rather than
retaining an Application service locator. Existing operation registration and
bundle worker behavior remain in their current module.
- transports.git_http.GitHTTPAdapter composes a NativeGitStore and owns all HTTP
envelopes, streaming/spooling and signed/Basic protocol adaptation.
- extensions.git_publication owns the existing ReferenceGuard and guarded Git
subprocess lifecycle. SSH and HTTP call this same implementation. Historical
ssh_git imports remain identical compatibility aliases, not duplicate wrappers.
- transports.http_common owns the three existing body/JSON/error helpers and base
headers; HTTP routes and Git HTTP import it, avoiding a new router/adapter cycle.

These are source ownership changes, not a new Git workflow/authorization engine.
Original capacity limits, request IDs, signed envelopes, current-authority checks,
reference-transaction phases, replay cache, uncertainty reporting, LFS digest and
shared-volume constraints, and bundle job fences remain. No protocol version,
database schema, receipt, real repository migration or deployment change.

## Cloud acceptance

The dedicated workflow runs real PostgreSQL/Valkey/Git/git-lfs and the existing
HTTP/SSH/bundle/LFS/read-only and completion-fence tests. The full four-shard exact
node-ID/conformance/build and applicable Recovery safety gates remain required on
the final combined tree. No local project execution, skipped test, weakened
assertion, production credentials, real funds/outbound delivery or recovery promotion.
140 changes: 140 additions & 0 deletions tests/test_git_transport_boundaries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"""Git owns files/publication; protocol adapters own requests and responses."""
from __future__ import annotations

import ast
import importlib
from pathlib import Path
import subprocess
import sys

import httpx
import pytest

from msg.core.errors import Failure
from msg.core.codec import wire
from msg.extensions.repositories import NativeGitStore
from msg.extensions.ssh_git import ReferenceGuard
from msg.transports.http import create_app
from test_service import call, register
from test_native_git_read_only import business_snapshot, refs_snapshot, seed_ref


def dependencies(module):
tree=ast.parse(Path(importlib.import_module(module).__file__).read_text())
return [node.module or '' for node in ast.walk(tree) if isinstance(node,ast.ImportFrom)] + [
alias.name for node in ast.walk(tree) if isinstance(node,ast.Import) for alias in node.names]


def test_shared_repository_module_does_not_import_http_or_ssh_adapters():
forbidden=('starlette','msg.transports.http','msg.transports.git_http','msg.extensions.ssh_git')
assert not [name for name in dependencies('msg.extensions.repositories')
if any(name==prefix or name.startswith(prefix+'.') for prefix in forbidden)]


def test_repository_store_has_no_http_handlers_or_request_conversion_methods():
assert not {'http','http_push','http_lfs','_http_push_receive','_lfs_packet',
'_lfs_authorize','_lfs_preflight_publish','_repo_read_path','_job'}.intersection(vars(NativeGitStore))
assert {'path','lfs','lfs_capacity','create','refs','import_bundle','update_refs'}<=set(vars(NativeGitStore))


def test_shared_publication_has_no_protocol_adapter_dependency():
module=importlib.import_module('msg.extensions.git_publication')
assert not hasattr(module,'receive_pack')
forbidden=('starlette','msg.transports.http','msg.transports.git_http','msg.extensions.ssh_git')
assert not [name for name in dependencies(module.__name__)
if any(name==prefix or name.startswith(prefix+'.') for prefix in forbidden)]


def test_both_adapters_use_the_same_publication_implementation():
owner=importlib.import_module('msg.extensions.git_publication')
ssh=importlib.import_module('msg.extensions.ssh_git')
http=importlib.import_module('msg.transports.git_http')
for name in ('ReferenceGuard','hook_program','relay_bounded_stdin','guarded_command'):
assert getattr(ssh,name) is getattr(owner,name)
assert getattr(owner,name).__module__==owner.__name__
assert http.guarded_command is owner.guarded_command
assert 'msg.extensions.ssh_git' not in dependencies(http.__name__)


def test_http_helpers_have_one_owner_without_a_router_import_cycle():
common=importlib.import_module('msg.transports.http_common')
routes=importlib.import_module('msg.transports.http_routes')
public=importlib.import_module('msg.transports.http')
git=importlib.import_module('msg.transports.git_http')
for name in ('body_bytes','json_response','error_status'):
assert getattr(common,name) is getattr(routes,name) is getattr(public,name) is getattr(git,name)
assert getattr(common,name).__module__==common.__name__
assert routes.BASE_HEADERS is common.BASE_HEADERS
assert not {'msg.transports.http','msg.transports.http_routes'}.intersection(dependencies(git.__name__))
assert not {'msg.transports.http','msg.transports.http_routes','msg.transports.git_http'}.intersection(dependencies(common.__name__))


def test_git_storage_and_publication_import_without_loading_protocol_adapters():
code='''
import importlib.abc,sys
class NoAdapters(importlib.abc.MetaPathFinder):
def find_spec(self,fullname,path=None,target=None):
if any(fullname==name or fullname.startswith(name+'.') for name in
('starlette','msg.transports.http','msg.transports.http_routes',
'msg.transports.git_http','msg.extensions.ssh_git')):
raise AssertionError('shared Git imported adapter '+fullname)
sys.meta_path.insert(0,NoAdapters())
from msg.extensions.repositories import NativeGitStore
from msg.extensions.git_publication import ReferenceGuard,guarded_command
'''
result=subprocess.run([sys.executable,'-c',code],capture_output=True,text=True)
assert result.returncode==0,result.stderr


async def test_http_uses_explicit_store_composition_not_storage_inheritance(installed):
app,_=installed
adapter=importlib.import_module('msg.transports.git_http').GitHTTPAdapter
store=NativeGitStore(app)
assert not hasattr(store,'app')
assert store.root==app.settings.server.repositories_dir
assert store.blob_root==app.settings.server.blob_dir
http=adapter(app,store=store)
assert http.store is store and http.app is app
assert not isinstance(http,NativeGitStore)
assert '__getattr__' not in vars(adapter)


async def test_public_git_read_and_rejected_push_preserve_all_business_state(installed):
app,_=installed
key,uid,_=await register(app,'git-boundary-read')
created=await call(app,'git.create',{'parent':'/@git-boundary-read','name':'code.git'},
key=key,subject=uid)
assert created.status=='ok',wire(created)
rid=created.resources[0].id
repo=NativeGitStore(app).path(rid)
commit=seed_ref(repo)
before=await business_snapshot(app)
refs=refs_snapshot(repo)
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=create_app(app)),
base_url=app.settings.service_url) as client:
path='/@git-boundary-read/code.git'
# Dumb HTTP HEAD is intentionally disabled by http.getanyfile=false.
# Exercise the supported smart fetch, not a new endpoint contract.
want=f'want {commit}\n'.encode()
packet=f'{len(want)+4:04x}'.encode()+want+b'00000009done\n'
fetched=await client.post(path+'/git-upload-pack',content=packet,
headers={'Content-Type':'application/x-git-upload-pack-request'})
assert fetched.status_code==200 and b'PACK' in fetched.content, fetched.text
advertised=await client.get(path+'/info/refs?service=git-upload-pack')
assert advertised.status_code==200 and b'git-upload-pack' in advertised.content
rejected=await client.post(path+'/git-receive-pack',content=b'0000')
assert rejected.status_code in {401,403,405}
unauthenticated=await client.get('/-/git/'+rid+'/info/refs?service=git-receive-pack')
assert unauthenticated.status_code==401
assert refs_snapshot(repo)==refs
assert await business_snapshot(app)==before


def test_reference_batch_validation_preserves_exact_git_change_contract():
old,new='0'*40,'1'*40
rows=ReferenceGuard.validate_changes(f'{old} {new} refs/heads/main\n{old} {new} refs/tags/v1\n')
assert rows==[{'ref':'refs/heads/main','old':old,'new':new},
{'ref':'refs/tags/v1','old':old,'new':new}]
for value in ('',f'{old} {new} refs/heads/main\n{old} {new} refs/heads/main',
f'bad {new} refs/heads/main',f'{old} {new} refs/remotes/private'):
with pytest.raises(Failure):ReferenceGuard.validate_changes(value)
Loading