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
@@ -0,0 +1,682 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git/commands/clone'
4
+ require 'git/commands/init'
5
+ require 'git/execution_context/global'
6
+ require 'git/execution_context/repository'
7
+ require 'git/repository/path_resolver'
8
+ require 'pathname'
9
+
10
+ module Git
11
+ class Repository
12
+ # Factory class methods for constructing {Git::Repository} instances
13
+ #
14
+ # The four public factories — {clone}, {init}, {open}, {bare} — mirror the
15
+ # top-level `Git.*` entry points and return a {Git::Repository}.
16
+ #
17
+ # Extended by {Git::Repository}.
18
+ #
19
+ # @api public
20
+ #
21
+ module Factories # rubocop:disable Metrics/ModuleLength
22
+ # Clone a repository into a new directory
23
+ #
24
+ # @example Clone into the default directory
25
+ # repository = Git::Repository.clone('https://github.com/ruby-git/ruby-git.git')
26
+ #
27
+ # @example Clone into a specific directory
28
+ # repo_url = 'https://github.com/ruby-git/ruby-git.git'
29
+ # repository = Git::Repository.clone(repo_url, 'local')
30
+ #
31
+ # @example Clone a bare repository
32
+ # repo_url = 'https://github.com/ruby-git/ruby-git.git'
33
+ # repository = Git::Repository.clone(repo_url, nil, bare: true)
34
+ #
35
+ # @param repository_url [String] the URL or path of the repository to clone
36
+ #
37
+ # @param directory [String, nil] the local directory name to clone into;
38
+ # git derives the name from the URL when `nil`
39
+ #
40
+ # @param options [Hash] options that control cloning
41
+ #
42
+ # Some options configure the returned {Git::Repository} instance after
43
+ # the clone completes. Supported `git clone` options are forwarded.
44
+ #
45
+ # @option options [String, nil] :template template directory to use
46
+ #
47
+ # @option options [Boolean, nil] :local use the local clone optimization
48
+ #
49
+ # @option options [Boolean, nil] :no_local disable the local clone optimization
50
+ #
51
+ # @option options [Boolean, nil] :shared set up a shared clone
52
+ #
53
+ # @option options [Boolean, nil] :no_hardlinks copy files instead of hardlinks
54
+ #
55
+ # @option options [Boolean, nil] :quiet suppress progress output
56
+ #
57
+ # @option options [Boolean, nil] :verbose run verbosely
58
+ #
59
+ # @option options [Boolean, nil] :progress force progress output
60
+ #
61
+ # @option options [Boolean, nil] :no_checkout skip checking out `HEAD`
62
+ #
63
+ # @option options [Boolean, nil] :bare clone as a bare repository
64
+ #
65
+ # @option options [Boolean, nil] :mirror set up a mirror of the source
66
+ # (implies `:bare`)
67
+ #
68
+ # @option options [String, nil] :origin remote name to use instead of `origin`
69
+ #
70
+ # @option options [String, nil] :branch the branch or tag to check out after cloning
71
+ #
72
+ # @option options [String, nil] :revision revision to check out after cloning
73
+ #
74
+ # @option options [String, nil] :upload_pack remote `git-upload-pack` path
75
+ #
76
+ # @option options [String, Array<String>, nil] :reference reference repository
77
+ #
78
+ # @option options [String, Array<String>, nil] :reference_if_able
79
+ # optional reference repository
80
+ #
81
+ # @option options [Boolean, nil] :dissociate stop borrowing from references
82
+ #
83
+ # @option options [String, nil] :separate_git_dir alternate git directory path
84
+ #
85
+ # @option options [String, Array<String>, nil] :server_option
86
+ # protocol-v2 server options
87
+ #
88
+ # @option options [Integer, String, nil] :depth create a shallow clone
89
+ #
90
+ # @option options [String, nil] :shallow_since create a shallow clone by date
91
+ #
92
+ # @option options [String, Array<String>, nil] :shallow_exclude
93
+ # exclude commits reachable from a ref
94
+ #
95
+ # @option options [Boolean, nil] :single_branch clone one branch's history
96
+ #
97
+ # @option options [Boolean, nil] :no_single_branch clone all branch history
98
+ #
99
+ # @option options [Boolean, nil] :tags include tags in the clone
100
+ #
101
+ # @option options [Boolean, nil] :no_tags exclude tags from the clone
102
+ #
103
+ # @option options [Boolean, String, Array<String>, nil] :recurse_submodules
104
+ # initialize submodules after cloning
105
+ #
106
+ # Pass `true` to initialize all submodules, or pass a pathspec string or
107
+ # array for a subset.
108
+ #
109
+ # @option options [Boolean, nil] :shallow_submodules use depth 1 for submodules
110
+ #
111
+ # @option options [Boolean, nil] :no_shallow_submodules use full submodule history
112
+ #
113
+ # @option options [Boolean, nil] :remote_submodules use submodule remote branches
114
+ #
115
+ # @option options [Boolean, nil] :no_remote_submodules use recorded submodule SHAs
116
+ #
117
+ # @option options [Integer, String, nil] :jobs submodule jobs to run concurrently
118
+ #
119
+ # @option options [Boolean, nil] :sparse enable sparse checkout
120
+ #
121
+ # @option options [Boolean, nil] :reject_shallow reject shallow source repositories
122
+ #
123
+ # @option options [Boolean, nil] :no_reject_shallow allow shallow sources
124
+ #
125
+ # @option options [String, nil] :filter specify a partial clone filter
126
+ #
127
+ # @option options [Boolean, nil] :also_filter_submodules filter submodules too
128
+ #
129
+ # @option options [String, Array<String>, nil] :config repository config entries
130
+ #
131
+ # @option options [String, nil] :bundle_uri bundle URI to prefetch
132
+ #
133
+ # @option options [String, nil] :ref_format ref storage format
134
+ #
135
+ # @option options [Numeric, nil] :timeout command timeout in seconds
136
+ #
137
+ # @option options [String, nil] :repository alternate git directory path
138
+ #
139
+ # Preferred facade spelling for `git clone --separate-git-dir`.
140
+ #
141
+ # @option options [String, nil, :use_global_config] :git_ssh path to a custom
142
+ # SSH executable
143
+ #
144
+ # Pass `:use_global_config` (the default) to use
145
+ # `Git.config.git_ssh`.
146
+ #
147
+ # @option options [String, :use_global_config] :binary_path path to the git
148
+ # binary
149
+ #
150
+ # Pass `:use_global_config` (the default) to use
151
+ # `Git.config.binary_path`.
152
+ #
153
+ # @option options [Logger, nil] :log logger used for git operations
154
+ #
155
+ # @option options [String, nil] :index a non-standard path to the index file
156
+ #
157
+ # @option options [String, Pathname, nil] :chdir run `git clone` from within
158
+ # this directory
159
+ #
160
+ # @option options [String, Pathname, nil] :path deprecated; use `:chdir` instead
161
+ #
162
+ # @option options [Boolean, nil] :recursive deprecated; use
163
+ # `:recurse_submodules` instead
164
+ #
165
+ # @option options [String, nil] :remote deprecated; use `:origin` instead
166
+ #
167
+ # @return [Git::Repository] a repository bound to the cloned working copy or
168
+ # bare repository
169
+ #
170
+ # @raise [ArgumentError] if unsupported options are provided
171
+ #
172
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
173
+ #
174
+ # @raise [Git::UnexpectedResultError] if the cloned directory cannot be
175
+ # determined from git's output
176
+ #
177
+ # @api public
178
+ #
179
+ def clone(repository_url, directory = nil, options = {})
180
+ opts, context_opts = prepare_clone_options(options)
181
+ clone_result = run_clone_command(repository_url, directory, opts, context_opts)
182
+ paths = resolve_paths_from_clone_result(clone_result, opts, context_opts)
183
+
184
+ from_paths(clone_repository_options(context_opts), paths)
185
+ end
186
+
187
+ # Create an empty Git repository or reinitialize an existing one
188
+ #
189
+ # @example Initialize in the current directory
190
+ # repository = Git::Repository.init
191
+ #
192
+ # @example Initialize in a specific directory
193
+ # repository = Git::Repository.init('/path/to/project')
194
+ #
195
+ # @example Initialize a bare repository
196
+ # repository = Git::Repository.init('/path/to/project.git', bare: true)
197
+ #
198
+ # @param directory [String] the directory to initialize; defaults to `'.'`
199
+ #
200
+ # @param options [Hash] options that control initialization
201
+ #
202
+ # Some options configure the returned {Git::Repository} instance after
203
+ # the repository is initialized.
204
+ #
205
+ # @option options [Boolean, nil] :bare create a bare repository at `directory`
206
+ #
207
+ # @option options [String, nil] :initial_branch the name for the initial branch
208
+ #
209
+ # @option options [String, nil] :repository path for the `.git` directory
210
+ #
211
+ # Writes a gitfile in the working tree. Alias: `:separate_git_dir`.
212
+ #
213
+ # @option options [String, nil] :separate_git_dir alias for `:repository`
214
+ #
215
+ # @option options [String, nil, :use_global_config] :git_ssh path to a custom
216
+ # SSH executable
217
+ #
218
+ # Pass `:use_global_config` (the default) to use
219
+ # `Git.config.git_ssh`.
220
+ #
221
+ # @option options [String, :use_global_config] :binary_path path to the git
222
+ # binary
223
+ #
224
+ # Pass `:use_global_config` (the default) to use
225
+ # `Git.config.binary_path`.
226
+ #
227
+ # @option options [Logger, nil] :log logger used for git operations
228
+ #
229
+ # @option options [String, nil] :index custom index path for the returned
230
+ # repository
231
+ #
232
+ # Ignored when `:bare` is `true`.
233
+ #
234
+ # @return [Git::Repository] a repository bound to the newly initialized repository
235
+ #
236
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
237
+ #
238
+ # @api public
239
+ #
240
+ def init(directory = '.', options = {})
241
+ options = options.dup
242
+ if options.key?(:separate_git_dir) && options[:repository].nil?
243
+ options[:repository] = options.delete(:separate_git_dir)
244
+ end
245
+
246
+ run_init_command(directory, options)
247
+ open_after_init(directory, options)
248
+ end
249
+
250
+ # Open a working copy at an existing path
251
+ #
252
+ # Note: this method opens working copies only. To open a bare repository, use
253
+ # `Git::Repository.bare`.
254
+ #
255
+ # @example Open the working copy in the current directory
256
+ # repository = Git::Repository.open('.')
257
+ #
258
+ # @param working_dir [String] the path to the root of the working copy
259
+ #
260
+ # May be any path inside the working tree when `:repository` is not given.
261
+ #
262
+ # @param options [Hash] options that control how the repository is located
263
+ #
264
+ # @option options [String, nil] :repository a non-standard path to the
265
+ # `.git` directory
266
+ #
267
+ # When given, `working_dir` is used as-is (the working tree root is not
268
+ # auto-detected).
269
+ #
270
+ # @option options [String, nil] :index a non-standard path to the index file
271
+ #
272
+ # @option options [Logger, nil] :log logger used for git operations
273
+ #
274
+ # @option options [String, nil, :use_global_config] :git_ssh
275
+ # path to a custom SSH executable
276
+ #
277
+ # Pass `:use_global_config` (the default) to use
278
+ # `Git.config.git_ssh`.
279
+ #
280
+ # @option options [String, :use_global_config] :binary_path
281
+ # path to the git binary
282
+ #
283
+ # Pass `:use_global_config` (the default) to use
284
+ # `Git.config.binary_path`.
285
+ #
286
+ # @return [Git::Repository] a repository bound to the resolved paths
287
+ #
288
+ # @raise [ArgumentError] if `working_dir` is not a directory or is not inside
289
+ # a git working tree
290
+ #
291
+ # @api public
292
+ #
293
+ def open(working_dir, options = {})
294
+ raise ArgumentError, "'#{working_dir}' is not a directory" unless Dir.exist?(working_dir)
295
+
296
+ working_dir = resolve_open_working_dir(working_dir, options) unless options[:repository]
297
+
298
+ paths = PathResolver.resolve_paths(
299
+ working_directory: working_dir,
300
+ repository: options[:repository],
301
+ index: options[:index]
302
+ )
303
+
304
+ from_paths(options, paths)
305
+ end
306
+
307
+ # Open an existing bare repository at `git_dir`
308
+ #
309
+ # @example Open a bare repository
310
+ # repository = Git::Repository.bare('/path/to/repo.git')
311
+ #
312
+ # @param git_dir [String] the path to the bare repository directory
313
+ #
314
+ # @param options [Hash] options used to configure the repository instance
315
+ #
316
+ # @option options [Logger, nil] :log logger used for git operations
317
+ #
318
+ # @option options [String, nil, :use_global_config] :git_ssh
319
+ # path to a custom SSH executable
320
+ #
321
+ # Pass `:use_global_config` (the default) to use
322
+ # `Git.config.git_ssh`.
323
+ #
324
+ # @option options [String, :use_global_config] :binary_path
325
+ # path to the git binary
326
+ #
327
+ # Pass `:use_global_config` (the default) to use
328
+ # `Git.config.binary_path`.
329
+ #
330
+ # @return [Git::Repository] a repository bound to the bare repository directory
331
+ #
332
+ # @api public
333
+ #
334
+ def bare(git_dir, options = {})
335
+ paths = PathResolver.resolve_paths(repository: git_dir, bare: true)
336
+
337
+ from_paths(options, paths)
338
+ end
339
+
340
+ private
341
+
342
+ # Run the `git clone` command using a global execution context
343
+ #
344
+ # @param repository_url [String] the URL or path of the repository to clone
345
+ #
346
+ # @param directory [String, nil] the local directory name to clone into
347
+ #
348
+ # @param opts [Hash] command-ready clone options
349
+ #
350
+ # @param context_opts [Hash] context options produced while preparing clone
351
+ # options
352
+ #
353
+ # @return [Git::CommandLineResult] the result of running `git clone`
354
+ #
355
+ # @raise [ArgumentError] if unsupported options are provided
356
+ #
357
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
358
+ #
359
+ # @api private
360
+ #
361
+ def run_clone_command(repository_url, directory, opts, context_opts)
362
+ context = Git::ExecutionContext::Global.new(
363
+ binary_path: context_opts[:binary_path],
364
+ git_ssh: context_opts[:git_ssh],
365
+ logger: context_opts[:logger]
366
+ )
367
+
368
+ Git::Commands::Clone.new(context).call(repository_url, directory, **opts)
369
+ end
370
+
371
+ # Resolve repository paths from a completed clone result
372
+ #
373
+ # @param clone_result [Git::CommandLineResult] the completed clone result
374
+ #
375
+ # @param opts [Hash] command-ready clone options
376
+ #
377
+ # @param context_opts [Hash] context options produced while preparing clone
378
+ # options
379
+ #
380
+ # @return [Hash{Symbol => (String, nil)}] resolved path hash
381
+ #
382
+ # @raise [Git::UnexpectedResultError] if the clone directory cannot be parsed
383
+ #
384
+ # @api private
385
+ #
386
+ def resolve_paths_from_clone_result(clone_result, opts, context_opts)
387
+ clone_dir, cloned_bare = parse_clone_stderr(clone_result.stderr)
388
+ chdir = opts[:chdir]
389
+ clone_dir = File.join(chdir, clone_dir) if chdir && !Pathname.new(clone_dir).absolute?
390
+
391
+ bare = opts[:bare] || opts[:mirror] || cloned_bare
392
+ resolve_clone_paths(clone_dir, bare, context_opts[:index])
393
+ end
394
+
395
+ # Build repository construction options from clone context options
396
+ #
397
+ # @param context_opts [Hash] context options produced while preparing clone
398
+ # options
399
+ #
400
+ # @return [Hash{Symbol => Object}] repository construction options
401
+ #
402
+ # @api private
403
+ #
404
+ def clone_repository_options(context_opts)
405
+ {
406
+ git_ssh: context_opts[:git_ssh],
407
+ binary_path: context_opts[:binary_path],
408
+ log: context_opts[:logger]
409
+ }
410
+ end
411
+
412
+ # Build the `:binary_path` and `:git_ssh` execution-context defaults
413
+ #
414
+ # Reads the values from the caller-supplied options, falling back to the
415
+ # `:use_global_config` sentinel for any the caller did not provide so the
416
+ # value is resolved from `Git::Config.instance` at call time.
417
+ #
418
+ # @param options [Hash] the caller-supplied options hash
419
+ #
420
+ # @return [Hash] context defaults with two keys: `:binary_path`
421
+ # (`String` or `:use_global_config` — `nil` is not valid and raises
422
+ # `ArgumentError` in {Git::ExecutionContext#initialize}) and `:git_ssh`
423
+ # (`String`, `nil`, or `:use_global_config`)
424
+ #
425
+ # @api private
426
+ #
427
+ def context_defaults(options)
428
+ {
429
+ binary_path: options.fetch(:binary_path, :use_global_config),
430
+ git_ssh: options.fetch(:git_ssh, :use_global_config)
431
+ }
432
+ end
433
+
434
+ # Resolve the worktree root to use as the working directory for {.open}
435
+ #
436
+ # @param working_dir [String] a path inside the working tree
437
+ #
438
+ # @param options [Hash] the caller-supplied options hash from {.open}
439
+ #
440
+ # @return [String] the absolute path to the root of the working tree
441
+ #
442
+ # @raise [ArgumentError] if `working_dir` is not inside a git working tree
443
+ #
444
+ # @api private
445
+ #
446
+ def resolve_open_working_dir(working_dir, options)
447
+ PathResolver.root_of_worktree(working_dir, **context_defaults(options))
448
+ end
449
+
450
+ # Build a repository from caller options and resolved paths
451
+ #
452
+ # @param options [Hash] the caller-supplied options (`:git_ssh`,
453
+ # `:binary_path`, `:log`)
454
+ #
455
+ # @param paths [Hash{Symbol => (String, nil)}] the resolved paths
456
+ #
457
+ # @return [Git::Repository] the constructed repository
458
+ #
459
+ # @api private
460
+ #
461
+ def from_paths(options, paths)
462
+ new(execution_context: Git::ExecutionContext::Repository.from_hash(
463
+ options.merge(paths), logger: options[:log]
464
+ ))
465
+ end
466
+
467
+ # Extract facade-level options from the raw clone options and return
468
+ # command-ready options
469
+ #
470
+ # Returns `[command_opts, context_opts]` where `command_opts` is the
471
+ # caller's options with facade-level keys removed. Remaining keys are
472
+ # forwarded to `Git::Commands::Clone`, which raises `ArgumentError` for
473
+ # unsupported ones. `context_opts` contains values used for the execution
474
+ # context and post-clone path resolution.
475
+ #
476
+ # @param options [Hash] raw caller-supplied options
477
+ #
478
+ # @return [Array<Hash>] a two-element tuple `[command_opts, context_opts]`
479
+ #
480
+ # @api private
481
+ #
482
+ def prepare_clone_options(options)
483
+ opts = options.dup
484
+ deprecate_clone_path_option!(opts)
485
+ deprecate_clone_recursive_option!(opts)
486
+ deprecate_clone_remote_option!(opts)
487
+ context_opts = extract_clone_context_options!(opts)
488
+ normalize_clone_repository_option!(opts)
489
+
490
+ [opts, context_opts]
491
+ end
492
+
493
+ # Extract clone context options from command-ready options
494
+ #
495
+ # @param opts [Hash] clone options (mutated in place)
496
+ #
497
+ # @return [Hash{Symbol => Object}] context options for clone setup
498
+ #
499
+ # @api private
500
+ #
501
+ def extract_clone_context_options!(opts)
502
+ {
503
+ logger: opts.delete(:log),
504
+ git_ssh: opts.key?(:git_ssh) ? opts.delete(:git_ssh) : :use_global_config,
505
+ binary_path: opts.key?(:binary_path) ? opts.delete(:binary_path) : :use_global_config,
506
+ index: opts.delete(:index)
507
+ }
508
+ end
509
+
510
+ # Normalize the clone repository option for `git clone`
511
+ #
512
+ # @param opts [Hash] clone options (mutated in place)
513
+ #
514
+ # @return [void] mutates `opts` in place
515
+ #
516
+ # @api private
517
+ #
518
+ def normalize_clone_repository_option!(opts)
519
+ return unless opts.key?(:repository)
520
+
521
+ repository_val = opts.delete(:repository)
522
+ opts[:separate_git_dir] = repository_val if repository_val
523
+ end
524
+
525
+ # Resolve paths for the cloned repository
526
+ #
527
+ # @param clone_dir [String] the directory reported by `git clone`
528
+ #
529
+ # @param bare [Boolean] whether the clone is bare
530
+ #
531
+ # @param index [String, nil] optional custom index path
532
+ #
533
+ # @return [Hash{Symbol => (String, nil)}] resolved path hash
534
+ #
535
+ # @api private
536
+ #
537
+ def resolve_clone_paths(clone_dir, bare, index)
538
+ args = bare ? { repository: clone_dir, bare: true } : { working_directory: clone_dir }
539
+ PathResolver.resolve_paths(**args, index: index)
540
+ end
541
+
542
+ # Run the `git init` command using a global execution context
543
+ #
544
+ # @param directory [String] the directory to initialize
545
+ #
546
+ # @param options [Hash] the normalized options hash (after alias resolution)
547
+ #
548
+ # @return [Git::CommandLineResult] the result of running `git init`
549
+ #
550
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
551
+ #
552
+ # @api private
553
+ #
554
+ def run_init_command(directory, options)
555
+ init_opts = options.slice(:bare, :initial_branch)
556
+ init_opts[:separate_git_dir] = options[:repository] if options.key?(:repository)
557
+
558
+ context = Git::ExecutionContext::Global.new(**context_defaults(options), logger: options[:log])
559
+ Git::Commands::Init.new(context).call(directory, **init_opts)
560
+ end
561
+
562
+ # Open the repository produced by `git init`
563
+ #
564
+ # @param directory [String] the initialized directory
565
+ #
566
+ # @param options [Hash] the normalized options hash
567
+ #
568
+ # @return [Git::Repository] the repository opened after initialization
569
+ #
570
+ # @api private
571
+ #
572
+ def open_after_init(directory, options)
573
+ return bare(options[:repository] || directory, base_open_options_after_init(options)) if options[:bare]
574
+
575
+ self.open(directory, worktree_open_options_after_init(options))
576
+ end
577
+
578
+ # Build common options for opening a repository after `git init`
579
+ #
580
+ # @param options [Hash] the normalized options hash
581
+ #
582
+ # @return [Hash{Symbol => Object}] options accepted by {.open} and {.bare}
583
+ #
584
+ # @api private
585
+ #
586
+ def base_open_options_after_init(options)
587
+ {
588
+ git_ssh: options.fetch(:git_ssh, :use_global_config),
589
+ binary_path: options.fetch(:binary_path, :use_global_config)
590
+ }.tap do |open_opts|
591
+ open_opts[:log] = options[:log] if options[:log]
592
+ end
593
+ end
594
+
595
+ # Build worktree options for opening a repository after `git init`
596
+ #
597
+ # @param options [Hash] the normalized options hash
598
+ #
599
+ # @return [Hash{Symbol => Object}] options accepted by {.open}
600
+ #
601
+ # @api private
602
+ #
603
+ def worktree_open_options_after_init(options)
604
+ base_open_options_after_init(options).tap do |open_opts|
605
+ open_opts[:index] = options[:index] if options[:index]
606
+ open_opts[:repository] = options[:repository] if options[:repository]
607
+ end
608
+ end
609
+
610
+ # Parse the clone directory and bare status from `git clone` stderr output
611
+ #
612
+ # @param stderr [String] stderr output from `git clone`
613
+ #
614
+ # @return [Array] a two-element tuple `[clone_dir, bare]`
615
+ #
616
+ # @raise [Git::UnexpectedResultError] if the stderr output cannot be parsed
617
+ #
618
+ # @api private
619
+ #
620
+ def parse_clone_stderr(stderr)
621
+ match = stderr.match(/Cloning into (?:(bare repository) )?'(.+)'\.\.\./)
622
+ raise Git::UnexpectedResultError, "Unable to determine clone directory from: #{stderr}" unless match
623
+
624
+ [match[2], !match[1].nil?]
625
+ end
626
+
627
+ # Handle the deprecated `:path` option for {clone}
628
+ #
629
+ # @param opts [Hash] clone options (mutated in place)
630
+ #
631
+ # @return [void] mutates `opts` in place
632
+ #
633
+ # @api private
634
+ #
635
+ def deprecate_clone_path_option!(opts)
636
+ return unless opts.key?(:path)
637
+
638
+ if defined?(Git::Deprecation)
639
+ Git::Deprecation.warn('The :path option for Git::Repository.clone is deprecated, use :chdir instead')
640
+ end
641
+ path = opts.delete(:path)
642
+ opts[:chdir] ||= path
643
+ end
644
+
645
+ # Handle the deprecated `:recursive` option for {clone}
646
+ #
647
+ # @param opts [Hash] clone options (mutated in place)
648
+ #
649
+ # @return [void] mutates `opts` in place
650
+ #
651
+ # @api private
652
+ #
653
+ def deprecate_clone_recursive_option!(opts)
654
+ return unless opts.key?(:recursive)
655
+
656
+ if defined?(Git::Deprecation)
657
+ Git::Deprecation.warn(
658
+ 'The :recursive option for Git::Repository.clone is deprecated, use :recurse_submodules instead'
659
+ )
660
+ end
661
+ opts[:recurse_submodules] = opts.delete(:recursive)
662
+ end
663
+
664
+ # Handle the deprecated `:remote` option for {clone}
665
+ #
666
+ # @param opts [Hash] clone options (mutated in place)
667
+ #
668
+ # @return [void] mutates `opts` in place
669
+ #
670
+ # @api private
671
+ #
672
+ def deprecate_clone_remote_option!(opts)
673
+ return unless opts.key?(:remote)
674
+
675
+ if defined?(Git::Deprecation)
676
+ Git::Deprecation.warn('The :remote option for Git::Repository.clone is deprecated, use :origin instead')
677
+ end
678
+ opts[:origin] = opts.delete(:remote)
679
+ end
680
+ end
681
+ end
682
+ end