git 5.7.1 → 6.0.0

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +132 -42
  3. data/CONTRIBUTING.md +44 -31
  4. data/README.md +138 -100
  5. data/UPGRADING.md +864 -259
  6. data/git.gemspec +6 -8
  7. data/lib/git/author_info.rb +1 -2
  8. data/lib/git/commands/base.rb +22 -2
  9. data/lib/git/commands/cat_file/raw.rb +0 -68
  10. data/lib/git/commands/maintenance/register.rb +0 -4
  11. data/lib/git/commands/maintenance/run.rb +0 -4
  12. data/lib/git/commands/maintenance/start.rb +0 -4
  13. data/lib/git/commands/maintenance/stop.rb +0 -4
  14. data/lib/git/commands/maintenance/unregister.rb +0 -4
  15. data/lib/git/commands/maintenance.rb +0 -2
  16. data/lib/git/commands/show_ref/exclude_existing.rb +1 -1
  17. data/lib/git/commands/show_ref/exists.rb +0 -5
  18. data/lib/git/commands/show_ref/list.rb +1 -1
  19. data/lib/git/commands/show_ref/verify.rb +1 -1
  20. data/lib/git/commands/show_ref.rb +1 -1
  21. data/lib/git/commands/worktree/repair.rb +0 -5
  22. data/lib/git/diff_path_status.rb +1 -1
  23. data/lib/git/encoding_utils.rb +25 -0
  24. data/lib/git/errors.rb +24 -18
  25. data/lib/git/factories.rb +361 -125
  26. data/lib/git/log.rb +3 -75
  27. data/lib/git/object.rb +4 -185
  28. data/lib/git/path_resolver.rb +104 -36
  29. data/lib/git/repository/branching.rb +21 -189
  30. data/lib/git/repository/committing.rb +0 -25
  31. data/lib/git/repository/context_helpers.rb +249 -155
  32. data/lib/git/repository/diffing.rb +2 -72
  33. data/lib/git/repository/merging.rb +32 -54
  34. data/lib/git/repository/object_operations.rb +120 -497
  35. data/lib/git/repository/remote_operations.rb +7 -219
  36. data/lib/git/repository/staging.rb +6 -184
  37. data/lib/git/repository/stashing.rb +126 -130
  38. data/lib/git/repository/status_operations.rb +1 -54
  39. data/lib/git/repository/worktree_operations.rb +30 -125
  40. data/lib/git/repository.rb +20 -299
  41. data/lib/git/system_call_guard.rb +95 -0
  42. data/lib/git/version.rb +1 -1
  43. data/lib/git.rb +47 -364
  44. metadata +24 -18
  45. data/lib/git/author.rb +0 -46
  46. data/lib/git/branch.rb +0 -693
  47. data/lib/git/branches.rb +0 -194
  48. data/lib/git/remote.rb +0 -191
  49. data/lib/git/stash.rb +0 -124
  50. data/lib/git/stashes.rb +0 -198
  51. data/lib/git/status.rb +0 -384
  52. data/lib/git/worktree.rb +0 -180
  53. data/lib/git/worktrees.rb +0 -152
@@ -4,14 +4,33 @@ require 'fileutils'
4
4
  require 'pathname'
5
5
  require 'tmpdir'
6
6
  require 'git/execution_context/repository'
7
+ require 'git/system_call_guard'
7
8
 
8
9
  module Git
9
10
  class Repository
10
11
  # Facade methods for block-based directory and index context helpers
11
12
  #
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.
13
+ # The `with_*` helpers build a second {Git::Repository} bound to a different
14
+ # index file or working directory, yield it, and leave the receiver bound to
15
+ # its original index and working directory. Nothing on the receiver changes,
16
+ # so nothing on it is restored when the block exits. The working directory
17
+ # variants change the process working directory for the duration of the
18
+ # block and change it back afterward. The temporary variants remove the
19
+ # scratch directory they created, even if the block raises an exception.
20
+ #
21
+ # The second repository is built with `self.class.new(execution_context:)`,
22
+ # so a subclass of {Git::Repository} must accept that constructor call for
23
+ # the helpers to yield an instance of the subclass.
24
+ #
25
+ # Each `with_*` helper requires a block that declares a positional parameter
26
+ # for the yielded repository and raises `ArgumentError` otherwise. A block with no
27
+ # positional parameter is the v5.x form, which relied on the receiver being
28
+ # rebound; failing fast keeps that form from running against the original
29
+ # index or working directory.
30
+ #
31
+ # {#set_index} and {#set_working} are deprecated and warn on every call
32
+ # through {Git::Deprecation}; UPGRADING.md explains why and shows the
33
+ # replacements.
15
34
  #
16
35
  # Included by {Git::Repository}.
17
36
  #
@@ -22,6 +41,18 @@ module Git
22
41
  CONTEXT_HELPERS_POSITIONAL_PARAMETERS = %i[req opt rest].freeze
23
42
  private_constant :CONTEXT_HELPERS_POSITIONAL_PARAMETERS
24
43
 
44
+ # Deprecation message for {#set_index}
45
+ SET_INDEX_DEPRECATION = 'Git::Repository#set_index is deprecated and will be removed in a future major ' \
46
+ 'release. Open a repository bound to the index instead: ' \
47
+ 'Git.open(dir, index: path) or Git.bare(git_dir, index: path).'
48
+ private_constant :SET_INDEX_DEPRECATION
49
+
50
+ # Deprecation message for {#set_working}
51
+ SET_WORKING_DEPRECATION = 'Git::Repository#set_working is deprecated and will be removed in a future major ' \
52
+ 'release. Open a repository bound to the working tree instead: ' \
53
+ 'Git.open(work_dir, repository: git_dir).'
54
+ private_constant :SET_WORKING_DEPRECATION
55
+
25
56
  # Changes the current working directory to the repository working directory
26
57
  # for the duration of the block
27
58
  #
@@ -36,6 +67,8 @@ module Git
36
67
  # @raise [ArgumentError] if the repository has no working directory (bare
37
68
  # repository)
38
69
  #
70
+ # @raise [Git::Error] if the working directory cannot be entered
71
+ #
39
72
  # @yield [dir] the repository working directory
40
73
  #
41
74
  # @yieldparam dir [Pathname] the working directory path
@@ -45,104 +78,114 @@ module Git
45
78
  def chdir
46
79
  raise ArgumentError, 'cannot chdir: repository has no working directory (bare repository)' if dir.nil?
47
80
 
48
- Dir.chdir(dir.to_s) { yield dir }
81
+ context_helpers_chdir(dir.to_s) { yield dir }
49
82
  end
50
83
 
51
- # Temporarily switches the git index to `new_index` for the duration of
52
- # the block
84
+ # Yields a repository bound to `new_index` in place of the receiver's index
53
85
  #
54
- # Rebuilds the repository execution context to point to the new index file,
55
- # yields `self`, then unconditionally restores the original execution
56
- # context — even if the block raises an exception.
57
- #
58
- # Deprecated form: a block that declares no positional parameter emits a
59
- # deprecation warning, because v6.0.0 yields a separate repository
60
- # instead of `self`.
86
+ # Yields a second repository bound to `new_index`; its working directory
87
+ # and git directory are the receiver's. The receiver is not changed, so
88
+ # calls on it inside the block still use the original index.
61
89
  #
62
90
  # @example Read a tree into a custom index
63
- # repo.with_index('/tmp/custom.index') do |r|
64
- # r.read_tree('HEAD')
91
+ # repo.with_index('/tmp/custom.index') do |indexed|
92
+ # indexed.read_tree('HEAD')
65
93
  # end
66
94
  #
67
95
  # @param new_index [String, Pathname] path to the replacement index file
68
96
  #
69
97
  # @return [Object] the value returned by the block
70
98
  #
71
- # @yield [repo] the repository instance with the new index active
99
+ # @raise [ArgumentError] if no block is given or the block declares no
100
+ # positional parameter
101
+ #
102
+ # @raise [Git::Error] if `new_index` cannot be expanded to an absolute path
103
+ #
104
+ # @yield [repo] the repository bound to `new_index`
72
105
  #
73
- # @yieldparam repo [Git::Repository] `self`
106
+ # @yieldparam repo [Git::Repository] a new repository of the receiver's
107
+ # class bound to `new_index`
74
108
  #
75
109
  # @yieldreturn [Object] returned as the method's return value
76
110
  #
77
- def with_index(new_index, &block) # :yields: self
78
- context_helpers_warn_if_block_declares_no_parameter(:with_index, block)
79
- old_context = @execution_context
80
- set_index(new_index, must_exist: false)
81
- yield self
82
- ensure
83
- @execution_context = old_context
111
+ def with_index(new_index, &block)
112
+ context_helpers_require_repository_block(block)
113
+ new_path = context_helpers_validate_path(new_index, false)
114
+ yield context_helpers_derived_repository(git_index_file: new_path.to_s)
84
115
  end
85
116
 
86
- # Temporarily switches the git index to a new temporary file for the
87
- # duration of the block, then removes the file
117
+ # Yields a repository bound to a new temporary index file, then removes
118
+ # the file
88
119
  #
89
120
  # The temporary index file does not exist until git creates it on first
90
121
  # write. A unique temporary directory is created to hold the index path,
91
122
  # avoiding the risk of presenting an empty file to git (which git would
92
- # reject as a corrupt index). The directory — and any files inside it —
93
- # are removed unconditionally after the block exits, even if the block
94
- # raises an exception.
95
- #
96
- # Deprecated form: a block that declares no positional parameter emits a
97
- # deprecation warning, because v6.0.0 yields a separate repository
98
- # instead of `self`.
99
- #
100
- # @example Stage changes using a temporary index
101
- # repo.with_temp_index do |r|
102
- # r.read_tree('HEAD')
103
- # r.write_tree
123
+ # reject as a corrupt index). The directory, and any files inside it, are
124
+ # removed unconditionally after the block exits, even if the block raises
125
+ # an exception. The yielded repository is bound as {#with_index} binds it.
126
+ #
127
+ # @example Write a tree from a temporary index
128
+ # tree_sha = repo.with_temp_index do |indexed|
129
+ # indexed.read_tree('HEAD')
130
+ # indexed.add('generated.txt')
131
+ # indexed.write_tree
104
132
  # end
105
133
  #
106
134
  # @return [Object] the value returned by the block
107
135
  #
108
- # @yield [repo] the repository instance with the temporary index active
136
+ # @raise [ArgumentError] if no block is given or the block declares no
137
+ # positional parameter
109
138
  #
110
- # @yieldparam repo [Git::Repository] `self`
139
+ # @raise [Git::Error] if the temporary directory cannot be created
140
+ #
141
+ # @yield [repo] the repository bound to the temporary index
142
+ #
143
+ # @yieldparam repo [Git::Repository] a new repository of the receiver's
144
+ # class bound to an index file inside the temporary directory
111
145
  #
112
146
  # @yieldreturn [Object] returned as the method's return value
113
147
  #
114
- def with_temp_index(&block) # :yields: self
115
- context_helpers_warn_if_block_declares_no_parameter(:with_temp_index, block)
148
+ # @note The yielded repository stays bound to the temporary index path
149
+ # after the directory that held it is removed. Do not let it, or an
150
+ # object that holds it and later runs commands against the index,
151
+ # escape the block. Git treats the missing index file, even with its
152
+ # directory gone, as an empty index, so reads such as {Git::Repository::StatusOperations#ls_files}
153
+ # return nothing and {Git::Repository::StatusOperations#status_info}
154
+ # reports every tracked file both as deleted and as untracked; writes
155
+ # such as {Git::Repository::Staging#add} raise {Git::Error}. Reads from
156
+ # the object database, such as {Git::Object::AbstractObject#contents},
157
+ # still work.
158
+ #
159
+ def with_temp_index(&block)
160
+ context_helpers_require_repository_block(block)
116
161
  # Use a unique temp directory so the index file path is collision-free
117
162
  # and does not exist until git writes it. An existing empty file would
118
163
  # be treated as a corrupt index by git.
119
- temp_dir = Dir.mktmpdir('git-temp-index-')
164
+ temp_dir = context_helpers_mktmpdir('git-temp-index-')
120
165
  begin
121
- # The inner block declares a parameter so with_index does not warn a
122
- # second time under its own name; forwarding &block would.
123
- # rubocop:disable-next Style/ExplicitBlockArgument
124
- with_index(File.join(temp_dir, 'index')) { |repo| yield repo }
166
+ with_index(File.join(temp_dir, 'index'), &block)
125
167
  ensure
126
168
  FileUtils.remove_entry(temp_dir, true)
127
169
  end
128
170
  end
129
171
 
130
- # Temporarily switches the git working directory to `work_dir` for the
131
- # duration of the block
172
+ # Yields a repository bound to `work_dir` in place of the receiver's
173
+ # working directory
132
174
  #
133
- # Rebuilds the repository execution context to point to the new working
134
- # directory, changes the process working directory via `Dir.chdir`, yields
135
- # `self`, then unconditionally restores the original execution context —
136
- # even if the block raises an exception.
175
+ # Yields a second repository bound to `work_dir`. It shares the
176
+ # receiver's index and git directory, so staging through it changes what
177
+ # the receiver sees as staged. The process working directory is changed
178
+ # to `work_dir` via `Dir.chdir` for the duration of the block and changed
179
+ # back afterward. The receiver is not changed, so calls on it inside the
180
+ # block still use the original working directory.
137
181
  #
138
- # Deprecated form: a block that declares no positional parameter emits a
139
- # deprecation warning, because v6.0.0 yields a separate repository
140
- # instead of `self`.
182
+ # `Dir.chdir` is process-global, so this method is not safe to call from
183
+ # more than one thread at a time.
141
184
  #
142
185
  # @example Commit changes from a different worktree path
143
- # repo.with_working('/path/to/worktree') do |r|
144
- # r.add('.')
145
- # r.commit('chore: automated update')
186
+ # repo.with_working('/path/to/worktree') do |worktree|
187
+ # worktree.add('.')
188
+ # worktree.commit('chore: automated update')
146
189
  # end
147
190
  #
148
191
  # @param work_dir [String, Pathname] path to the replacement working
@@ -150,56 +193,69 @@ module Git
150
193
  #
151
194
  # @return [Object] the value returned by the block
152
195
  #
153
- # @raise [ArgumentError] if `work_dir` does not exist on disk
196
+ # @raise [ArgumentError] if `work_dir` does not exist on disk, if no block
197
+ # is given, or if the block declares no positional parameter
198
+ #
199
+ # @raise [Git::Error] if `work_dir` cannot be expanded to an absolute path
200
+ # or cannot be entered
154
201
  #
155
- # @yield [repo] the repository instance with the new working directory
156
- # active
202
+ # @yield [repo] the repository bound to `work_dir`
157
203
  #
158
- # @yieldparam repo [Git::Repository] `self`
204
+ # @yieldparam repo [Git::Repository] a new repository of the receiver's
205
+ # class bound to `work_dir`
159
206
  #
160
207
  # @yieldreturn [Object] returned as the method's return value
161
208
  #
162
- def with_working(work_dir, &block) # :yields: self
163
- context_helpers_warn_if_block_declares_no_parameter(:with_working, block)
164
- old_context = @execution_context
165
- set_working(work_dir)
166
- Dir.chdir(dir.to_s) { yield self }
167
- ensure
168
- @execution_context = old_context
209
+ def with_working(work_dir, &block)
210
+ context_helpers_require_repository_block(block)
211
+ new_path = context_helpers_validate_path(work_dir, true)
212
+ repo = context_helpers_derived_repository(git_work_dir: new_path.to_s)
213
+ context_helpers_chdir(new_path.to_s) { yield repo }
169
214
  end
170
215
 
171
- # Temporarily switches the git working directory to a new temporary
172
- # directory for the duration of the block, then removes the directory and
173
- # its contents
216
+ # Yields a repository bound to a new temporary working directory, then
217
+ # removes the directory and its contents
174
218
  #
175
- # The temporary directory is removed unconditionally after the block
176
- # exits, even if the block raises an exception.
177
- #
178
- # Deprecated form: a block that declares no positional parameter emits a
179
- # deprecation warning, because v6.0.0 yields a separate repository
180
- # instead of `self`.
219
+ # The process working directory is changed to the temporary directory for
220
+ # the duration of the block, as {#with_working} does, so this method is
221
+ # not safe to call from more than one thread at a time. The directory is
222
+ # removed unconditionally after the block exits, even if the block raises
223
+ # an exception. The yielded repository is bound as {#with_working} binds
224
+ # it.
181
225
  #
182
226
  # @example Write files in an isolated temporary working directory
183
- # repo.with_temp_working do |r|
227
+ # repo.with_temp_working do |scratch|
184
228
  # File.write('scratch.txt', 'temporary content')
185
- # r.add('scratch.txt')
229
+ # scratch.untracked_files #=> ["scratch.txt"]
186
230
  # end
187
231
  #
188
232
  # @return [Object] the value returned by the block
189
233
  #
190
- # @yield [repo] the repository instance with the temporary working
191
- # directory active
234
+ # @raise [ArgumentError] if no block is given or the block declares no
235
+ # positional parameter
236
+ #
237
+ # @raise [Git::Error] if the temporary directory cannot be created or
238
+ # removed
239
+ #
240
+ # @yield [repo] the repository bound to the temporary working directory
192
241
  #
193
- # @yieldparam repo [Git::Repository] `self`
242
+ # @yieldparam repo [Git::Repository] a new repository of the receiver's
243
+ # class bound to the temporary directory
194
244
  #
195
245
  # @yieldreturn [Object] returned as the method's return value
196
246
  #
197
- def with_temp_working(&block) # :yields: self
198
- context_helpers_warn_if_block_declares_no_parameter(:with_temp_working, block)
199
- # The inner block declares a parameter so with_working does not warn a
200
- # second time under its own name; forwarding &block would.
201
- # rubocop:disable-next Style/ExplicitBlockArgument
202
- Dir.mktmpdir('temp-workdir') { |temp_dir| with_working(temp_dir) { |repo| yield repo } }
247
+ # @note The yielded repository stays bound to the temporary working
248
+ # directory after it is removed. Do not let it, or an object that holds
249
+ # it and later runs commands against the working tree, escape the
250
+ # block: calls that read or write the working tree then raise
251
+ # {Git::Error}. Reads from the object database, such as
252
+ # {Git::Object::AbstractObject#contents}, still work.
253
+ #
254
+ def with_temp_working(&block)
255
+ context_helpers_require_repository_block(block)
256
+ Git::SystemCallGuard.call('Failed to create or remove a temporary directory') do |guard|
257
+ Dir.mktmpdir('temp-workdir') { |temp_dir| guard.unguarded { with_working(temp_dir, &block) } }
258
+ end
203
259
  end
204
260
 
205
261
  # Sets the git index to `index_file` and rebuilds the execution context
@@ -208,15 +264,20 @@ module Git
208
264
  # false` to skip the existence check (useful when the index will be
209
265
  # created by git later).
210
266
  #
267
+ # Every call warns through {Git::Deprecation}; see {ContextHelpers}.
268
+ #
211
269
  # @example Set the index to a custom path
212
270
  # repo.set_index('/path/to/custom.index')
213
271
  #
214
- # @param index_file [String, Pathname] path to the new index file
272
+ # @example Open a repository bound to the index instead
273
+ # repo = Git.open('/path/to/repo', index: '/path/to/custom.index')
215
274
  #
216
- # @param check [Boolean, nil] deprecated positional argument — use
217
- # `must_exist:` instead; emits a deprecation warning when non-`nil`
275
+ # @example Open a bare repository bound to the index instead
276
+ # repo = Git.bare('/srv/repo.git', index: '/tmp/scratch.index')
218
277
  #
219
- # @param must_exist [Boolean, nil] when `true` (the default), raises
278
+ # @param index_file [String, Pathname] path to the new index file
279
+ #
280
+ # @param must_exist [Boolean] when `true` (the default), raises
220
281
  # `ArgumentError` if `index_file` does not exist on disk
221
282
  #
222
283
  # @return [void]
@@ -224,10 +285,15 @@ module Git
224
285
  # @raise [ArgumentError] if `must_exist: true` (the default) and
225
286
  # `index_file` does not exist
226
287
  #
227
- def set_index(index_file, check = nil, must_exist: nil)
228
- must_exist = context_helpers_deprecate_check_argument(check, must_exist)
288
+ # @deprecated Use `Git.open(dir, index: path)` or
289
+ # `Git.bare(git_dir, index: path)` to open a repository bound to the
290
+ # index, or {#with_index} to bind a copy for the duration of a block.
291
+ # Will be removed in a future major release.
292
+ #
293
+ def set_index(index_file, must_exist: true)
294
+ Git::Deprecation.warn(SET_INDEX_DEPRECATION)
229
295
  new_path = context_helpers_validate_path(index_file, must_exist)
230
- context_helpers_rebuild_context(git_index_file: new_path.to_s)
296
+ @execution_context = @execution_context.dup_with(git_index_file: new_path.to_s)
231
297
  nil
232
298
  end
233
299
 
@@ -237,15 +303,25 @@ module Git
237
303
  # By default raises if `work_dir` does not exist. Pass `must_exist:
238
304
  # false` to skip the existence check.
239
305
  #
306
+ # Every call warns through {Git::Deprecation}; see {ContextHelpers}.
307
+ #
308
+ # `set_working` keeps the receiver's index. `Git.open` with `:repository`
309
+ # alone resolves the index to `<git_dir>/index`, so pass `:index` as well
310
+ # whenever the receiver's index was custom.
311
+ #
240
312
  # @example Set the working directory to a custom path
241
313
  # repo.set_working('/path/to/working')
242
314
  #
243
- # @param work_dir [String, Pathname] path to the new working directory
315
+ # @example Open a repository bound to the working tree instead
316
+ # repo = Git.open('/path/to/working', repository: '/path/to/repo/.git')
244
317
  #
245
- # @param check [Boolean, nil] deprecated positional argument — use
246
- # `must_exist:` instead; emits a deprecation warning when non-`nil`
318
+ # @example Open a repository bound to the working tree and a custom index
319
+ # repo = Git.open('/path/to/working', repository: '/path/to/repo/.git',
320
+ # index: '/path/to/custom.index')
247
321
  #
248
- # @param must_exist [Boolean, nil] when `true` (the default), raises
322
+ # @param work_dir [String, Pathname] path to the new working directory
323
+ #
324
+ # @param must_exist [Boolean] when `true` (the default), raises
249
325
  # `ArgumentError` if `work_dir` does not exist on disk
250
326
  #
251
327
  # @return [void]
@@ -253,76 +329,86 @@ module Git
253
329
  # @raise [ArgumentError] if `must_exist: true` (the default) and
254
330
  # `work_dir` does not exist
255
331
  #
256
- def set_working(work_dir, check = nil, must_exist: nil)
257
- must_exist = context_helpers_deprecate_check_argument(check, must_exist)
332
+ # @deprecated Use `Git.open(work_dir, repository: git_dir)` to open a
333
+ # repository bound to the working tree, adding `index:` when the
334
+ # receiver's index was custom, or {#with_working} to bind a copy for
335
+ # the duration of a block. Will be removed in a future major release.
336
+ #
337
+ def set_working(work_dir, must_exist: true)
338
+ Git::Deprecation.warn(SET_WORKING_DEPRECATION)
258
339
  new_path = context_helpers_validate_path(work_dir, must_exist)
259
- context_helpers_rebuild_context(git_work_dir: new_path.to_s)
340
+ @execution_context = @execution_context.dup_with(git_work_dir: new_path.to_s)
260
341
  nil
261
342
  end
262
343
 
263
344
  private
264
345
 
265
- # Emits a deprecation warning when `block` declares no positional parameter
346
+ # Runs the block with the process working directory set to `path`
347
+ #
348
+ # A `SystemCallError` from entering or leaving `path` is raised as
349
+ # {Git::Error}. A `SystemCallError` raised by the block propagates
350
+ # unchanged.
351
+ #
352
+ # The message does not name a directory because `Dir.chdir` fails on
353
+ # either leg: entering `path`, or restoring the previous directory
354
+ # afterward. The underlying `SystemCallError` names the directory that
355
+ # actually failed.
356
+ #
357
+ # @param path [String] the directory to enter
266
358
  #
267
- # In v6.0.0 the `with_*` helpers yield a separate repository instead of
268
- # rebinding the receiver, so a block that calls methods on the receiver
269
- # (the v5.x form) acts on the wrong repository there. v6.0.0 raises
270
- # `ArgumentError` for that form using this same rule: the block must
271
- # declare a required, optional, or splat positional parameter. The rule
272
- # reads `Proc#parameters` rather than arity because an optional parameter
273
- # gives a proc an arity of zero although it receives the repository,
274
- # while keyword and block parameters cannot receive it.
359
+ # @return [Object] the value returned by the block
360
+ #
361
+ # @raise [Git::Error] if `path` cannot be entered
275
362
  #
276
- # A missing block is not checked here; the helper's `yield` raises
277
- # `LocalJumpError` as before.
363
+ # @yield the code to run inside `path`
278
364
  #
279
- # The warning reports the helper's caller, not this helper's caller, so
280
- # the location points at the block that needs changing.
365
+ # @yieldreturn [Object] returned as the method's return value
281
366
  #
282
- # @param helper [Symbol] the public helper name used in the warning
367
+ # @api private
283
368
  #
284
- # @param block [Proc, nil] the block passed to the helper
369
+ def context_helpers_chdir(path, &block)
370
+ Git::SystemCallGuard.call('Failed to change directory') do |guard|
371
+ Dir.chdir(path) { guard.unguarded(&block) }
372
+ end
373
+ end
374
+
375
+ # Raises unless `block` declares a positional parameter for the yielded
376
+ # repository
377
+ #
378
+ # A block with no positional parameter is the v5.x form that relied on
379
+ # the receiver being rebound; it fails fast rather than running against
380
+ # the original index or working directory. The check reads the block's
381
+ # parameter list rather than its arity because an optional parameter
382
+ # (`|repo = nil|`) gives a proc an arity of zero although it receives the
383
+ # repository, while keyword and block parameters cannot receive it.
384
+ #
385
+ # @param block [Proc, nil] the block passed to a `with_*` helper
285
386
  #
286
387
  # @return [void]
287
388
  #
288
- def context_helpers_warn_if_block_declares_no_parameter(helper, block)
289
- return if block.nil?
290
- return if block.parameters.any? { |type, _name| CONTEXT_HELPERS_POSITIONAL_PARAMETERS.include?(type) }
389
+ # @raise [ArgumentError] if `block` is nil or declares no positional
390
+ # parameter
391
+ #
392
+ # @api private
393
+ #
394
+ def context_helpers_require_repository_block(block)
395
+ return if block&.parameters&.any? { |type, _name| CONTEXT_HELPERS_POSITIONAL_PARAMETERS.include?(type) }
291
396
 
292
- Git::Deprecation.warn(
293
- "Calling Git::Repository##{helper} with a block that declares no positional " \
294
- 'parameter is deprecated and will be removed in v6.0.0, where the helper ' \
295
- 'yields a separate repository instead of self. Declare a block parameter and ' \
296
- "call methods on it instead: #{helper} { |repo| repo.some_method }",
297
- caller_locations(2)
298
- )
397
+ raise ArgumentError, 'a block that accepts the yielded repository is required'
299
398
  end
300
399
 
301
- # Resolves deprecated `check` argument semantics with `must_exist:`
400
+ # Creates a temporary directory, raising {Git::Error} on failure
302
401
  #
303
- # @param check [Boolean, nil] deprecated positional existence-check value
402
+ # @param prefix [String] the directory name prefix
304
403
  #
305
- # @param must_exist [Boolean, nil] keyword existence-check override
404
+ # @return [String] the path of the new directory
306
405
  #
307
- # @return [Boolean] whether path existence must be enforced
406
+ # @raise [Git::Error] if the directory cannot be created
308
407
  #
309
- def context_helpers_deprecate_check_argument(check, must_exist)
310
- if !check.nil? && defined?(Git::Deprecation)
311
- Git::Deprecation.warn(
312
- 'The "check" argument is deprecated and will be removed in v6.0.0. ' \
313
- 'Use "must_exist:" instead.'
314
- )
315
- end
316
- # Preserve the original Git::Base semantics: when both the deprecated
317
- # positional `check` and the new `must_exist:` keyword are given, OR
318
- # them so the more restrictive value wins.
319
- #
320
- # NilClass#| is defined in Ruby: nil | false → false, nil | true → true.
321
- # This means single-argument callers (check only, or must_exist: only)
322
- # are handled correctly without any nil-special-casing.
323
- return true if must_exist.nil? && check.nil?
324
-
325
- must_exist | check
408
+ # @api private
409
+ #
410
+ def context_helpers_mktmpdir(prefix)
411
+ Git::SystemCallGuard.call('Failed to create a temporary directory') { Dir.mktmpdir(prefix) }
326
412
  end
327
413
 
328
414
  # Expands `path` and validates existence when required
@@ -335,13 +421,19 @@ module Git
335
421
  #
336
422
  # @raise [ArgumentError] if `must_exist` is `true` and the path does not exist
337
423
  #
424
+ # @raise [Git::Error] if the path cannot be expanded, which happens when
425
+ # `path` is relative and the process working directory has been removed
426
+ #
338
427
  def context_helpers_validate_path(path, must_exist)
339
- Pathname.new(File.expand_path(path.to_s)).tap do |expanded_path|
428
+ expanded = Git::SystemCallGuard.call('Failed to expand the path') { File.expand_path(path.to_s) }
429
+
430
+ Pathname.new(expanded).tap do |expanded_path|
340
431
  raise ArgumentError, "path does not exist: #{expanded_path}" if must_exist && !expanded_path.exist?
341
432
  end
342
433
  end
343
434
 
344
- # Rebuilds the repository execution context with selected overrides
435
+ # Builds a repository of the receiver's class bound to a copy of this
436
+ # execution context with selected overrides, leaving the receiver untouched
345
437
  #
346
438
  # @param overrides [Hash] execution-context attributes to override
347
439
  #
@@ -350,10 +442,12 @@ module Git
350
442
  # @option overrides [String, nil] :git_work_dir replacement working directory
351
443
  # path
352
444
  #
353
- # @return [void]
445
+ # @return [Git::Repository] a new repository built from the derived context
446
+ #
447
+ # @api private
354
448
  #
355
- def context_helpers_rebuild_context(**overrides)
356
- @execution_context = @execution_context.dup_with(**overrides)
449
+ def context_helpers_derived_repository(**overrides)
450
+ self.class.new(execution_context: @execution_context.dup_with(**overrides))
357
451
  end
358
452
  end
359
453
  end