Skip to content

Add Svn component: a pure-PHP Subversion client#295

Merged
adamziel merged 4 commits into
trunkfrom
adamziel/php-svn-client
Jun 12, 2026
Merged

Add Svn component: a pure-PHP Subversion client#295
adamziel merged 4 commits into
trunkfrom
adamziel/php-svn-client

Conversation

@adamziel

@adamziel adamziel commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

What

A new components/Svn/ component: a Subversion client in pure PHP. It checks out, updates, and commits to SVN repositories — including WordPress.org's — over both svn:// (the ra_svn wire protocol) and http(s):// (mod_dav_svn), with no svn binary and no PHP extensions beyond the toolkit baseline.

$client = new SvnClient( array( 'username' => '', 'password' => '' ) );
$client->checkout( 'https://develop.svn.wordpress.org/trunk', '/tmp/wordpress-develop' );
// …edit files…
$client->add( '/tmp/wordpress-develop', 'src/new-file.php' );
$client->commit( '/tmp/wordpress-develop', 'Add a new file.' );
$client->update( '/tmp/wordpress-develop' );

Why

WordPress lives in Subversion — core development, plugins, and themes all ship through it. Automating anything around that ecosystem from PHP currently means shelling out to a binary most hosted environments don't have. This brings SVN into the same pure-PHP fold as the existing Git component.

How it works

Both transports are implemented around Subversion's core abstraction, the editor drive (a stream of tree-change operations). A checkout or update is one request; the server walks the client through the tree, and file contents arrive as svndiff deltas applied against a local pristine store, MD5-verified per file:

  • RaSvnSession speaks the ra_svn wire protocol: handshake, ANONYMOUS/CRAM-MD5 auth, command tuples, server-driven update editor, client-driven commit editor with svndiff0 windows.
  • DavSession speaks HTTP protocol v2 (Subversion 1.7+): OPTIONS stub discovery, PROPFIND/GET reads, streaming update-report REPORT parsing (via the XML component), and commits as POST create-txnPUT/MKCOL/DELETE/PROPPATCHMERGE.
  • SvnWorkingCopy + WorkingCopyEditor manage the working copy: JSON metadata plus a checksum-keyed pristine store under .svn/, status detection, svn:eol-style translation, and conflict handling (local file kept, incoming saved as <name>.r<rev>, commit blocked until resolved()).
  • Nested repositories: svn:externals (modern + pre-1.5 syntax, ^/ ../ // / relative URLs, pinned revisions) are checked out as nested working copies, updated with their parent, and skipped by status/commit — matching the official client.
  • Every wire detail was pinned down empirically against live servers before implementation (svnserve 1.14, mod_dav_svn on Apache, and WordPress.org itself).

Proof: wordpress-develop

Full checkout of https://develop.svn.wordpress.org/trunk @ r62480 with memory_limit=512M:

Nodes checked out 7,078 — exactly matches svn ls -R
Wall time ~12 s (single streamed response)
Peak memory 38–46 MB
Content Spot files byte-identical to svn cat; every file MD5-verified during the drive; full status walk clean
Externals tests/phpunit/data/plugins/wordpress-importer (a cross-server external to plugins.svn.wordpress.org) checked out automatically
Update r62400 → r62480 replay: 161 updated / 12 added / 1 deleted, 0 conflicts, 1.8 s

Reproducible via SVN_TESTS_ONLINE=1 SVN_TESTS_HUGE=1 vendor/bin/phpunit components/Svn/Tests/WordPressDevelopCheckoutTest.php.

Tests

All scenarios run against real SVN servers, never mocks:

  • Local svn://SvnserveClientTest spawns svnadmin-created repositories behind a real svnserve (auto-skips without the binaries). Includes an interop test: working copies byte-identical with the official client's, commits visible in both directions.
  • Local http://DavClientTest runs Apache + mod_dav_svn in Docker (Tests/http-server/, auto-skips without Docker), anonymous and Basic-auth repositories.
  • Both share SvnClientBehaviorTrait: checkout (incl. depth + old revisions), status, commit roundtrips, updates, conflict/resolve flows, revert, forced deletes, out-of-date rejection, auth failure/success, externals.
  • RemoteWordPressOrgTest (gated by SVN_TESTS_ONLINE=1) exercises develop.svn.wordpress.org and plugins.svn.wordpress.org.
  • Unit — svndiff codec (incl. hand-crafted windows, overlapping target copies, truncation), ra_svn item codec, externals parser, working copy/eol logic, DAV report parser against a captured mod_dav_svn fixture (incl. 7-byte chunked feeding).

composer lint clean for the component; PHPCompatibility clean for 7.2; full project suite passes (5600 tests).

Design notes & limitations

  • Working copy format is our own (.svn/wc.json + pristine store), not the official client's SQLite wc.db. Repositories interoperate fully; working copies don't transfer between clients.
  • HTTP requires Subversion 1.7+ servers (HTTP protocol v2). The pre-2011 DeltaV dance is not implemented; connect fails with a clear message.
  • Subtle bug found & fixed during development: for fixed svn:eol-style values (CRLF/CR/LF) the repository itself stores that line ending — normalizing to LF made fresh checkouts look locally modified and could silently commit eol rewrites. There's a regression test (test_commit_does_not_rewrite_eol_styled_files).
  • Two @TODOs for the XML component surfaced here: it can't pause mid-XML-declaration in streaming mode (worked around with a tiny prelude buffer) and its 1 GiB memory budget isn't publicly configurable (worked around with a guarded reflection tweak; that's what keeps 600 MB checkouts at ~40 MB RAM).
  • Not supported yet: file://, svn+ssh://, locks, svn:keywords, svn:special symlinks, merge tracking, mixed-depth sparse checkouts. Documented in the README.
  • Root composer.json was edited by hand (classmap + files) — bin/regenerate_composer.json.php mentioned in AGENTS.md doesn't exist in the repo.

adamziel and others added 3 commits June 10, 2026 13:14
Check out, update, and commit to Subversion repositories over both
svn:// (the ra_svn wire protocol) and http(s):// (mod_dav_svn, HTTP
protocol v2) without the svn binary or any PHP extension.

- Checkouts and updates stream the server's editor drive (single
  request per operation) with per-file MD5 verification; svndiff0/1
  deltas are applied against a pristine store.
- svn:externals are checked out as nested working copies, kept in
  sync on update, and skipped by status/commit.
- Conflict-aware updates, status, add/delete/revert/resolved, commits
  with out-of-date detection, svn:eol-style translation, ANONYMOUS +
  CRAM-MD5 auth on svn:// and HTTP Basic on http(s)://.

Tested against svnserve spawned locally, Apache + mod_dav_svn in
Docker, and the live WordPress.org repositories, including a full
checkout of wordpress-develop trunk (7078 nodes, checksum-verified,
~46 MB peak memory) and an 80-revision update of real history.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@adamziel adamziel marked this pull request as draft June 11, 2026 10:11
@adamziel adamziel marked this pull request as ready for review June 12, 2026 11:47
@adamziel adamziel merged commit 947734a into trunk Jun 12, 2026
29 checks passed
@adamziel adamziel deleted the adamziel/php-svn-client branch June 12, 2026 11:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant