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