git 1.19.1 → 4.4.5

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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/.commitlintrc.yml +38 -0
  3. data/.github/copilot-instructions.md +2733 -0
  4. data/.github/pull_request_template.md +17 -0
  5. data/.github/workflows/continuous_integration.yml +92 -21
  6. data/.github/workflows/enforce_conventional_commits.yml +29 -0
  7. data/.github/workflows/experimental_continuous_integration.yml +59 -0
  8. data/.github/workflows/release.yml +53 -0
  9. data/.gitignore +5 -0
  10. data/.husky/commit-msg +1 -0
  11. data/.release-please-manifest.json +3 -0
  12. data/.rubocop.yml +55 -0
  13. data/.rubocop_todo.yml +12 -0
  14. data/.yardopts +4 -1
  15. data/AI_POLICY.md +24 -0
  16. data/CHANGELOG.md +501 -0
  17. data/CODE_OF_CONDUCT.md +25 -0
  18. data/CONTRIBUTING.md +323 -102
  19. data/GOVERNANCE.md +106 -0
  20. data/LICENSE +1 -1
  21. data/MAINTAINERS.md +17 -4
  22. data/README.md +575 -246
  23. data/Rakefile +13 -55
  24. data/git.gemspec +36 -30
  25. data/lib/git/args_builder.rb +111 -0
  26. data/lib/git/author.rb +9 -7
  27. data/lib/git/base.rb +602 -173
  28. data/lib/git/branch.rb +318 -38
  29. data/lib/git/branches.rb +21 -24
  30. data/lib/git/command_line.rb +330 -0
  31. data/lib/git/command_line_result.rb +9 -3
  32. data/lib/git/config.rb +10 -6
  33. data/lib/git/diff.rb +149 -81
  34. data/lib/git/diff_path_status.rb +46 -0
  35. data/lib/git/diff_stats.rb +59 -0
  36. data/lib/git/errors.rb +212 -0
  37. data/lib/git/escaped_path.rb +2 -2
  38. data/lib/git/fsck_object.rb +48 -0
  39. data/lib/git/fsck_result.rb +121 -0
  40. data/lib/git/index.rb +2 -1
  41. data/lib/git/lib.rb +1648 -643
  42. data/lib/git/log.rb +143 -106
  43. data/lib/git/object.rb +151 -125
  44. data/lib/git/path.rb +23 -16
  45. data/lib/git/remote.rb +5 -4
  46. data/lib/git/repository.rb +2 -2
  47. data/lib/git/stash.rb +11 -12
  48. data/lib/git/stashes.rb +16 -15
  49. data/lib/git/status.rb +104 -143
  50. data/lib/git/url.rb +3 -3
  51. data/lib/git/version.rb +3 -1
  52. data/lib/git/working_directory.rb +2 -0
  53. data/lib/git/worktree.rb +6 -5
  54. data/lib/git/worktrees.rb +6 -6
  55. data/lib/git.rb +131 -28
  56. data/package.json +10 -0
  57. data/redesign/1_architecture_existing.md +66 -0
  58. data/redesign/2_architecture_redesign.md +130 -0
  59. data/redesign/3_architecture_implementation.md +138 -0
  60. data/redesign/index.md +34 -0
  61. data/release-please-config.json +36 -0
  62. data/tasks/gem_tasks.rake +10 -0
  63. data/tasks/rubocop.rake +12 -0
  64. data/tasks/test.rake +13 -0
  65. data/tasks/test_gem.rake +12 -0
  66. data/tasks/yard.rake +23 -0
  67. metadata +114 -37
  68. data/.github/stale.yml +0 -25
  69. data/Dockerfile.changelog-rs +0 -12
  70. data/PULL_REQUEST_TEMPLATE.md +0 -9
  71. data/RELEASING.md +0 -70
  72. data/lib/git/base/factory.rb +0 -99
  73. data/lib/git/failed_error.rb +0 -53
  74. data/lib/git/git_execute_error.rb +0 -7
  75. data/lib/git/signaled_error.rb +0 -50
  76. /data/{ISSUE_TEMPLATE.md → .github/issue_template.md} +0 -0
@@ -0,0 +1,330 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git/base'
4
+ require 'git/command_line_result'
5
+ require 'git/errors'
6
+ require 'stringio'
7
+
8
+ module Git
9
+ # Runs a git command and returns the result
10
+ #
11
+ # @api public
12
+ #
13
+ class CommandLine
14
+ # Create a Git::CommandLine object
15
+ #
16
+ # @example
17
+ # env = { 'GIT_DIR' => '/path/to/git/dir' }
18
+ # binary_path = '/usr/bin/git'
19
+ # global_opts = %w[--git-dir /path/to/git/dir]
20
+ # logger = Logger.new(STDOUT)
21
+ # cli = CommandLine.new(env, binary_path, global_opts, logger)
22
+ # cli.run('version') #=> #<Git::CommandLineResult:0x00007f9b0c0b0e00
23
+ #
24
+ # @param env [Hash<String, String>] environment variables to set
25
+ # @param global_opts [Array<String>] global options to pass to git
26
+ # @param logger [Logger] the logger to use
27
+ #
28
+ def initialize(env, binary_path, global_opts, logger)
29
+ @env = env
30
+ @binary_path = binary_path
31
+ @global_opts = global_opts
32
+ @logger = logger
33
+ end
34
+
35
+ # @attribute [r] env
36
+ #
37
+ # Variables to set (or unset) in the git command's environment
38
+ #
39
+ # @example
40
+ # env = { 'GIT_DIR' => '/path/to/git/dir' }
41
+ # command_line = Git::CommandLine.new(env, '/usr/bin/git', [], Logger.new(STDOUT))
42
+ # command_line.env #=> { 'GIT_DIR' => '/path/to/git/dir' }
43
+ #
44
+ # @return [Hash<String, String>]
45
+ #
46
+ # @see https://ruby-doc.org/3.2.1/Process.html#method-c-spawn Process.spawn
47
+ # for details on how to set environment variables using the `env` parameter
48
+ #
49
+ attr_reader :env
50
+
51
+ # @attribute [r] binary_path
52
+ #
53
+ # The path to the command line binary to run
54
+ #
55
+ # @example
56
+ # binary_path = '/usr/bin/git'
57
+ # command_line = Git::CommandLine.new({}, binary_path, ['version'], Logger.new(STDOUT))
58
+ # command_line.binary_path #=> '/usr/bin/git'
59
+ #
60
+ # @return [String]
61
+ #
62
+ attr_reader :binary_path
63
+
64
+ # @attribute [r] global_opts
65
+ #
66
+ # The global options to pass to git
67
+ #
68
+ # These are options that are passed to git before the command name and
69
+ # arguments. For example, in `git --git-dir /path/to/git/dir version`, the
70
+ # global options are %w[--git-dir /path/to/git/dir].
71
+ #
72
+ # @example
73
+ # env = {}
74
+ # global_opts = %w[--git-dir /path/to/git/dir]
75
+ # logger = Logger.new(nil)
76
+ # cli = CommandLine.new(env, '/usr/bin/git', global_opts, logger)
77
+ # cli.global_opts #=> %w[--git-dir /path/to/git/dir]
78
+ #
79
+ # @return [Array<String>]
80
+ #
81
+ attr_reader :global_opts
82
+
83
+ # @attribute [r] logger
84
+ #
85
+ # The logger to use for logging git commands and results
86
+ #
87
+ # @example
88
+ # env = {}
89
+ # global_opts = %w[]
90
+ # logger = Logger.new(STDOUT)
91
+ # cli = CommandLine.new(env, '/usr/bin/git', global_opts, logger)
92
+ # cli.logger == logger #=> true
93
+ #
94
+ # @return [Logger]
95
+ #
96
+ attr_reader :logger
97
+
98
+ # Execute a git command, wait for it to finish, and return the result
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
+ #
104
+ # NORMALIZATION
105
+ #
106
+ # The command output is returned as a Unicde string containing the binary output
107
+ # from the command. If the binary output is not valid UTF-8, the output will
108
+ # cause problems because the encoding will be invalid.
109
+ #
110
+ # Normalization is a process that trys to convert the binary output to a valid
111
+ # UTF-8 string. It uses the `rchardet` gem to detect the encoding of the binary
112
+ # output and then converts it to UTF-8.
113
+ #
114
+ # Normalization is not enabled by default. Pass `normalize: true` to Git::CommandLine#run
115
+ # to enable it. Normalization will only be performed on stdout and only if the `out:`` option
116
+ # is nil or is a StringIO object. If the out: option is set to a file or other IO object,
117
+ # the normalize option will be ignored.
118
+ #
119
+ # @example Run a command and return the output
120
+ # cli.run('version') #=> "git version 2.39.1\n"
121
+ #
122
+ # @example The args array should be splatted into the parameter list
123
+ # args = %w[log -n 1 --oneline]
124
+ # cli.run(*args) #=> "f5baa11 beginning of Ruby/Git project\n"
125
+ #
126
+ # @example Run a command and return the chomped output
127
+ # cli.run('version', chomp: true) #=> "git version 2.39.1"
128
+ #
129
+ # @example Run a command and without normalizing the output
130
+ # cli.run('version', normalize: false) #=> "git version 2.39.1\n"
131
+ #
132
+ # @example Capture stdout in a temporary file
133
+ # require 'tempfile'
134
+ # tempfile = Tempfile.create('git') do |file|
135
+ # cli.run('version', out: file)
136
+ # file.rewind
137
+ # file.read #=> "git version 2.39.1\n"
138
+ # end
139
+ #
140
+ # @example Capture stderr in a StringIO object
141
+ # require 'stringio'
142
+ # stderr = StringIO.new
143
+ # begin
144
+ # cli.run('log', 'nonexistent-branch', err: stderr)
145
+ # rescue Git::FailedError => e
146
+ # stderr.string #=> "unknown revision or path not in the working tree.\n"
147
+ # end
148
+ #
149
+ # @param options_hash [Hash] the options to pass to the command
150
+ #
151
+ # @option options_hash [#write, nil] :out the object to write stdout to or nil to ignore stdout
152
+ #
153
+ # If this is a 'StringIO' object, then `stdout_writer.string` will be returned.
154
+ #
155
+ # In general, only specify a `stdout_writer` object when you want to redirect
156
+ # stdout to a file or some other object that responds to `#write`. The default
157
+ # behavior will return the output of the command.
158
+ #
159
+ # @option options_hash [#write, nil] :err the object to write stderr to or nil to ignore stderr
160
+ #
161
+ # If this is a 'StringIO' object and `merged_output` is `true`, then
162
+ # `stderr_writer.string` will be merged into the output returned by this method.
163
+ #
164
+ # @option options_hash [Boolean] :normalize whether to normalize the output of stdout and stderr
165
+ #
166
+ # @option options_hash [Boolean] :chomp whether to chomp both stdout and stderr output
167
+ #
168
+ # @option options_hash [Boolean] :merge whether to merge stdout and stderr in the string returned
169
+ #
170
+ # @option options_hash [String, nil] :chdir the directory to run the command in
171
+ #
172
+ # @option options_hash [Numeric, nil] :timeout the maximum seconds to wait for the command to complete
173
+ #
174
+ # If timeout is zero, the timeout will not be enforced.
175
+ #
176
+ # If the command times out, it is killed via a `SIGKILL` signal and `Git::TimeoutError` is raised.
177
+ #
178
+ # If the command does not respond to SIGKILL, it will hang this method.
179
+ #
180
+ # @return [Git::CommandLineResult] the output of the command
181
+ #
182
+ # This result of running the command.
183
+ #
184
+ # @raise [ArgumentError] if `args` is not an array of strings
185
+ #
186
+ # @raise [Git::SignaledError] if the command was terminated because of an uncaught signal
187
+ #
188
+ # @raise [Git::FailedError] if the command returned a non-zero exitstatus
189
+ #
190
+ # @raise [Git::ProcessIOError] if an exception was raised while collecting subprocess output
191
+ #
192
+ # @raise [Git::TimeoutError] if the command times out
193
+ #
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)
208
+ git_cmd = build_git_cmd(args)
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?
225
+ end
226
+ end
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
+
238
+ private
239
+
240
+ # Build the git command line from the available sources to send to `Process.spawn`
241
+ # @return [Array<String>]
242
+ # @api private
243
+ #
244
+ def build_git_cmd(args)
245
+ raise ArgumentError, 'The args array can not contain an array' if args.any?(Array)
246
+
247
+ [binary_path, *global_opts, *args].map(&:to_s)
248
+ end
249
+
250
+ # Process the result of the command and return a Git::CommandLineResult
251
+ #
252
+ # Post process output, log the command and result, and raise an error if the
253
+ # command failed.
254
+ #
255
+ # @param result [ProcessExecuter::Command::Result] the result it is a
256
+ # Process::Status and include command, stdout, and stderr
257
+ #
258
+ # @param normalize [Boolean] whether to normalize the output of each writer
259
+ #
260
+ # @param chomp [Boolean] whether to chomp the output of each writer
261
+ #
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
267
+ #
268
+ # @raise [Git::FailedError] if the command failed
269
+ #
270
+ # @raise [Git::SignaledError] if the command was signaled
271
+ #
272
+ # @raise [Git::TimeoutError] if the command times out
273
+ #
274
+ # @raise [Git::ProcessIOError] if an exception was raised while collecting
275
+ # subprocess output
276
+ #
277
+ # @api private
278
+ #
279
+ def process_result(result, normalize, chomp, timeout)
280
+ command = result.command
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)
287
+ logger.info { "#{command} exited with status #{result}" }
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)
292
+ Git::CommandLineResult.new(command, result, processed_out, processed_err).tap do |processed_result|
293
+ raise Git::TimeoutError.new(processed_result, timeout) if result.timeout?
294
+
295
+ raise Git::SignaledError, processed_result if result.signaled?
296
+
297
+ raise Git::FailedError, processed_result unless result.success?
298
+ end
299
+ end
300
+
301
+ # Post-process and return an array of raw output strings
302
+ #
303
+ # For each raw output string:
304
+ #
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.
308
+ #
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.
311
+ #
312
+ # @param result [ProcessExecuter::ResultWithCapture] the command's output to post-process
313
+ #
314
+ # @param normalize [Boolean] whether to normalize the output of each writer
315
+ # @param chomp [Boolean] whether to chomp the output of each writer
316
+ #
317
+ # @return [Array<String>]
318
+ #
319
+ # @api private
320
+ #
321
+ def post_process_output(result, normalize, chomp)
322
+ [result.stdout, result.stderr].map do |raw_output|
323
+ output = raw_output.dup
324
+ output = output.lines.map { |l| Git::EncodingUtils.normalize_encoding(l) }.join if normalize
325
+ output.chomp! if chomp
326
+ output
327
+ end
328
+ end
329
+ end
330
+ 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 [Process::Status] the status of the process
23
- # @param stdout [String] the output of the process
24
- # @param stderr [String] the error output of the process
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,22 +1,26 @@
1
- module Git
1
+ # frozen_string_literal: true
2
2
 
3
+ module Git
4
+ # The global configuration for this gem
3
5
  class Config
4
-
5
- attr_writer :binary_path, :git_ssh
6
+ attr_writer :binary_path, :git_ssh, :timeout
6
7
 
7
8
  def initialize
8
9
  @binary_path = nil
9
10
  @git_ssh = nil
11
+ @timeout = nil
10
12
  end
11
13
 
12
14
  def binary_path
13
- @binary_path || ENV['GIT_PATH'] && File.join(ENV['GIT_PATH'], 'git') || 'git'
15
+ @binary_path || (ENV.fetch('GIT_PATH', nil) && File.join(ENV.fetch('GIT_PATH', nil), 'git')) || 'git'
14
16
  end
15
17
 
16
18
  def git_ssh
17
- @git_ssh || ENV['GIT_SSH']
19
+ @git_ssh || ENV.fetch('GIT_SSH', nil)
18
20
  end
19
21
 
22
+ def timeout
23
+ @timeout || (ENV.fetch('GIT_TIMEOUT', nil) && ENV['GIT_TIMEOUT'].to_i)
24
+ end
20
25
  end
21
-
22
26
  end
data/lib/git/diff.rb CHANGED
@@ -1,78 +1,110 @@
1
- module Git
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'diff_path_status'
4
+ require_relative 'diff_stats'
2
5
 
3
- # object that holds the last X commits on given branch
6
+ module Git
7
+ # object that holds the diff between two commits
4
8
  class Diff
5
9
  include Enumerable
6
10
 
7
11
  def initialize(base, from = nil, to = nil)
8
12
  @base = base
9
- @from = from && from.to_s
10
- @to = to && to.to_s
13
+ @from = from&.to_s
14
+ @to = to&.to_s
11
15
 
12
16
  @path = nil
13
- @full_diff = nil
14
17
  @full_diff_files = nil
15
- @stats = nil
16
18
  end
17
19
  attr_reader :from, :to
18
20
 
19
- def name_status
20
- cache_name_status
21
+ # Limits the diff to the specified path(s)
22
+ #
23
+ # When called with no arguments (or only nil arguments), removes any existing
24
+ # path filter, showing all files in the diff. Internally stores a single path
25
+ # as a String and multiple paths as an Array for efficiency.
26
+ #
27
+ # @example Limit diff to a single path
28
+ # git.diff('HEAD~3', 'HEAD').path('lib/')
29
+ #
30
+ # @example Limit diff to multiple paths
31
+ # git.diff('HEAD~3', 'HEAD').path('src/', 'docs/', 'README.md')
32
+ #
33
+ # @example Remove path filtering (show all files)
34
+ # diff.path # or diff.path(nil)
35
+ #
36
+ # @param paths [String, Pathname] one or more paths to filter the diff. Pass no arguments to remove filtering.
37
+ # @return [self] returns self for method chaining
38
+ # @raise [ArgumentError] if any path is an Array (use splatted arguments instead)
39
+ #
40
+ def path(*paths)
41
+ validate_paths_not_arrays(paths)
42
+
43
+ cleaned_paths = paths.compact
44
+
45
+ @path = if cleaned_paths.empty?
46
+ nil
47
+ elsif cleaned_paths.length == 1
48
+ cleaned_paths.first
49
+ else
50
+ cleaned_paths
51
+ end
52
+
53
+ self
21
54
  end
22
55
 
23
- def path(path)
24
- @path = path
25
- return self
56
+ def patch
57
+ @base.lib.diff_full(@from, @to, { path_limiter: @path })
26
58
  end
59
+ alias to_s patch
27
60
 
28
- def size
29
- cache_stats
30
- @stats[:total][:files]
61
+ def [](key)
62
+ process_full
63
+ @full_diff_files.assoc(key)[1]
31
64
  end
32
65
 
33
- def lines
34
- cache_stats
35
- @stats[:total][:lines]
66
+ def each(&)
67
+ process_full
68
+ @full_diff_files.map { |file| file[1] }.each(&)
36
69
  end
37
70
 
38
- def deletions
39
- cache_stats
40
- @stats[:total][:deletions]
71
+ def size
72
+ stats_provider.total[:files]
41
73
  end
42
74
 
43
- def insertions
44
- cache_stats
45
- @stats[:total][:insertions]
46
- end
75
+ #
76
+ # DEPRECATED METHODS
77
+ #
47
78
 
48
- def stats
49
- cache_stats
50
- @stats
79
+ def name_status
80
+ path_status_provider.to_h
51
81
  end
52
82
 
53
- # if file is provided and is writable, it will write the patch into the file
54
- def patch(file = nil)
55
- cache_full
56
- @full_diff
83
+ def lines
84
+ stats_provider.lines
57
85
  end
58
- alias_method :to_s, :patch
59
86
 
60
- # enumerable methods
87
+ def deletions
88
+ stats_provider.deletions
89
+ end
61
90
 
62
- def [](key)
63
- process_full
64
- @full_diff_files.assoc(key)[1]
91
+ def insertions
92
+ stats_provider.insertions
65
93
  end
66
94
 
67
- def each(&block) # :yields: each Git::DiffFile in turn
68
- process_full
69
- @full_diff_files.map { |file| file[1] }.each(&block)
95
+ def stats
96
+ {
97
+ files: stats_provider.files,
98
+ total: stats_provider.total
99
+ }
70
100
  end
71
101
 
102
+ # The changes for a single file within a diff
72
103
  class DiffFile
73
104
  attr_accessor :patch, :path, :mode, :src, :dst, :type
105
+
74
106
  @base = nil
75
- NIL_BLOB_REGEXP = /\A0{4,40}\z/.freeze
107
+ NIL_BLOB_REGEXP = /\A0{4,40}\z/
76
108
 
77
109
  def initialize(base, hash)
78
110
  @base = base
@@ -100,56 +132,92 @@ module Git
100
132
 
101
133
  private
102
134
 
103
- def cache_full
104
- @full_diff ||= @base.lib.diff_full(@from, @to, {:path_limiter => @path})
135
+ def validate_paths_not_arrays(paths)
136
+ return unless paths.any?(Array)
137
+
138
+ raise ArgumentError,
139
+ 'path expects individual arguments, not arrays. ' \
140
+ "Use path('lib/', 'docs/') not path(['lib/', 'docs/'])"
141
+ end
142
+
143
+ def process_full
144
+ return if @full_diff_files
145
+
146
+ @full_diff_files = process_full_diff
147
+ end
148
+
149
+ def path_status_provider
150
+ @path_status_provider ||= Git::DiffPathStatus.new(@base, @from, @to, @path)
151
+ end
152
+
153
+ def stats_provider
154
+ @stats_provider ||= Git::DiffStats.new(@base, @from, @to, @path)
155
+ end
156
+
157
+ def process_full_diff
158
+ FullDiffParser.new(@base, patch).parse
159
+ end
160
+
161
+ # A private parser class to process the output of `git diff`
162
+ # @api private
163
+ class FullDiffParser
164
+ def initialize(base, patch_text)
165
+ @base = base
166
+ @patch_text = patch_text
167
+ @final_files = {}
168
+ @current_file_data = nil
169
+ @defaults = { mode: '', src: '', dst: '', type: 'modified', binary: false }
105
170
  end
106
171
 
107
- def process_full
108
- return if @full_diff_files
109
- cache_full
110
- @full_diff_files = process_full_diff
172
+ def parse
173
+ @patch_text.split("\n").each { |line| process_line(line) }
174
+ @final_files.map { |filename, data| [filename, DiffFile.new(@base, data)] }
111
175
  end
112
176
 
113
- def cache_stats
114
- @stats ||= @base.lib.diff_stats(@from, @to, {:path_limiter => @path})
177
+ private
178
+
179
+ def process_line(line)
180
+ if (new_file_match = line.match(%r{\Adiff --git ("?)a/(.+?)\1 ("?)b/(.+?)\3\z}))
181
+ start_new_file(new_file_match, line)
182
+ else
183
+ append_to_current_file(line)
184
+ end
115
185
  end
116
186
 
117
- def cache_name_status
118
- @name_status ||= @base.lib.diff_name_status(@from, @to, {:path => @path})
187
+ def start_new_file(match, line)
188
+ filename = Git::EscapedPath.new(match[2]).unescape
189
+ @current_file_data = @defaults.merge({ patch: line, path: filename })
190
+ @final_files[filename] = @current_file_data
119
191
  end
120
192
 
121
- # break up @diff_full
122
- def process_full_diff
123
- defaults = {
124
- :mode => '',
125
- :src => '',
126
- :dst => '',
127
- :type => 'modified'
128
- }
129
- final = {}
130
- current_file = nil
131
- @full_diff.split("\n").each do |line|
132
- if m = %r{\Adiff --git ("?)a/(.+?)\1 ("?)b/(.+?)\3\z}.match(line)
133
- current_file = Git::EscapedPath.new(m[2]).unescape
134
- final[current_file] = defaults.merge({:patch => line, :path => current_file})
135
- else
136
- if m = /^index ([0-9a-f]{4,40})\.\.([0-9a-f]{4,40})( ......)*/.match(line)
137
- final[current_file][:src] = m[1]
138
- final[current_file][:dst] = m[2]
139
- final[current_file][:mode] = m[3].strip if m[3]
140
- end
141
- if m = /^([[:alpha:]]*?) file mode (......)/.match(line)
142
- final[current_file][:type] = m[1]
143
- final[current_file][:mode] = m[2]
144
- end
145
- if m = /^Binary files /.match(line)
146
- final[current_file][:binary] = true
147
- end
148
- final[current_file][:patch] << "\n" + line
149
- end
150
- end
151
- final.map { |e| [e[0], DiffFile.new(@base, e[1])] }
193
+ def append_to_current_file(line)
194
+ return unless @current_file_data
195
+
196
+ parse_index_line(line)
197
+ parse_file_mode_line(line)
198
+ check_for_binary(line)
199
+
200
+ @current_file_data[:patch] << "\n#{line}"
152
201
  end
153
202
 
203
+ def parse_index_line(line)
204
+ return unless (match = line.match(/^index ([0-9a-f]{4,40})\.\.([0-9a-f]{4,40})( ......)*/))
205
+
206
+ @current_file_data[:src] = match[1]
207
+ @current_file_data[:dst] = match[2]
208
+ @current_file_data[:mode] = match[3].strip if match[3]
209
+ end
210
+
211
+ def parse_file_mode_line(line)
212
+ return unless (match = line.match(/^([[:alpha:]]*?) file mode (......)/))
213
+
214
+ @current_file_data[:type] = match[1]
215
+ @current_file_data[:mode] = match[2]
216
+ end
217
+
218
+ def check_for_binary(line)
219
+ @current_file_data[:binary] = true if line.match?(/^Binary files /)
220
+ end
221
+ end
154
222
  end
155
223
  end