git 3.1.1 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/continuous_integration.yml +11 -3
- data/.github/workflows/experimental_continuous_integration.yml +9 -0
- data/.gitignore +1 -0
- data/.release-please-manifest.json +1 -1
- data/.rubocop.yml +51 -0
- data/.rubocop_todo.yml +12 -0
- data/CHANGELOG.md +63 -0
- data/README.md +19 -0
- data/Rakefile +13 -1
- data/git.gemspec +35 -30
- data/lib/git/args_builder.rb +103 -0
- data/lib/git/author.rb +6 -5
- data/lib/git/base.rb +274 -164
- data/lib/git/branch.rb +14 -15
- data/lib/git/branches.rb +9 -13
- data/lib/git/command_line.rb +95 -52
- data/lib/git/command_line_result.rb +9 -3
- data/lib/git/config.rb +4 -6
- data/lib/git/diff.rb +115 -80
- data/lib/git/diff_path_status.rb +46 -0
- data/lib/git/diff_stats.rb +59 -0
- data/lib/git/errors.rb +8 -2
- data/lib/git/escaped_path.rb +1 -1
- data/lib/git/lib.rb +869 -561
- data/lib/git/log.rb +90 -146
- data/lib/git/object.rb +85 -66
- data/lib/git/path.rb +18 -8
- data/lib/git/remote.rb +3 -4
- data/lib/git/repository.rb +0 -2
- data/lib/git/stash.rb +13 -6
- data/lib/git/stashes.rb +5 -5
- data/lib/git/status.rb +108 -247
- data/lib/git/url.rb +3 -3
- data/lib/git/version.rb +1 -1
- data/lib/git/worktree.rb +4 -5
- data/lib/git/worktrees.rb +4 -6
- data/lib/git.rb +17 -16
- metadata +40 -6
data/lib/git/branch.rb
CHANGED
@@ -3,7 +3,8 @@
|
|
3
3
|
require 'git/path'
|
4
4
|
|
5
5
|
module Git
|
6
|
-
|
6
|
+
# Represents a Git branch
|
7
|
+
class Branch
|
7
8
|
attr_accessor :full, :remote, :name
|
8
9
|
|
9
10
|
def initialize(base, name)
|
@@ -56,12 +57,12 @@ module Git
|
|
56
57
|
@base.lib.branch_delete(@name)
|
57
58
|
end
|
58
59
|
|
59
|
-
def current
|
60
|
-
|
60
|
+
def current # rubocop:disable Naming/PredicateMethod
|
61
|
+
@base.lib.branch_current == @name
|
61
62
|
end
|
62
63
|
|
63
64
|
def contains?(commit)
|
64
|
-
!@base.lib.branch_contains(commit,
|
65
|
+
!@base.lib.branch_contains(commit, name).empty?
|
65
66
|
end
|
66
67
|
|
67
68
|
def merge(branch = nil, message = nil)
|
@@ -93,16 +94,6 @@ module Git
|
|
93
94
|
@full
|
94
95
|
end
|
95
96
|
|
96
|
-
private
|
97
|
-
|
98
|
-
def check_if_create
|
99
|
-
@base.lib.branch_new(@name) rescue nil
|
100
|
-
end
|
101
|
-
|
102
|
-
def determine_current
|
103
|
-
@base.lib.branch_current == @name
|
104
|
-
end
|
105
|
-
|
106
97
|
BRANCH_NAME_REGEXP = %r{
|
107
98
|
^
|
108
99
|
# Optional 'refs/remotes/' at the beggining to specify a remote tracking branch
|
@@ -114,6 +105,8 @@ module Git
|
|
114
105
|
$
|
115
106
|
}x
|
116
107
|
|
108
|
+
private
|
109
|
+
|
117
110
|
# Given a full branch name return an Array containing the remote and branch names.
|
118
111
|
#
|
119
112
|
# Removes 'remotes' from the beggining of the name (if present).
|
@@ -139,7 +132,13 @@ module Git
|
|
139
132
|
match = name.match(BRANCH_NAME_REGEXP)
|
140
133
|
remote = match[:remote_name] ? Git::Remote.new(@base, match[:remote_name]) : nil
|
141
134
|
branch_name = match[:branch_name]
|
142
|
-
[
|
135
|
+
[remote, branch_name]
|
136
|
+
end
|
137
|
+
|
138
|
+
def check_if_create
|
139
|
+
@base.lib.branch_new(@name)
|
140
|
+
rescue StandardError
|
141
|
+
nil
|
143
142
|
end
|
144
143
|
end
|
145
144
|
end
|
data/lib/git/branches.rb
CHANGED
@@ -1,10 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Git
|
4
|
-
|
5
4
|
# object that holds all the available branches
|
6
5
|
class Branches
|
7
|
-
|
8
6
|
include Enumerable
|
9
7
|
|
10
8
|
def initialize(base)
|
@@ -18,11 +16,11 @@ module Git
|
|
18
16
|
end
|
19
17
|
|
20
18
|
def local
|
21
|
-
|
19
|
+
reject(&:remote)
|
22
20
|
end
|
23
21
|
|
24
22
|
def remote
|
25
|
-
self.select
|
23
|
+
self.select(&:remote)
|
26
24
|
end
|
27
25
|
|
28
26
|
# array like methods
|
@@ -31,8 +29,8 @@ module Git
|
|
31
29
|
@branches.size
|
32
30
|
end
|
33
31
|
|
34
|
-
def each(&
|
35
|
-
@branches.values.each(&
|
32
|
+
def each(&)
|
33
|
+
@branches.values.each(&)
|
36
34
|
end
|
37
35
|
|
38
36
|
# Returns the target branch
|
@@ -49,24 +47,22 @@ module Git
|
|
49
47
|
# @param [#to_s] branch_name the target branch name.
|
50
48
|
# @return [Git::Branch] the target branch.
|
51
49
|
def [](branch_name)
|
52
|
-
@branches.values.
|
50
|
+
@branches.values.each_with_object(@branches) do |branch, branches|
|
53
51
|
branches[branch.full] ||= branch
|
54
52
|
|
55
53
|
# This is how Git (version 1.7.9.5) works.
|
56
|
-
# Lets you ignore the 'remotes' if its at the beginning of the branch full
|
57
|
-
|
58
|
-
|
59
|
-
branches
|
54
|
+
# Lets you ignore the 'remotes' if its at the beginning of the branch full
|
55
|
+
# name (even if is not a real remote branch).
|
56
|
+
branches[branch.full.sub('remotes/', '')] ||= branch if branch.full =~ %r{^remotes/.+}
|
60
57
|
end[branch_name.to_s]
|
61
58
|
end
|
62
59
|
|
63
60
|
def to_s
|
64
61
|
out = ''
|
65
|
-
@branches.
|
62
|
+
@branches.each_value do |b|
|
66
63
|
out << (b.current ? '* ' : ' ') << b.to_s << "\n"
|
67
64
|
end
|
68
65
|
out
|
69
66
|
end
|
70
67
|
end
|
71
|
-
|
72
68
|
end
|
data/lib/git/command_line.rb
CHANGED
@@ -97,6 +97,10 @@ module Git
|
|
97
97
|
|
98
98
|
# Execute a git command, wait for it to finish, and return the result
|
99
99
|
#
|
100
|
+
# Non-option the command line arguements to pass to git. If you collect
|
101
|
+
# the command line arguments in an array, make sure you splat the array
|
102
|
+
# into the parameter list.
|
103
|
+
#
|
100
104
|
# NORMALIZATION
|
101
105
|
#
|
102
106
|
# The command output is returned as a Unicde string containing the binary output
|
@@ -142,11 +146,9 @@ module Git
|
|
142
146
|
# stderr.string #=> "unknown revision or path not in the working tree.\n"
|
143
147
|
# end
|
144
148
|
#
|
145
|
-
# @param
|
146
|
-
#
|
147
|
-
# This array should be splatted into the parameter list.
|
149
|
+
# @param options_hash [Hash] the options to pass to the command
|
148
150
|
#
|
149
|
-
# @
|
151
|
+
# @option options_hash [#write, nil] :out the object to write stdout to or nil to ignore stdout
|
150
152
|
#
|
151
153
|
# If this is a 'StringIO' object, then `stdout_writer.string` will be returned.
|
152
154
|
#
|
@@ -154,20 +156,20 @@ module Git
|
|
154
156
|
# stdout to a file or some other object that responds to `#write`. The default
|
155
157
|
# behavior will return the output of the command.
|
156
158
|
#
|
157
|
-
# @
|
159
|
+
# @option options_hash [#write, nil] :err the object to write stderr to or nil to ignore stderr
|
158
160
|
#
|
159
161
|
# If this is a 'StringIO' object and `merged_output` is `true`, then
|
160
162
|
# `stderr_writer.string` will be merged into the output returned by this method.
|
161
163
|
#
|
162
|
-
# @
|
164
|
+
# @option options_hash [Boolean] :normalize whether to normalize the output of stdout and stderr
|
163
165
|
#
|
164
|
-
# @
|
166
|
+
# @option options_hash [Boolean] :chomp whether to chomp both stdout and stderr output
|
165
167
|
#
|
166
|
-
# @
|
168
|
+
# @option options_hash [Boolean] :merge whether to merge stdout and stderr in the string returned
|
167
169
|
#
|
168
|
-
# @
|
170
|
+
# @option options_hash [String, nil] :chdir the directory to run the command in
|
169
171
|
#
|
170
|
-
# @
|
172
|
+
# @option options_hash [Numeric, nil] :timeout the maximum seconds to wait for the command to complete
|
171
173
|
#
|
172
174
|
# If timeout is zero, the timeout will not be enforced.
|
173
175
|
#
|
@@ -189,16 +191,50 @@ module Git
|
|
189
191
|
#
|
190
192
|
# @raise [Git::TimeoutError] if the command times out
|
191
193
|
#
|
192
|
-
def run(
|
194
|
+
def run(*, **options_hash)
|
195
|
+
options_hash = RUN_ARGS.merge(options_hash)
|
196
|
+
extra_options = options_hash.keys - RUN_ARGS.keys
|
197
|
+
raise ArgumentError, "Unknown options: #{extra_options.join(', ')}" if extra_options.any?
|
198
|
+
|
199
|
+
result = run_with_capture(*, **options_hash)
|
200
|
+
process_result(result, options_hash[:normalize], options_hash[:chomp], options_hash[:timeout])
|
201
|
+
end
|
202
|
+
|
203
|
+
# @return [Git::CommandLineResult] the result of running the command
|
204
|
+
#
|
205
|
+
# @api private
|
206
|
+
#
|
207
|
+
def run_with_capture(*args, **options_hash)
|
193
208
|
git_cmd = build_git_cmd(args)
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
209
|
+
options = run_with_capture_options(**options_hash)
|
210
|
+
ProcessExecuter.run_with_capture(env, *git_cmd, **options)
|
211
|
+
rescue ProcessExecuter::ProcessIOError => e
|
212
|
+
raise Git::ProcessIOError.new(e.message), cause: e.exception.cause
|
213
|
+
end
|
214
|
+
|
215
|
+
def run_with_capture_options(**options_hash)
|
216
|
+
chdir = options_hash[:chdir] || :not_set
|
217
|
+
timeout_after = options_hash[:timeout]
|
218
|
+
out = options_hash[:out]
|
219
|
+
err = options_hash[:err]
|
220
|
+
merge_output = options_hash[:merge] || false
|
221
|
+
|
222
|
+
{ chdir:, timeout_after:, merge_output:, raise_errors: false }.tap do |options|
|
223
|
+
options[:out] = out unless out.nil?
|
224
|
+
options[:err] = err unless err.nil?
|
198
225
|
end
|
199
|
-
process_result(result, normalize, chomp, timeout)
|
200
226
|
end
|
201
227
|
|
228
|
+
RUN_ARGS = {
|
229
|
+
normalize: false,
|
230
|
+
chomp: false,
|
231
|
+
merge: false,
|
232
|
+
out: nil,
|
233
|
+
err: nil,
|
234
|
+
chdir: nil,
|
235
|
+
timeout: nil
|
236
|
+
}.freeze
|
237
|
+
|
202
238
|
private
|
203
239
|
|
204
240
|
# Build the git command line from the available sources to send to `Process.spawn`
|
@@ -206,9 +242,9 @@ module Git
|
|
206
242
|
# @api private
|
207
243
|
#
|
208
244
|
def build_git_cmd(args)
|
209
|
-
raise ArgumentError
|
245
|
+
raise ArgumentError, 'The args array can not contain an array' if args.any? { |a| a.is_a?(Array) }
|
210
246
|
|
211
|
-
[binary_path, *global_opts, *args].map
|
247
|
+
[binary_path, *global_opts, *args].map(&:to_s)
|
212
248
|
end
|
213
249
|
|
214
250
|
# Process the result of the command and return a Git::CommandLineResult
|
@@ -216,71 +252,78 @@ module Git
|
|
216
252
|
# Post process output, log the command and result, and raise an error if the
|
217
253
|
# command failed.
|
218
254
|
#
|
219
|
-
# @param result [ProcessExecuter::Command::Result] the result it is a
|
255
|
+
# @param result [ProcessExecuter::Command::Result] the result it is a
|
256
|
+
# Process::Status and include command, stdout, and stderr
|
257
|
+
#
|
220
258
|
# @param normalize [Boolean] whether to normalize the output of each writer
|
259
|
+
#
|
221
260
|
# @param chomp [Boolean] whether to chomp the output of each writer
|
222
|
-
# @param timeout [Numeric, nil] the maximum seconds to wait for the command to complete
|
223
261
|
#
|
224
|
-
# @
|
262
|
+
# @param timeout [Numeric, nil] the maximum seconds to wait for the command to
|
263
|
+
# complete
|
264
|
+
#
|
265
|
+
# @return [Git::CommandLineResult] the result of the command to return to the
|
266
|
+
# caller
|
225
267
|
#
|
226
268
|
# @raise [Git::FailedError] if the command failed
|
269
|
+
#
|
227
270
|
# @raise [Git::SignaledError] if the command was signaled
|
271
|
+
#
|
228
272
|
# @raise [Git::TimeoutError] if the command times out
|
229
|
-
#
|
273
|
+
#
|
274
|
+
# @raise [Git::ProcessIOError] if an exception was raised while collecting
|
275
|
+
# subprocess output
|
230
276
|
#
|
231
277
|
# @api private
|
232
278
|
#
|
233
279
|
def process_result(result, normalize, chomp, timeout)
|
234
280
|
command = result.command
|
235
|
-
processed_out, processed_err =
|
281
|
+
processed_out, processed_err = post_process_output(result, normalize, chomp)
|
282
|
+
log_result(result, command, processed_out, processed_err)
|
283
|
+
command_line_result(command, result, processed_out, processed_err, timeout)
|
284
|
+
end
|
285
|
+
|
286
|
+
def log_result(result, command, processed_out, processed_err)
|
236
287
|
logger.info { "#{command} exited with status #{result}" }
|
237
288
|
logger.debug { "stdout:\n#{processed_out.inspect}\nstderr:\n#{processed_err.inspect}" }
|
289
|
+
end
|
290
|
+
|
291
|
+
def command_line_result(command, result, processed_out, processed_err, timeout)
|
238
292
|
Git::CommandLineResult.new(command, result, processed_out, processed_err).tap do |processed_result|
|
239
293
|
raise Git::TimeoutError.new(processed_result, timeout) if result.timeout?
|
240
|
-
|
241
|
-
raise Git::
|
294
|
+
|
295
|
+
raise Git::SignaledError, processed_result if result.signaled?
|
296
|
+
|
297
|
+
raise Git::FailedError, processed_result unless result.success?
|
242
298
|
end
|
243
299
|
end
|
244
300
|
|
245
|
-
# Post-process
|
301
|
+
# Post-process and return an array of raw output strings
|
246
302
|
#
|
247
|
-
#
|
248
|
-
# @param normalize [Boolean] whether to normalize the output of each writer
|
249
|
-
# @param chomp [Boolean] whether to chomp the output of each writer
|
303
|
+
# For each raw output string:
|
250
304
|
#
|
251
|
-
#
|
305
|
+
# * If normalize: is true, normalize the encoding by transcoding each line from
|
306
|
+
# the detected encoding to UTF-8.
|
307
|
+
# * If chomp: is true chomp the output after normalization.
|
252
308
|
#
|
253
|
-
#
|
254
|
-
#
|
255
|
-
def post_process_all(raw_outputs, normalize, chomp)
|
256
|
-
Array.new.tap do |result|
|
257
|
-
raw_outputs.each { |raw_output| result << post_process(raw_output, normalize, chomp) }
|
258
|
-
end
|
259
|
-
end
|
260
|
-
|
261
|
-
# Determine the output to return in the `CommandLineResult`
|
309
|
+
# Even if no post-processing is done based on the options, the strings returned
|
310
|
+
# are a copy of the raw output strings. The raw output strings are not modified.
|
262
311
|
#
|
263
|
-
#
|
264
|
-
# then return the result of normalizing the encoding and chomping the output
|
265
|
-
# as requested.
|
312
|
+
# @param result [ProcessExecuter::ResultWithCapture] the command's output to post-process
|
266
313
|
#
|
267
|
-
#
|
268
|
-
#
|
269
|
-
# is a file instead of a StringIO.
|
314
|
+
# @param normalize [Boolean] whether to normalize the output of each writer
|
315
|
+
# @param chomp [Boolean] whether to chomp the output of each writer
|
270
316
|
#
|
271
|
-
# @
|
272
|
-
# @return [String, nil]
|
317
|
+
# @return [Array<String>]
|
273
318
|
#
|
274
319
|
# @api private
|
275
320
|
#
|
276
|
-
def
|
277
|
-
|
278
|
-
output = raw_output.
|
321
|
+
def post_process_output(result, normalize, chomp)
|
322
|
+
[result.stdout, result.stderr].map do |raw_output|
|
323
|
+
output = raw_output.dup
|
279
324
|
output = output.lines.map { |l| Git::EncodingUtils.normalize_encoding(l) }.join if normalize
|
280
325
|
output.chomp! if chomp
|
281
326
|
output
|
282
|
-
else
|
283
|
-
nil
|
284
327
|
end
|
285
328
|
end
|
286
329
|
end
|
@@ -19,15 +19,21 @@ module Git
|
|
19
19
|
# result = Git::CommandLineResult.new(git_cmd, status, stdout, stderr)
|
20
20
|
#
|
21
21
|
# @param git_cmd [Array<String>] the git command that was executed
|
22
|
-
# @param status [
|
23
|
-
# @param stdout [String] the
|
24
|
-
# @param stderr [String] the
|
22
|
+
# @param status [ProcessExecuter::ResultWithCapture] the status of the process
|
23
|
+
# @param stdout [String] the processed stdout of the process
|
24
|
+
# @param stderr [String] the processed stderr of the process
|
25
25
|
#
|
26
26
|
def initialize(git_cmd, status, stdout, stderr)
|
27
27
|
@git_cmd = git_cmd
|
28
28
|
@status = status
|
29
29
|
@stdout = stdout
|
30
30
|
@stderr = stderr
|
31
|
+
|
32
|
+
# ProcessExecuter::ResultWithCapture changed the timeout? method to timed_out?
|
33
|
+
# in version 4.x. This is a compatibility layer to maintain the old method name
|
34
|
+
# for backward compatibility.
|
35
|
+
#
|
36
|
+
status.define_singleton_method(:timeout?) { timed_out? }
|
31
37
|
end
|
32
38
|
|
33
39
|
# @attribute [r] git_cmd
|
data/lib/git/config.rb
CHANGED
@@ -1,9 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Git
|
4
|
-
|
4
|
+
# The global configuration for this gem
|
5
5
|
class Config
|
6
|
-
|
7
6
|
attr_writer :binary_path, :git_ssh, :timeout
|
8
7
|
|
9
8
|
def initialize
|
@@ -13,16 +12,15 @@ module Git
|
|
13
12
|
end
|
14
13
|
|
15
14
|
def binary_path
|
16
|
-
@binary_path || ENV
|
15
|
+
@binary_path || (ENV.fetch('GIT_PATH', nil) && File.join(ENV.fetch('GIT_PATH', nil), 'git')) || 'git'
|
17
16
|
end
|
18
17
|
|
19
18
|
def git_ssh
|
20
|
-
@git_ssh || ENV
|
19
|
+
@git_ssh || ENV.fetch('GIT_SSH', nil)
|
21
20
|
end
|
22
21
|
|
23
22
|
def timeout
|
24
|
-
@timeout || (ENV
|
23
|
+
@timeout || (ENV.fetch('GIT_TIMEOUT', nil) && ENV['GIT_TIMEOUT'].to_i)
|
25
24
|
end
|
26
25
|
end
|
27
|
-
|
28
26
|
end
|
data/lib/git/diff.rb
CHANGED
@@ -1,80 +1,87 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require_relative 'diff_path_status'
|
4
|
+
require_relative 'diff_stats'
|
4
5
|
|
5
|
-
|
6
|
+
module Git
|
7
|
+
# object that holds the diff between two commits
|
6
8
|
class Diff
|
7
9
|
include Enumerable
|
8
10
|
|
9
11
|
def initialize(base, from = nil, to = nil)
|
10
12
|
@base = base
|
11
|
-
@from = from
|
12
|
-
@to = to
|
13
|
+
@from = from&.to_s
|
14
|
+
@to = to&.to_s
|
13
15
|
|
14
16
|
@path = nil
|
15
|
-
@full_diff = nil
|
16
17
|
@full_diff_files = nil
|
17
|
-
@stats = nil
|
18
18
|
end
|
19
19
|
attr_reader :from, :to
|
20
20
|
|
21
|
-
def name_status
|
22
|
-
cache_name_status
|
23
|
-
end
|
24
|
-
|
25
21
|
def path(path)
|
26
22
|
@path = path
|
27
|
-
|
23
|
+
self
|
28
24
|
end
|
29
25
|
|
30
|
-
def
|
31
|
-
|
32
|
-
@stats[:total][:files]
|
26
|
+
def patch
|
27
|
+
@base.lib.diff_full(@from, @to, { path_limiter: @path })
|
33
28
|
end
|
29
|
+
alias to_s patch
|
34
30
|
|
35
|
-
def
|
36
|
-
|
37
|
-
@
|
31
|
+
def [](key)
|
32
|
+
process_full
|
33
|
+
@full_diff_files.assoc(key)[1]
|
38
34
|
end
|
39
35
|
|
40
|
-
def
|
41
|
-
|
42
|
-
@
|
36
|
+
def each(&)
|
37
|
+
process_full
|
38
|
+
@full_diff_files.map { |file| file[1] }.each(&)
|
43
39
|
end
|
44
40
|
|
45
|
-
|
46
|
-
|
47
|
-
|
41
|
+
#
|
42
|
+
# DEPRECATED METHODS
|
43
|
+
#
|
44
|
+
|
45
|
+
def name_status
|
46
|
+
Git::Deprecation.warn('Git::Diff#name_status is deprecated. Use Git::Base#diff_path_status instead.')
|
47
|
+
path_status_provider.to_h
|
48
48
|
end
|
49
49
|
|
50
|
-
def
|
51
|
-
|
52
|
-
|
50
|
+
def size
|
51
|
+
Git::Deprecation.warn('Git::Diff#size is deprecated. Use Git::Base#diff_stats(...).total[:files] instead.')
|
52
|
+
stats_provider.total[:files]
|
53
53
|
end
|
54
54
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
@full_diff
|
55
|
+
def lines
|
56
|
+
Git::Deprecation.warn('Git::Diff#lines is deprecated. Use Git::Base#diff_stats(...).lines instead.')
|
57
|
+
stats_provider.lines
|
59
58
|
end
|
60
|
-
alias_method :to_s, :patch
|
61
59
|
|
62
|
-
|
60
|
+
def deletions
|
61
|
+
Git::Deprecation.warn('Git::Diff#deletions is deprecated. Use Git::Base#diff_stats(...).deletions instead.')
|
62
|
+
stats_provider.deletions
|
63
|
+
end
|
63
64
|
|
64
|
-
def
|
65
|
-
|
66
|
-
|
65
|
+
def insertions
|
66
|
+
Git::Deprecation.warn('Git::Diff#insertions is deprecated. Use Git::Base#diff_stats(...).insertions instead.')
|
67
|
+
stats_provider.insertions
|
67
68
|
end
|
68
69
|
|
69
|
-
def
|
70
|
-
|
71
|
-
|
70
|
+
def stats
|
71
|
+
Git::Deprecation.warn('Git::Diff#stats is deprecated. Use Git::Base#diff_stats instead.')
|
72
|
+
# CORRECTED: Re-create the original hash structure for backward compatibility
|
73
|
+
{
|
74
|
+
files: stats_provider.files,
|
75
|
+
total: stats_provider.total
|
76
|
+
}
|
72
77
|
end
|
73
78
|
|
79
|
+
# The changes for a single file within a diff
|
74
80
|
class DiffFile
|
75
81
|
attr_accessor :patch, :path, :mode, :src, :dst, :type
|
82
|
+
|
76
83
|
@base = nil
|
77
|
-
NIL_BLOB_REGEXP = /\A0{4,40}\z
|
84
|
+
NIL_BLOB_REGEXP = /\A0{4,40}\z/
|
78
85
|
|
79
86
|
def initialize(base, hash)
|
80
87
|
@base = base
|
@@ -102,56 +109,84 @@ module Git
|
|
102
109
|
|
103
110
|
private
|
104
111
|
|
105
|
-
|
106
|
-
|
112
|
+
def process_full
|
113
|
+
return if @full_diff_files
|
114
|
+
|
115
|
+
@full_diff_files = process_full_diff
|
116
|
+
end
|
117
|
+
|
118
|
+
def path_status_provider
|
119
|
+
@path_status_provider ||= Git::DiffPathStatus.new(@base, @from, @to, @path)
|
120
|
+
end
|
121
|
+
|
122
|
+
def stats_provider
|
123
|
+
@stats_provider ||= Git::DiffStats.new(@base, @from, @to, @path)
|
124
|
+
end
|
125
|
+
|
126
|
+
def process_full_diff
|
127
|
+
FullDiffParser.new(@base, patch).parse
|
128
|
+
end
|
129
|
+
|
130
|
+
# A private parser class to process the output of `git diff`
|
131
|
+
# @api private
|
132
|
+
class FullDiffParser
|
133
|
+
def initialize(base, patch_text)
|
134
|
+
@base = base
|
135
|
+
@patch_text = patch_text
|
136
|
+
@final_files = {}
|
137
|
+
@current_file_data = nil
|
138
|
+
@defaults = { mode: '', src: '', dst: '', type: 'modified', binary: false }
|
107
139
|
end
|
108
140
|
|
109
|
-
def
|
110
|
-
|
111
|
-
|
112
|
-
@full_diff_files = process_full_diff
|
141
|
+
def parse
|
142
|
+
@patch_text.split("\n").each { |line| process_line(line) }
|
143
|
+
@final_files.map { |filename, data| [filename, DiffFile.new(@base, data)] }
|
113
144
|
end
|
114
145
|
|
115
|
-
|
116
|
-
|
146
|
+
private
|
147
|
+
|
148
|
+
def process_line(line)
|
149
|
+
if (new_file_match = line.match(%r{\Adiff --git ("?)a/(.+?)\1 ("?)b/(.+?)\3\z}))
|
150
|
+
start_new_file(new_file_match, line)
|
151
|
+
else
|
152
|
+
append_to_current_file(line)
|
153
|
+
end
|
117
154
|
end
|
118
155
|
|
119
|
-
def
|
120
|
-
|
156
|
+
def start_new_file(match, line)
|
157
|
+
filename = Git::EscapedPath.new(match[2]).unescape
|
158
|
+
@current_file_data = @defaults.merge({ patch: line, path: filename })
|
159
|
+
@final_files[filename] = @current_file_data
|
121
160
|
end
|
122
161
|
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
}
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
end
|
147
|
-
if m = /^Binary files /.match(line)
|
148
|
-
final[current_file][:binary] = true
|
149
|
-
end
|
150
|
-
final[current_file][:patch] << "\n" + line
|
151
|
-
end
|
152
|
-
end
|
153
|
-
final.map { |e| [e[0], DiffFile.new(@base, e[1])] }
|
162
|
+
def append_to_current_file(line)
|
163
|
+
return unless @current_file_data
|
164
|
+
|
165
|
+
parse_index_line(line)
|
166
|
+
parse_file_mode_line(line)
|
167
|
+
check_for_binary(line)
|
168
|
+
|
169
|
+
@current_file_data[:patch] << "\n#{line}"
|
170
|
+
end
|
171
|
+
|
172
|
+
def parse_index_line(line)
|
173
|
+
return unless (match = line.match(/^index ([0-9a-f]{4,40})\.\.([0-9a-f]{4,40})( ......)*/))
|
174
|
+
|
175
|
+
@current_file_data[:src] = match[1]
|
176
|
+
@current_file_data[:dst] = match[2]
|
177
|
+
@current_file_data[:mode] = match[3].strip if match[3]
|
178
|
+
end
|
179
|
+
|
180
|
+
def parse_file_mode_line(line)
|
181
|
+
return unless (match = line.match(/^([[:alpha:]]*?) file mode (......)/))
|
182
|
+
|
183
|
+
@current_file_data[:type] = match[1]
|
184
|
+
@current_file_data[:mode] = match[2]
|
154
185
|
end
|
155
186
|
|
187
|
+
def check_for_binary(line)
|
188
|
+
@current_file_data[:binary] = true if line.match?(/^Binary files /)
|
189
|
+
end
|
190
|
+
end
|
156
191
|
end
|
157
192
|
end
|