From 0d3d6d4c19604e541582bb0cc74ac70aa776dd26 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 20 Oct 2026 02:58:25 -0400 Subject: [PATCH] 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