From 74ff794597858d0f047b46878f687fc7dd9b35ea Mon Sep 17 00:00:00 2001 From: James Couball Date: Mon, 5 Feb 2024 08:14:12 -0800 Subject: [PATCH 1/2] Refactor the Error heriarchy Signed-off-by: James Couball --- README.md | 64 ++++++++++++++++++++++++-- lib/git.rb | 1 + lib/git/command_line_error.rb | 59 ++++++++++++++++++++++++ lib/git/error.rb | 7 +++ lib/git/failed_error.rb | 45 ++---------------- lib/git/git_execute_error.rb | 9 +++- lib/git/signaled_error.rb | 42 ++--------------- lib/git/timeout_error.rb | 60 ++++++++++++++++++++++++ tests/units/test_command_line_error.rb | 23 +++++++++ tests/units/test_failed_error.rb | 9 ++-- tests/units/test_signaled_error.rb | 9 ++-- tests/units/test_timeout_error.rb | 24 ++++++++++ 12 files changed, 256 insertions(+), 96 deletions(-) create mode 100644 lib/git/command_line_error.rb create mode 100644 lib/git/error.rb create mode 100644 lib/git/timeout_error.rb create mode 100644 tests/units/test_command_line_error.rb create mode 100644 tests/units/test_timeout_error.rb diff --git a/README.md b/README.md index f0c42db7..78d042c2 100644 --- a/README.md +++ b/README.md @@ -90,11 +90,65 @@ Pass the `--all` option to `git log` as follows: **Git::Worktrees** - Enumerable object that holds `Git::Worktree objects`. +## Errors Raised By This Gem + +This gem raises custom errors that derive from `Git::Error`. These errors are +arranged in the following class heirarchy: + +Error heirarchy: + +```text +Error +└── CommandLineError + ├── FailedError + └── SignaledError + └── TimeoutError +``` + +Other standard errors may also be raised like `ArgumentError`. Each method should +document the errors it may raise. + +Description of each Error class: + +* `Error`: This catch-all error serves as the base class for other custom errors in this + gem. Errors of this class are raised when no more approriate specific error to + raise. +* `CommandLineError`: This error is raised when there's a problem executing the git + command line. This gem will raise a more specific error depending on how the + command line failed. +* `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. +* `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 this parameter. + +`Git::GitExecuteError` remains as an alias for `Git::Error`. It is considered +deprecated as of git-2.0.0. + +Here is an example of catching errors when using the git gem: + +```ruby +begin + timeout_duration = 0.001 # seconds + repo = Git.clone('https://github.com/ruby-git/ruby-git', 'ruby-git-temp', timeout: timeout_duration) +rescue Git::TimeoutError => e # Catch the more specific error first! + puts "Git clone took too long and timed out #{e}" +rescue Git::Error => e + puts "Received the following error: #{e}" +end +``` + ## Examples Here are a bunch of examples of how to use the Ruby/Git package. Require the 'git' gem. + ```ruby require 'git' ``` @@ -261,11 +315,11 @@ g.add(:all=>true) # git add --all -- "." g.add('file_path') # git add -- "file_path" g.add(['file_path_1', 'file_path_2']) # git add -- "file_path_1" "file_path_2" -g.remove() # git rm -f -- "." -g.remove('file.txt') # git rm -f -- "file.txt" -g.remove(['file.txt', 'file2.txt']) # git rm -f -- "file.txt" "file2.txt" -g.remove('file.txt', :recursive => true) # git rm -f -r -- "file.txt" -g.remove('file.txt', :cached => true) # git rm -f --cached -- "file.txt" +g.remove() # git rm -f -- "." +g.remove('file.txt') # git rm -f -- "file.txt" +g.remove(['file.txt', 'file2.txt']) # git rm -f -- "file.txt" "file2.txt" +g.remove('file.txt', :recursive => true) # git rm -f -r -- "file.txt" +g.remove('file.txt', :cached => true) # git rm -f --cached -- "file.txt" g.commit('message') g.commit_all('message') diff --git a/lib/git.rb b/lib/git.rb index f4825206..20519fca 100644 --- a/lib/git.rb +++ b/lib/git.rb @@ -27,6 +27,7 @@ require 'git/signaled_error' require 'git/stash' require 'git/stashes' +require 'git/timeout_error' require 'git/url' require 'git/version' require 'git/working_directory' diff --git a/lib/git/command_line_error.rb b/lib/git/command_line_error.rb new file mode 100644 index 00000000..269ef3cd --- /dev/null +++ b/lib/git/command_line_error.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require_relative 'error' + +module Git + # 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 + # object. + # + # Rather than creating a CommandLineError object directly, it is recommended to use + # one of the derived classes for the appropriate type of error: + # + # * {Git::FailedError}: when the git command exits with a non-zero status + # * {Git::SignaledError}: when the git command exits because of an uncaught signal + # * {Git::TimeoutError}: when the git command times out + # + # @api public + # + class CommandLineError < Git::Error + # Create a CommandLineError object + # + # @example + # `exit 1` # set $? appropriately for this example + # result = Git::CommandLineResult.new(%w[git status], $?, 'stdout', 'stderr') + # error = Git::CommandLineError.new(result) + # error.to_s #=> '["git", "status"], status: pid 89784 exit 1, stderr: "stderr"' + # + # @param result [Git::CommandLineResult] the result of the git command including + # the git command, status, stdout, and stderr + # + def initialize(result) + @result = result + super() + end + + # The human readable representation of this error + # + # @example + # error.to_s #=> '["git", "status"], status: pid 89784 exit 1, stderr: "stderr"' + # + # @return [String] + # + def to_s = <<~MESSAGE.chomp + #{result.git_cmd}, status: #{result.status}, stderr: #{result.stderr.inspect} + MESSAGE + + # @attribute [r] result + # + # The result of the git command including the git command and its status and output + # + # @example + # error.result #=> # + # + # @return [Git::CommandLineResult] + # + attr_reader :result + end +end diff --git a/lib/git/error.rb b/lib/git/error.rb new file mode 100644 index 00000000..1b2e44be --- /dev/null +++ b/lib/git/error.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Git + # Base class for all custom git module errors + # + class Error < StandardError; end +end \ No newline at end of file diff --git a/lib/git/failed_error.rb b/lib/git/failed_error.rb index 75973f6f..5c6e1f62 100644 --- a/lib/git/failed_error.rb +++ b/lib/git/failed_error.rb @@ -1,51 +1,14 @@ # frozen_string_literal: true -require 'git/git_execute_error' +require_relative 'command_line_error' module Git - # This error is raised when a git command fails + # This error is raised when a git command returns a non-zero exitstatus # # The git command executed, status, stdout, and stderr are available from this - # object. The #message includes the git command, the status of the process, and - # the stderr of the process. + # object. # # @api public # - class FailedError < Git::GitExecuteError - # Create a FailedError object - # - # @example - # `exit 1` # set $? appropriately for this example - # result = Git::CommandLineResult.new(%w[git status], $?, 'stdout', 'stderr') - # error = Git::FailedError.new(result) - # error.message #=> - # "[\"git\", \"status\"]\nstatus: pid 89784 exit 1\nstderr: \"stderr\"" - # - # @param result [Git::CommandLineResult] the result of the git command including - # the git command, status, stdout, and stderr - # - def initialize(result) - super("#{result.git_cmd}\nstatus: #{result.status}\nstderr: #{result.stderr.inspect}") - @result = result - end - - # @attribute [r] result - # - # The result of the git command including the git command and its status and output - # - # @example - # `exit 1` # set $? appropriately for this example - # result = Git::CommandLineResult.new(%w[git status], $?, 'stdout', 'stderr') - # error = Git::FailedError.new(result) - # error.result #=> - # #, - # @stderr="stderr", - # @stdout="stdout"> - # - # @return [Git::CommandLineResult] - # - attr_reader :result - end + class FailedError < Git::CommandLineError; end end diff --git a/lib/git/git_execute_error.rb b/lib/git/git_execute_error.rb index 52d2c80f..654dfc5b 100644 --- a/lib/git/git_execute_error.rb +++ b/lib/git/git_execute_error.rb @@ -1,7 +1,14 @@ # frozen_string_literal: true +require_relative 'error' + module Git # This error is raised when a git command fails # - class GitExecuteError < StandardError; end + # This error class is used as an alias for Git::Error for backwards compatibility. + # It is recommended to use Git::Error directly. + # + # @deprecated Use Git::Error instead + # + GitExecuteError = Git::Error end \ No newline at end of file diff --git a/lib/git/signaled_error.rb b/lib/git/signaled_error.rb index 279f0fb0..cb24ea30 100644 --- a/lib/git/signaled_error.rb +++ b/lib/git/signaled_error.rb @@ -1,50 +1,14 @@ # frozen_string_literal: true -require 'git/git_execute_error' +require_relative 'command_line_error' module Git # This error is raised when a git command exits because of an uncaught signal # # The git command executed, status, stdout, and stderr are available from this - # object. The #message includes the git command, the status of the process, and - # the stderr of the process. + # object. # # @api public # - class SignaledError < Git::GitExecuteError - # Create a SignaledError object - # - # @example - # `kill -9 $$` # set $? appropriately for this example - # result = Git::CommandLineResult.new(%w[git status], $?, '', "killed") - # error = Git::SignaledError.new(result) - # error.message #=> - # "[\"git\", \"status\"]\nstatus: pid 88811 SIGKILL (signal 9)\nstderr: \"killed\"" - # - # @param result [Git::CommandLineResult] the result of the git command including the git command, status, stdout, and stderr - # - def initialize(result) - super("#{result.git_cmd}\nstatus: #{result.status}\nstderr: #{result.stderr.inspect}") - @result = result - end - - # @attribute [r] result - # - # The result of the git command including the git command, status, and output - # - # @example - # `kill -9 $$` # set $? appropriately for this example - # result = Git::CommandLineResult.new(%w[git status], $?, '', "killed") - # error = Git::SignaledError.new(result) - # error.result #=> - # #, - # @stderr="killed", - # @stdout=""> - # - # @return [Git::CommandLineResult] - # - attr_reader :result - end + class SignaledError < Git::CommandLineError; end end diff --git a/lib/git/timeout_error.rb b/lib/git/timeout_error.rb new file mode 100644 index 00000000..ed482e73 --- /dev/null +++ b/lib/git/timeout_error.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require_relative 'signaled_error' + +module Git + # This error is raised when a git command takes longer than the configured timeout + # + # The git command executed, status, stdout, and stderr, and the timeout duration + # are available from this object. + # + # result.status.timeout? will be `true` + # + # @api public + # + class TimeoutError < Git::SignaledError + # Create a TimeoutError object + # + # @example + # command = %w[sleep 10] + # timeout_duration = 1 + # status = ProcessExecuter.spawn(*command, timeout: timeout_duration) + # result = Git::CommandLineResult.new(command, status, 'stdout', 'err output') + # error = Git::TimeoutError.new(result, timeout_duration) + # error.to_s #=> '["sleep", "10"], status: pid 70144 SIGKILL (signal 9), stderr: "err output", timed out after 1s' + # + # @param result [Git::CommandLineResult] the result of the git command including + # the git command, status, stdout, and stderr + # + # @param timeout_duration [Numeric] the amount of time the subprocess was allowed + # to run before being killed + # + def initialize(result, timeout_duration) + @timeout_duration = timeout_duration + super(result) + end + + # The human readable representation of this error + # + # @example + # error.to_s #=> '["sleep", "10"], status: pid 88811 SIGKILL (signal 9), stderr: "err output", timed out after 1s' + # + # @return [String] + # + def to_s = <<~MESSAGE.chomp + #{super}, timed out after #{timeout_duration}s + MESSAGE + + # The amount of time the subprocess was allowed to run before being killed + # + # @example + # `kill -9 $$` # set $? appropriately for this example + # result = Git::CommandLineResult.new(%w[git status], $?, '', "killed") + # error = Git::TimeoutError.new(result, 10) + # error.timeout_duration #=> 10 + # + # @return [Numeric] + # + attr_reader :timeout_duration + end +end diff --git a/tests/units/test_command_line_error.rb b/tests/units/test_command_line_error.rb new file mode 100644 index 00000000..30b859ab --- /dev/null +++ b/tests/units/test_command_line_error.rb @@ -0,0 +1,23 @@ +require 'test_helper' + +class TestCommandLineError < Test::Unit::TestCase + def test_initializer + status = Struct.new(:to_s).new('pid 89784 exit 1') + result = Git::CommandLineResult.new(%w[git status], status, 'stdout', 'stderr') + + error = Git::CommandLineError.new(result) + + assert(error.is_a?(Git::Error)) + assert_equal(result, error.result) + end + + def test_to_s + status = Struct.new(:to_s).new('pid 89784 exit 1') + result = Git::CommandLineResult.new(%w[git status], status, 'stdout', 'stderr') + + error = Git::CommandLineError.new(result) + + expected_message = '["git", "status"], status: pid 89784 exit 1, stderr: "stderr"' + assert_equal(expected_message, error.to_s) + end +end diff --git a/tests/units/test_failed_error.rb b/tests/units/test_failed_error.rb index ea4ad4b2..63b894f7 100644 --- a/tests/units/test_failed_error.rb +++ b/tests/units/test_failed_error.rb @@ -7,17 +7,16 @@ def test_initializer error = Git::FailedError.new(result) - assert(error.is_a?(Git::GitExecuteError)) - assert_equal(result, error.result) + assert(error.is_a?(Git::CommandLineError)) end - def test_message + def test_to_s status = Struct.new(:to_s).new('pid 89784 exit 1') result = Git::CommandLineResult.new(%w[git status], status, 'stdout', 'stderr') error = Git::FailedError.new(result) - expected_message = "[\"git\", \"status\"]\nstatus: pid 89784 exit 1\nstderr: \"stderr\"" - assert_equal(expected_message, error.message) + expected_message = '["git", "status"], status: pid 89784 exit 1, stderr: "stderr"' + assert_equal(expected_message, error.to_s) end end diff --git a/tests/units/test_signaled_error.rb b/tests/units/test_signaled_error.rb index 25922aa9..6bf46c2b 100644 --- a/tests/units/test_signaled_error.rb +++ b/tests/units/test_signaled_error.rb @@ -7,17 +7,16 @@ def test_initializer error = Git::SignaledError.new(result) - assert(error.is_a?(Git::GitExecuteError)) - assert_equal(result, error.result) + assert(error.is_a?(Git::Error)) end - def test_message + def test_to_s status = Struct.new(:to_s).new('pid 65628 SIGKILL (signal 9)') # `kill -9 $$` result = Git::CommandLineResult.new(%w[git status], status, '', "uncaught signal") error = Git::SignaledError.new(result) - expected_message = "[\"git\", \"status\"]\nstatus: pid 65628 SIGKILL (signal 9)\nstderr: \"uncaught signal\"" - assert_equal(expected_message, error.message) + expected_message = '["git", "status"], status: pid 65628 SIGKILL (signal 9), stderr: "uncaught signal"' + assert_equal(expected_message, error.to_s) end end diff --git a/tests/units/test_timeout_error.rb b/tests/units/test_timeout_error.rb new file mode 100644 index 00000000..3bfc90b6 --- /dev/null +++ b/tests/units/test_timeout_error.rb @@ -0,0 +1,24 @@ +require 'test_helper' + +class TestTimeoutError < Test::Unit::TestCase + def test_initializer + status = Struct.new(:to_s).new('pid 65628 SIGKILL (signal 9)') # `kill -9 $$` + result = Git::CommandLineResult.new(%w[git status], status, 'stdout', 'stderr') + timeout_diration = 10 + + error = Git::TimeoutError.new(result, timeout_diration) + + assert(error.is_a?(Git::SignaledError)) + end + + def test_to_s + status = Struct.new(:to_s).new('pid 65628 SIGKILL (signal 9)') # `kill -9 $$` + result = Git::CommandLineResult.new(%w[git status], status, 'stdout', 'Waiting...') + timeout_duration = 10 + + error = Git::TimeoutError.new(result, timeout_duration) + + expected_message = '["git", "status"], status: pid 65628 SIGKILL (signal 9), stderr: "Waiting...", timed out after 10s' + assert_equal(expected_message, error.to_s) + end +end From d3aceb6e437c6011b48ac6698352edab2c644ca7 Mon Sep 17 00:00:00 2001 From: James Couball Date: Mon, 5 Feb 2024 08:26:51 -0800 Subject: [PATCH 2/2] Bump truffleruby to 24.0.0 to get support for endless methods Signed-off-by: James Couball --- .github/workflows/continuous_integration.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 3a2cd0df..bc207a9e 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -18,7 +18,7 @@ jobs: fail-fast: false matrix: # Only the latest versions of JRuby and TruffleRuby are tested - ruby: ["3.0", "3.1", "3.2", "3.3", "truffleruby-23.1.1", "jruby-9.4.5.0"] + ruby: ["3.0", "3.1", "3.2", "3.3", "truffleruby-24.0.0", "jruby-9.4.5.0"] operating-system: [ubuntu-latest] experimental: [No] include: @@ -38,7 +38,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Ruby uses: ruby/setup-ruby@v1