Skip to content

[6.x] Partial refresh Stache items from git history - #15354

Open
godismyjudge95 wants to merge 6 commits into
statamic:6.xfrom
godismyjudge95:feat/stache-refresh-git
Open

[6.x] Partial refresh Stache items from git history#15354
godismyjudge95 wants to merge 6 commits into
statamic:6.xfrom
godismyjudge95:feat/stache-refresh-git

Conversation

@godismyjudge95

@godismyjudge95 godismyjudge95 commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

This PR is a solution to a long standing problem for us: whenever we push to deploy we have to avoid doing a php please stache:refresh like the plague because it can take larger sites minutes to complete and while it runs it takes the entire site down.

My solution here is to add a flag stache:refresh --git which stores the the current SHA ref each time it is run. Then it simply refreshes only stache items for the content that has been changed since the last time it has been ran. This means you still get to push to deploy content changes, but don't take the entire site down when you need to run stache:refresh to display those changes.

I could see a few downsides to this:

  • At the moment it relies on an arbitrary file in storage/statamic/.stache-git-ref instead of a cache item - this is so it survives deploys, but this means it isn't compatible with things like Laravel Cloud
  • It can't detect content changes that are linked to other pages. The stache refreshes aren't cascaded

Since it isn't changing the default behavior I think both of those items are fine?

Let me know what you think.


AI Explanation:

Why

A deploy that changes a few content files should not rebuild every Stache store. statamic:stache:refresh --git diffs from the SHA in storage/statamic/.stache-git-ref to HEAD, maps each path to a store action, then writes the current HEAD SHA.

Scope

Adds --git and --include-dirty on statamic:stache:refresh. --exclude from 6.x stays.

Git::stacheDiff() parses git diff --name-status, including R### renames and C### copies. --include-dirty also reads staged files, unstaged tracked files, and untracked files via git ls-files --others --exclude-standard.

GitPathMapper maps content/, users/, and resources/blueprints/ onto update-item, forget-item, warm-store, or full-refresh. Collection yaml also warms entries::{handle}. Taxonomy yaml also warms terms::{handle}. Collection tree yaml also warms entries::{handle}. Fieldsets, config/statamic/, and other blueprints still map to full-refresh. Unmapped paths such as src/, views, and public/ produce no actions.

Store::updateItemFromPath() loads items through getItemFromModifiedPath() so a term file updates every locale. Store::forgetItemByPath() tidies the path and tries slash and resolved variants.

A missing, empty, or whitespace-only ref file is a first run. A successful full clear and warm writes the ref when the site is a git repo. A failed git diff exits FAILURE and does not write the ref.

Tradeoffs

Unmapped paths are ignored so a mixed deploy stays targeted. A new store that the mapper does not know will stay stale until you map it or run a full refresh.

GitProcess::diff(), diffDirty(), and diffStaged() now throw when git writes to stderr or exits non-zero. Only stacheDiff() calls those methods. A git warning on stderr with exit 0 fails the command.

Targeted update is not a full Stache::warm(). Entry deletes do not rebuild term associations. Asset binaries and form submissions stay unmapped. An id or slug change can leave the old key until a full refresh.

Blast Radius

Default statamic:stache:refresh now writes storage/statamic/.stache-git-ref when the site is a git repo, so the next --git run does not replay the whole history.

--exclude now skips targeted update-item, forget-item, and warm-store actions as well as full warm. --exclude=entries skips entries::blog.

Control Panel git commit and push do not call diff(). They should be unchanged.

Verification

From /home/daniel/ddev/sites/statamic/cms-worktrees/stache-refresh-git with /home/daniel/ddev/sites/server/bin/php vendor/bin/phpunit --configuration phpunit.dist.xml:

  • tests/Stache/GitPathMapperTest.php, tests/Console/Commands/StacheRefreshTest.php, tests/Stache/Stores/EntriesStoreTest.php: 62 passed, 234 assertions, 1 incomplete test that was already in EntriesStoreTest.
  • tests/Git/GitTest.php and tests/Git/GitProcessTest.php filtered to parse, copy, untracked, ref, and stache diff cases: 15 passed, 45 assertions.
  • mago lint -s on the changed PHP files: no issues.

Unmapped files no longer force a full rebuild, so a mixed deploy can still do a targeted --git refresh. Fieldsets, config/statamic, and other blueprints still invalidate the whole stache.
A failed git diff no longer looks like an empty change set, so a bad stored SHA cannot skip content updates. --include-dirty now lists untracked files, copy lines add the new path, and forgetItemByPath matches tidy path variants.
An empty ref file now bootstraps like a missing one, and a full clear+warm writes HEAD when the site is a git repo. Collection, taxonomy, and collection-tree yaml also warm the child store. Targeted --git skips excluded stores, and term yaml updates every locale.
@godismyjudge95 godismyjudge95 changed the title feat(stache): refresh the Stache from git history [6.x] Partial refresh Stache items from git history Sep 2, 2026
updateItemFromPath and forgetItemByPath call cacheItem, forgetItem, and getKeyFromPath more times than the baseline allowed.
cacheItem, forgetItem, and getKeyFromPath already live there. The new methods belong next to them, not on Store.
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