Add --preserve-local-content to merge wp-content instead of replacing it#383
Open
epeicher wants to merge 2 commits into
Open
Add --preserve-local-content to merge wp-content instead of replacing it#383epeicher wants to merge 2 commits into
epeicher wants to merge 2 commits into
Conversation
With --force, flat-docroot replaces a conflicting non-symlink target with a symlink, recursively deleting whatever that directory already held. For wp-content that loses every file present only in the target. --preserve-local-content keeps a directory that exists on both sides and places the source's entries inside it: real directories on both sides recurse, anything else is symlinked (the source wins), and entries present only in the target are left untouched. Recursion stops as soon as one side is not a real directory, so a subtree the target lacks still costs a single symlink. Merging also prunes symlinks pointing into the fs root whose target is gone, because walking the source alone cannot notice entries it no longer has.
1 task
Contributor
Pull pipeline performance —
|
| Stage | Wall time | Resume attempts | Status | Details |
|---|---|---|---|---|
playground-sqlite-db-pull |
7.01 s | 1 | ✓ | condition=db-pull in PHP.wasm runtime=php.wasm 8.3 wp_mysql_parser=enabled mode=lexer native_lexer=verified native_token_stream=WP_MySQL_Native_Token_Stream native_token_count=18 native_parser=selected |
playground-sqlite-db-apply |
2.87 s | 1 | ✓ | condition=db-apply to SQLite in PHP.wasm runtime=php.wasm 8.3 wp_mysql_parser=enabled mode=parser native_lexer=verified native_token_stream=WP_MySQL_Native_Token_Stream native_token_count=18 native_parser=verified native_ast=WP_MySQL_Native_Parser_Node sqlite_driver_parser=verified |
| Total | 9.88 s |
Numbers carry runner noise; treat single-run deltas as directional, not authoritative.
📈 Trunk performance history — commit-by-commit timeline.
The prune resolved a link lexically against the target path, while get_filesystem_root_path() returns a canonical one. Any link whose route passed through a symlink therefore failed the containment check and was never removed — /tmp and /var on macOS, or a symlinked project directory. Resolve against the target's real path so both sides are canonical. The tests cover the merge keeping target-only entries, the source winning a collision without --force, pruning a link whose source entry is gone, leaving a dangling link that resolves outside the fs root alone, a subtree the target lacks costing a single symlink, and the flag being opt-in.
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.
What it does
Adds
--preserve-local-contenttoflat-docrootandpull. With it, an existingwp-contentis merged rather than replaced: entries the source also has get linked, entries that exist only in the target are left alone.Opt-in — without the flag, behaviour is unchanged.
Rationale
flat-docroot --forcereplaces a conflicting non-symlink target with a symlink, and for a directory that means deleting it first:That's fine for core paths, which the source owns entirely. It isn't fine for
wp-content: flattening onto a docroot that already has content deletes every plugin, theme and upload the target had. A consumer flattening onto a pre-existing site needs--forceto overwrite core, but must not losewp-contentin the process.Implementation
flatten_merge_directory()wrapsflatten_place_symlink(). When either side isn't a real directory it delegates unchanged — one symlink for the whole subtree. When both sides are real directories it keeps the target and walks the source:Recursion stops as soon as one side isn't a real directory, so a subtree the target doesn't have still costs a single symlink.
uploads/only expands into per-entry links if the target already has matching directories.Two consequences of walking the source instead of aliasing it:
Collisions must resolve, not raise. A merge implies the source wins, so the delegated call passes
$preserve || $force. Otherwise every path present on both sides —wp-content/database, for one — would fail unless the caller also passed--force, which replaces whole directories and defeats the merge.Removals need pruning. Walking the source can't notice entries it no longer has, so links to deleted files would accumulate. After merging, symlinks whose target is gone are unlinked — but only when the link resolves back inside the fs root, since those are the ones the flatten created. Real files, and symlinks the target owns, are untouched.
Only
wp-contentmerges; the rest of ABSPATH is core and is still replaced.Testing instructions
Seven cases: the merge keeps target-only entries; the source wins a collision without
--force; a link whose source entry is gone is pruned; a dangling link resolving outside the fs root is left alone; a subtree the target lacks costs a single symlink; and without the flag an existing directory is still a conflict, with--forcestill replacing it wholesale.Writing them turned up a bug in the prune, fixed in the second commit: it resolved the link lexically while
get_filesystem_root_path()returns a canonical path, so any link routed through a symlink —/tmpand/varon macOS, or a symlinked project directory — failed the containment check and was never pruned.The rest of the suite is unaffected. Note that a local run without MySQL fails ~151 database-dependent tests with
Connection refused; those are unrelated.