Make Git hook configuration idempotent to avoid config.lock churn#3972
Make Git hook configuration idempotent to avoid config.lock churn#3972hadamrd wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR addresses build serialization and checkout failures caused by repeated .git/config writes when disabling Git hooks. It makes hook disabling idempotent by skipping StoredConfig.save() when core.hooksPath is already set to the disabled sentinel value, preventing unnecessary creation of .git/config.lock (JENKINS-71349 / JENKINS-65277).
Changes:
- Update
DisableHooksto only write/savecore.hooksPathwhen it differs from the disabled value. - Add regression tests covering stale
config.lockbehavior, repeated disables as a no-op, and overwriting a non-disabled hooks path.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/main/java/jenkins/plugins/git/DisableHooks.java | Adds an idempotency guard to avoid unnecessary .git/config saves/locks when hooks are already disabled. |
| src/test/java/jenkins/plugins/git/GitHooksConfigurationTest.java | Adds regression tests to validate the no-op behavior and ensure non-disabled values are still overwritten. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // every checkout of a reused workspace serializes builds on that lock and, if a | ||
| // build is interrupted between lock creation and the rename, leaves behind a stale | ||
| // config.lock that blocks all subsequent checkouts of that workspace (JENKINS-71349). | ||
| // The sibling UnsetHooks already guards its save() the same way. |
There was a problem hiding this comment.
Reworded to drop the UnsetHooks reference. You're right that UnsetHooks wasn't idempotent either — it called unset()+save() on every checkout when core.hooksPath was already absent, taking the same config.lock. Added an early return there and a test for that path.
DisableHooks and UnsetHooks rewrote core.hooksPath on every checkout, and StoredConfig.save() creates a .git/config.lock each time. On shared-library and pipeline-definition checkouts that reuse one controller-side .git across builds this serializes builds on that lock, and a build interrupted between the lock creation and the rename leaves an orphaned config.lock that blocks every later checkout of that workspace until it is removed by hand (JENKINS-71349, JENKINS-65277). DisableHooks now writes only when core.hooksPath differs from the disabled value. UnsetHooks returns early when it is already absent instead of calling unset() + save(). A value set to something else is still overwritten by DisableHooks or left intact with the existing warning by UnsetHooks.
43ca656 to
52b2613
Compare
core.hooksPath rewrite when hooks are already disabled
JENKINS-71349
DisableHooksandUnsetHooksrewritecore.hooksPathon every checkout, andStoredConfig.save()creates.git/config.lockeach time. On shared-library and pipeline-definition checkouts that reuse one controller-side.gitacross builds, this serializes builds on that lock; a build interrupted between the lock creation and the rename leaves an orphanedconfig.lockthat blocks every later checkout of that workspace until it is removed by hand. Same report in JENKINS-65277.DisableHooks: writecore.hooksPathonly when it differs from the disabled value.UnsetHooks: return early whencore.hooksPathis already absent instead of callingunset()+save().A value set to something else is still overwritten (
DisableHooks) or left intact with the existing warning (UnsetHooks).Testing done
GitHooksConfigurationTest: a staleconfig.lockplanted on an already-disabled repo is ignored (errors withLockFailedExceptionbefore this change); the allow path is a no-op whenhooksPathis already absent; repeated disable is a no-op; a non-disabled value is still overwritten. 17 tests green locally (Linux, JDK 21).