git 2.0.0.pre3 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,8 +2,7 @@
2
2
 
3
3
  require 'git/base'
4
4
  require 'git/command_line_result'
5
- require 'git/failed_error'
6
- require 'git/signaled_error'
5
+ require 'git/errors'
7
6
  require 'stringio'
8
7
 
9
8
  module Git
@@ -114,7 +113,6 @@ module Git
114
113
  # the normalize option will be ignored.
115
114
  #
116
115
  # @example Run a command and return the output
117
- #
118
116
  # cli.run('version') #=> "git version 2.39.1\n"
119
117
  #
120
118
  # @example The args array should be splatted into the parameter list
@@ -162,14 +160,18 @@ module Git
162
160
  # `stderr_writer.string` will be merged into the output returned by this method.
163
161
  #
164
162
  # @param normalize [Boolean] whether to normalize the output to a valid encoding
163
+ #
165
164
  # @param chomp [Boolean] whether to chomp the output
165
+ #
166
166
  # @param merge [Boolean] whether to merge stdout and stderr in the string returned
167
+ #
167
168
  # @param chdir [String] the directory to run the command in
168
169
  #
169
170
  # @param timeout [Numeric, nil] the maximum seconds to wait for the command to complete
170
171
  #
171
- # If timeout is zero or nil, the command will not time out. If the command
172
- # times out, it is killed via a SIGKILL signal and `Git::TimeoutError` is raised.
172
+ # If timeout is zero, the timeout will not be enforced.
173
+ #
174
+ # If the command times out, it is killed via a `SIGKILL` signal and `Git::TimeoutError` is raised.
173
175
  #
174
176
  # If the command does not respond to SIGKILL, it will hang this method.
175
177
  #
@@ -178,9 +180,13 @@ module Git
178
180
  # This result of running the command.
179
181
  #
180
182
  # @raise [ArgumentError] if `args` is not an array of strings
183
+ #
181
184
  # @raise [Git::SignaledError] if the command was terminated because of an uncaught signal
185
+ #
182
186
  # @raise [Git::FailedError] if the command returned a non-zero exitstatus
183
- # @raise [Git::GitExecuteError] if an exception was raised while collecting subprocess output
187
+ #
188
+ # @raise [Git::ProcessIOError] if an exception was raised while collecting subprocess output
189
+ #
184
190
  # @raise [Git::TimeoutError] if the command times out
185
191
  #
186
192
  def run(*args, out:, err:, normalize:, chomp:, merge:, chdir: nil, timeout: nil)
@@ -253,28 +259,28 @@ module Git
253
259
  # @param pipe_name [Symbol] the name of the pipe that raised the exception
254
260
  # @param pipe [ProcessExecuter::MonitoredPipe] the pipe that raised the exception
255
261
  #
256
- # @raise [Git::GitExecuteError]
262
+ # @raise [Git::ProcessIOError]
257
263
  #
258
264
  # @return [void] this method always raises an error
259
265
  #
260
266
  # @api private
261
267
  #
262
268
  def raise_pipe_error(git_cmd, pipe_name, pipe)
263
- raise Git::GitExecuteError.new("Pipe Exception for #{git_cmd}: #{pipe_name}"), cause: pipe.exception
269
+ raise Git::ProcessIOError.new("Pipe Exception for #{git_cmd}: #{pipe_name}"), cause: pipe.exception
264
270
  end
265
271
 
266
272
  # Execute the git command and collect the output
267
273
  #
268
274
  # @param cmd [Array<String>] the git command to execute
269
275
  # @param chdir [String] the directory to run the command in
270
- # @param timeout [Float, Integer, nil] the maximum seconds to wait for the command to complete
276
+ # @param timeout [Numeric, nil] the maximum seconds to wait for the command to complete
271
277
  #
272
278
  # If timeout is zero of nil, the command will not time out. If the command
273
279
  # times out, it is killed via a SIGKILL signal and `Git::TimeoutError` is raised.
274
280
  #
275
281
  # If the command does not respond to SIGKILL, it will hang this method.
276
282
  #
277
- # @raise [Git::GitExecuteError] if an exception was raised while collecting subprocess output
283
+ # @raise [Git::ProcessIOError] if an exception was raised while collecting subprocess output
278
284
  # @raise [Git::TimeoutError] if the command times out
279
285
  #
280
286
  # @return [ProcessExecuter::Status] the status of the completed subprocess
@@ -321,11 +327,14 @@ module Git
321
327
  # @param err [#write] the object that stderr was written to
322
328
  # @param normalize [Boolean] whether to normalize the output of each writer
323
329
  # @param chomp [Boolean] whether to chomp the output of each writer
330
+ # @param timeout [Numeric, nil] the maximum seconds to wait for the command to complete
324
331
  #
325
332
  # @return [Git::CommandLineResult] the result of the command to return to the caller
326
333
  #
327
334
  # @raise [Git::FailedError] if the command failed
328
335
  # @raise [Git::SignaledError] if the command was signaled
336
+ # @raise [Git::TimeoutError] if the command times out
337
+ # @raise [Git::ProcessIOError] if an exception was raised while collecting subprocess output
329
338
  #
330
339
  # @api private
331
340
  #
@@ -346,14 +355,14 @@ module Git
346
355
  # @param out [#write] the object to write stdout to
347
356
  # @param err [#write] the object to write stderr to
348
357
  # @param chdir [String] the directory to run the command in
349
- # @param timeout [Float, Integer, nil] the maximum seconds to wait for the command to complete
358
+ # @param timeout [Numeric, nil] the maximum seconds to wait for the command to complete
350
359
  #
351
360
  # If timeout is zero of nil, the command will not time out. If the command
352
361
  # times out, it is killed via a SIGKILL signal and `Git::TimeoutError` is raised.
353
362
  #
354
363
  # If the command does not respond to SIGKILL, it will hang this method.
355
364
  #
356
- # @raise [Git::GitExecuteError] if an exception was raised while collecting subprocess output
365
+ # @raise [Git::ProcessIOError] if an exception was raised while collecting subprocess output
357
366
  # @raise [Git::TimeoutError] if the command times out
358
367
  #
359
368
  # @return [Git::CommandLineResult] the result of the command to return to the caller
data/lib/git/errors.rb ADDED
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Git
4
+ # Base class for all custom git module errors
5
+ #
6
+ # The git gem will only raise an `ArgumentError` or an error that is a subclass of
7
+ # `Git::Error`. It does not explicitly raise any other types of errors.
8
+ #
9
+ # It is recommended to rescue `Git::Error` to catch any runtime error raised by
10
+ # this gem unless you need more specific error handling.
11
+ #
12
+ # Git's custom errors are arranged in the following class heirarchy:
13
+ #
14
+ # ```text
15
+ # StandardError
16
+ # └─> Git::Error
17
+ # ├─> Git::CommandLineError
18
+ # │ ├─> Git::FailedError
19
+ # │ └─> Git::SignaledError
20
+ # │ └─> Git::TimeoutError
21
+ # ├─> Git::ProcessIOError
22
+ # └─> Git::UnexpectedResultError
23
+ # ```
24
+ #
25
+ # | Error Class | Description |
26
+ # | --- | --- |
27
+ # | `Error` | This catch-all error serves as the base class for other custom errors raised by the git gem. |
28
+ # | `CommandLineError` | A subclass of this error is raised when there is a problem executing the git command line. |
29
+ # | `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. |
30
+ # | `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. |
31
+ # | `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. |
32
+ # | `ProcessIOError` | An error was encountered reading or writing to a subprocess. |
33
+ # | `UnexpectedResultError` | The command line ran without error but did not return the expected results. |
34
+ #
35
+ # @example Rescuing a generic error
36
+ # begin
37
+ # # some git operation
38
+ # rescue Git::Error => e
39
+ # puts "An error occurred: #{e.message}"
40
+ # end
41
+ #
42
+ # @example Rescuing a timeout error
43
+ # begin
44
+ # timeout_duration = 0.001 # seconds
45
+ # repo = Git.clone('https://github.com/ruby-git/ruby-git', 'ruby-git-temp', timeout: timeout_duration)
46
+ # rescue Git::TimeoutError => e # Catch the more specific error first!
47
+ # puts "Git clone took too long and timed out #{e}"
48
+ # rescue Git::Error => e
49
+ # puts "Received the following error: #{e}"
50
+ # end
51
+ #
52
+ # @see Git::CommandLineError
53
+ # @see Git::FailedError
54
+ # @see Git::SignaledError
55
+ # @see Git::TimeoutError
56
+ # @see Git::ProcessIOError
57
+ # @see Git::UnexpectedResultError
58
+ #
59
+ # @api public
60
+ #
61
+ class Error < StandardError; end
62
+
63
+ # An alias for Git::Error
64
+ #
65
+ # Git::GitExecuteError error class is an alias for Git::Error for backwards
66
+ # compatibility. It is recommended to use Git::Error directly.
67
+ #
68
+ # @deprecated Use Git::Error instead
69
+ #
70
+ GitExecuteError = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Git::GitExecuteError', 'Git::Error', Git::Deprecation)
71
+
72
+ # Raised when a git command fails or exits because of an uncaught signal
73
+ #
74
+ # The git command executed, status, stdout, and stderr are available from this
75
+ # object.
76
+ #
77
+ # The Gem will raise a more specific error for each type of failure:
78
+ #
79
+ # * {Git::FailedError}: when the git command exits with a non-zero status
80
+ # * {Git::SignaledError}: when the git command exits because of an uncaught signal
81
+ # * {Git::TimeoutError}: when the git command times out
82
+ #
83
+ # @api public
84
+ #
85
+ class CommandLineError < Git::Error
86
+ # Create a CommandLineError object
87
+ #
88
+ # @example
89
+ # `exit 1` # set $? appropriately for this example
90
+ # result = Git::CommandLineResult.new(%w[git status], $?, 'stdout', 'stderr')
91
+ # error = Git::CommandLineError.new(result)
92
+ # error.to_s #=> '["git", "status"], status: pid 89784 exit 1, stderr: "stderr"'
93
+ #
94
+ # @param result [Git::CommandLineResult] the result of the git command including
95
+ # the git command, status, stdout, and stderr
96
+ #
97
+ def initialize(result)
98
+ @result = result
99
+ super(error_message)
100
+ end
101
+
102
+ # The human readable representation of this error
103
+ #
104
+ # @example
105
+ # error.error_message #=> '["git", "status"], status: pid 89784 exit 1, stderr: "stderr"'
106
+ #
107
+ # @return [String]
108
+ #
109
+ def error_message = <<~MESSAGE.chomp
110
+ #{result.git_cmd}, status: #{result.status}, stderr: #{result.stderr.inspect}
111
+ MESSAGE
112
+
113
+ # @attribute [r] result
114
+ #
115
+ # The result of the git command including the git command and its status and output
116
+ #
117
+ # @example
118
+ # error.result #=> #<Git::CommandLineResult:0x00000001046bd488 ...>
119
+ #
120
+ # @return [Git::CommandLineResult]
121
+ #
122
+ attr_reader :result
123
+ end
124
+
125
+ # This error is raised when a git command returns a non-zero exitstatus
126
+ #
127
+ # The git command executed, status, stdout, and stderr are available from this
128
+ # object.
129
+ #
130
+ # @api public
131
+ #
132
+ class FailedError < Git::CommandLineError; end
133
+
134
+ # This error is raised when a git command exits because of an uncaught signal
135
+ #
136
+ # @api public
137
+ #
138
+ class SignaledError < Git::CommandLineError; end
139
+
140
+ # This error is raised when a git command takes longer than the configured timeout
141
+ #
142
+ # The git command executed, status, stdout, and stderr, and the timeout duration
143
+ # are available from this object.
144
+ #
145
+ # result.status.timeout? will be `true`
146
+ #
147
+ # @api public
148
+ #
149
+ class TimeoutError < Git::SignaledError
150
+ # Create a TimeoutError object
151
+ #
152
+ # @example
153
+ # command = %w[sleep 10]
154
+ # timeout_duration = 1
155
+ # status = ProcessExecuter.spawn(*command, timeout: timeout_duration)
156
+ # result = Git::CommandLineResult.new(command, status, 'stdout', 'err output')
157
+ # error = Git::TimeoutError.new(result, timeout_duration)
158
+ # error.error_message #=> '["sleep", "10"], status: pid 70144 SIGKILL (signal 9), stderr: "err output", timed out after 1s'
159
+ #
160
+ # @param result [Git::CommandLineResult] the result of the git command including
161
+ # the git command, status, stdout, and stderr
162
+ #
163
+ # @param timeout_duration [Numeric] the amount of time the subprocess was allowed
164
+ # to run before being killed
165
+ #
166
+ def initialize(result, timeout_duration)
167
+ @timeout_duration = timeout_duration
168
+ super(result)
169
+ end
170
+
171
+ # The human readable representation of this error
172
+ #
173
+ # @example
174
+ # error.error_message #=> '["sleep", "10"], status: pid 88811 SIGKILL (signal 9), stderr: "err output", timed out after 1s'
175
+ #
176
+ # @return [String]
177
+ #
178
+ def error_message = <<~MESSAGE.chomp
179
+ #{super}, timed out after #{timeout_duration}s
180
+ MESSAGE
181
+
182
+ # The amount of time the subprocess was allowed to run before being killed
183
+ #
184
+ # @example
185
+ # `kill -9 $$` # set $? appropriately for this example
186
+ # result = Git::CommandLineResult.new(%w[git status], $?, '', "killed")
187
+ # error = Git::TimeoutError.new(result, 10)
188
+ # error.timeout_duration #=> 10
189
+ #
190
+ # @return [Numeric]
191
+ #
192
+ attr_reader :timeout_duration
193
+ end
194
+
195
+ # Raised when the output of a git command can not be read
196
+ #
197
+ # @api public
198
+ #
199
+ class ProcessIOError < Git::Error; end
200
+
201
+ # Raised when the git command result was not as expected
202
+ #
203
+ # @api public
204
+ #
205
+ class UnexpectedResultError < Git::Error; end
206
+ end
data/lib/git/lib.rb CHANGED
@@ -1,5 +1,5 @@
1
- require 'git/failed_error'
2
1
  require 'git/command_line'
2
+ require 'git/errors'
3
3
  require 'logger'
4
4
  require 'pp'
5
5
  require 'process_executer'
@@ -80,22 +80,34 @@ module Git
80
80
  command('init', *arr_opts)
81
81
  end
82
82
 
83
- # tries to clone the given repo
83
+ # Clones a repository into a newly created directory
84
84
  #
85
- # accepts options:
86
- # :bare:: no working directory
87
- # :branch:: name of branch to track (rather than 'master')
88
- # :depth:: the number of commits back to pull
89
- # :filter:: specify partial clone
90
- # :origin:: name of remote (same as remote)
91
- # :path:: directory where the repo will be cloned
92
- # :remote:: name of remote (rather than 'origin')
93
- # :recursive:: after the clone is created, initialize all submodules within, using their default settings.
85
+ # @param [String] repository_url the URL of the repository to clone
86
+ # @param [String, nil] directory the directory to clone into
87
+ #
88
+ # If nil, the repository is cloned into a directory with the same name as
89
+ # the repository.
90
+ #
91
+ # @param [Hash] opts the options for this command
92
+ #
93
+ # @option opts [Boolean] :bare (false) if true, clone as a bare repository
94
+ # @option opts [String] :branch the branch to checkout
95
+ # @option opts [String, Array] :config one or more configuration options to set
96
+ # @option opts [Integer] :depth the number of commits back to pull
97
+ # @option opts [String] :filter specify partial clone
98
+ # @option opts [String] :mirror set up a mirror of the source repository
99
+ # @option opts [String] :origin the name of the remote
100
+ # @option opts [String] :path an optional prefix for the directory parameter
101
+ # @option opts [String] :remote the name of the remote
102
+ # @option opts [Boolean] :recursive after the clone is created, initialize all submodules within, using their default settings
103
+ # @option opts [Numeric, nil] :timeout the number of seconds to wait for the command to complete
94
104
  #
95
- # TODO - make this work with SSH password or auth_key
105
+ # See {Git::Lib#command} for more information about :timeout
96
106
  #
97
107
  # @return [Hash] the options to pass to {Git::Base.new}
98
108
  #
109
+ # @todo make this work with SSH password or auth_key
110
+ #
99
111
  def clone(repository_url, directory, opts = {})
100
112
  @path = opts[:path] || '.'
101
113
  clone_dir = opts[:path] ? File.join(@path, directory) : directory
@@ -143,7 +155,7 @@ module Git
143
155
  match_data = output.match(%r{^ref: refs/heads/(?<default_branch>[^\t]+)\tHEAD$})
144
156
  return match_data[:default_branch] if match_data
145
157
 
146
- raise 'Unable to determine the default branch'
158
+ raise Git::UnexpectedResultError, 'Unable to determine the default branch'
147
159
  end
148
160
 
149
161
  ## READ COMMANDS ##
@@ -408,7 +420,7 @@ module Git
408
420
  def branches_all
409
421
  command_lines('branch', '-a').map do |line|
410
422
  match_data = line.match(BRANCH_LINE_REGEXP)
411
- raise GitExecuteError, 'Unexpected branch line format' unless match_data
423
+ raise Git::UnexpectedResultError, 'Unexpected branch line format' unless match_data
412
424
  next nil if match_data[:not_a_branch] || match_data[:detached_ref]
413
425
  [
414
426
  match_data[:refname],
@@ -588,6 +600,9 @@ module Git
588
600
  command_lines('ls-files', '--others', '-i', '--exclude-standard')
589
601
  end
590
602
 
603
+ def untracked_files
604
+ command_lines('ls-files', '--others', '--exclude-standard', chdir: @git_work_dir)
605
+ end
591
606
 
592
607
  def config_remote(name)
593
608
  hsh = {}
@@ -692,6 +707,19 @@ module Git
692
707
  command('rm', *arr_opts)
693
708
  end
694
709
 
710
+ # Returns true if the repository is empty (meaning it has no commits)
711
+ #
712
+ # @return [Boolean]
713
+ #
714
+ def empty?
715
+ command('rev-parse', '--verify', 'HEAD')
716
+ false
717
+ rescue Git::FailedError => e
718
+ raise unless e.result.status.exitstatus == 128 &&
719
+ e.result.stderr == 'fatal: Needed a single revision'
720
+ true
721
+ end
722
+
695
723
  # Takes the commit message with the options and executes the commit command
696
724
  #
697
725
  # accepts options:
@@ -933,7 +961,7 @@ module Git
933
961
  opts = opts.last.instance_of?(Hash) ? opts.last : {}
934
962
 
935
963
  if (opts[:a] || opts[:annotate]) && !(opts[:m] || opts[:message])
936
- raise "Can not create an [:a|:annotate] tag without the precense of [:m|:message]."
964
+ raise ArgumentError, 'Cannot create an annotated tag without a message.'
937
965
  end
938
966
 
939
967
  arr_opts = []
@@ -1215,15 +1243,25 @@ module Git
1215
1243
  #
1216
1244
  # @param chdir [String, nil] the directory to run the command in
1217
1245
  #
1218
- # @param timeout [Numeric, nil] the maximum time to wait for the command to
1219
- # complete
1246
+ # @param timeout [Numeric, nil] the maximum seconds to wait for the command to complete
1247
+ #
1248
+ # If timeout is nil, the global timeout from {Git::Config} is used.
1249
+ #
1250
+ # If timeout is zero, the timeout will not be enforced.
1251
+ #
1252
+ # If the command times out, it is killed via a `SIGKILL` signal and `Git::TimeoutError` is raised.
1253
+ #
1254
+ # If the command does not respond to SIGKILL, it will hang this method.
1220
1255
  #
1221
1256
  # @see Git::CommandLine#run
1222
1257
  #
1223
1258
  # @return [String] the command's stdout (or merged stdout and stderr if `merge`
1224
1259
  # is true)
1225
1260
  #
1226
- # @raise [Git::GitExecuteError] if the command fails
1261
+ # @raise [Git::FailedError] if the command failed
1262
+ # @raise [Git::SignaledError] if the command was signaled
1263
+ # @raise [Git::TimeoutError] if the command times out
1264
+ # @raise [Git::ProcessIOError] if an exception was raised while collecting subprocess output
1227
1265
  #
1228
1266
  # The exception's `result` attribute is a {Git::CommandLineResult} which will
1229
1267
  # contain the result of the command including the exit status, stdout, and