From 0d3d6d4c19604e541582bb0cc74ac70aa776dd26 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 20 Oct 2026 02:58:25 -0400 Subject: [PATCH 1/3] Store signatures per worktree when enabled Signatures live in the shared repository config, so signing in one worktree invalidates every other worktree whose configuration or hooks differ. When extensions.worktreeConfig is enabled, read and write them with `git config --worktree` instead; without the extension `--worktree` fails in a repo with linked worktrees, so `--local` remains the default. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01Wo4FerFK9aaxrGrmVE9ncZ --- README.md | 6 ++ lib/overcommit/configuration.rb | 11 ++- lib/overcommit/git_config.rb | 8 ++ lib/overcommit/hook_signer.rb | 6 +- spec/integration/worktree_signing_spec.rb | 104 ++++++++++++++++++++++ spec/overcommit/git_config_spec.rb | 36 ++++++++ 6 files changed, 165 insertions(+), 6 deletions(-) create mode 100644 spec/integration/worktree_signing_spec.rb diff --git a/README.md b/README.md index b4482ac1..74a7a284 100644 --- a/README.md +++ b/README.md @@ -708,6 +708,12 @@ and any configuration for the plugin. Thus a change to the plugin's source code or your local repo's `.overcommit.yml` file could result in a signature change. +Signatures are stored in your local repo's git configuration. If you use +multiple [worktrees](https://git-scm.com/docs/git-worktree) whose +configurations differ, enable `extensions.worktreeConfig` (`git config +extensions.worktreeConfig true`) so that Overcommit stores signatures per +worktree and signing in one worktree does not invalidate the others. + ### Disabling Signature Checking In typical usage, your plugins usually don't change too often, so this warning diff --git a/lib/overcommit/configuration.rb b/lib/overcommit/configuration.rb index 72086457..f5151031 100644 --- a/lib/overcommit/configuration.rb +++ b/lib/overcommit/configuration.rb @@ -214,8 +214,9 @@ def verify_signatures? return false if ENV['OVERCOMMIT_NO_VERIFY'] return true if @hash['verify_signatures'] != false + scope = Overcommit::GitConfig.signature_scope result = Overcommit::Utils.execute( - %W[git config --local --get #{verify_signature_config_key}] + %W[git config #{scope} --get #{verify_signature_config_key}] ) if result.status == 1 # Key doesn't exist @@ -232,13 +233,14 @@ def verify_signatures? # Update the currently stored signature for this hook. def update_signature! + scope = Overcommit::GitConfig.signature_scope result = Overcommit::Utils.execute( - %w[git config --local] + [signature_config_key, signature] + %W[git config #{scope}] + [signature_config_key, signature] ) verify_signature_value = @hash['verify_signatures'] ? 1 : 0 result &&= Overcommit::Utils.execute( - %W[git config --local #{verify_signature_config_key} #{verify_signature_value}] + %W[git config #{scope} #{verify_signature_config_key} #{verify_signature_value}] ) unless result.success? @@ -325,8 +327,9 @@ def signature # # @return [String] def stored_signature + scope = Overcommit::GitConfig.signature_scope result = Overcommit::Utils.execute( - %w[git config --local --get] + [signature_config_key] + %W[git config #{scope} --get] + [signature_config_key] ) if result.status == 1 # Key doesn't exist diff --git a/lib/overcommit/git_config.rb b/lib/overcommit/git_config.rb index c1243861..a82392bf 100644 --- a/lib/overcommit/git_config.rb +++ b/lib/overcommit/git_config.rb @@ -19,5 +19,13 @@ def hooks_path File.expand_path(path, Dir.pwd) end + + # Config file option for storing hook signatures: the per-worktree file + # when extensions.worktreeConfig is on, so worktrees with different + # configurations do not overwrite each other's signatures. + def signature_scope + enabled = `git config --bool --get extensions.worktreeConfig`.chomp + enabled == 'true' ? '--worktree' : '--local' + end end end diff --git a/lib/overcommit/hook_signer.rb b/lib/overcommit/hook_signer.rb index fab99c37..67870488 100644 --- a/lib/overcommit/hook_signer.rb +++ b/lib/overcommit/hook_signer.rb @@ -68,8 +68,9 @@ def signature_changed? # Update the current stored signature for this hook. def update_signature! + scope = Overcommit::GitConfig.signature_scope result = Overcommit::Utils.execute( - %w[git config --local] + [signature_config_key, signature] + %W[git config #{scope}] + [signature_config_key, signature] ) unless result.success? @@ -103,8 +104,9 @@ def hook_contents end def stored_signature + scope = Overcommit::GitConfig.signature_scope result = Overcommit::Utils.execute( - %w[git config --local --get] + [signature_config_key] + %W[git config #{scope} --get] + [signature_config_key] ) if result.status == 1 # Key doesn't exist diff --git a/spec/integration/worktree_signing_spec.rb b/spec/integration/worktree_signing_spec.rb new file mode 100644 index 00000000..1468a197 --- /dev/null +++ b/spec/integration/worktree_signing_spec.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'yaml' + +describe 'configuration file signing with multiple worktrees' do + let(:enable_verification) { true } + let(:verify_signatures) { true } + + let(:config) do + { + 'verify_signatures' => verify_signatures, + 'CommitMsg' => { + 'ALL' => { 'enabled' => false }, + }, + 'PreCommit' => { + 'ALL' => { 'enabled' => false }, + }, + } + end + + let(:linked_config) { config } + let(:sign_linked_worktree) { true } + + around do |example| + repo do + `git config --local extensions.worktreeConfig true` if worktree_config + `git commit --allow-empty -m "Initial commit"` + linked_worktree = File.join(File.dirname(Dir.pwd), 'linked-worktree') + result = Overcommit::Utils.execute( + %W[git worktree add -b linked-worktree #{linked_worktree}], + ) + raise "Unable to create worktree: #{result.stderr}" unless result.success? + + echo(config.to_yaml, '.overcommit.yml') + `overcommit --install > #{File::NULL}` + `overcommit --sign` + + Dir.chdir(linked_worktree) do + echo(linked_config.to_yaml, '.overcommit.yml') + `overcommit --sign` if sign_linked_worktree + end + + @linked_worktree = linked_worktree + example.run + end + end + + def commit_in_main_worktree + shell(%w[git commit --allow-empty -m Test]) + end + + def commit_in_linked_worktree + Dir.chdir(@linked_worktree) { shell(%w[git commit --allow-empty -m Test]) } + end + + context 'when the worktrees have different configurations' do + let(:linked_config) do + config.merge('PreCommit' => config['PreCommit'].merge('AuthorName' => { 'enabled' => true })) + end + + context 'and extensions.worktreeConfig is not enabled' do + let(:worktree_config) { false } + + it 'reports a signature error in the worktree signed first' do + result = commit_in_main_worktree + result.status.should_not == 0 + result.stderr.should include 'Signature of configuration file has changed!' + end + end + + context 'and extensions.worktreeConfig is enabled' do + let(:worktree_config) { true } + + it 'does not report a signature error in either worktree' do + commit_in_main_worktree.status.should == 0 + commit_in_linked_worktree.status.should == 0 + end + end + end + + context 'when verify_signatures is false and only the main worktree is signed' do + let(:verify_signatures) { false } + let(:sign_linked_worktree) { false } + + context 'and extensions.worktreeConfig is not enabled' do + let(:worktree_config) { false } + + it 'shares the recorded verify_signatures setting with the linked worktree' do + commit_in_linked_worktree.status.should == 0 + end + end + + context 'and extensions.worktreeConfig is enabled' do + let(:worktree_config) { true } + + it 'requires the linked worktree to record its own verify_signatures setting' do + result = commit_in_linked_worktree + result.status.should_not == 0 + result.stderr.should include 'No previously recorded signature for configuration file.' + end + end + end +end diff --git a/spec/overcommit/git_config_spec.rb b/spec/overcommit/git_config_spec.rb index 9cc51862..8d5dbf28 100644 --- a/spec/overcommit/git_config_spec.rb +++ b/spec/overcommit/git_config_spec.rb @@ -93,4 +93,40 @@ end end end + + describe '.signature_scope' do + subject { described_class.signature_scope } + + context 'when extensions.worktreeConfig is not set' do + around do |example| + repo do + example.run + end + end + + it { should == '--local' } + end + + context 'when extensions.worktreeConfig is false' do + around do |example| + repo do + `git config --local extensions.worktreeConfig false` + example.run + end + end + + it { should == '--local' } + end + + context 'when extensions.worktreeConfig is true' do + around do |example| + repo do + `git config --local extensions.worktreeConfig true` + example.run + end + end + + it { should == '--worktree' } + end + end end From 63fbc963566b08dd74c7adb952f8c1d2283b7d7d Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 20 Oct 2026 03:11:01 -0400 Subject: [PATCH 2/3] Pin CI runners to ubuntu-24.04 ubuntu-latest now resolves to ubuntu-26.04, for which setup-ruby has no Ruby 2.6-3.1 builds ("Unavailable version 3.0.7 for ruby on ubuntu-26.04"), so those matrix entries fail before running specs. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01Wo4FerFK9aaxrGrmVE9ncZ --- .github/workflows/lint.yml | 2 +- .github/workflows/release-gem.yml | 2 +- .github/workflows/tests.yml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8cf8b4a0..9c27a07a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,7 +8,7 @@ on: jobs: overcommit: timeout-minutes: 10 - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/release-gem.yml b/.github/workflows/release-gem.yml index fee439eb..3cc8d1a4 100644 --- a/.github/workflows/release-gem.yml +++ b/.github/workflows/release-gem.yml @@ -7,7 +7,7 @@ on: jobs: release: timeout-minutes: 10 - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 environment: release permissions: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6688cd90..17b1db5d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,7 +8,7 @@ on: jobs: rspec: timeout-minutes: 15 - runs-on: ${{ matrix.os }}-latest + runs-on: ${{ matrix.os }}-24.04 strategy: fail-fast: false @@ -53,7 +53,7 @@ jobs: finish: needs: rspec - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Finalize code coverage report From 11f62b50334384222d1da45ce97c8fe1f5995341 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 21 Oct 2026 08:24:49 -0400 Subject: [PATCH 3/3] Run CI on ubuntu-26.04, old Rubies on 24.04 Name ubuntu-26.04 explicitly so runners do not move when ubuntu-latest does. setup-ruby has no Ruby 2.6-3.1 builds for ubuntu-26.04, so a matrix include keeps those rows on ubuntu-24.04. Non-ubuntu rows still use -latest. The explicit job name keeps check names as "rspec (, )", which upstream requires. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01Wo4FerFK9aaxrGrmVE9ncZ --- .github/workflows/lint.yml | 2 +- .github/workflows/release-gem.yml | 2 +- .github/workflows/tests.yml | 13 +++++++++++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9c27a07a..c53b7193 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,7 +8,7 @@ on: jobs: overcommit: timeout-minutes: 10 - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/release-gem.yml b/.github/workflows/release-gem.yml index 3cc8d1a4..cfdfa678 100644 --- a/.github/workflows/release-gem.yml +++ b/.github/workflows/release-gem.yml @@ -7,7 +7,7 @@ on: jobs: release: timeout-minutes: 10 - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 environment: release permissions: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 17b1db5d..734abca0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,8 +7,9 @@ on: jobs: rspec: + name: rspec (${{ matrix.ruby-version }}, ${{ matrix.os }}) timeout-minutes: 15 - runs-on: ${{ matrix.os }}-24.04 + runs-on: ${{ matrix.runner || format('{0}-latest', matrix.os) }} strategy: fail-fast: false @@ -28,6 +29,14 @@ jobs: # Any contributor is welcome to fix them and enable the Windows build. # Please see Issue #836 for more details. # - windows + include: + - os: ubuntu + runner: ubuntu-26.04 + # setup-ruby has no builds of these Rubies for ubuntu-26.04 + - { ruby-version: "2.6", os: ubuntu, runner: ubuntu-24.04 } + - { ruby-version: "2.7", os: ubuntu, runner: ubuntu-24.04 } + - { ruby-version: "3.0", os: ubuntu, runner: ubuntu-24.04 } + - { ruby-version: "3.1", os: ubuntu, runner: ubuntu-24.04 } steps: - uses: actions/checkout@v4 @@ -53,7 +62,7 @@ jobs: finish: needs: rspec - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - name: Finalize code coverage report