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.
- checksums.yaml +4 -4
- data/.github/copilot-instructions.md +6 -0
- data/.github/prompts/iteratively-address-copilot-reviews.prompt.md +188 -0
- data/.github/skills/extract-facade-from-base-lib/KEYWORD_ARG_REMEDIATION.md +22 -0
- data/.github/skills/extract-facade-from-base-lib/SKILL.md +28 -14
- data/.github/skills/facade-implementation/SKILL.md +14 -0
- data/.github/skills/facade-test-conventions/SKILL.md +14 -0
- data/.rubocop.yml +5 -0
- data/README.md +51 -11
- data/UPGRADING.md +141 -0
- data/git.gemspec +5 -0
- data/lib/git/branch.rb +7 -18
- data/lib/git/branches.rb +2 -10
- data/lib/git/command_line/base.rb +10 -0
- data/lib/git/command_line/capturing.rb +5 -3
- data/lib/git/command_line/streaming.rb +5 -3
- data/lib/git/command_line.rb +3 -3
- data/lib/git/commands/base.rb +7 -6
- data/lib/git/commands/cat_file/batch.rb +6 -1
- data/lib/git/commands/cat_file/raw.rb +7 -1
- data/lib/git/commands/config_option_syntax/get_urlmatch.rb +5 -0
- data/lib/git/commands/show_ref/exclude_existing.rb +1 -1
- data/lib/git/commands/update_ref/batch.rb +1 -1
- data/lib/git/commands/version.rb +5 -0
- data/lib/git/commands.rb +5 -7
- data/lib/git/config.rb +17 -0
- data/lib/git/config_entry_info.rb +106 -0
- data/lib/git/configuring.rb +665 -0
- data/lib/git/deprecation.rb +9 -0
- data/lib/git/diff.rb +4 -8
- data/lib/git/diff_path_status.rb +2 -13
- data/lib/git/diff_stats.rb +1 -9
- data/lib/git/execution_context/global.rb +3 -28
- data/lib/git/execution_context/repository.rb +30 -41
- data/lib/git/execution_context.rb +43 -24
- data/lib/git/log.rb +3 -9
- data/lib/git/object.rb +14 -21
- data/lib/git/parsers/config_entry.rb +110 -0
- data/lib/git/parsers/ls_remote.rb +79 -0
- data/lib/git/remote.rb +7 -20
- data/lib/git/repository/branching.rb +183 -12
- data/lib/git/repository/committing.rb +64 -68
- data/lib/git/repository/configuring.rb +208 -13
- data/lib/git/repository/context_helpers.rb +264 -0
- data/lib/git/repository/factories.rb +682 -0
- data/lib/git/repository/inspecting.rb +99 -0
- data/lib/git/repository/maintenance.rb +65 -0
- data/lib/git/repository/merging.rb +63 -1
- data/lib/git/repository/object_operations.rb +133 -35
- data/lib/git/repository/path_resolver.rb +1 -1
- data/lib/git/repository/remote_operations.rb +166 -21
- data/lib/git/repository/staging.rb +187 -23
- data/lib/git/repository/stashing.rb +39 -3
- data/lib/git/repository/status_operations.rb +21 -0
- data/lib/git/repository.rb +68 -129
- data/lib/git/stash.rb +2 -9
- data/lib/git/stashes.rb +2 -7
- data/lib/git/status.rb +8 -17
- data/lib/git/version.rb +2 -2
- data/lib/git/worktree.rb +2 -15
- data/lib/git/worktrees.rb +2 -15
- data/lib/git.rb +180 -77
- data/redesign/3_architecture_implementation.md +148 -111
- data/redesign/Phase 4 - Step A.md +360 -0
- data/redesign/beta_release.md +107 -0
- data/redesign/c1c2_audit.md +566 -0
- data/redesign/c1c2_bucket6_lib_orphans.md +626 -0
- data/redesign/config_design.rb +501 -0
- metadata +19 -5
- data/lib/git/base.rb +0 -1204
- 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
|
-
#
|
|
8
|
+
# Legacy facade methods for reading and writing git configuration
|
|
9
9
|
#
|
|
10
|
-
# Provides the {#config}
|
|
11
|
-
#
|
|
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
|
-
#
|
|
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 [
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|