git 5.0.0.beta.1 → 5.0.0.beta.2

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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/.github/copilot-instructions.md +6 -0
  3. data/.github/prompts/iteratively-address-copilot-reviews.prompt.md +188 -0
  4. data/.github/skills/extract-facade-from-base-lib/KEYWORD_ARG_REMEDIATION.md +22 -0
  5. data/.github/skills/extract-facade-from-base-lib/SKILL.md +28 -14
  6. data/.github/skills/facade-implementation/SKILL.md +14 -0
  7. data/.github/skills/facade-test-conventions/SKILL.md +14 -0
  8. data/.rubocop.yml +5 -0
  9. data/README.md +51 -11
  10. data/UPGRADING.md +141 -0
  11. data/git.gemspec +5 -0
  12. data/lib/git/branch.rb +7 -18
  13. data/lib/git/branches.rb +2 -10
  14. data/lib/git/command_line/base.rb +10 -0
  15. data/lib/git/command_line/capturing.rb +5 -3
  16. data/lib/git/command_line/streaming.rb +5 -3
  17. data/lib/git/command_line.rb +3 -3
  18. data/lib/git/commands/base.rb +7 -6
  19. data/lib/git/commands/cat_file/batch.rb +6 -1
  20. data/lib/git/commands/cat_file/raw.rb +7 -1
  21. data/lib/git/commands/config_option_syntax/get_urlmatch.rb +5 -0
  22. data/lib/git/commands/show_ref/exclude_existing.rb +1 -1
  23. data/lib/git/commands/update_ref/batch.rb +1 -1
  24. data/lib/git/commands/version.rb +5 -0
  25. data/lib/git/commands.rb +5 -7
  26. data/lib/git/config.rb +17 -0
  27. data/lib/git/config_entry_info.rb +106 -0
  28. data/lib/git/configuring.rb +665 -0
  29. data/lib/git/deprecation.rb +9 -0
  30. data/lib/git/diff.rb +4 -8
  31. data/lib/git/diff_path_status.rb +2 -13
  32. data/lib/git/diff_stats.rb +1 -9
  33. data/lib/git/execution_context/global.rb +3 -28
  34. data/lib/git/execution_context/repository.rb +30 -41
  35. data/lib/git/execution_context.rb +43 -24
  36. data/lib/git/log.rb +3 -9
  37. data/lib/git/object.rb +14 -21
  38. data/lib/git/parsers/config_entry.rb +110 -0
  39. data/lib/git/parsers/ls_remote.rb +79 -0
  40. data/lib/git/remote.rb +7 -20
  41. data/lib/git/repository/branching.rb +183 -12
  42. data/lib/git/repository/committing.rb +64 -68
  43. data/lib/git/repository/configuring.rb +208 -13
  44. data/lib/git/repository/context_helpers.rb +264 -0
  45. data/lib/git/repository/factories.rb +682 -0
  46. data/lib/git/repository/inspecting.rb +99 -0
  47. data/lib/git/repository/maintenance.rb +65 -0
  48. data/lib/git/repository/merging.rb +63 -1
  49. data/lib/git/repository/object_operations.rb +133 -35
  50. data/lib/git/repository/path_resolver.rb +1 -1
  51. data/lib/git/repository/remote_operations.rb +166 -21
  52. data/lib/git/repository/staging.rb +187 -23
  53. data/lib/git/repository/stashing.rb +39 -3
  54. data/lib/git/repository/status_operations.rb +21 -0
  55. data/lib/git/repository.rb +68 -129
  56. data/lib/git/stash.rb +2 -9
  57. data/lib/git/stashes.rb +2 -7
  58. data/lib/git/status.rb +8 -17
  59. data/lib/git/version.rb +2 -2
  60. data/lib/git/worktree.rb +2 -15
  61. data/lib/git/worktrees.rb +2 -15
  62. data/lib/git.rb +180 -77
  63. data/redesign/3_architecture_implementation.md +148 -111
  64. data/redesign/Phase 4 - Step A.md +360 -0
  65. data/redesign/beta_release.md +107 -0
  66. data/redesign/c1c2_audit.md +566 -0
  67. data/redesign/c1c2_bucket6_lib_orphans.md +626 -0
  68. data/redesign/config_design.rb +501 -0
  69. metadata +19 -5
  70. data/lib/git/base.rb +0 -1204
  71. data/lib/git/lib.rb +0 -2855
@@ -5,10 +5,16 @@ require 'git/repository/shared_private'
5
5
 
6
6
  module Git
7
7
  class Repository
8
- # Facade methods for reading and writing git configuration
8
+ # Legacy facade methods for reading and writing git configuration
9
9
  #
10
- # Provides the {#config} method, which dispatches to read a single entry,
11
- # list all entries, or write a value depending on the arguments supplied.
10
+ # Provides the {#config} and {#global_config} dispatch methods for 4.x
11
+ # compatibility. These methods return raw `String` / `Hash` values instead of
12
+ # {Git::ConfigEntryInfo} objects and are retained so that internal callers such
13
+ # as `Git::Status` continue to work unchanged.
14
+ #
15
+ # The structured `config_*` methods (e.g. `config_get`, `config_list`) are
16
+ # provided by {Git::Configuring}, which is included directly into
17
+ # {Git::Repository}.
12
18
  #
13
19
  # Included by {Git::Repository}.
14
20
  #
@@ -19,6 +25,10 @@ module Git
19
25
  CONFIG_SET_ALLOWED_OPTS = %i[file].freeze
20
26
  private_constant :CONFIG_SET_ALLOWED_OPTS
21
27
 
28
+ # Option keys accepted by {#config} when reading a single value or listing
29
+ CONFIG_READ_ALLOWED_OPTS = %i[file].freeze
30
+ private_constant :CONFIG_READ_ALLOWED_OPTS
31
+
22
32
  # Read or write a git configuration entry
23
33
  #
24
34
  # Dispatches to one of three modes depending on the arguments supplied:
@@ -28,24 +38,45 @@ module Git
28
38
  # * **Set** — `config(name, value)` writes a value and returns the raw
29
39
  # command result.
30
40
  #
31
- # @overload config
41
+ # @overload config(options = {})
32
42
  #
33
43
  # @example List all config entries
34
44
  # repo.config #=> { "user.name" => "Alice", "core.bare" => "false" }
35
45
  #
46
+ # @example List all entries from a custom config file
47
+ # repo.config(file: '/path/to/.gitconfig')
48
+ # #=> { "user.name" => "Alice", "core.bare" => "false" }
49
+ #
50
+ # @param options [Hash] options for the list operation
51
+ #
52
+ # @option options [String, nil] :file (nil) path to a custom config file
53
+ # to read from instead of the default resolution chain
54
+ #
36
55
  # @return [Hash{String => String}] all visible config entries, keyed by
37
56
  # their full dotted key names (e.g. `"user.name"`)
38
57
  #
39
- # @overload config(name)
58
+ # @raise [ArgumentError] if unsupported options are provided
59
+ #
60
+ # @overload config(name, options = {})
40
61
  #
41
62
  # @example Read the committer name from config
42
63
  # repo.config('user.name') #=> "Alice"
43
64
  #
65
+ # @example Read a value from a custom config file
66
+ # repo.config('user.name', file: '/path/to/.gitconfig') #=> "Alice"
67
+ #
44
68
  # @param name [String] the dotted config key to look up (e.g.
45
69
  # `"user.name"`)
46
70
  #
71
+ # @param options [Hash] options for the get operation
72
+ #
73
+ # @option options [String, nil] :file (nil) path to a custom config file
74
+ # to read from instead of the default resolution chain
75
+ #
47
76
  # @return [String] the value of the config entry
48
77
  #
78
+ # @raise [ArgumentError] if unsupported options are provided
79
+ #
49
80
  # @overload config(name, value, options = {})
50
81
  #
51
82
  # @example Set the committer name in local config
@@ -57,7 +88,12 @@ module Git
57
88
  # @param name [String] the dotted config key to write (e.g.
58
89
  # `"user.name"`)
59
90
  #
60
- # @param value [String] the value to assign
91
+ # @param value [#to_s] the value to assign; must not be `nil` (a `nil`
92
+ # value is treated as "no value" and routes to the get overload).
93
+ # Must not be a `Hash` (a Hash is treated as the `options` argument;
94
+ # call `value.to_s` explicitly before passing if a stringified Hash
95
+ # is genuinely needed). Any other non-nil object is converted to a
96
+ # String via `#to_s` before being passed to git
61
97
  #
62
98
  # @param options [Hash] options for the set operation
63
99
  #
@@ -72,12 +108,70 @@ module Git
72
108
  # @raise [Git::FailedError] if git exits with a non-zero exit status
73
109
  #
74
110
  def config(name = nil, value = nil, options = {})
75
- if name && value
111
+ name, value, options = Private.normalize_config_args(name, value, options)
112
+
113
+ if !name.nil? && !value.nil?
76
114
  Private.config_set(@execution_context, name, value, **options)
77
115
  elsif name
78
- Private.config_get(@execution_context, name)
116
+ Private.config_get(@execution_context, name, **options)
117
+ else
118
+ Private.config_list(@execution_context, **options)
119
+ end
120
+ end
121
+
122
+ # Read or write a global git configuration entry
123
+ #
124
+ # Dispatches to one of three modes depending on the arguments supplied,
125
+ # targeting the git global config scope (`git config --global`):
126
+ #
127
+ # * **List** — `global_config()` returns all global config entries as a `Hash`.
128
+ # * **Get** — `global_config(name)` returns the value for a single key as a `String`.
129
+ # * **Set** — `global_config(name, value)` writes a value and returns the raw
130
+ # command result.
131
+ #
132
+ # @overload global_config
133
+ #
134
+ # @example List all global config entries
135
+ # repo.global_config #=> { "user.name" => "Alice", "core.autocrlf" => "false" }
136
+ #
137
+ # @return [Hash{String => String}] all global config entries, keyed by their
138
+ # full dotted key names (e.g. `"user.name"`)
139
+ #
140
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
141
+ #
142
+ # @overload global_config(name)
143
+ #
144
+ # @example Read the global committer name
145
+ # repo.global_config('user.name') #=> "Alice"
146
+ #
147
+ # @param name [String] the dotted config key to look up (e.g. `"user.name"`)
148
+ #
149
+ # @return [String] the value of the global config entry
150
+ #
151
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
152
+ #
153
+ # @overload global_config(name, value)
154
+ #
155
+ # @example Set the global committer name
156
+ # repo.global_config('user.name', 'Alice')
157
+ #
158
+ # @param name [String] the dotted config key to write (e.g. `"user.name"`)
159
+ #
160
+ # @param value [#to_s] the value to assign; any object is accepted and
161
+ # converted to a String via `#to_s` before being passed to git
162
+ #
163
+ # @return [Git::CommandLineResult] the raw result of
164
+ # `git config --global <name> <value>`
165
+ #
166
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
167
+ #
168
+ def global_config(name = nil, value = nil)
169
+ if !name.nil? && !value.nil?
170
+ Private.global_config_set(@execution_context, name, value)
171
+ elsif !name.nil?
172
+ Private.global_config_get(@execution_context, name)
79
173
  else
80
- Private.config_list(@execution_context)
174
+ Private.global_config_list(@execution_context)
81
175
  end
82
176
  end
83
177
 
@@ -88,6 +182,39 @@ module Git
88
182
  module Private
89
183
  module_function
90
184
 
185
+ # Normalize `config()` positional arguments
186
+ #
187
+ # In Ruby 3.x, calling `config(file: '/path')` passes the hash as the
188
+ # first positional argument. This helper re-maps those patterns so that
189
+ # `name`, `value`, and `options` are always in their canonical positions.
190
+ #
191
+ # Raises `ArgumentError` for call shapes that are ambiguous or clearly
192
+ # wrong, such as passing extra positional arguments after an options Hash.
193
+ #
194
+ # @param name [String, Hash, nil] the raw first argument to {#config}
195
+ #
196
+ # @param value [Object, Hash, nil] the raw second argument to {#config}
197
+ #
198
+ # @param options [Hash] the raw third argument to {#config}
199
+ #
200
+ # @return [Array(String|nil, Object|nil, Hash)] normalized [name, value, options]
201
+ #
202
+ # @raise [ArgumentError] if extra positional arguments follow an options Hash
203
+ #
204
+ def normalize_config_args(name, value, options)
205
+ if name.is_a?(Hash)
206
+ raise ArgumentError, 'unexpected positional arguments after options hash' if !value.nil? || !options.empty?
207
+
208
+ [nil, nil, name]
209
+ elsif value.is_a?(Hash)
210
+ raise ArgumentError, 'unexpected third argument when second argument is options hash' unless options.empty?
211
+
212
+ [name, nil, value]
213
+ else
214
+ [name, value, options]
215
+ end
216
+ end
217
+
91
218
  # Set a config value by key name
92
219
  #
93
220
  # @overload config_set(execution_context, name, value, **options)
@@ -121,12 +248,20 @@ module Git
121
248
  #
122
249
  # @param name [String] the dotted config key to look up (e.g. `"user.name"`)
123
250
  #
251
+ # @param options [Hash] keyword options
252
+ #
253
+ # @option options [String, nil] :file (nil) path to a custom config file to read from
254
+ #
124
255
  # @return [String] the value of the config entry
125
256
  #
257
+ # @raise [ArgumentError] if unsupported options are provided
258
+ #
126
259
  # @raise [Git::FailedError] if git exits with a non-zero exit status
127
260
  #
128
- def config_get(execution_context, name)
129
- result = Git::Commands::ConfigOptionSyntax::Get.new(execution_context).call(name)
261
+ def config_get(execution_context, name, **options)
262
+ SharedPrivate.assert_valid_opts!(CONFIG_READ_ALLOWED_OPTS, **options)
263
+ opts = options[:file] ? { file: options[:file] } : {}
264
+ result = Git::Commands::ConfigOptionSyntax::Get.new(execution_context).call(name, **opts)
130
265
  raise Git::FailedError, result if result.status.exitstatus != 0
131
266
 
132
267
  result.stdout
@@ -136,18 +271,78 @@ module Git
136
271
  #
137
272
  # @param execution_context [Git::ExecutionContext] the execution context
138
273
  #
274
+ # @param options [Hash] keyword options
275
+ #
276
+ # @option options [String, nil] :file (nil) path to a custom config file to read from
277
+ #
139
278
  # @return [Hash{String => String}] all config entries, keyed by their full
140
279
  # dotted key names (e.g. `"user.name"`)
141
280
  #
281
+ # @raise [ArgumentError] if unsupported options are provided
282
+ #
283
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
284
+ #
285
+ def config_list(execution_context, **options)
286
+ SharedPrivate.assert_valid_opts!(CONFIG_READ_ALLOWED_OPTS, **options)
287
+ opts = options[:file] ? { file: options[:file] } : {}
288
+ lines = Git::Commands::ConfigOptionSyntax::List.new(execution_context).call(**opts).stdout.split("\n")
289
+ lines.each_with_object({}) do |line, hsh|
290
+ key, value = line.split('=', 2)
291
+ hsh[key] = value || ''
292
+ end
293
+ end
294
+
295
+ # Retrieve a global config value by key name
296
+ #
297
+ # @param execution_context [Git::ExecutionContext] the execution context
298
+ #
299
+ # @param name [String] the dotted config key to look up (e.g. `"user.name"`)
300
+ #
301
+ # @return [String] the value of the global config entry
302
+ #
303
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
304
+ #
305
+ def global_config_get(execution_context, name)
306
+ result = Git::Commands::ConfigOptionSyntax::Get.new(execution_context).call(name, global: true)
307
+ raise Git::FailedError, result if result.status.exitstatus != 0
308
+
309
+ result.stdout
310
+ end
311
+
312
+ # Retrieve all global config entries as a hash
313
+ #
314
+ # @param execution_context [Git::ExecutionContext] the execution context
315
+ #
316
+ # @return [Hash{String => String}] all global config entries, keyed by their full
317
+ # dotted key names (e.g. `"user.name"`)
318
+ #
142
319
  # @raise [Git::FailedError] if git exits with a non-zero exit status
143
320
  #
144
- def config_list(execution_context)
145
- lines = Git::Commands::ConfigOptionSyntax::List.new(execution_context).call.stdout.split("\n")
321
+ def global_config_list(execution_context)
322
+ lines = Git::Commands::ConfigOptionSyntax::List.new(execution_context).call(global: true).stdout.split("\n")
146
323
  lines.each_with_object({}) do |line, hsh|
147
324
  key, value = line.split('=', 2)
148
325
  hsh[key] = value || ''
149
326
  end
150
327
  end
328
+
329
+ # Set a global config value by key name
330
+ #
331
+ # @param execution_context [Git::ExecutionContext] the execution context
332
+ #
333
+ # @param name [String] the dotted config key to write (e.g. `"user.name"`)
334
+ #
335
+ # @param value [#to_s] the value to assign; any object is accepted and
336
+ # converted to a String via `#to_s` before being passed to git
337
+ #
338
+ # @return [Git::CommandLineResult] the raw result of
339
+ # `git config --global <name> <value>`
340
+ #
341
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
342
+ #
343
+ def global_config_set(execution_context, name, value)
344
+ Git::Commands::ConfigOptionSyntax::Set.new(execution_context).call(name, value, global: true)
345
+ end
151
346
  end
152
347
 
153
348
  private_constant :Private
@@ -0,0 +1,264 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'pathname'
5
+ require 'tmpdir'
6
+ require 'git/execution_context/repository'
7
+
8
+ module Git
9
+ class Repository
10
+ # Facade methods for block-based directory and index context helpers
11
+ #
12
+ # These helpers allow callers to temporarily change the working directory,
13
+ # the git index, or both, restoring the original state unconditionally when
14
+ # the block exits — even if the block raises an exception.
15
+ #
16
+ # Included by {Git::Repository}.
17
+ #
18
+ # @api public
19
+ #
20
+ module ContextHelpers
21
+ # Changes the current working directory to the repository working directory
22
+ # for the duration of the block
23
+ #
24
+ # @example Write a file inside the repository working directory
25
+ # repo.chdir do |dir|
26
+ # File.write('hello.txt', 'Hello, world!')
27
+ # repo.add('hello.txt')
28
+ # end
29
+ #
30
+ # @yield [dir] the repository working directory
31
+ #
32
+ # @yieldparam dir [Pathname] the working directory path
33
+ #
34
+ # @yieldreturn [Object] returned as the method's return value
35
+ #
36
+ # @return [Object] the value returned by the block
37
+ #
38
+ # @raise [ArgumentError] if the repository has no working directory (bare
39
+ # repository)
40
+ #
41
+ def chdir
42
+ raise ArgumentError, 'cannot chdir: repository has no working directory (bare repository)' if dir.nil?
43
+
44
+ Dir.chdir(dir.to_s) { yield dir }
45
+ end
46
+
47
+ # Temporarily switches the git index to `new_index` for the duration of
48
+ # the block
49
+ #
50
+ # Rebuilds the repository execution context to point to the new index file,
51
+ # yields `self`, then unconditionally restores the original execution
52
+ # context — even if the block raises an exception.
53
+ #
54
+ # @example Read a tree into a custom index
55
+ # repo.with_index('/tmp/custom.index') do
56
+ # repo.read_tree('HEAD')
57
+ # end
58
+ #
59
+ # @param new_index [String, Pathname] path to the replacement index file
60
+ #
61
+ # @yield [repo] the repository instance with the new index active
62
+ #
63
+ # @yieldparam repo [Git::Repository] `self`
64
+ #
65
+ # @yieldreturn [Object] returned as the method's return value
66
+ #
67
+ # @return [Object] the value returned by the block
68
+ #
69
+ def with_index(new_index) # :yields: self
70
+ old_context = @execution_context
71
+ set_index(new_index, must_exist: false)
72
+ yield self
73
+ ensure
74
+ @execution_context = old_context
75
+ end
76
+
77
+ # Temporarily switches the git index to a new temporary file for the
78
+ # duration of the block, then removes the file
79
+ #
80
+ # The temporary index file does not exist until git creates it on first
81
+ # write. A unique temporary directory is created to hold the index path,
82
+ # avoiding the risk of presenting an empty file to git (which git would
83
+ # reject as a corrupt index). The directory — and any files inside it —
84
+ # are removed unconditionally after the block exits, even if the block
85
+ # raises an exception.
86
+ #
87
+ # @example Stage changes using a temporary index
88
+ # repo.with_temp_index do
89
+ # repo.read_tree('HEAD')
90
+ # repo.write_tree
91
+ # end
92
+ #
93
+ # @yield [repo] the repository instance with the temporary index active
94
+ #
95
+ # @yieldparam repo [Git::Repository] `self`
96
+ #
97
+ # @yieldreturn [Object] returned as the method's return value
98
+ #
99
+ # @return [Object] the value returned by the block
100
+ #
101
+ def with_temp_index(&) # :yields: self
102
+ # Use a unique temp directory so the index file path is collision-free
103
+ # and does not exist until git writes it. An existing empty file would
104
+ # be treated as a corrupt index by git.
105
+ temp_dir = Dir.mktmpdir('git-temp-index-')
106
+ begin
107
+ with_index(File.join(temp_dir, 'index'), &)
108
+ ensure
109
+ FileUtils.remove_entry(temp_dir, true)
110
+ end
111
+ end
112
+
113
+ # Temporarily switches the git working directory to `work_dir` for the
114
+ # duration of the block
115
+ #
116
+ # Rebuilds the repository execution context to point to the new working
117
+ # directory, changes the process working directory via `Dir.chdir`, yields
118
+ # `self`, then unconditionally restores the original execution context —
119
+ # even if the block raises an exception.
120
+ #
121
+ # @example Commit changes from a different worktree path
122
+ # repo.with_working('/path/to/worktree') do
123
+ # repo.add('.')
124
+ # repo.commit('chore: automated update')
125
+ # end
126
+ #
127
+ # @param work_dir [String, Pathname] path to the replacement working
128
+ # directory
129
+ #
130
+ # @yield [repo] the repository instance with the new working directory
131
+ # active
132
+ #
133
+ # @yieldparam repo [Git::Repository] `self`
134
+ #
135
+ # @yieldreturn [Object] returned as the method's return value
136
+ #
137
+ # @return [Object] the value returned by the block
138
+ #
139
+ # @raise [ArgumentError] if `work_dir` does not exist on disk
140
+ #
141
+ def with_working(work_dir) # :yields: self
142
+ old_context = @execution_context
143
+ set_working(work_dir)
144
+ Dir.chdir(dir.to_s) { yield self }
145
+ ensure
146
+ @execution_context = old_context
147
+ end
148
+
149
+ # Temporarily switches the git working directory to a new temporary
150
+ # directory for the duration of the block, then removes the directory and
151
+ # its contents
152
+ #
153
+ # The temporary directory is removed unconditionally after the block
154
+ # exits, even if the block raises an exception.
155
+ #
156
+ # @example Write files in an isolated temporary working directory
157
+ # repo.with_temp_working do
158
+ # File.write('scratch.txt', 'temporary content')
159
+ # end
160
+ #
161
+ # @yield [repo] the repository instance with the temporary working
162
+ # directory active
163
+ #
164
+ # @yieldparam repo [Git::Repository] `self`
165
+ #
166
+ # @yieldreturn [Object] returned as the method's return value
167
+ #
168
+ # @return [Object] the value returned by the block
169
+ #
170
+ def with_temp_working(&block) # :yields: self
171
+ Dir.mktmpdir('temp-workdir') { |temp_dir| with_working(temp_dir, &block) }
172
+ end
173
+
174
+ # Sets the git index to `index_file` and rebuilds the execution context
175
+ #
176
+ # By default raises if `index_file` does not exist. Pass `must_exist:
177
+ # false` to skip the existence check (useful when the index will be
178
+ # created by git later).
179
+ #
180
+ # @example Set the index to a custom path
181
+ # repo.set_index('/path/to/custom.index')
182
+ #
183
+ # @param index_file [String, Pathname] path to the new index file
184
+ #
185
+ # @param check [Boolean, nil] deprecated positional argument — use
186
+ # `must_exist:` instead; emits a deprecation warning when non-`nil`
187
+ #
188
+ # @param must_exist [Boolean, nil] when `true` (the default), raises
189
+ # `ArgumentError` if `index_file` does not exist on disk
190
+ #
191
+ # @return [void]
192
+ #
193
+ # @raise [ArgumentError] if `must_exist: true` (the default) and
194
+ # `index_file` does not exist
195
+ #
196
+ def set_index(index_file, check = nil, must_exist: nil)
197
+ must_exist = context_helpers_deprecate_check_argument(check, must_exist)
198
+ new_path = context_helpers_validate_path(index_file, must_exist)
199
+ context_helpers_rebuild_context(git_index_file: new_path.to_s)
200
+ nil
201
+ end
202
+
203
+ # Sets the git working directory to `work_dir` and rebuilds the execution
204
+ # context
205
+ #
206
+ # By default raises if `work_dir` does not exist. Pass `must_exist:
207
+ # false` to skip the existence check.
208
+ #
209
+ # @example Set the working directory to a custom path
210
+ # repo.set_working('/path/to/working')
211
+ #
212
+ # @param work_dir [String, Pathname] path to the new working directory
213
+ #
214
+ # @param check [Boolean, nil] deprecated positional argument — use
215
+ # `must_exist:` instead; emits a deprecation warning when non-`nil`
216
+ #
217
+ # @param must_exist [Boolean, nil] when `true` (the default), raises
218
+ # `ArgumentError` if `work_dir` does not exist on disk
219
+ #
220
+ # @return [void]
221
+ #
222
+ # @raise [ArgumentError] if `must_exist: true` (the default) and
223
+ # `work_dir` does not exist
224
+ #
225
+ def set_working(work_dir, check = nil, must_exist: nil)
226
+ must_exist = context_helpers_deprecate_check_argument(check, must_exist)
227
+ new_path = context_helpers_validate_path(work_dir, must_exist)
228
+ context_helpers_rebuild_context(git_work_dir: new_path.to_s)
229
+ nil
230
+ end
231
+
232
+ private
233
+
234
+ def context_helpers_deprecate_check_argument(check, must_exist)
235
+ if !check.nil? && defined?(Git::Deprecation)
236
+ Git::Deprecation.warn(
237
+ 'The "check" argument is deprecated and will be removed in a future version. ' \
238
+ 'Use "must_exist:" instead.'
239
+ )
240
+ end
241
+ # Preserve the original Git::Base semantics: when both the deprecated
242
+ # positional `check` and the new `must_exist:` keyword are given, OR
243
+ # them so the more restrictive value wins.
244
+ #
245
+ # NilClass#| is defined in Ruby: nil | false → false, nil | true → true.
246
+ # This means single-argument callers (check only, or must_exist: only)
247
+ # are handled correctly without any nil-special-casing.
248
+ return true if must_exist.nil? && check.nil?
249
+
250
+ must_exist | check
251
+ end
252
+
253
+ def context_helpers_validate_path(path, must_exist)
254
+ Pathname.new(File.expand_path(path.to_s)).tap do |expanded_path|
255
+ raise ArgumentError, "path does not exist: #{expanded_path}" if must_exist && !expanded_path.exist?
256
+ end
257
+ end
258
+
259
+ def context_helpers_rebuild_context(**overrides)
260
+ @execution_context = @execution_context.dup_with(**overrides)
261
+ end
262
+ end
263
+ end
264
+ end