From a1c34ac9ef24d3e9f98a6940c4abd63e487d63c4 Mon Sep 17 00:00:00 2001 From: Eric Defore Date: Tue, 25 Aug 2026 14:49:13 -0400 Subject: [PATCH 1/7] Keep the standalone's git history when its files are absorbed --- docs/installing.md | 65 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/docs/installing.md b/docs/installing.md index 39bede0..74770b2 100644 --- a/docs/installing.md +++ b/docs/installing.md @@ -17,3 +17,68 @@ Two plugins shipping different versions of it will collide otherwise. > such a name out of your config, so its own source is safe to prefix in full — but if your build > also runs the bundled plugin's files through Strauss, keep `extra.strauss.constant_prefix` away > from them. + +## Bringing the standalone's git history with it + +Copying the standalone's files into your repository loses its git history. The commit messages, the +blame, and with them the reason any given line is written the way it is, all stay behind in the old +repository — so a developer or an agent working on the bundled copy later has nothing to go on, and +`git blame` answers every question with the single commit that copied the files in. + +A submodule would keep that history, but it is the wrong shape here: a bundled copy usually needs +small host-only edits that have no business in the standalone's repository, which is normally +archived soon afterwards anyway. + +Merge the standalone in as an unrelated history instead, nesting its whole tree under the sub-plugin +directory first so that the two share no path and the merge has nothing to conflict over: + +```bash +# On a branch of the host plugin's repo. +git switch -c absorb-give-recurring + +# Pull the standalone's commits in as a second, unrelated history. +git remote add old-repo git@github.com:givewp/give-recurring.git +git fetch --no-tags old-repo + +# Nest every top-level entry it tracks, so it shares no path with the host. +git switch -c old-repo-import old-repo/main +mkdir -p sub-plugins/give-recurring +git ls-tree --name-only -z HEAD | xargs -0 -I{} git mv {} sub-plugins/give-recurring/ +git commit -m "Move Give Recurring under sub-plugins/give-recurring" + +# Nothing overlaps now, so this merge has nothing to conflict over. +git switch absorb-give-recurring +git merge --allow-unrelated-histories old-repo-import + +# The history is part of your branch now; the remote and import branch can be deleted. +git remote remove old-repo +git branch -d old-repo-import +``` + +**Move everything, so the merge only ever touches the sub-plugin directory.** The standalone's +`README.md`, `LICENSE` and `.gitignore` sit at *its* top level: leave them there and they merge into +*your* repository root, conflicting with the files of the same name and adding the ones with no +counterpart. `git ls-tree` moves whatever the standalone tracks, which a hand-written list of paths +will not. + +**`--no-tags`.** A plain fetch also takes any tag reachable from what it downloads. The standalone's +`1.1.0` then sits among your own releases, while a `1.0.0` you already have silently does not import +at all, since git will not move an existing tag. + +**The move commit moves and nothing else.** `git mv` alone leaves every blob identical, so rename +detection ties each file to its past. Rewrite a file in that same commit and it drops below the +similarity threshold, taking its history with it — blame stops at the move. Host-only edits belong in +commits after the merge. + +**Merge the PR with a merge commit.** Squash flattens the imported commits into one and rebase +replays them onto your trunk; either discards what this was for. + +Afterwards `git blame` and `git log --follow` cross the move with no extra flags, attributing each +line to the author who wrote it in the standalone. Path-filtered +`git log -- sub-plugins/give-recurring` is the exception and stops at the move, since that is where +the directory begins. + +Where the files were copied in by hand already, that merge conflicts `add/add` on every shared file +and `-X ours` resolves toward what you have, leaving the tree byte-identical and blame intact. It +only settles files present on both sides, so read `git show --stat HEAD` for anything the standalone +tracked and you never copied. From da0273f2c52f0529f81708f0dd69b73109aabbf7 Mon Sep 17 00:00:00 2001 From: Eric Defore Date: Tue, 25 Aug 2026 14:52:09 -0400 Subject: [PATCH 2/7] Teach cspell the GiveWP vendor name --- cspell.json | 1 + 1 file changed, 1 insertion(+) diff --git a/cspell.json b/cspell.json index ce8ed8e..759d2db 100644 --- a/cspell.json +++ b/cspell.json @@ -18,6 +18,7 @@ "fataled", "fataling", "fatals", + "givewp", "invokable", "kadence", "kses", From eed4a3b7995b2610473b5e334ea0300f5d82ea8a Mon Sep 17 00:00:00 2001 From: Eric Defore Date: Tue, 25 Aug 2026 15:11:38 -0400 Subject: [PATCH 3/7] Stage the move where the destination cannot sit inside it --- docs/installing.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/installing.md b/docs/installing.md index 74770b2..d87d214 100644 --- a/docs/installing.md +++ b/docs/installing.md @@ -42,8 +42,10 @@ git fetch --no-tags old-repo # Nest every top-level entry it tracks, so it shares no path with the host. git switch -c old-repo-import old-repo/main -mkdir -p sub-plugins/give-recurring -git ls-tree --name-only -z HEAD | xargs -0 -I{} git mv {} sub-plugins/give-recurring/ +mkdir __absorb-import +git ls-tree --name-only -z HEAD | xargs -0 -I{} git mv {} __absorb-import/ +mkdir -p sub-plugins +git mv __absorb-import sub-plugins/give-recurring git commit -m "Move Give Recurring under sub-plugins/give-recurring" # Nothing overlaps now, so this merge has nothing to conflict over. @@ -61,6 +63,13 @@ git branch -d old-repo-import counterpart. `git ls-tree` moves whatever the standalone tracks, which a hand-written list of paths will not. +**The staging directory keeps the destination out of the tree being moved.** That move runs against +the standalone's own checkout, so a destination like `includes/notifications` lands under an +`includes/` the standalone tracks itself, and git will not move a directory into itself. Only that +one entry fails while every other one succeeds, so the error scrolls past in a screen of moves that +worked. Setting the whole tree aside under a name nothing uses, then renaming it into place once, +does not care where the destination sits. + **`--no-tags`.** A plain fetch also takes any tag reachable from what it downloads. The standalone's `1.1.0` then sits among your own releases, while a `1.0.0` you already have silently does not import at all, since git will not move an existing tag. From 1ff2ade95010554cc0b19f5c3697ac42e53b0d7b Mon Sep 17 00:00:00 2001 From: Eric Defore Date: Tue, 25 Aug 2026 15:58:51 -0400 Subject: [PATCH 4/7] Move the history import beside the rest of the bundled-plugin setup --- docs/configuration.md | 74 +++++++++++++++++++++++++++++++++++++++++++ docs/installing.md | 74 ------------------------------------------- 2 files changed, 74 insertions(+), 74 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index b1f753a..86f22df 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -105,6 +105,80 @@ load pass first; a site created afterwards never gets it. Per-site work is yours a later site yours to catch on `wp_initialize_site`: see [Do per-site work on multisite](recipes.md#do-per-site-work-on-multisite). +## Bringing the standalone's git history with it + +Copying the standalone's files into your repository loses its git history. The commit messages, the +blame, and with them the reason any given line is written the way it is, all stay behind in the old +repository — so a developer or an agent working on the bundled copy later has nothing to go on, and +`git blame` answers every question with the single commit that copied the files in. + +A submodule would keep that history, but it is the wrong shape here: a bundled copy usually needs +small host-only edits that have no business in the standalone's repository, which is normally +archived soon afterwards anyway. + +Merge the standalone in as an unrelated history instead, nesting its whole tree under the sub-plugin +directory first so that the two share no path and the merge has nothing to conflict over: + +```bash +# On a branch of the host plugin's repo. +git switch -c absorb-give-recurring + +# Pull the standalone's commits in as a second, unrelated history. +git remote add old-repo git@github.com:givewp/give-recurring.git +git fetch --no-tags old-repo + +# Nest every top-level entry it tracks, so it shares no path with the host. +git switch -c old-repo-import old-repo/main +mkdir __absorb-import +git ls-tree --name-only -z HEAD | xargs -0 -I{} git mv {} __absorb-import/ +mkdir -p sub-plugins +git mv __absorb-import sub-plugins/give-recurring +git commit -m "Move Give Recurring under sub-plugins/give-recurring" + +# Nothing overlaps now, so this merge has nothing to conflict over. +git switch absorb-give-recurring +git merge --allow-unrelated-histories old-repo-import + +# The history is part of your branch now; the remote and import branch can be deleted. +git remote remove old-repo +git branch -d old-repo-import +``` + +**Move everything, so the merge only ever touches the sub-plugin directory.** The standalone's +`README.md`, `LICENSE` and `.gitignore` sit at *its* top level: leave them there and they merge into +*your* repository root, conflicting with the files of the same name and adding the ones with no +counterpart. `git ls-tree` moves whatever the standalone tracks, which a hand-written list of paths +will not. + +**The staging directory keeps the destination out of the tree being moved.** That move runs against +the standalone's own checkout, so a destination like `includes/notifications` lands under an +`includes/` the standalone tracks itself, and git will not move a directory into itself. Only that +one entry fails while every other one succeeds, so the error scrolls past in a screen of moves that +worked. Setting the whole tree aside under a name nothing uses, then renaming it into place once, +does not care where the destination sits. + +**`--no-tags`.** A plain fetch also takes any tag reachable from what it downloads. The standalone's +`1.1.0` then sits among your own releases, while a `1.0.0` you already have silently does not import +at all, since git will not move an existing tag. + +**The move commit moves and nothing else.** `git mv` alone leaves every blob identical, so rename +detection ties each file to its past. Rewrite a file in that same commit and it drops below the +similarity threshold, taking its history with it — blame stops at the move. Host-only edits belong in +commits after the merge. + +**Merge the PR with a merge commit.** Squash flattens the imported commits into one and rebase +replays them onto your trunk; either discards what this was for. + +Afterwards `git blame` and `git log --follow` cross the move with no extra flags, attributing each +line to the author who wrote it in the standalone. Path-filtered +`git log -- sub-plugins/give-recurring` is the exception and stops at the move, since that is where +the directory begins. + +Where the files were copied in by hand already, that merge conflicts `add/add` on every shared file +and `-X ours` resolves toward what you have, leaving the tree byte-identical and blame intact. It +only settles files present on both sides, so read `git show --stat HEAD` for anything the standalone +tracked and you never copied. + ## What changes for the bundled plugin This library includes bundled plugins from inside a method, not at global scope, so variables diff --git a/docs/installing.md b/docs/installing.md index d87d214..39bede0 100644 --- a/docs/installing.md +++ b/docs/installing.md @@ -17,77 +17,3 @@ Two plugins shipping different versions of it will collide otherwise. > such a name out of your config, so its own source is safe to prefix in full — but if your build > also runs the bundled plugin's files through Strauss, keep `extra.strauss.constant_prefix` away > from them. - -## Bringing the standalone's git history with it - -Copying the standalone's files into your repository loses its git history. The commit messages, the -blame, and with them the reason any given line is written the way it is, all stay behind in the old -repository — so a developer or an agent working on the bundled copy later has nothing to go on, and -`git blame` answers every question with the single commit that copied the files in. - -A submodule would keep that history, but it is the wrong shape here: a bundled copy usually needs -small host-only edits that have no business in the standalone's repository, which is normally -archived soon afterwards anyway. - -Merge the standalone in as an unrelated history instead, nesting its whole tree under the sub-plugin -directory first so that the two share no path and the merge has nothing to conflict over: - -```bash -# On a branch of the host plugin's repo. -git switch -c absorb-give-recurring - -# Pull the standalone's commits in as a second, unrelated history. -git remote add old-repo git@github.com:givewp/give-recurring.git -git fetch --no-tags old-repo - -# Nest every top-level entry it tracks, so it shares no path with the host. -git switch -c old-repo-import old-repo/main -mkdir __absorb-import -git ls-tree --name-only -z HEAD | xargs -0 -I{} git mv {} __absorb-import/ -mkdir -p sub-plugins -git mv __absorb-import sub-plugins/give-recurring -git commit -m "Move Give Recurring under sub-plugins/give-recurring" - -# Nothing overlaps now, so this merge has nothing to conflict over. -git switch absorb-give-recurring -git merge --allow-unrelated-histories old-repo-import - -# The history is part of your branch now; the remote and import branch can be deleted. -git remote remove old-repo -git branch -d old-repo-import -``` - -**Move everything, so the merge only ever touches the sub-plugin directory.** The standalone's -`README.md`, `LICENSE` and `.gitignore` sit at *its* top level: leave them there and they merge into -*your* repository root, conflicting with the files of the same name and adding the ones with no -counterpart. `git ls-tree` moves whatever the standalone tracks, which a hand-written list of paths -will not. - -**The staging directory keeps the destination out of the tree being moved.** That move runs against -the standalone's own checkout, so a destination like `includes/notifications` lands under an -`includes/` the standalone tracks itself, and git will not move a directory into itself. Only that -one entry fails while every other one succeeds, so the error scrolls past in a screen of moves that -worked. Setting the whole tree aside under a name nothing uses, then renaming it into place once, -does not care where the destination sits. - -**`--no-tags`.** A plain fetch also takes any tag reachable from what it downloads. The standalone's -`1.1.0` then sits among your own releases, while a `1.0.0` you already have silently does not import -at all, since git will not move an existing tag. - -**The move commit moves and nothing else.** `git mv` alone leaves every blob identical, so rename -detection ties each file to its past. Rewrite a file in that same commit and it drops below the -similarity threshold, taking its history with it — blame stops at the move. Host-only edits belong in -commits after the merge. - -**Merge the PR with a merge commit.** Squash flattens the imported commits into one and rebase -replays them onto your trunk; either discards what this was for. - -Afterwards `git blame` and `git log --follow` cross the move with no extra flags, attributing each -line to the author who wrote it in the standalone. Path-filtered -`git log -- sub-plugins/give-recurring` is the exception and stops at the move, since that is where -the directory begins. - -Where the files were copied in by hand already, that merge conflicts `add/add` on every shared file -and `-X ours` resolves toward what you have, leaving the tree byte-identical and blame intact. It -only settles files present on both sides, so read `git show --stat HEAD` for anything the standalone -tracked and you never copied. From 339bee198e6c6f42e1b65ba196d5dd70323efa49 Mon Sep 17 00:00:00 2001 From: Eric Defore Date: Tue, 25 Aug 2026 16:20:11 -0400 Subject: [PATCH 5/7] Do the branch work in a worktree, not over the host checkout --- docs/configuration.md | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 86f22df..62236ee 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -127,23 +127,34 @@ git switch -c absorb-give-recurring git remote add old-repo git@github.com:givewp/give-recurring.git git fetch --no-tags old-repo -# Nest every top-level entry it tracks, so it shares no path with the host. -git switch -c old-repo-import old-repo/main +# Nest every top-level entry it tracks, in a scratch worktree so that this checkout +# is never switched away from. +git worktree add -b old-repo-import ../absorb-worktree old-repo/main +cd ../absorb-worktree mkdir __absorb-import git ls-tree --name-only -z HEAD | xargs -0 -I{} git mv {} __absorb-import/ mkdir -p sub-plugins git mv __absorb-import sub-plugins/give-recurring git commit -m "Move Give Recurring under sub-plugins/give-recurring" +cd - # Nothing overlaps now, so this merge has nothing to conflict over. -git switch absorb-give-recurring git merge --allow-unrelated-histories old-repo-import -# The history is part of your branch now; the remote and import branch can be deleted. +# The history is part of your branch now; the rest can be deleted. +git worktree remove ../absorb-worktree git remote remove old-repo git branch -d old-repo-import ``` +**The scratch worktree is what stops this deleting your build output.** Checking the standalone's +tree out over your own means git writing a tree that knows nothing about your repository, and git +does not protect ignored files: one that collides is overwritten in place, and switching back then +removes any directory left holding nothing but ignored files. A `dist/` or `assets/` the standalone +also tracks takes your untracked build artifacts with it, silently, and a `.gitignore`d database dump +under one goes the same way. A worktree is a second checkout of the same repository in another +directory, so the branch work happens there and yours is only ever merged into. + **Move everything, so the merge only ever touches the sub-plugin directory.** The standalone's `README.md`, `LICENSE` and `.gitignore` sit at *its* top level: leave them there and they merge into *your* repository root, conflicting with the files of the same name and adding the ones with no From f9c5decf50b472914f9be82e573216f44f9f3b68 Mon Sep 17 00:00:00 2001 From: Eric Defore Date: Tue, 25 Aug 2026 16:31:14 -0400 Subject: [PATCH 6/7] Lead with the move, not the precaution around it --- docs/configuration.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 62236ee..0fa4e40 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -147,14 +147,6 @@ git remote remove old-repo git branch -d old-repo-import ``` -**The scratch worktree is what stops this deleting your build output.** Checking the standalone's -tree out over your own means git writing a tree that knows nothing about your repository, and git -does not protect ignored files: one that collides is overwritten in place, and switching back then -removes any directory left holding nothing but ignored files. A `dist/` or `assets/` the standalone -also tracks takes your untracked build artifacts with it, silently, and a `.gitignore`d database dump -under one goes the same way. A worktree is a second checkout of the same repository in another -directory, so the branch work happens there and yours is only ever merged into. - **Move everything, so the merge only ever touches the sub-plugin directory.** The standalone's `README.md`, `LICENSE` and `.gitignore` sit at *its* top level: leave them there and they merge into *your* repository root, conflicting with the files of the same name and adding the ones with no @@ -168,6 +160,14 @@ one entry fails while every other one succeeds, so the error scrolls past in a s worked. Setting the whole tree aside under a name nothing uses, then renaming it into place once, does not care where the destination sits. +**The scratch worktree is what stops this deleting your build output.** Checking the standalone's +tree out over your own means git writing a tree that knows nothing about your repository, and git +does not protect ignored files: one that collides is overwritten in place, and switching back then +removes any directory left holding nothing but ignored files. A `dist/` or `assets/` the standalone +also tracks takes your untracked build artifacts with it, silently, and a `.gitignore`d database dump +under one goes the same way. A worktree is a second checkout of the same repository in another +directory, so the branch work happens there and yours is only ever merged into. + **`--no-tags`.** A plain fetch also takes any tag reachable from what it downloads. The standalone's `1.1.0` then sits among your own releases, while a `1.0.0` you already have silently does not import at all, since git will not move an existing tag. From 8465e720cb0e7090c97b0774a71588b4ff6991fa Mon Sep 17 00:00:00 2001 From: Eric Defore Date: Tue, 25 Aug 2026 16:35:27 -0400 Subject: [PATCH 7/7] Teach the procedure, not the salvage job --- docs/configuration.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 0fa4e40..7d6e24f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -185,11 +185,6 @@ line to the author who wrote it in the standalone. Path-filtered `git log -- sub-plugins/give-recurring` is the exception and stops at the move, since that is where the directory begins. -Where the files were copied in by hand already, that merge conflicts `add/add` on every shared file -and `-X ours` resolves toward what you have, leaving the tree byte-identical and blame intact. It -only settles files present on both sides, so read `git show --stat HEAD` for anything the standalone -tracked and you never copied. - ## What changes for the bundled plugin This library includes bundled plugins from inside a method, not at global scope, so variables