Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,4 @@

source 'https://rubygems.org'

# Specify your gem's dependencies in ruby_git.gemspec
gemspec
4 changes: 1 addition & 3 deletions lib/ruby_git.rb
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,7 @@ def self.clone(repository_url, to_path: '')
# or git was not found on the path.
#
def self.binary_version
command = %w[version]
options = { out: StringIO.new, err: StringIO.new }
version_string = RubyGit::CommandLine.run(*command, **options).stdout[/\d+\.\d+(\.\d+)+/]
version_string = RubyGit::CommandLine.run('version').stdout[/\d+\.\d+(\.\d+)+/]
version_string.split('.').collect(&:to_i)
end
end
25 changes: 11 additions & 14 deletions lib/ruby_git/command_line/options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,40 +9,37 @@ module CommandLine
#
# @api public
#
class Options < ProcessExecuter::Options::RunOptions
class Options < ProcessExecuter::Options::RunWithCaptureOptions
# Alias for brevity
OptionDefinition = ProcessExecuter::Options::OptionDefinition

private

# :nocov: SimpleCov on JRuby reports the last with the last argument line is not covered

# The options allowed for objects of this class
# @return [Array<OptionDefinition>]
# @api private
def define_options
[
*super,
OptionDefinition.new(:normalize_encoding, default: false, validator: method(:validate_normalize_encoding)),
OptionDefinition.new(:chomp, default: false, validator: method(:validate_chomp)),
OptionDefinition.new(:raise_git_errors, default: true, validator: method(:validate_raise_git_errors))
OptionDefinition.new(:chomp, default: false, validator: method(:validate_chomp))
].freeze
end
# :nocov:

# Validate the raise_git_errors option value
# @return [String, nil] the error message if the value is not valid
# Wrap ProcessExecuter::ArgumentError in a RubyGit::ArgumentError
# @return [void]
# @raise [RubyGit::ArgumentError] if the options are invalid
# @api private
def validate_raise_git_errors
return if [true, false].include?(raise_git_errors)

errors << "raise_git_errors must be true or false but was #{raise_git_errors.inspect}"
def validate_options
super
rescue ProcessExecuter::ArgumentError => e
raise RubyGit::ArgumentError, e.message, cause: e
end

# Validate the normalize_encoding option value
# @return [String, nil] the error message if the value is not valid
# @api private
def validate_normalize_encoding
def validate_normalize_encoding(_key, _value)
return if [true, false].include?(normalize_encoding)

errors << "normalize_encoding must be true or false but was #{normalize_encoding.inspect}"
Expand All @@ -51,7 +48,7 @@ def validate_normalize_encoding
# Validate the chomp option value
# @return [String, nil] the error message if the value is not valid
# @api private
def validate_chomp
def validate_chomp(_key, _value)
return if [true, false].include?(chomp)

errors << "chomp must be true or false but was #{chomp.inspect}"
Expand Down
57 changes: 39 additions & 18 deletions lib/ruby_git/command_line/result.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,30 @@ module RubyGit
module CommandLine
# The result of running a git command
#
# Adds stdout and stderr processing to the `ProcessExecuter::Result` class.
# Adds stdout and stderr processing to the `ProcessExecuter::ResultWithCapture` class.
#
# @api public
#
class Result < SimpleDelegator
# @!method initialize(result)
# Initialize a new result object
#
# @example
# result = ProcessExecuter.run('echo hello')
# result = Git::CommandLine.run_with_capture('git', 'status')
# RubyGit::CommandLine::Result.new(result)
# @param [ProcessExecuter::Result] result The result of running the command
#
# @param [ProcessExecuter::ResultWithCapture] result The result of running the command
#
# @return [RubyGit::CommandLine::Result]
#
# @api public

# Return the processed stdout output (or original if it was not processed)
#
# This output is only returned if a stdout redirection is a
# `ProcessExecuter::MonitoredPipe`.
#
# @example
# result = ProcessExecuter.run('echo hello': out: StringIO.new)
# result = RubyGit::CommandLine::Result.new(
# ProcessExecuter.run_with_capture('echo hello')
# )
# result.stdout #=> "hello\n"
#
# @return [String, nil]
Expand All @@ -39,20 +42,25 @@ def stdout
# Process the captured stdout output
#
# @example
# result = ProcessExecuter.run('echo hello', out: StringIO.new)
# result = RubyGit::CommandLine::Result.new(
# ProcessExecuter.run_with_capture('echo hello')
# )
# result.stdout #=> "hello\n"
# result.process_stdout { |stdout, _result| stdout.upcase }
# result.stdout #=> "HELLO\n"
# result.unprocessed_stdout #=> "hello\n"
#
# @example Chain processing
# result = ProcessExecuter.run('echo hello', out: StringIO.new)
# result = RubyGit::CommandLine::Result.new(
# ProcessExecuter.run_with_capture('echo hello')
# )
# result.stdout #=> "hello\n"
# # Here is the chain processing:
# result.process_stdout { |s| s.upcase }.process_stdout { |s| s.reverse }
# result.stdout #=> "OLLEH\n"
# result.unprocessed_stdout #=> "hello\n"
#
# @return [String, nil]
# @return [self]
#
# @yield [stdout, result] Yields the stdout output and the result object
# @yieldparam stdout [String] The output to process
Expand All @@ -62,16 +70,19 @@ def stdout
# @api public
#
def process_stdout(&block)
return if block.nil?
return self if block.nil?

@processed_stdout = block.call(stdout, self)

self
end

# Returns the original stdout output before it was processed
#
# @example
# result = ProcessExecuter.run('echo hello', out: StringIO.new)
# result = RubyGit::CommandLine::Result.new(
# ProcessExecuter.run_with_capture('echo hello')
# )
# result.stdout #=> "hello\n"
# result.unprocessed_stdout #=> "hello\n"
# result.process_stdout { |s| s.upcase }
Expand All @@ -92,7 +103,9 @@ def unprocessed_stdout
# `ProcessExecuter::MonitoredPipe`.
#
# @example
# result = ProcessExecuter.run('echo hello 1>&2': err: StringIO.new)
# result = RubyGit::CommandLine::Result.new(
# ProcessExecuter.run_with_capture('echo hello >&2')
# )
# result.stderr #=> "hello\n"
#
# @return [String, nil]
Expand All @@ -104,20 +117,25 @@ def stderr
# Process the captured stderr output
#
# @example
# result = ProcessExecuter.run('echo hello 1>&2', err: StringIO.new)
# result = RubyGit::CommandLine::Result.new(
# ProcessExecuter.run_with_capture('echo hello >&2')
# )
# result.stderr #=> "hello\n"
# result.process_stderr { |stderr, _result| stderr.upcase }
# result.stderr #=> "HELLO\n"
# result.unprocessed_stderr #=> "hello\n"
#
# @example Chain processing
# result = ProcessExecuter.run('echo hello 1>&2', err: StringIO.new)
# result = RubyGit::CommandLine::Result.new(
# ProcessExecuter.run_with_capture('echo hello >&2')
# )
# result.stderr #=> "hello\n"
# # Here is the chain processing:
# result.process_stderr { |s| s.upcase }.process_stderr { |s| s.reverse }
# result.stderr #=> "OLLEH\n"
# result.unprocessed_stderr #=> "hello\n"
#
# @return [String, nil]
# @return [self]
#
# @yield [stderr, result] Yields the stderr output and the result object
# @yieldparam stderr [String] The output to process
Expand All @@ -127,16 +145,19 @@ def stderr
# @api public
#
def process_stderr(&block)
return if block.nil?
return self if block.nil?

@processed_stderr = block.call(stderr, self)

self
end

# Returns the original stderr output before it was processed
#
# @example
# result = ProcessExecuter.run('echo hello 1>&2', err: StringIO.new)
# result = RubyGit::CommandLine::Result.new(
# ProcessExecuter.run_with_capture('echo hello >&2')
# )
# result.stderr #=> "hello\n"
# result.unprocessed_stderr #=> "hello\n"
# result.process_stderr { |stderr| stderr.upcase }
Expand Down
52 changes: 35 additions & 17 deletions lib/ruby_git/command_line/runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ def initialize(env, binary_path, global_options, logger)
#
# @return [RubyGit::CommandLine::Result] the result of the command
#
# @raise [ArgumentError] if `args` or `options_hash` are not valid
# @raise [RubyGit::ArgumentError] if `args` or `options_hash` are not valid
#
# @raise [RubyGit::FailedError] if the command returned a non-zero exitstatus
#
Expand All @@ -165,18 +165,27 @@ def initialize(env, binary_path, global_options, logger)
# @raise [RubyGit::ProcessIOError] if an exception was raised while collecting subprocess output
#
def call(*args, **options_hash)
options_hash[:raise_errors] = false
options = RubyGit::CommandLine::Options.new(logger: logger, **options_hash)
begin
result = run_with_chdir([env, *build_git_cmd(args)], options)
result = run(*args, options)
process_result(result, options)
end

private

# Run the git command with the given arguments and options
# @return [ProcessExecuter::ResultWithCapture] the result of the command
# @api private
def run(*args, options)
# We don't want ProcessExecuter to raise an error if the command fails, but
# we want to preserve the value of the raise_errors option to use in
# process_result.
options.merge(raise_errors: false).then do |options|
run_with_chdir([env, *build_git_cmd(args)], options)
rescue ProcessExecuter::ProcessIOError => e
raise RubyGit::ProcessIOError.new(e.message), cause: e.exception.cause
end
process_result(result)
end

private

# Run command with options with special handling for the `chdir` option on JRuby
#
# JRuby does not support the `chdir` option in `Process.spawn`. Note that this
Expand All @@ -185,7 +194,7 @@ def call(*args, **options_hash)
# @param args [Array<String>] the command to run
# @param options [RubyGit::CommandLine::Options] the options to pass to `Process.spawn`
#
# @return [ProcessExecuter::Result] the result of the command
# @return [ProcessExecuter::ResultWithCapture] the result of the command
#
# @api private
#
Expand All @@ -197,7 +206,9 @@ def run_with_chdir(args, options) # rubocop:disable Metrics/MethodLength
Dir.chdir(options.chdir) do
saved_chdir = options.chdir
options.merge!(chdir: :not_set)
run_and_handle_spawn_error(args, options).tap do
begin
run_and_handle_spawn_error(args, options)
ensure
options.merge!(chdir: saved_chdir)
end
end
Expand All @@ -212,14 +223,14 @@ def run_with_chdir(args, options) # rubocop:disable Metrics/MethodLength
# @param args [Array<String>] the command to run
# @param options [RubyGit::CommandLine::Options] the options to pass to `Process.spawn`
#
# @return [ProcessExecuter::Result] the result of the command
# @return [ProcessExecuter::ResultWithCapture] the result of the command
#
# @api private
#
def run_and_handle_spawn_error(args, options)
ProcessExecuter.run_with_options(args, options)
ProcessExecuter.run_with_capture(*args, options)
rescue ProcessExecuter::SpawnError => e
raise RubyGit::SpawnError, e.message
raise RubyGit::SpawnError, e.message, cause: e
end

# Returns true if running on JRuby
Expand All @@ -230,11 +241,15 @@ def run_and_handle_spawn_error(args, options)
def jruby? = RUBY_ENGINE == 'jruby'

# Build the git command line from the available sources to send to `Process.spawn`
#
# @raise [RubyGit::ArgumentError] if the args array contains an array
#
# @return [Array<String>]
#
# @api private
#
def build_git_cmd(args)
raise ArgumentError, 'The args array can not contain an array' if args.any? { |a| a.is_a?(Array) }
raise RubyGit::ArgumentError, 'The args array can not contain an array' if args.any? { |a| a.is_a?(Array) }

[binary_path, *global_options, *args].map(&:to_s)
end
Expand All @@ -250,18 +265,21 @@ def build_git_cmd(args)
# @return [RubyGit::CommandLineResult] the result of the command to return to the caller
#
# @raise [RubyGit::FailedError] if the command failed
#
# @raise [RubyGit::SignaledError] if the command was signaled
#
# @raise [RubyGit::TimeoutError] if the command times out
#
# @raise [RubyGit::ProcessIOError] if an exception was raised while collecting subprocess output
#
# @api private
#
def process_result(result)
def process_result(result, options)
RubyGit::CommandLine::Result.new(result).tap do |processed_result|
raise_any_errors(processed_result) if processed_result.options.raise_git_errors
raise_any_errors(processed_result) if options.raise_errors

processed_result.process_stdout { |s, r| process_output(s, r) }
processed_result.process_stderr { |s, r| process_output(s, r) }
processed_result.process_stdout { |output, result| process_output(output, result) }
processed_result.process_stderr { |output, result| process_output(output, result) }
end
end

Expand Down
15 changes: 15 additions & 0 deletions lib/ruby_git/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ module RubyGit
# ```text
# StandardError
# └─> RubyGit::Error
# ├─> RubyGit::ArgumentError
# ├─> RubyGit::CommandLineError
# │ ├─> RubyGit::FailedError
# │ └─> RubyGit::SignaledError
Expand All @@ -28,6 +29,7 @@ module RubyGit
# | Error Class | Description |
# | --- | --- |
# | `Error` | This catch-all error serves as the base class for other custom errors raised by the git gem. |
# | `ArgumentError` | Raised when an invalid argument is passed to a method. |
# | `CommandLineError` | A subclass of this error is raised when there is a problem executing the git command line. |
# | `FailedError` | This error is raised when the git command line exits with a non-zero status code that is not expected by the git gem. |
# | `SignaledError` | This error is raised when the git command line is terminated as a result of receiving a signal. This could happen if the process is forcibly terminated or if there is a serious system error. |
Expand Down Expand Up @@ -66,6 +68,19 @@ class Error < StandardError; end

# rubocop:enable Layout/LineLength

# Raised when an invalid argument is passed to a method
#
# @example Raising RubyGit::ArgumentError due to invalid option value
# begin
# RubyGit::CommandLine.run('status', timeout_after: 'not_a_number')
# rescue RubyGit::ArgumentError => e
# e.message #=> 'timeout_after must be nil or a non-negative real number but was "not_a_number"'
# end
#
# @api public
#
class ArgumentError < RubyGit::Error; end

# Raised when a git command fails or exits because of an uncaught signal
#
# The git command executed, status, stdout, and stderr are available from this
Expand Down
2 changes: 1 addition & 1 deletion ruby_git.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Gem::Specification.new do |spec|
spec.add_development_dependency 'yardstick', '~> 0.9'
end

spec.add_dependency 'process_executer', '~> 3.2'
spec.add_dependency 'process_executer', '~> 4.0'
spec.add_dependency 'rchardet', '~> 1.9'

spec.metadata['rubygems_mfa_required'] = 'true'
Expand Down
Loading