• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

sds / overcommit / 42864725008

10 Nov 2026 05:24PM UTC coverage: 93.251% (-0.3%) from 93.508%
42864725008

Pull #931

github

web-flow
Merge 04a1e8514 into 66494580a
Pull Request #931: Support gem hook plugins with explicit sources and signature approvals

122 of 143 new or added lines in 10 files covered. (85.31%)

1 existing line in 1 file now uncovered.

3565 of 3823 relevant lines covered (93.25%)

2720.45 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

52.56
/lib/overcommit/hook_loader/plugin_hook_loader.rb
1
# frozen_string_literal: true
2

3
require 'digest'
84 ✔
4

5
module Overcommit::HookLoader
84 ✔
6
  # Responsible for loading hooks that are specific to the repository Overcommit
7
  # is running in.
8
  class PluginHookLoader < Base # rubocop:disable Metrics/ClassLength
84 ✔
9
    def load_hooks
84 ✔
10
      check_for_unverified_plugins if @config.verify_signatures?
294 ✔
11

12
      hooks = load_gem_hooks + plugin_paths.map do |plugin_path|
294 ✔
13
        require plugin_path
×
14

15
        hook_name = Overcommit::Utils.camel_case(File.basename(plugin_path, '.rb'))
×
16
        create_hook(hook_name)
×
17
      end
18

19
      hooks + ad_hoc_hook_names.map do |hook_name|
294 ✔
20
        create_ad_hoc_hook(hook_name)
282 ✔
21
      end
22
    end
23

24
    def update_signatures
84 ✔
25
      log.success('All plugin signatures have been verified') if unverified_plugins.empty?
×
26

27
      unverified_plugins.each do |plugin|
×
28
        plugin.update_signature!
×
29
        log.warning "Updated signature of plugin #{plugin.hook_name}"
×
30
      end
31
    end
32

33
    private
84 ✔
34

35
    def gem_plugins
84 ✔
36
      @gem_plugins ||= @config.enabled_gem_hooks(@context).
294 ✔
NEW
37
        map { |name| name.split('::').first }.uniq.sort.map do |namespace|
×
NEW
38
          Overcommit::GemPlugin.new(namespace, @config, @context)
×
39
        end
40
    end
41

42
    def load_gem_hooks
84 ✔
43
      sources = gem_plugins.map { |plugin| [plugin, plugin.hook_paths] }
294 ✔
44
      sources.flat_map do |plugin, paths|
294 ✔
NEW
45
        hook_module = Overcommit::Hook.const_get(@context.hook_class_name)
×
NEW
46
        if hook_module.const_defined?(plugin.namespace, false)
×
NEW
47
          namespace = hook_module.const_get(plugin.namespace, false)
×
NEW
48
          unless namespace.instance_of?(Module)
×
NEW
49
            raise Overcommit::Exceptions::HookLoadError,
×
50
                  "Gem namespace #{plugin.namespace} conflicts with an existing hook"
51
          end
52
        else
NEW
53
          hook_module.const_set(plugin.namespace, Module.new)
×
54
        end
NEW
55
        plugin.activate
×
NEW
56
        paths.zip(plugin.hook_names).map do |path, name|
×
NEW
57
          require path
×
NEW
58
          create_hook(name)
×
59
        end
60
      end
61
    end
62

63
    def plugin_paths
84 ✔
64
      directory = File.join(@config.plugin_directory, @context.hook_type_name)
294 ✔
65
      Dir[File.join(directory, '*.rb')].sort
294 ✔
66
    end
67

68
    def plugin_hook_names
84 ✔
69
      plugin_paths.map do |path|
×
70
        Overcommit::Utils.camel_case(File.basename(path, '.rb'))
×
71
      end
72
    end
73

74
    def ad_hoc_hook_names
84 ✔
75
      @config.enabled_ad_hoc_hooks(@context)
294 ✔
76
    end
77

78
    def unverified_plugins
84 ✔
NEW
79
      signers = (plugin_hook_names + ad_hoc_hook_names).
×
NEW
80
        map { |hook_name| Overcommit::HookSigner.new(hook_name, @config, @context) }
×
NEW
81
      (gem_plugins + signers).reject(&:signature_verified?)
×
82
    end
83

84
    def check_for_unverified_plugins
84 ✔
85
      return if unverified_plugins.empty?
×
86

87
      log.bold_warning "The following #{@context.hook_script_name} plugins " \
×
88
                       'have been added, changed, or had their configuration modified:'
89
      log.newline
×
90

91
      unverified_plugins.each do |signer|
×
92
        log.warning " * #{signer.hook_name} in #{signer.hook_path}"
×
93
      end
94

95
      log.newline
×
96
      log.bold_warning 'You should verify the changes and then run:'
×
97
      log.newline
×
98
      log.warning "overcommit --sign #{@context.hook_script_name}"
×
99
      log.newline
×
100
      log.log "For more information, see #{Overcommit::REPO_URL}#security"
×
101

102
      raise Overcommit::Exceptions::InvalidHookSignature
×
103
    end
104

105
    def create_git_hook_class(hook_base)
84 ✔
106
      # Implement a simple class that executes the command and returns pass/fail
107
      # based on the exit status
108
      Class.new(hook_base) do
18 ✔
109
        def run
18 ✔
110
          result = @context.execute_hook(command)
12 ✔
111
          if result.success?
12 ✔
112
            :pass
6 ✔
113
          else
114
            [:fail, result.stdout + result.stderr]
6 ✔
115
          end
116
        end
117
      end
118
    end
119

120
    def create_ad_hoc_hook(hook_name)
84 ✔
121
      hook_module = Overcommit::Hook.const_get(@context.hook_class_name)
282 ✔
122
      hook_base = hook_module.const_get('Base')
282 ✔
123

124
      hook_config = @config.for_hook(hook_name, @context.hook_class_name)
282 ✔
125
      hook_class =
126
        if hook_config['message_pattern']
282 ✔
127
          validate_line_aware_config(hook_config)
264 ✔
128
          create_line_aware_command_hook_class(hook_base)
246 ✔
129
        else
130
          create_git_hook_class(hook_base)
18 ✔
131
        end
132

133
      # A later load may configure the same hook to use a different command format.
134
      if hook_module.const_defined?(hook_name, false)
264 ✔
135
        hook_module.send(:remove_const, hook_name)
6 ✔
136
      end
137

138
      hook_module.const_set(hook_name, hook_class).new(@config, @context)
264 ✔
139
    rescue LoadError, NameError => e
140
      raise Overcommit::Exceptions::HookLoadError,
×
141
            "Unable to load hook '#{hook_name}': #{e}",
142
            e.backtrace
143
    end
144
  end
145
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc