Add C# (ASP.NET Core) rewrite of the hgresume API - #19
Merged
Conversation
Drop-in, wire-compatible reimplementation of the PHP hgresume API targeting the dockerize (c905288) lineage: same /api/v03/<method> endpoints, X-HgR-* header protocol, status-code mapping, and hg CLI behaviour, so existing Chorus clients work unchanged. Auth is left to the surrounding platform. - csharp/src/HgResume.Api: net10.0 ASP.NET Core port of RestServer, HgResumeApi, HgRunner, AsyncRunner, BundleHelper, HgResumeResponse. Contract verified against the Chorus client (incl. the PendingResponse -> 202 poll with sow = bundleSize - 10, and the INPROGRESS/TIMEOUT const collision). - csharp/test/HgResume.HttpTests: 35 HTTP-level xUnit tests ported from api/test/HgResumeApi_Test.php, driven against the container via podman (podman cp/exec repo seeding). All 35 pass against the built image. - csharp/Dockerfile: dotnet sdk/aspnet 10.0 + mercurial, port 80, same volumes as the PHP image (drop-in for docker-compose.yaml). - CI: build the C# image and push a pr-<n> tag to GHCR on pull_request for real-environment testing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a `test` job that builds the C# image (amd64, loaded into the runner's docker) and runs the HTTP-level xUnit suite against it, driving the container with docker via the fixture's HGRESUME_PODMAN override. The `build` (push) job now depends on `test`, so images publish only when the suite passes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Chorus resumable client reads response bodies with WebResponseHelper, which returns EMPTY content when there is no Content-Length header (it never falls back to chunked/Transfer-Encoding). The C# server wrote bodies via Body.WriteAsync without a length, so Kestrel used chunked encoding -> the real client read every body as empty -> getRevisions returned nothing -> "Push failed: A common revision could not be found with the server" (and pulls got empty bundles). Set Response.ContentLength before writing, as the PHP RestServer did. - ContractFacts: raw-socket HTTP test asserting a body response carries Content-Length and is not chunked (fails without the fix; HttpClient-based tests can't catch it because HttpClient handles chunked transparently). - ServerFixture: add HGRESUME_REPO_OWNER / HGRESUME_MAINT_PATH knobs (used when running the suite against the PHP reference image). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adapted from LexBox backend/Testing/SyncReverseProxy/SendReceiveServiceTests.cs. Drives the real Chorus resumable client (HgResumeTransport via HgRepository) against the C# hgresume container: clone / modify+send / send-new. LexBox's server/API/auth coupling is replaced by creating repos with `hg init` in the container; the project code contains "resumable" so Chorus selects its resumable transport. LfMergeBridge is bypassed (its FixFwData fixup needs the full FieldWorks stack and never touches the server); the resumable protocol exercised is identical. This harness reproduced the missing-Content-Length bug fixed in the previous commit. Windows-only (Chorus pulls in .NET Framework deps) and needs podman + the image, so it is not part of the Linux CI test job. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The harness drives the Chorus hg repo + resumable transport directly (HgRepository / HgResumeTransport), not the LfMergeBridge flow, so SIL.ChorusPlugin.LfMergeBridge is unused. Remove it; only SIL.Chorus.LibChorus, SIL.Chorus.Mercurial and SIL.Core are needed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Push a project to the server, then clone it back into a fresh directory over the resumable protocol and assert the cloned fwdata is byte-identical to what was pushed. CloneProject now returns the actual clone path (Chorus may adjust it). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Missing required params and BundleHelper ValidationException were escaping as bare HTTP 500s without protocol headers; mirror PHP RestServer::serverError instead. Co-authored-by: Cursor <cursoragent@cursor.com>
GetRepoPath now requires a single path segment via Path.GetFileName so callers cannot escape configured search roots. Co-authored-by: Cursor <cursoragent@cursor.com>
Fork PRs only get a read-only GITHUB_TOKEN; build still runs, but login/push only when the head repo matches. Co-authored-by: Cursor <cursoragent@cursor.com>
Re-remove the registry entry if the task already completed before insert, and use value-conditional TryRemove. Co-authored-by: Cursor <cursoragent@cursor.com>
Ignore test fixtures in the build context, and seed repos from the host so the production image only needs mercurial. Co-authored-by: Cursor <cursoragent@cursor.com>
Expose HGRESUME_MAX_REQUEST_BODY_SIZE (default 30 MB) so the push body cap is explicit and tunable for non-Chorus clients. Co-authored-by: Cursor <cursoragent@cursor.com>
Contributor
|
cool to see this rewrite coming online! |
At offset == bundleSize the server returns SUCCESS only once the transaction has flipped from the Bundle to the Downloading state; until then it returns INPROGRESS (which the real client polls through). The test asserted SUCCESS on the first request, which is timing-dependent — it passed locally but failed intermittently in CI. Poll via PullFirstChunk instead, matching the client. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Use aspnet for hgresumable
AI summary
Drop-in, wire-compatible C# / ASP.NET Core rewrite of the PHP
hgresumeAPI (the resumable Mercurial bundle-transfer backend used by Chorus / Language Depot / LexBox). It targets the behaviour of thedockerize(c905288) PHP lineage so existing field clients work unchanged. Authentication is intentionally left to the surrounding platform (no Apache/mod_perl/Redmine); deployment stays the Docker image.What's here (all new code under
csharp/; the PHPapi/tree is left in place for reference):csharp/src/HgResume.Api(net10.0) — port of the six core classes:RestDispatcherroutes on the last path segment (/api/v03/<method>), binds query/body params PHP-style (includingbaseHashes[]), and writes theX-HgR-*response contract.HgResumeApi— push/pull/getRevisions/finish*/isAvailable, a faithful port of the transaction state machine.HgRunnershells out tohg(incoming / unbundle / bundle-t v1/ log / branches / tip), parsing stdout verbatim (same templates & regexes).AsyncRunnerruns long hg commands in the background and signals completion via a.async_runfile (what makes transfers resumable), replacing the PHP GNU-time/shell-backgrounding trick withSystem.Diagnostics.Process.BundleHelper— transaction state + metadata (JSON instead of PHPserialize()).HgResumeResponse— status constants +PendingResponse.Wire contract verified against the Chorus client (
HgResumeTransport.cs,IApiServer.cs): URL shape, GET/POSTtext/plain, query param names,baseHashes[]encoding, and thex-hgr-*response headers. Notably,INPROGRESSandTIMEOUTshare the const value9, so a pending push maps to 202 (not 408) withsow = bundleSize - 10; the client reads that and resends the tail as a poll. This behaviour is reproduced exactly.csharp/Dockerfile— multi-stagedotnet/sdk:10.0→aspnet:10.0, installsmercurial, listens on port 80, and exposes the same/var/cache/hgresume+/var/vcs/publicvolumes, so it drops into the existingdocker-compose.yaml.csharp/test/HgResume.HttpTests— 35 HTTP-level xUnit tests ported fromapi/test/HgResumeApi_Test.php. A podman-managed fixture builds the image, runs the container, and seeds fixture repos viapodman cp+podman exec unzip, then drives the protocol end-to-end. 35/35 pass against the built image.CI (
.github/workflows/docker-image.yml) — now builds the C# image (csharp/Dockerfile, amd64+arm64) and, onpull_request, pushes apr-<number>tag to GHCR so the image can be pulled and tested in a real environment.Test plan
cd csharp && ./run-tests.sh(orpwsh ./run-tests.ps1) — builds the image with podman and runs the HTTP-level suite against the container. Locally: 35/35 pass (~3 min; includes deliberately slow 50-byte-chunk transfers and the long-makeBundle INPROGRESS case).podman run -p 8034:80 hgresume-csharp:test, seed a repo,curlisAvailable/getRevisions— confirmed correctX-HgR-*headers and octet-stream body.ghcr.io/sillsdev/hgresume:pr-<number>and point a real Chorus client at it for a full push + pull (including an interrupted/resumed transfer) before any cutover.This change is