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: 1 addition & 0 deletions lib/ruby_git.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require_relative 'ruby_git/command_line'
require_relative 'ruby_git/encoding_normalizer'
require_relative 'ruby_git/errors'
require_relative 'ruby_git/option_validators'
require_relative 'ruby_git/repository'
require_relative 'ruby_git/status'
require_relative 'ruby_git/version'
Expand Down
33 changes: 26 additions & 7 deletions lib/ruby_git/command_line/runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -187,20 +187,39 @@ def call(*args, **options_hash)
#
# @api private
#
def run_with_chdir(args, options)
return ProcessExecuter.run_with_options(args, options) unless jruby? && options.chdir != :not_set
def run_with_chdir(args, options) # rubocop:disable Metrics/MethodLength
return run_and_handle_spawn_error(args, options) unless jruby? && options.chdir != :not_set

# :nocov: Not executed in MRI Ruby
Dir.chdir(options.chdir) do
saved_chdir = options.chdir
options.merge!(chdir: :not_set)
ProcessExecuter.run_with_options(args, options).tap do
options.merge!(chdir: saved_chdir)
begin
Dir.chdir(options.chdir) do
saved_chdir = options.chdir
options.merge!(chdir: :not_set)
run_and_handle_spawn_error(args, options).tap do
options.merge!(chdir: saved_chdir)
end
end
rescue Errno::ENOENT, Errno::ENOTDIR => e
raise RubyGit::SpawnError, "chdir(#{options.chdir}) failed: #{e.message}"
end
# :nocov:
end

# Catch ProcessExecuter::SpawnError and raise a RubyGit::SpawnError in its place
#
# @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
#
# @api private
#
def run_and_handle_spawn_error(args, options)
ProcessExecuter.run_with_options(args, options)
rescue ProcessExecuter::SpawnError => e
raise RubyGit::SpawnError, e.message
end

# Returns true if running on JRuby
#
# @return [Boolean]
Expand Down
8 changes: 8 additions & 0 deletions lib/ruby_git/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ module RubyGit
# │ └─> RubyGit::SignaledError
# │ └─> RubyGit::TimeoutError
# ├─> RubyGit::ProcessIOError
# ├─> RubyGit::SpawnError
# └─> RubyGit::UnexpectedResultError
# ```
#
Expand All @@ -32,6 +33,7 @@ module RubyGit
# | `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. |
# | `TimeoutError` | This is a specific type of `SignaledError` that is raised when the git command line operation times out and is killed via the SIGKILL signal. This happens if the operation takes longer than the timeout duration configured in `Git.config.timeout` or via the `:timeout` parameter given in git methods that support timeouts. |
# | `ProcessIOError` | An error was encountered reading or writing to a subprocess. |
# | `SpawnError` | An error was encountered when spawning a subprocess and it never started. |
# | `UnexpectedResultError` | The command line ran without error but did not return the expected results. |
#
# @example Rescuing a generic error
Expand Down Expand Up @@ -166,4 +168,10 @@ class ProcessIOError < RubyGit::Error; end
# @api public
#
class UnexpectedResultError < RubyGit::Error; end

# Raised when the git command could not be spawned
#
# @api public
#
class SpawnError < RubyGit::Error; end
end
37 changes: 37 additions & 0 deletions lib/ruby_git/option_validators.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# frozen_string_literal: true

module RubyGit
# Module containing option validators for RubyGit
# @api public
module OptionValidators
# Raise an error if an option is not a Boolean (or optionally nil) value
# @param name [String] the name of the option
# @param value [Object] the value of the option
# @param nullable [Boolean] whether the option can be nil (default is false)
# @return [void]
# @raise [ArgumentError] if the option is not a Boolean (or optionally nil) value
# @api private
def validate_boolean_option(name:, value:, nullable: false)
return if nullable && value.nil?

return if [true, false].include?(value)

raise ArgumentError, "The '#{name}:' option must be a Boolean value but was #{value.inspect}"
end

# Raise an error if an option is not a String (or optionally nil) value
# @param name [String] the name of the option
# @param value [Object] the value of the option
# @param nullable [Boolean] whether the option can be nil (default is false)
# @return [void]
# @raise [ArgumentError] if the option is not a String (or optionally nil) value
# @api private
def validate_string_option(name:, value:, nullable: false)
return if nullable && value.nil?

return if value.is_a?(String)

raise ArgumentError, "The '#{name}:' option must be a String or nil but was #{value.inspect}"
end
end
end
31 changes: 12 additions & 19 deletions lib/ruby_git/worktree.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ module RubyGit
# Create a new Worktree using {.init}, {.clone}, or {.open}.
#
class Worktree
extend RubyGit::OptionValidators
include RubyGit::OptionValidators

# The root path of the working tree
#
# @example
Expand All @@ -28,17 +31,22 @@ class Worktree
# @example
# worktree = Worktree.init(worktree_path)
#
# @param [String] worktree_path the root path of a Git working tree
# @param worktree_path [String] the root path of a Git working tree
# @param initial_branch [String] the initial branch in the newly created repository
#
# @raise [RubyGit::Error] if worktree_path is not a directory
# @raise [ArgumentError] if worktree_path does not exist or is not a directory
# @raise [RubyGit::Error] if there is an error initializing the repository
#
# @return [RubyGit::Worktree] the working tree whose root is at `path`
#
def self.init(worktree_path)
raise RubyGit::Error, "Path '#{worktree_path}' not valid." unless File.directory?(worktree_path)
def self.init(worktree_path, initial_branch: nil)
validate_string_option(name: :initial_branch, value: initial_branch, nullable: true)

command = ['init']
command << '--initial-branch' << initial_branch unless initial_branch.nil?

options = { chdir: worktree_path, out: StringIO.new, err: StringIO.new }

RubyGit::CommandLine.run(*command, **options)

new(worktree_path)
Expand Down Expand Up @@ -300,20 +308,5 @@ def root_path(worktree_path)
def run_with_context(*command, **options)
RubyGit::CommandLine.run(*command, repository_path: repository.path, worktree_path: path, **options)
end

# Raise an error if an option is not a Boolean (or optionally nil) value
# @param name [String] the name of the option
# @param value [Object] the value of the option
# @param nullable [Boolean] whether the option can be nil (default is false)
# @return [void]
# @raise [ArgumentError] if the option is not a Boolean (or optionally nil) value
# @api private
def validate_boolean_option(name:, value:, nullable: false)
return if nullable && value.nil?

return if [true, false].include?(value)

raise ArgumentError, "The '#{name}:' option must be a Boolean value but was #{value.inspect}"
end
end
end
133 changes: 103 additions & 30 deletions spec/lib/ruby_git/worktree_init_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,119 @@
require 'tmpdir'

RSpec.describe RubyGit::Worktree do
describe '.init(worktree_path)' do
subject { described_class.init(worktree_path) }
let(:tmpdir) { Dir.mktmpdir }
after { FileUtils.rm_rf(tmpdir) }

context 'when worktree_path does not exist' do
let(:worktree_path) { tmpdir }
before { FileUtils.rmdir(tmpdir) }
it 'should raise a RubyGit::Error' do
expect { subject }.to raise_error(RubyGit::Error)
end
end
describe '.init' do
subject { described_class.init(worktree_path, initial_branch: initial_branch) }
let(:initial_branch) { nil }

describe 'initializing a worktree' do
let(:tmpdir) { @tmpdir }

context 'when worktree_path exists' do
let(:worktree_path) { tmpdir }
context 'and is not a directory' do
before do
FileUtils.rmdir(worktree_path)
FileUtils.touch(worktree_path)
around do |example|
in_temp_dir do |tmpdir|
@tmpdir = tmpdir
example.run
end
it 'should raise RubyGit::Error' do
expect { subject }.to raise_error(RubyGit::Error)
end

context 'when worktree_path does not exist' do
let(:worktree_path) { File.join(tmpdir, 'subdir') }

expected_error = truffleruby? ? RubyGit::FailedError : RubyGit::SpawnError

it "should raise an error #{expected_error}" do
expect { subject }.to raise_error(expected_error)
end
end

context 'and is a directory' do
context 'and is in a working tree' do
context 'when worktree_path exists' do
let(:worktree_path) { tmpdir }
context 'and is not a directory' do
before do
raise RuntimeError unless system('git init', chdir: worktree_path, %i[out err] => IO::NULL)
FileUtils.rmdir(worktree_path)
FileUtils.touch(worktree_path)
end
it 'should return a Worktree object to the existing working tree' do
expect(subject).to be_kind_of(RubyGit::Worktree)
expect(subject).to have_attributes(path: File.realpath(worktree_path))

expected_error = truffleruby? ? RubyGit::FailedError : RubyGit::SpawnError

it "should raise a #{expected_error} " do
expect { subject }.to raise_error(expected_error)
end
end

context 'and is not in the working tree' do
it 'should initialize the working tree and return a Worktree object' do
expect(subject).to be_kind_of(RubyGit::Worktree)
expect(subject).to have_attributes(path: File.realpath(worktree_path))
context 'and is a directory' do
context 'and is in a working tree' do
before do
raise RuntimeError unless system('git init', chdir: worktree_path, %i[out err] => IO::NULL)
end
it 'should return a Worktree object to the existing working tree' do
expect(subject).to be_kind_of(RubyGit::Worktree)
expect(subject).to have_attributes(path: File.realpath(worktree_path))
end
end

context 'and is not in the working tree' do
it 'should initialize the working tree and return a Worktree object' do
expect(subject).to be_kind_of(RubyGit::Worktree)
expect(subject).to have_attributes(path: File.realpath(worktree_path))
end
end
end
end
end

describe 'constructing the git command line' do
let(:worktree_path) { '/my/worktree/path' }
let(:result) { instance_double(RubyGit::CommandLine::Result, stdout: '') }

before do
allow(described_class).to(
receive(:new).with(worktree_path).and_return(instance_double(RubyGit::Worktree))
)
end

context 'called with no arguments' do
let(:expected_command) { %w[init] }

it 'should run the expected git command' do
expect(RubyGit::CommandLine).to(
receive(:run).with(*expected_command, Hash).and_return(result)
)

subject
end
end

describe 'initial_branch option' do
context 'when nil' do
let(:expected_command) { %w[init] }

it 'should run the expected git command' do
expect(RubyGit::CommandLine).to(
receive(:run).with(*expected_command, Hash).and_return(result)
)
subject
end
end

context 'when a string' do
let(:expected_command) { ['init', '--initial-branch', initial_branch] }
let(:initial_branch) { 'my-branch' }

it 'should run the expected git command' do
expect(RubyGit::CommandLine).to(
receive(:run).with(*expected_command, Hash).and_return(result)
)
subject
end
end

context 'when not a string' do
let(:initial_branch) { 123 }

it 'should raise an ArgumentError' do
expect { subject }.to(
raise_error(ArgumentError, %(The 'initial_branch:' option must be a String or nil but was 123))
)
end
end
end
Expand Down