[6.x] Prevent overlapping git commit jobs#14672
Conversation
Mark CommitJob as ShouldBeUnique so concurrent dispatches collapse to a single queued job. Without this, multiple workers running CommitJobs against the same repo race on `git add` (producing `index.lock: File exists` errors) and on `git push` (producing non-fast-forward rejections because each worker's push uses a stale view of origin). The unique lock TTL is 120s, comfortably outlasting the default queue worker timeout so a hard worker crash recovers within 2 minutes.
When multiple saves are dispatched within a single CommitJob's lock window, ShouldBeUnique drops the duplicate dispatches and the queued job's `git add` sweeps up everyone's changes — but the queued job still carries the first dispatcher's user. Attributing a multi-author commit to a single user is misleading. Track dispatch attempts in cache via Cache::increment in dispatchCommit, then in CommitJob::handle null out the committer when the pull reveals more than one dispatch occurred. Git's existing fallback then uses the configured user.name / user.email (the bot account) — consistent with how scheduler-driven saves are already attributed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When multiple users save during the dispatch delay window, their
individual dispatches are coalesced into a single CommitJob. Rather
than silently attributing that commit to the configured bot user,
this records every contributor as a Co-Authored-By trailer so the
commit log reflects who actually made changes.
dispatchCommit() now appends {name, email} to a cache array instead
of incrementing a counter. CommitJob::handle() pulls that array,
deduplicates by email, and when more than one unique author is found
it: (a) commits as the bot user, and (b) appends Co-Authored-By
lines to the message before handing it to Git::commit().
shellEscape() is updated from escapeshellcmd() to targeted escaping
of only \, $, and ` — the three characters that retain special
meaning inside a double-quoted shell string per the Bash Reference
Manual §3.1.2.3. escapeshellcmd() was over-escaping characters like
<, >, ;, and newlines that are literal inside "...", which would
corrupt the Co-Authored-By trailer format and was also producing
spurious backslashes in git log for ordinary user names and messages.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tests the full dispatchCommit → cache accumulation → CommitJob::handle → real git commit path. Two users dispatch; the second is dropped by ShouldBeUnique but their save is still recorded in the cache array. Running handle() directly then verifies both Co-Authored-By trailers appear in the git log. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Made a few additions on top, some flagged by AI review. Configurable lock expiry — Co-Authored-By trailers instead of bot fallback — rather than silently attributing a coalesced commit to the configured bot user, the pending-saves tracker now stores
Additional tests — deduplication of repeated saves by the same user, a real git integration test for the trailer format, and an end-to-end pipeline test that runs through |
Closes #11322
Issue
Concurrent
CommitJobs running against the same.git/directory cause two related failures:fatal: Unable to create '.git/index.lock': File exists.(twogit add/commitprocesses racing on git's index mutex)error: failed to push some refs ... non-fast-forward(each worker pushing with a stale view of origin, so the second push fails git's atomic ref CAS)Both root-cause to the same thing: multiple queue workers running
CommitJobs concurrently against one repo.Solution
Mark
CommitJobasShouldBeUniqueso duplicate dispatches are dropped at the dispatch layer. The first dispatch wins; itsgit add {{ paths }}sweeps up any changes from saves that landed during the lock window. No two git processes fromCommitJobever run simultaneously.uniqueForis set to 120 seconds. In normal operation the lock is released the momenthandle()returns;uniqueForis only the crash-safety net for SIGKILL / OOM scenarios where Laravel's release-on-completion code never runs. 120s comfortably outlasts the default Laravel queue workertimeout(60s), so a hard crash recovers within 2 minutes.Attribution
Coalescing changes the attribution story: a burst of saves now produces one commit, and naively that commit gets attributed to whichever user happened to dispatch first. Other users' changes are in the commit but their name isn't on it.
The second commit in this PR addresses that with a small counter pattern:
Git::dispatchCommit()callsCache::increment('statamic-git-pending-saves')on every attempt (including dispatches that will be dropped).CommitJob::handle()doesCache::pull(...)at the start. If the count is greater than 1, the committer is replaced withnull.Git's existing fallback ingitUserName()/gitUserEmail()then attributes the commit to the configureduser.name/user.email(the bot account).This is the same fallback Statamic already uses today for scheduler-driven and CLI-driven saves where there's no authenticated user, so we're extending an existing pattern, not introducing a new one.