Skip to content

Commit

Permalink
Refactor the Error heriarchy (#693)
Browse files Browse the repository at this point in the history
* Refactor the Error heriarchy
* Bump truffleruby to 24.0.0 to get support for endless methods

Signed-off-by: James Couball <jcouball@yahoo.com>
  • Loading branch information
jcouball committed Feb 5, 2024
1 parent f984b77 commit 8286ceb
Show file tree
Hide file tree
Showing 13 changed files with 258 additions and 98 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/continuous_integration.yml
Expand Up @@ -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:
Expand All @@ -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
Expand Down
64 changes: 59 additions & 5 deletions README.md
Expand Up @@ -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'
```
Expand Down Expand Up @@ -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')
Expand Down
1 change: 1 addition & 0 deletions lib/git.rb
Expand Up @@ -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'
Expand Down
59 changes: 59 additions & 0 deletions 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 #=> #<Git::CommandLineResult:0x00000001046bd488 ...>
#
# @return [Git::CommandLineResult]
#
attr_reader :result
end
end
7 changes: 7 additions & 0 deletions 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
45 changes: 4 additions & 41 deletions 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 #=>
# #<Git::CommandLineResult:0x00000001046bd488
# @git_cmd=["git", "status"],
# @status=#<Process::Status: pid 89784 exit 1>,
# @stderr="stderr",
# @stdout="stdout">
#
# @return [Git::CommandLineResult]
#
attr_reader :result
end
class FailedError < Git::CommandLineError; end
end
9 changes: 8 additions & 1 deletion 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
42 changes: 3 additions & 39 deletions 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 #=>
# #<Git::CommandLineResult:0x000000010470f6e8
# @git_cmd=["git", "status"],
# @status=#<Process::Status: pid 88811 SIGKILL (signal 9)>,
# @stderr="killed",
# @stdout="">
#
# @return [Git::CommandLineResult]
#
attr_reader :result
end
class SignaledError < Git::CommandLineError; end
end
60 changes: 60 additions & 0 deletions 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
23 changes: 23 additions & 0 deletions 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

0 comments on commit 8286ceb

Please sign in to comment.