Skip to content

Make Git hook configuration idempotent to avoid config.lock churn#3972

Open
hadamrd wants to merge 1 commit into
jenkinsci:masterfrom
hadamrd:fix/disablehooks-idempotent-config-lock
Open

Make Git hook configuration idempotent to avoid config.lock churn#3972
hadamrd wants to merge 1 commit into
jenkinsci:masterfrom
hadamrd:fix/disablehooks-idempotent-config-lock

Conversation

@hadamrd

@hadamrd hadamrd commented Jun 15, 2026

Copy link
Copy Markdown

JENKINS-71349

DisableHooks and UnsetHooks rewrite core.hooksPath on every checkout, and StoredConfig.save() creates .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; 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. Same report in JENKINS-65277.

  • DisableHooks: write core.hooksPath only when it differs from the disabled value.
  • UnsetHooks: return early when core.hooksPath is already absent instead of calling unset() + save().

A value set to something else is still overwritten (DisableHooks) or left intact with the existing warning (UnsetHooks).

Testing done

GitHooksConfigurationTest: a stale config.lock planted on an already-disabled repo is ignored (errors with LockFailedException before this change); the allow path is a no-op when hooksPath is already absent; repeated disable is a no-op; a non-disabled value is still overwritten. 17 tests green locally (Linux, JDK 21).

@hadamrd hadamrd requested a review from a team as a code owner June 15, 2026 18:13
@github-actions github-actions Bot added the tests Automated test addition or improvement label Jun 15, 2026
@MarkEWaite MarkEWaite requested a review from Copilot June 15, 2026 19:43

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 DisableHooks to only write/save core.hooksPath when it differs from the disabled value.
  • Add regression tests covering stale config.lock behavior, 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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@hadamrd hadamrd force-pushed the fix/disablehooks-idempotent-config-lock branch from 43ca656 to 52b2613 Compare June 15, 2026 20:19
@hadamrd hadamrd changed the title Skip core.hooksPath rewrite when hooks are already disabled Make Git hook configuration idempotent to avoid config.lock churn Jun 15, 2026
@MarkEWaite MarkEWaite added bug Incorrect or flawed behavior and removed tests Automated test addition or improvement labels Jun 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Incorrect or flawed behavior

Projects

None yet

3 participants