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

sds / overcommit / 23622090454

26 Mar 2026 10:56PM UTC coverage: 91.547% (+0.5%) from 91.09%
23622090454

Pull #876

github

web-flow
Merge 9581db11e into 11838c674
Pull Request #876: Use Etc.nprocessors and handle sandboxed environments

3 of 4 new or added lines in 1 file covered. (75.0%)

3249 of 3549 relevant lines covered (91.55%)

2266.52 hits per line

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

92.86
/lib/overcommit/utils.rb
1
# frozen_string_literal: true
2

3
require 'etc'
12✔
4
require 'pathname'
12✔
5
require 'overcommit/os'
12✔
6
require 'overcommit/subprocess'
12✔
7
require 'overcommit/command_splitter'
12✔
8
require 'tempfile'
12✔
9

10
module Overcommit
12✔
11
  # Utility functions for general use.
12
  module Utils
12✔
13
    # Helper class for doing quick constraint validations on version numbers.
14
    #
15
    # This allows us to execute code based on the git version.
16
    class Version < Gem::Version
12✔
17
      # Overload comparison operators so we can conveniently compare this
18
      # version directly to a string in code.
19
      %w[< <= > >= == !=].each do |operator|
12✔
20
        define_method operator do |version|
72✔
21
          case version
984✔
22
          when String
23
            super(Gem::Version.new(version))
984✔
24
          else
25
            super(version)
×
26
          end
27
        end
28
      end
29
    end
30

31
    class << self
12✔
32
      # @return [Overcommit::Logger] logger with which to send debug output
33
      attr_accessor :log
12✔
34

35
      def script_path(script)
12✔
36
        File.join(Overcommit::HOME, 'libexec', script)
72✔
37
      end
38

39
      # Returns an absolute path to the root of the repository.
40
      #
41
      # We do this ourselves rather than call `git rev-parse --show-toplevel` to
42
      # solve an issue where the .git directory might not actually be valid in
43
      # tests.
44
      #
45
      # @return [String]
46
      def repo_root
12✔
47
        @repo_root ||=
1,488✔
48
          begin
49
            result = execute(%w[git rev-parse --show-toplevel])
576✔
50
            unless result.success?
576✔
51
              raise Overcommit::Exceptions::InvalidGitRepo,
12✔
52
                    'Unable to determine location of GIT_DIR. ' \
53
                    'Not a recognizable Git repository!'
54
            end
55
            result.stdout.chomp("\n")
564✔
56
          end
57
      end
58

59
      # Returns an absolute path to the .git directory for a repo.
60
      #
61
      # @return [String]
62
      def git_dir
12✔
63
        @git_dir ||=
864✔
64
          begin
65
            cmd = %w[git rev-parse]
864✔
66
            cmd << (GIT_VERSION < '2.5' ? '--git-dir' : '--git-common-dir')
864✔
67
            result = execute(cmd)
864✔
68
            unless result.success?
864✔
69
              raise Overcommit::Exceptions::InvalidGitRepo,
×
70
                    'Unable to determine location of GIT_DIR. ' \
71
                    'Not a recognizable Git repository!'
72
            end
73
            File.expand_path(result.stdout.chomp("\n"), Dir.pwd)
864✔
74
          end
75
      end
76

77
      # Remove ANSI escape sequences from a string.
78
      #
79
      # This is useful for stripping colorized output from external tools.
80
      #
81
      # @param text [String]
82
      # @return [String]
83
      def strip_color_codes(text)
12✔
84
        text.gsub(/\e\[(\d+)(;\d+)*m/, '')
60✔
85
      end
86

87
      # Shamelessly stolen from:
88
      # stackoverflow.com/questions/1509915/converting-camel-case-to-underscore-case-in-ruby
89
      def snake_case(str)
12✔
90
        str.gsub(/::/, '/').
6,804✔
91
            gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').
92
            gsub(/([a-z\d])([A-Z])/, '\1_\2').
93
            tr('-', '_').
94
            downcase
95
      end
96

97
      # Converts a string containing underscores/hyphens/spaces into CamelCase.
98
      def camel_case(str)
12✔
99
        str.split(/_|-| /).map { |part| part.sub(/^\w/, &:upcase) }.join
7,116✔
100
      end
101

102
      # Returns a list of supported hook types (pre-commit, commit-msg, etc.)
103
      def supported_hook_types
12✔
104
        Dir[File.join(HOOK_DIRECTORY, '*')].
13,704✔
105
          select { |file| File.directory?(file) }.
150,744✔
106
          reject { |file| File.basename(file) == 'shared' }.
137,040✔
107
          map { |file| File.basename(file).tr('_', '-') }
123,336✔
108
      end
109

110
      # Returns a list of supported hook classes (PreCommit, CommitMsg, etc.)
111
      def supported_hook_type_classes
12✔
112
        supported_hook_types.map do |file|
13,008✔
113
          file.split('-').map(&:capitalize).join
117,072✔
114
        end
115
      end
116

117
      # @param cmd [String]
118
      # @return [true,false] whether a command can be found given the current
119
      #   environment path.
120
      def in_path?(cmd)
12✔
121
        # ENV['PATH'] doesn't include the repo root, but that is a valid
122
        # location for executables, so we want to add it to the list of places
123
        # we are checking for the executable.
124
        paths = [repo_root] + ENV['PATH'].split(File::PATH_SEPARATOR)
12✔
125
        exts  = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
12✔
126
        paths.each do |path|
12✔
127
          exts.each do |ext|
168✔
128
            cmd_with_ext = cmd.upcase.end_with?(ext.upcase) ? cmd : "#{cmd}#{ext}"
168✔
129
            full_path = File.join(path, cmd_with_ext)
168✔
130
            return true if File.executable?(full_path)
168✔
131
          end
132
        end
133
        false
×
134
      end
135

136
      # Return the parent command that triggered this hook run
137
      #
138
      # @return [String,nil] the command as a string, if a parent exists.
139
      def parent_command
12✔
140
        # When run in Docker containers, there may be no parent process.
141
        return if Process.ppid.zero?
624✔
142

143
        if OS.windows?
612✔
144
          `wmic process where ProcessId=#{Process.ppid} get CommandLine /FORMAT:VALUE`.
×
145
            strip.
146
            slice(/(?<=CommandLine=).+/)
147
        elsif OS.cygwin?
612✔
148
          # Cygwin's `ps` command behaves differently than the traditional
149
          # Linux version, but a comparable `procps` is provided to compensate.
150
          `procps -ocommand= -p #{Process.ppid}`.chomp
×
151
        else
152
          `ps -ocommand= -p #{Process.ppid}`.chomp
612✔
153
        end
154
      rescue Errno::EPERM, Errno::ENOENT
155
        # Process information may not be available, such as inside sandboxed environments
NEW
156
        nil
×
157
      end
158

159
      # Execute a command in a subprocess, capturing exit status and output from
160
      # both standard and error streams.
161
      #
162
      # This is intended to provide a centralized place to perform any checks or
163
      # filtering of the command before executing it.
164
      #
165
      # The `args` option provides a convenient way of splitting up long
166
      # argument lists which would otherwise exceed the maximum command line
167
      # length of the OS. It will break up the list into chunks and run the
168
      # command with the same prefix `initial_args`, finally combining the
169
      # output together at the end.
170
      #
171
      # This requires that the external command you are running can have its
172
      # work split up in this way and still produce the same resultant output
173
      # when outputs of the individual commands are concatenated back together.
174
      #
175
      # @param initial_args [Array<String>]
176
      # @param options [Hash]
177
      # @option options [Array<String>] :args long list of arguments to split up
178
      # @return [Overcommit::Subprocess::Result] status, stdout, and stderr
179
      def execute(initial_args, options = {})
12✔
180
        if initial_args.include?('|')
10,548✔
181
          raise Overcommit::Exceptions::InvalidCommandArgs,
12✔
182
                'Cannot pipe commands with the `execute` helper'
183
        end
184

185
        result =
186
          if (splittable_args = options.fetch(:args) { [] }).any?
20,604✔
187
            debug(initial_args.join(' ') + " ... (#{splittable_args.length} splittable args)")
456✔
188
            Overcommit::CommandSplitter.execute(initial_args, options)
456✔
189
          else
190
            debug(initial_args.join(' '))
10,080✔
191
            Overcommit::Subprocess.spawn(initial_args, options)
10,080✔
192
          end
193

194
        debug("EXIT STATUS: #{result.status}")
10,536✔
195
        debug("STDOUT: #{result.stdout.inspect}")
10,536✔
196
        debug("STDERR: #{result.stderr.inspect}")
10,536✔
197

198
        result
10,536✔
199
      end
200

201
      # Execute a command in a subprocess, returning immediately.
202
      #
203
      # This provides a convenient way to execute long-running processes for
204
      # which we do not need to know the result.
205
      #
206
      # @param args [Array<String>]
207
      # @return [ChildProcess] detached process spawned in the background
208
      def execute_in_background(args)
12✔
209
        if args.include?('|')
12✔
210
          raise Overcommit::Exceptions::InvalidCommandArgs,
×
211
                'Cannot pipe commands with the `execute_in_background` helper'
212
        end
213

214
        debug("Spawning background task: #{args.join(' ')}")
12✔
215
        Subprocess.spawn_detached(args)
12✔
216
      end
217

218
      # Return the number of processors used by the OS for process scheduling.
219
      def processor_count
12✔
220
        @processor_count ||= Etc.nprocessors
3,120✔
221
      end
222

223
      # Calls a block of code with a modified set of environment variables,
224
      # restoring them once the code has executed.
225
      def with_environment(env)
12✔
226
        old_env = {}
20,376✔
227
        env.each do |var, value|
20,376✔
228
          old_env[var] = ENV[var.to_s]
20,148✔
229
          ENV[var.to_s] = value
20,148✔
230
        end
231

232
        yield
20,376✔
233
      ensure
234
        old_env.each { |var, value| ENV[var.to_s] = value }
40,524✔
235
      end
236

237
      # Returns whether a file is a broken symlink.
238
      #
239
      # @return [true,false]
240
      def broken_symlink?(file)
12✔
241
        # JRuby's implementation of File.exist? returns true for broken
242
        # symlinks, so we need use File.size?
243
        Overcommit::Utils::FileUtils.symlink?(file) && File.size?(file).nil?
7,416✔
244
      end
245

246
      # Convert a glob pattern to an absolute path glob pattern rooted from the
247
      # repository root directory.
248
      #
249
      # @param glob [String]
250
      # @return [String]
251
      def convert_glob_to_absolute(glob)
12✔
252
        File.join(repo_root, glob)
60✔
253
      end
254

255
      # Return whether a pattern matches the given path.
256
      #
257
      # @param pattern [String]
258
      # @param path [String]
259
      def matches_path?(pattern, path)
12✔
260
        File.fnmatch?(
24✔
261
          pattern, path,
262
          File::FNM_PATHNAME | # Wildcard doesn't match separator
263
          File::FNM_DOTMATCH   # Wildcards match dotfiles
264
        )
265
      end
266

267
      private
12✔
268

269
      # Log debug output.
270
      #
271
      # This is necessary since some specs indirectly call utility functions but
272
      # don't explicitly set the logger for the Utils class, so we do a quick
273
      # check here to see if it's set before we attempt to log.
274
      #
275
      # @param args [Array<String>]
276
      def debug(*args)
12✔
277
        log&.debug(*args)
42,156✔
278
      end
279
    end
280
  end
281
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