git 1.19.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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,14 +1,15 @@
1
- require 'git/failed_error'
1
+ require 'git/command_line'
2
+ require 'git/errors'
2
3
  require 'logger'
4
+ require 'pp'
5
+ require 'process_executer'
6
+ require 'stringio'
3
7
  require 'tempfile'
4
8
  require 'zlib'
5
9
  require 'open3'
6
10
 
7
11
  module Git
8
12
  class Lib
9
-
10
- @@semaphore = Mutex.new
11
-
12
13
  # The path to the Git working copy. The default is '"./.git"'.
13
14
  #
14
15
  # @return [Pathname] the path to the Git working copy.
@@ -79,22 +80,34 @@ module Git
79
80
  command('init', *arr_opts)
80
81
  end
81
82
 
82
- # tries to clone the given repo
83
+ # Clones a repository into a newly created directory
83
84
  #
84
- # accepts options:
85
- # :bare:: no working directory
86
- # :branch:: name of branch to track (rather than 'master')
87
- # :depth:: the number of commits back to pull
88
- # :filter:: specify partial clone
89
- # :origin:: name of remote (same as remote)
90
- # :path:: directory where the repo will be cloned
91
- # :remote:: name of remote (rather than 'origin')
92
- # :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
93
92
  #
94
- # TODO - make this work with SSH password or auth_key
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
104
+ #
105
+ # See {Git::Lib#command} for more information about :timeout
95
106
  #
96
107
  # @return [Hash] the options to pass to {Git::Base.new}
97
108
  #
109
+ # @todo make this work with SSH password or auth_key
110
+ #
98
111
  def clone(repository_url, directory, opts = {})
99
112
  @path = opts[:path] || '.'
100
113
  clone_dir = opts[:path] ? File.join(@path, directory) : directory
@@ -114,7 +127,7 @@ module Git
114
127
  arr_opts << repository_url
115
128
  arr_opts << clone_dir
116
129
 
117
- command('clone', *arr_opts)
130
+ command('clone', *arr_opts, timeout: opts[:timeout])
118
131
 
119
132
  return_base_opts_from_clone(clone_dir, opts)
120
133
  end
@@ -142,7 +155,7 @@ module Git
142
155
  match_data = output.match(%r{^ref: refs/heads/(?<default_branch>[^\t]+)\tHEAD$})
143
156
  return match_data[:default_branch] if match_data
144
157
 
145
- raise 'Unable to determine the default branch'
158
+ raise Git::UnexpectedResultError, 'Unable to determine the default branch'
146
159
  end
147
160
 
148
161
  ## READ COMMANDS ##
@@ -337,7 +350,19 @@ module Git
337
350
  end
338
351
 
339
352
  def object_contents(sha, &block)
340
- command('cat-file', '-p', sha, &block)
353
+ if block_given?
354
+ Tempfile.create do |file|
355
+ # If a block is given, write the output from the process to a temporary
356
+ # file and then yield the file to the block
357
+ #
358
+ command('cat-file', "-p", sha, out: file, err: file)
359
+ file.rewind
360
+ yield file
361
+ end
362
+ else
363
+ # If a block is not given, return stdout
364
+ command('cat-file', '-p', sha)
365
+ end
341
366
  end
342
367
 
343
368
  def ls_tree(sha)
@@ -395,7 +420,7 @@ module Git
395
420
  def branches_all
396
421
  command_lines('branch', '-a').map do |line|
397
422
  match_data = line.match(BRANCH_LINE_REGEXP)
398
- raise GitExecuteError, 'Unexpected branch line format' unless match_data
423
+ raise Git::UnexpectedResultError, 'Unexpected branch line format' unless match_data
399
424
  next nil if match_data[:not_a_branch] || match_data[:detached_ref]
400
425
  [
401
426
  match_data[:refname],
@@ -474,11 +499,15 @@ module Git
474
499
  grep_opts.push('--', *opts[:path_limiter]) if opts[:path_limiter].is_a?(Array)
475
500
 
476
501
  hsh = {}
477
- command_lines('grep', *grep_opts).each do |line|
478
- if m = /(.*?)\:(\d+)\:(.*)/.match(line)
479
- hsh[m[1]] ||= []
480
- hsh[m[1]] << [m[2].to_i, m[3]]
502
+ begin
503
+ command_lines('grep', *grep_opts).each do |line|
504
+ if m = /(.*?)\:(\d+)\:(.*)/.match(line)
505
+ hsh[m[1]] ||= []
506
+ hsh[m[1]] << [m[2].to_i, m[3]]
507
+ end
481
508
  end
509
+ rescue Git::FailedError => e
510
+ raise unless e.result.status.exitstatus == 1 && e.result.stderr == ''
482
511
  end
483
512
  hsh
484
513
  end
@@ -865,16 +894,17 @@ module Git
865
894
 
866
895
  def conflicts # :yields: file, your, their
867
896
  self.unmerged.each do |f|
868
- your_tempfile = Tempfile.new("YOUR-#{File.basename(f)}")
869
- your = your_tempfile.path
870
- your_tempfile.close # free up file for git command process
871
- command('show', ":2:#{f}", redirect: "> #{escape your}")
872
-
873
- their_tempfile = Tempfile.new("THEIR-#{File.basename(f)}")
874
- their = their_tempfile.path
875
- their_tempfile.close # free up file for git command process
876
- command('show', ":3:#{f}", redirect: "> #{escape their}")
877
- yield(f, your, their)
897
+ Tempfile.create("YOUR-#{File.basename(f)}") do |your|
898
+ command('show', ":2:#{f}", out: your)
899
+ your.close
900
+
901
+ Tempfile.create("THEIR-#{File.basename(f)}") do |their|
902
+ command('show', ":3:#{f}", out: their)
903
+ their.close
904
+
905
+ yield(f, your.path, their.path)
906
+ end
907
+ end
878
908
  end
879
909
  end
880
910
 
@@ -915,7 +945,7 @@ module Git
915
945
  opts = opts.last.instance_of?(Hash) ? opts.last : {}
916
946
 
917
947
  if (opts[:a] || opts[:annotate]) && !(opts[:m] || opts[:message])
918
- raise "Can not create an [:a|:annotate] tag without the precense of [:m|:message]."
948
+ raise ArgumentError, 'Cannot create an annotated tag without a message.'
919
949
  end
920
950
 
921
951
  arr_opts = []
@@ -948,7 +978,7 @@ module Git
948
978
  arr_opts << remote if remote
949
979
  arr_opts << opts[:ref] if opts[:ref]
950
980
 
951
- command('fetch', *arr_opts)
981
+ command('fetch', *arr_opts, merge: true)
952
982
  end
953
983
 
954
984
  def push(remote = nil, branch = nil, opts = nil)
@@ -988,10 +1018,11 @@ module Git
988
1018
  end
989
1019
  end
990
1020
 
991
- def pull(remote = nil, branch = nil)
1021
+ def pull(remote = nil, branch = nil, opts = {})
992
1022
  raise ArgumentError, "You must specify a remote if a branch is specified" if remote.nil? && !branch.nil?
993
1023
 
994
1024
  arr_opts = []
1025
+ arr_opts << '--allow-unrelated-histories' if opts[:allow_unrelated_histories]
995
1026
  arr_opts << remote if remote
996
1027
  arr_opts << branch if branch
997
1028
  command('pull', *arr_opts)
@@ -1001,7 +1032,13 @@ module Git
1001
1032
  head = File.join(@git_dir, 'refs', 'tags', tag_name)
1002
1033
  return File.read(head).chomp if File.exist?(head)
1003
1034
 
1004
- command('show-ref', '--tags', '-s', tag_name)
1035
+ begin
1036
+ command('show-ref', '--tags', '-s', tag_name)
1037
+ rescue Git::FailedError => e
1038
+ raise unless e.result.status.exitstatus == 1 && e.result.stderr == ''
1039
+
1040
+ ''
1041
+ end
1005
1042
  end
1006
1043
 
1007
1044
  def repack
@@ -1026,15 +1063,12 @@ module Git
1026
1063
 
1027
1064
  def commit_tree(tree, opts = {})
1028
1065
  opts[:message] ||= "commit tree #{tree}"
1029
- t = Tempfile.new('commit-message')
1030
- t.write(opts[:message])
1031
- t.close
1032
-
1033
1066
  arr_opts = []
1034
1067
  arr_opts << tree
1035
1068
  arr_opts << '-p' << opts[:parent] if opts[:parent]
1036
- arr_opts += Array(opts[:parents]).map { |p| ['-p', p] }.flatten if opts[:parents]
1037
- command('commit-tree', *arr_opts, redirect: "< #{escape t.path}")
1069
+ Array(opts[:parents]).each { |p| arr_opts << '-p' << p } if opts[:parents]
1070
+ arr_opts << '-m' << opts[:message]
1071
+ command('commit-tree', *arr_opts)
1038
1072
  end
1039
1073
 
1040
1074
  def update_ref(ref, commit)
@@ -1080,7 +1114,11 @@ module Git
1080
1114
  arr_opts << "--remote=#{opts[:remote]}" if opts[:remote]
1081
1115
  arr_opts << sha
1082
1116
  arr_opts << '--' << opts[:path] if opts[:path]
1083
- command('archive', *arr_opts, redirect: " > #{escape file}")
1117
+
1118
+ f = File.open(file, 'wb')
1119
+ command('archive', *arr_opts, out: f)
1120
+ f.close
1121
+
1084
1122
  if opts[:add_gzip]
1085
1123
  file_content = File.read(file)
1086
1124
  Zlib::GzipWriter.open(file) do |gz|
@@ -1115,7 +1153,7 @@ module Git
1115
1153
  end
1116
1154
 
1117
1155
  def required_command_version
1118
- [1, 6]
1156
+ [2, 28]
1119
1157
  end
1120
1158
 
1121
1159
  def meets_required_version?
@@ -1133,11 +1171,6 @@ module Git
1133
1171
 
1134
1172
  private
1135
1173
 
1136
- # Systen ENV variables involved in the git commands.
1137
- #
1138
- # @return [<String>] the names of the EVN variables involved in the git commands
1139
- ENV_VARIABLE_NAMES = ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_SSH']
1140
-
1141
1174
  def command_lines(cmd, *opts, chdir: nil)
1142
1175
  cmd_op = command(cmd, *opts, chdir: chdir)
1143
1176
  if cmd_op.encoding.name != "UTF-8"
@@ -1148,84 +1181,82 @@ module Git
1148
1181
  op.split("\n")
1149
1182
  end
1150
1183
 
1151
- # Takes the current git's system ENV variables and store them.
1152
- def store_git_system_env_variables
1153
- @git_system_env_variables = {}
1154
- ENV_VARIABLE_NAMES.each do |env_variable_name|
1155
- @git_system_env_variables[env_variable_name] = ENV[env_variable_name]
1156
- end
1184
+ def env_overrides
1185
+ {
1186
+ 'GIT_DIR' => @git_dir,
1187
+ 'GIT_WORK_TREE' => @git_work_dir,
1188
+ 'GIT_INDEX_FILE' => @git_index_file,
1189
+ 'GIT_SSH' => Git::Base.config.git_ssh
1190
+ }
1157
1191
  end
1158
1192
 
1159
- # Takes the previously stored git's ENV variables and set them again on ENV.
1160
- def restore_git_system_env_variables
1161
- ENV_VARIABLE_NAMES.each do |env_variable_name|
1162
- ENV[env_variable_name] = @git_system_env_variables[env_variable_name]
1193
+ def global_opts
1194
+ Array.new.tap do |global_opts|
1195
+ global_opts << "--git-dir=#{@git_dir}" if !@git_dir.nil?
1196
+ global_opts << "--work-tree=#{@git_work_dir}" if !@git_work_dir.nil?
1197
+ global_opts << '-c' << 'core.quotePath=true'
1198
+ global_opts << '-c' << 'color.ui=false'
1163
1199
  end
1164
1200
  end
1165
1201
 
1166
- # Sets git's ENV variables to the custom values for the current instance.
1167
- def set_custom_git_env_variables
1168
- ENV['GIT_DIR'] = @git_dir
1169
- ENV['GIT_WORK_TREE'] = @git_work_dir
1170
- ENV['GIT_INDEX_FILE'] = @git_index_file
1171
- ENV['GIT_SSH'] = Git::Base.config.git_ssh
1202
+ def command_line
1203
+ @command_line ||=
1204
+ Git::CommandLine.new(env_overrides, Git::Base.config.binary_path, global_opts, @logger)
1172
1205
  end
1173
1206
 
1174
- # Runs a block inside an environment with customized ENV variables.
1175
- # It restores the ENV after execution.
1207
+ # Runs a git command and returns the output
1176
1208
  #
1177
- # @param [Proc] block block to be executed within the customized environment
1178
- def with_custom_env_variables(&block)
1179
- @@semaphore.synchronize do
1180
- store_git_system_env_variables()
1181
- set_custom_git_env_variables()
1182
- return block.call()
1183
- end
1184
- ensure
1185
- restore_git_system_env_variables()
1186
- end
1187
-
1188
- def command(*cmd, redirect: '', chomp: true, chdir: nil, &block)
1189
- Git::Lib.warn_if_old_command(self)
1190
-
1191
- raise 'cmd can not include a nested array' if cmd.any? { |o| o.is_a? Array }
1192
-
1193
- global_opts = []
1194
- global_opts << "--git-dir=#{@git_dir}" if !@git_dir.nil?
1195
- global_opts << "--work-tree=#{@git_work_dir}" if !@git_work_dir.nil?
1196
- global_opts << '-c' << 'core.quotePath=true'
1197
- global_opts << '-c' << 'color.ui=false'
1198
-
1199
- escaped_cmd = cmd.map { |part| escape(part) }.join(' ')
1200
-
1201
- global_opts = global_opts.map { |s| escape(s) }.join(' ')
1202
-
1203
- git_cmd = "#{Git::Base.config.binary_path} #{global_opts} #{escaped_cmd} #{redirect} 2>&1"
1204
-
1205
- output = nil
1206
-
1207
- command_thread = nil;
1208
-
1209
- status = nil
1210
-
1211
- with_custom_env_variables do
1212
- command_thread = Thread.new do
1213
- output, status = run_command(git_cmd, chdir, &block)
1214
- end
1215
- command_thread.join
1216
- end
1217
-
1218
- @logger.info(git_cmd)
1219
- @logger.debug(output)
1220
-
1221
- if status.exitstatus > 1 || (status.exitstatus == 1 && output != '')
1222
- result = Git::CommandLineResult.new(git_cmd, status, output, '')
1223
- raise Git::FailedError.new(result)
1224
- end
1225
-
1226
- output.chomp! if output && chomp && !block_given?
1227
-
1228
- output
1209
+ # @param args [Array] the git command to run and its arguments
1210
+ #
1211
+ # This should exclude the 'git' command itself and global options.
1212
+ #
1213
+ # For example, to run `git log --pretty=oneline`, you would pass `['log',
1214
+ # '--pretty=oneline']`
1215
+ #
1216
+ # @param out [String, nil] the path to a file or an IO to write the command's
1217
+ # stdout to
1218
+ #
1219
+ # @param err [String, nil] the path to a file or an IO to write the command's
1220
+ # stdout to
1221
+ #
1222
+ # @param normalize [Boolean] true to normalize the output encoding
1223
+ #
1224
+ # @param chomp [Boolean] true to remove trailing newlines from the output
1225
+ #
1226
+ # @param merge [Boolean] true to merge stdout and stderr
1227
+ #
1228
+ # @param chdir [String, nil] the directory to run the command in
1229
+ #
1230
+ # @param timeout [Numeric, nil] the maximum seconds to wait for the command to complete
1231
+ #
1232
+ # If timeout is nil, the global timeout from {Git::Config} is used.
1233
+ #
1234
+ # If timeout is zero, the timeout will not be enforced.
1235
+ #
1236
+ # If the command times out, it is killed via a `SIGKILL` signal and `Git::TimeoutError` is raised.
1237
+ #
1238
+ # If the command does not respond to SIGKILL, it will hang this method.
1239
+ #
1240
+ # @see Git::CommandLine#run
1241
+ #
1242
+ # @return [String] the command's stdout (or merged stdout and stderr if `merge`
1243
+ # is true)
1244
+ #
1245
+ # @raise [Git::FailedError] if the command failed
1246
+ # @raise [Git::SignaledError] if the command was signaled
1247
+ # @raise [Git::TimeoutError] if the command times out
1248
+ # @raise [Git::ProcessIOError] if an exception was raised while collecting subprocess output
1249
+ #
1250
+ # The exception's `result` attribute is a {Git::CommandLineResult} which will
1251
+ # contain the result of the command including the exit status, stdout, and
1252
+ # stderr.
1253
+ #
1254
+ # @api private
1255
+ #
1256
+ def command(*args, out: nil, err: nil, normalize: true, chomp: true, merge: false, chdir: nil, timeout: nil)
1257
+ timeout = timeout || Git.config.timeout
1258
+ result = command_line.run(*args, out: out, err: err, normalize: normalize, chomp: chomp, merge: merge, chdir: chdir, timeout: timeout)
1259
+ result.stdout
1229
1260
  end
1230
1261
 
1231
1262
  # Takes the diff command line output (as Array) and parse it into a Hash
@@ -1291,38 +1322,5 @@ module Git
1291
1322
  end
1292
1323
  arr_opts
1293
1324
  end
1294
-
1295
- def run_command(git_cmd, chdir=nil, &block)
1296
- block ||= Proc.new do |io|
1297
- io.readlines.map { |l| Git::EncodingUtils.normalize_encoding(l) }.join
1298
- end
1299
-
1300
- opts = {}
1301
- opts[:chdir] = File.expand_path(chdir) if chdir
1302
-
1303
- Open3.popen2(git_cmd, opts) do |stdin, stdout, wait_thr|
1304
- [block.call(stdout), wait_thr.value]
1305
- end
1306
- end
1307
-
1308
- def escape(s)
1309
- windows_platform? ? escape_for_windows(s) : escape_for_sh(s)
1310
- end
1311
-
1312
- def escape_for_sh(s)
1313
- "'#{s && s.to_s.gsub('\'','\'"\'"\'')}'"
1314
- end
1315
-
1316
- def escape_for_windows(s)
1317
- # Escape existing double quotes in s and then wrap the result with double quotes
1318
- escaped_string = s.to_s.gsub('"','\\"')
1319
- %Q{"#{escaped_string}"}
1320
- end
1321
-
1322
- def windows_platform?
1323
- # Check if on Windows via RUBY_PLATFORM (CRuby) and RUBY_DESCRIPTION (JRuby)
1324
- win_platform_regex = /mingw|mswin/
1325
- RUBY_PLATFORM =~ win_platform_regex || RUBY_DESCRIPTION =~ win_platform_regex
1326
- end
1327
1325
  end
1328
1326
  end