Skip to content

Commit 8286ceb

Browse files
authored
Refactor the Error heriarchy (#693)
* Refactor the Error heriarchy * Bump truffleruby to 24.0.0 to get support for endless methods Signed-off-by: James Couball <jcouball@yahoo.com>
1 parent f984b77 commit 8286ceb

13 files changed

+258
-98
lines changed

.github/workflows/continuous_integration.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
fail-fast: false
1919
matrix:
2020
# Only the latest versions of JRuby and TruffleRuby are tested
21-
ruby: ["3.0", "3.1", "3.2", "3.3", "truffleruby-23.1.1", "jruby-9.4.5.0"]
21+
ruby: ["3.0", "3.1", "3.2", "3.3", "truffleruby-24.0.0", "jruby-9.4.5.0"]
2222
operating-system: [ubuntu-latest]
2323
experimental: [No]
2424
include:
@@ -38,7 +38,7 @@ jobs:
3838

3939
steps:
4040
- name: Checkout Code
41-
uses: actions/checkout@v3
41+
uses: actions/checkout@v4
4242

4343
- name: Setup Ruby
4444
uses: ruby/setup-ruby@v1

README.md

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,65 @@ Pass the `--all` option to `git log` as follows:
9090

9191
**Git::Worktrees** - Enumerable object that holds `Git::Worktree objects`.
9292

93+
## Errors Raised By This Gem
94+
95+
This gem raises custom errors that derive from `Git::Error`. These errors are
96+
arranged in the following class heirarchy:
97+
98+
Error heirarchy:
99+
100+
```text
101+
Error
102+
└── CommandLineError
103+
├── FailedError
104+
└── SignaledError
105+
└── TimeoutError
106+
```
107+
108+
Other standard errors may also be raised like `ArgumentError`. Each method should
109+
document the errors it may raise.
110+
111+
Description of each Error class:
112+
113+
* `Error`: This catch-all error serves as the base class for other custom errors in this
114+
gem. Errors of this class are raised when no more approriate specific error to
115+
raise.
116+
* `CommandLineError`: This error is raised when there's a problem executing the git
117+
command line. This gem will raise a more specific error depending on how the
118+
command line failed.
119+
* `FailedError`: This error is raised when the git command line exits with a non-zero
120+
status code that is not expected by the git gem.
121+
* `SignaledError`: This error is raised when the git command line is terminated as a
122+
result of receiving a signal. This could happen if the process is forcibly
123+
terminated or if there is a serious system error.
124+
* `TimeoutError`: This is a specific type of `SignaledError` that is raised when the
125+
git command line operation times out and is killed via the SIGKILL signal. This
126+
happens if the operation takes longer than the timeout duration configured in
127+
`Git.config.timeout` or via the `:timeout` parameter given in git methods that
128+
support this parameter.
129+
130+
`Git::GitExecuteError` remains as an alias for `Git::Error`. It is considered
131+
deprecated as of git-2.0.0.
132+
133+
Here is an example of catching errors when using the git gem:
134+
135+
```ruby
136+
begin
137+
timeout_duration = 0.001 # seconds
138+
repo = Git.clone('https://github.com/ruby-git/ruby-git', 'ruby-git-temp', timeout: timeout_duration)
139+
rescue Git::TimeoutError => e # Catch the more specific error first!
140+
puts "Git clone took too long and timed out #{e}"
141+
rescue Git::Error => e
142+
puts "Received the following error: #{e}"
143+
end
144+
```
145+
93146
## Examples
94147

95148
Here are a bunch of examples of how to use the Ruby/Git package.
96149

97150
Require the 'git' gem.
151+
98152
```ruby
99153
require 'git'
100154
```
@@ -261,11 +315,11 @@ g.add(:all=>true) # git add --all -- "."
261315
g.add('file_path') # git add -- "file_path"
262316
g.add(['file_path_1', 'file_path_2']) # git add -- "file_path_1" "file_path_2"
263317

264-
g.remove() # git rm -f -- "."
265-
g.remove('file.txt') # git rm -f -- "file.txt"
266-
g.remove(['file.txt', 'file2.txt']) # git rm -f -- "file.txt" "file2.txt"
267-
g.remove('file.txt', :recursive => true) # git rm -f -r -- "file.txt"
268-
g.remove('file.txt', :cached => true) # git rm -f --cached -- "file.txt"
318+
g.remove() # git rm -f -- "."
319+
g.remove('file.txt') # git rm -f -- "file.txt"
320+
g.remove(['file.txt', 'file2.txt']) # git rm -f -- "file.txt" "file2.txt"
321+
g.remove('file.txt', :recursive => true) # git rm -f -r -- "file.txt"
322+
g.remove('file.txt', :cached => true) # git rm -f --cached -- "file.txt"
269323

270324
g.commit('message')
271325
g.commit_all('message')

lib/git.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
require 'git/signaled_error'
2828
require 'git/stash'
2929
require 'git/stashes'
30+
require 'git/timeout_error'
3031
require 'git/url'
3132
require 'git/version'
3233
require 'git/working_directory'

lib/git/command_line_error.rb

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# frozen_string_literal: true
2+
3+
require_relative 'error'
4+
5+
module Git
6+
# Raised when a git command fails or exits because of an uncaught signal
7+
#
8+
# The git command executed, status, stdout, and stderr are available from this
9+
# object.
10+
#
11+
# Rather than creating a CommandLineError object directly, it is recommended to use
12+
# one of the derived classes for the appropriate type of error:
13+
#
14+
# * {Git::FailedError}: when the git command exits with a non-zero status
15+
# * {Git::SignaledError}: when the git command exits because of an uncaught signal
16+
# * {Git::TimeoutError}: when the git command times out
17+
#
18+
# @api public
19+
#
20+
class CommandLineError < Git::Error
21+
# Create a CommandLineError object
22+
#
23+
# @example
24+
# `exit 1` # set $? appropriately for this example
25+
# result = Git::CommandLineResult.new(%w[git status], $?, 'stdout', 'stderr')
26+
# error = Git::CommandLineError.new(result)
27+
# error.to_s #=> '["git", "status"], status: pid 89784 exit 1, stderr: "stderr"'
28+
#
29+
# @param result [Git::CommandLineResult] the result of the git command including
30+
# the git command, status, stdout, and stderr
31+
#
32+
def initialize(result)
33+
@result = result
34+
super()
35+
end
36+
37+
# The human readable representation of this error
38+
#
39+
# @example
40+
# error.to_s #=> '["git", "status"], status: pid 89784 exit 1, stderr: "stderr"'
41+
#
42+
# @return [String]
43+
#
44+
def to_s = <<~MESSAGE.chomp
45+
#{result.git_cmd}, status: #{result.status}, stderr: #{result.stderr.inspect}
46+
MESSAGE
47+
48+
# @attribute [r] result
49+
#
50+
# The result of the git command including the git command and its status and output
51+
#
52+
# @example
53+
# error.result #=> #<Git::CommandLineResult:0x00000001046bd488 ...>
54+
#
55+
# @return [Git::CommandLineResult]
56+
#
57+
attr_reader :result
58+
end
59+
end

lib/git/error.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# frozen_string_literal: true
2+
3+
module Git
4+
# Base class for all custom git module errors
5+
#
6+
class Error < StandardError; end
7+
end

lib/git/failed_error.rb

Lines changed: 4 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,14 @@
11
# frozen_string_literal: true
22

3-
require 'git/git_execute_error'
3+
require_relative 'command_line_error'
44

55
module Git
6-
# This error is raised when a git command fails
6+
# This error is raised when a git command returns a non-zero exitstatus
77
#
88
# The git command executed, status, stdout, and stderr are available from this
9-
# object. The #message includes the git command, the status of the process, and
10-
# the stderr of the process.
9+
# object.
1110
#
1211
# @api public
1312
#
14-
class FailedError < Git::GitExecuteError
15-
# Create a FailedError object
16-
#
17-
# @example
18-
# `exit 1` # set $? appropriately for this example
19-
# result = Git::CommandLineResult.new(%w[git status], $?, 'stdout', 'stderr')
20-
# error = Git::FailedError.new(result)
21-
# error.message #=>
22-
# "[\"git\", \"status\"]\nstatus: pid 89784 exit 1\nstderr: \"stderr\""
23-
#
24-
# @param result [Git::CommandLineResult] the result of the git command including
25-
# the git command, status, stdout, and stderr
26-
#
27-
def initialize(result)
28-
super("#{result.git_cmd}\nstatus: #{result.status}\nstderr: #{result.stderr.inspect}")
29-
@result = result
30-
end
31-
32-
# @attribute [r] result
33-
#
34-
# The result of the git command including the git command and its status and output
35-
#
36-
# @example
37-
# `exit 1` # set $? appropriately for this example
38-
# result = Git::CommandLineResult.new(%w[git status], $?, 'stdout', 'stderr')
39-
# error = Git::FailedError.new(result)
40-
# error.result #=>
41-
# #<Git::CommandLineResult:0x00000001046bd488
42-
# @git_cmd=["git", "status"],
43-
# @status=#<Process::Status: pid 89784 exit 1>,
44-
# @stderr="stderr",
45-
# @stdout="stdout">
46-
#
47-
# @return [Git::CommandLineResult]
48-
#
49-
attr_reader :result
50-
end
13+
class FailedError < Git::CommandLineError; end
5114
end

lib/git/git_execute_error.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
# frozen_string_literal: true
22

3+
require_relative 'error'
4+
35
module Git
46
# This error is raised when a git command fails
57
#
6-
class GitExecuteError < StandardError; end
8+
# This error class is used as an alias for Git::Error for backwards compatibility.
9+
# It is recommended to use Git::Error directly.
10+
#
11+
# @deprecated Use Git::Error instead
12+
#
13+
GitExecuteError = Git::Error
714
end

lib/git/signaled_error.rb

Lines changed: 3 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,14 @@
11
# frozen_string_literal: true
22

3-
require 'git/git_execute_error'
3+
require_relative 'command_line_error'
44

55
module Git
66
# This error is raised when a git command exits because of an uncaught signal
77
#
88
# The git command executed, status, stdout, and stderr are available from this
9-
# object. The #message includes the git command, the status of the process, and
10-
# the stderr of the process.
9+
# object.
1110
#
1211
# @api public
1312
#
14-
class SignaledError < Git::GitExecuteError
15-
# Create a SignaledError object
16-
#
17-
# @example
18-
# `kill -9 $$` # set $? appropriately for this example
19-
# result = Git::CommandLineResult.new(%w[git status], $?, '', "killed")
20-
# error = Git::SignaledError.new(result)
21-
# error.message #=>
22-
# "[\"git\", \"status\"]\nstatus: pid 88811 SIGKILL (signal 9)\nstderr: \"killed\""
23-
#
24-
# @param result [Git::CommandLineResult] the result of the git command including the git command, status, stdout, and stderr
25-
#
26-
def initialize(result)
27-
super("#{result.git_cmd}\nstatus: #{result.status}\nstderr: #{result.stderr.inspect}")
28-
@result = result
29-
end
30-
31-
# @attribute [r] result
32-
#
33-
# The result of the git command including the git command, status, and output
34-
#
35-
# @example
36-
# `kill -9 $$` # set $? appropriately for this example
37-
# result = Git::CommandLineResult.new(%w[git status], $?, '', "killed")
38-
# error = Git::SignaledError.new(result)
39-
# error.result #=>
40-
# #<Git::CommandLineResult:0x000000010470f6e8
41-
# @git_cmd=["git", "status"],
42-
# @status=#<Process::Status: pid 88811 SIGKILL (signal 9)>,
43-
# @stderr="killed",
44-
# @stdout="">
45-
#
46-
# @return [Git::CommandLineResult]
47-
#
48-
attr_reader :result
49-
end
13+
class SignaledError < Git::CommandLineError; end
5014
end

lib/git/timeout_error.rb

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# frozen_string_literal: true
2+
3+
require_relative 'signaled_error'
4+
5+
module Git
6+
# This error is raised when a git command takes longer than the configured timeout
7+
#
8+
# The git command executed, status, stdout, and stderr, and the timeout duration
9+
# are available from this object.
10+
#
11+
# result.status.timeout? will be `true`
12+
#
13+
# @api public
14+
#
15+
class TimeoutError < Git::SignaledError
16+
# Create a TimeoutError object
17+
#
18+
# @example
19+
# command = %w[sleep 10]
20+
# timeout_duration = 1
21+
# status = ProcessExecuter.spawn(*command, timeout: timeout_duration)
22+
# result = Git::CommandLineResult.new(command, status, 'stdout', 'err output')
23+
# error = Git::TimeoutError.new(result, timeout_duration)
24+
# error.to_s #=> '["sleep", "10"], status: pid 70144 SIGKILL (signal 9), stderr: "err output", timed out after 1s'
25+
#
26+
# @param result [Git::CommandLineResult] the result of the git command including
27+
# the git command, status, stdout, and stderr
28+
#
29+
# @param timeout_duration [Numeric] the amount of time the subprocess was allowed
30+
# to run before being killed
31+
#
32+
def initialize(result, timeout_duration)
33+
@timeout_duration = timeout_duration
34+
super(result)
35+
end
36+
37+
# The human readable representation of this error
38+
#
39+
# @example
40+
# error.to_s #=> '["sleep", "10"], status: pid 88811 SIGKILL (signal 9), stderr: "err output", timed out after 1s'
41+
#
42+
# @return [String]
43+
#
44+
def to_s = <<~MESSAGE.chomp
45+
#{super}, timed out after #{timeout_duration}s
46+
MESSAGE
47+
48+
# The amount of time the subprocess was allowed to run before being killed
49+
#
50+
# @example
51+
# `kill -9 $$` # set $? appropriately for this example
52+
# result = Git::CommandLineResult.new(%w[git status], $?, '', "killed")
53+
# error = Git::TimeoutError.new(result, 10)
54+
# error.timeout_duration #=> 10
55+
#
56+
# @return [Numeric]
57+
#
58+
attr_reader :timeout_duration
59+
end
60+
end
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
require 'test_helper'
2+
3+
class TestCommandLineError < Test::Unit::TestCase
4+
def test_initializer
5+
status = Struct.new(:to_s).new('pid 89784 exit 1')
6+
result = Git::CommandLineResult.new(%w[git status], status, 'stdout', 'stderr')
7+
8+
error = Git::CommandLineError.new(result)
9+
10+
assert(error.is_a?(Git::Error))
11+
assert_equal(result, error.result)
12+
end
13+
14+
def test_to_s
15+
status = Struct.new(:to_s).new('pid 89784 exit 1')
16+
result = Git::CommandLineResult.new(%w[git status], status, 'stdout', 'stderr')
17+
18+
error = Git::CommandLineError.new(result)
19+
20+
expected_message = '["git", "status"], status: pid 89784 exit 1, stderr: "stderr"'
21+
assert_equal(expected_message, error.to_s)
22+
end
23+
end

0 commit comments

Comments
 (0)