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
data/lib/git.rb CHANGED
@@ -3,11 +3,10 @@
3
3
  require 'active_support'
4
4
  require 'active_support/deprecation'
5
5
 
6
+ require 'git/deprecation'
6
7
  require 'git/version'
7
8
 
8
9
  module Git
9
- Deprecation = ActiveSupport::Deprecation.new('5.0.0', 'Git')
10
-
11
10
  # Minimum git version required by this gem
12
11
  #
13
12
  # Commands and features may require newer versions, but this is the absolute
@@ -26,8 +25,11 @@ require 'git/branch_info'
26
25
  require 'git/branches'
27
26
  require 'git/command_line_result'
28
27
  require 'git/command_line'
29
- require 'git/commands/init'
28
+ require 'process_executer'
30
29
  require 'git/config'
30
+ require 'git/config_entry_info'
31
+ require 'git/parsers/config_entry'
32
+ require 'git/configuring'
31
33
  require 'git/diff'
32
34
  require 'git/diff_file_numstat_info'
33
35
  require 'git/diff_file_patch_info'
@@ -40,16 +42,17 @@ require 'git/encoding_utils'
40
42
  require 'git/errors'
41
43
  require 'git/escaped_path'
42
44
  require 'git/execution_context'
45
+ require 'git/execution_context/global'
43
46
  require 'git/file_ref'
44
47
  require 'git/fsck_object'
45
48
  require 'git/fsck_result'
49
+ require 'git/parsers/ls_remote'
46
50
  require 'git/version_constraint'
47
- require 'git/lib'
51
+ require 'git/commands'
48
52
  require 'git/log'
49
53
  require 'git/object'
50
54
  require 'git/remote'
51
55
  require 'git/repository'
52
- require 'git/base'
53
56
  require 'git/status'
54
57
  require 'git/stash'
55
58
  require 'git/stash_info'
@@ -69,34 +72,55 @@ require 'git/worktrees'
69
72
  # @author Scott Chacon (mailto:schacon@gmail.com)
70
73
  #
71
74
  module Git
72
- # g.config('user.name', 'Scott Chacon') # sets value
73
- # g.config('user.email', 'email@email.com') # sets value
74
- # g.config('user.name') # returns 'Scott Chacon'
75
- # g.config # returns whole config hash
75
+ extend Git::Configuring
76
+
77
+ # @deprecated Mixing in the `Git` module is deprecated and will be removed in v6.0.0.
78
+ # Use `Git.open(Dir.pwd).config(...)` instead.
76
79
  def config(name = nil, value = nil)
77
- lib = Git::Lib.new
78
- if name && value
79
- # set value
80
- lib.config_set(name, value)
81
- elsif name
82
- # return value
83
- lib.config_get(name)
84
- else
85
- # return hash
86
- lib.config_list
87
- end
80
+ Git::Deprecation.warn(
81
+ 'Git#config is deprecated and will be removed in v6.0.0. ' \
82
+ 'Use Git.open(Dir.pwd).config(...) instead.'
83
+ )
84
+ Git.__send__(:run_config_utility, name, value, global: false)
88
85
  end
89
86
 
87
+ # Configures the gem by yielding {Git::Config.instance} to the block
88
+ #
89
+ # @example Set the global git binary path
90
+ # Git.configure { |c| c.binary_path = '/usr/local/bin/git' }
91
+ #
92
+ # @yield [config] yields the singleton config object
93
+ #
94
+ # @yieldparam config [Git::Config] the singleton config object
95
+ #
96
+ # @yieldreturn [void]
97
+ #
98
+ # @return [void]
99
+ #
90
100
  def self.configure
91
- yield Base.config
101
+ yield Git::Config.instance
102
+ nil
92
103
  end
93
104
 
105
+ # Returns the process-wide {Git::Config} singleton
106
+ #
107
+ # @example Read the configured binary path
108
+ # Git.config.binary_path #=> "git"
109
+ #
110
+ # @return [Git::Config] the singleton config object
111
+ #
94
112
  def self.config
95
- Base.config
113
+ Git::Config.instance
96
114
  end
97
115
 
116
+ # @deprecated Mixing in the `Git` module is deprecated and will be removed in v6.0.0.
117
+ # Use `Git.global_config(...)` instead.
98
118
  def global_config(name = nil, value = nil)
99
- self.class.global_config(name, value)
119
+ Git::Deprecation.warn(
120
+ 'Git#global_config is deprecated and will be removed in v6.0.0. ' \
121
+ 'Use Git.global_config(...) instead.'
122
+ )
123
+ Git.global_config(name, value)
100
124
  end
101
125
 
102
126
  # Open a bare repository
@@ -130,11 +154,11 @@ module Git
130
154
  # are logged at the `:info` level. Additional logging is done at the `:debug`
131
155
  # level.
132
156
  #
133
- # @return [Git::Base] an object that can execute git commands in the context
157
+ # @return [Git::Repository] an object that can execute git commands in the context
134
158
  # of the bare repository.
135
159
  #
136
160
  def self.bare(git_dir, options = {})
137
- Base.bare(git_dir, options)
161
+ Git::Repository.bare(git_dir, options)
138
162
  end
139
163
 
140
164
  # Clone a repository into an empty or newly created directory
@@ -248,11 +272,11 @@ module Git
248
272
  # git_ssh: 'ssh -i /path/to/private_key'
249
273
  # )
250
274
  #
251
- # @return [Git::Base] an object that can execute git commands in the context
275
+ # @return [Git::Repository] an object that can execute git commands in the context
252
276
  # of the cloned local working copy or cloned repository.
253
277
  #
254
278
  def self.clone(repository_url, directory = nil, options = {})
255
- Base.clone(repository_url, directory, options)
279
+ Git::Repository.clone(repository_url, directory, options)
256
280
  end
257
281
 
258
282
  # Returns the name of the default branch of the given repository
@@ -293,7 +317,9 @@ module Git
293
317
  # @return [String] the name of the default branch
294
318
  #
295
319
  def self.default_branch(repository, options = {})
296
- Base.repository_default_branch(repository, options)
320
+ context = Git::ExecutionContext::Global.new(logger: options[:log])
321
+ output = Git::Commands::LsRemote.new(context).call(repository, 'HEAD', symref: true).stdout
322
+ Git::Parsers::LsRemote.parse_default_branch(output)
297
323
  end
298
324
 
299
325
  # Export the current HEAD (or a branch, if <tt>options[:branch]</tt>
@@ -317,17 +343,7 @@ module Git
317
343
  # g.config('user.name') # returns 'Scott Chacon'
318
344
  # g.config # returns whole config hash
319
345
  def self.global_config(name = nil, value = nil)
320
- lib = Git::Lib.new(nil, nil)
321
- if name && value
322
- # set value
323
- lib.global_config_set(name, value)
324
- elsif name
325
- # return value
326
- lib.global_config_get(name)
327
- else
328
- # return hash
329
- lib.global_config_list
330
- end
346
+ run_config_utility(name, value, global: true)
331
347
  end
332
348
 
333
349
  # Create an empty Git repository or reinitialize an existing Git repository
@@ -367,11 +383,15 @@ module Git
367
383
  # - If nil, disables SSH for this instance.
368
384
  # - If a non-empty string, uses that value for this instance.
369
385
  #
386
+ # @option options [String, :use_global_config] :binary_path path to the git
387
+ # binary; defaults to `Git::Config.instance.binary_path` when not specified.
388
+ # Raises `ArgumentError` if set to `nil`.
389
+ #
370
390
  # @option options [Logger] :log A logger to use for Git operations. Git
371
391
  # commands are logged at the `:info` level. Additional logging is done
372
392
  # at the `:debug` level.
373
393
  #
374
- # @return [Git::Base] an object that can execute git commands in the context
394
+ # @return [Git::Repository] an object that can execute git commands in the context
375
395
  # of the newly initialized repository
376
396
  #
377
397
  # @example Initialize a repository in the current directory
@@ -389,31 +409,23 @@ module Git
389
409
  # @see https://git-scm.com/docs/git-init git init
390
410
  #
391
411
  def self.init(directory = '.', options = {})
392
- require_relative 'git/commands/init'
393
-
394
- options = options.dup
395
- options[:repository] ||= options.delete(:separate_git_dir)
396
- init_opts = options.slice(:bare, :initial_branch)
397
- init_opts[:separate_git_dir] = options[:repository] if options.key?(:repository)
398
- Git::Commands::Init.new(Git::Lib.new(nil, options[:log])).call(directory, **init_opts)
399
-
400
- open_initialized_repository(directory, options)
412
+ Git::Repository.init(directory, options)
401
413
  end
402
414
 
403
- # Open the repository after initialization
415
+ # Option keys accepted by {.ls_remote}
416
+ #
417
+ # Parser-incompatible options such as `:get_url` and `:symref` are intentionally
418
+ # excluded because {Git::Parsers::LsRemote.parse_output} cannot handle the
419
+ # non-standard output formats those flags produce.
420
+ #
421
+ # @return [Array<Symbol>]
404
422
  #
405
- # @param directory [String] the directory containing the repository
406
- # @param options [Hash] the options hash
407
- # @return [Git::Base] the opened repository
408
423
  # @api private
409
424
  #
410
- private_class_method def self.open_initialized_repository(directory, options)
411
- if options[:bare]
412
- Git.bare(options[:repository] || directory, options.slice(:log, :git_ssh).compact)
413
- else
414
- Git.open(directory, options.slice(:log, :git_ssh, :index, :repository).compact)
415
- end
416
- end
425
+ LS_REMOTE_ALLOWED_OPTS = %i[
426
+ branches b heads h tags t refs upload_pack quiet q exit_code sort server_option o timeout
427
+ ].freeze
428
+ private_constant :LS_REMOTE_ALLOWED_OPTS
417
429
 
418
430
  # returns a Hash containing information about the references
419
431
  # of the target repository
@@ -424,7 +436,15 @@ module Git
424
436
  # @param [String|NilClass] location the target repository location or nil for '.'
425
437
  # @return [{String=>Hash}] the available references of the target repo.
426
438
  def self.ls_remote(location = nil, options = {})
427
- Git::Lib.new.ls_remote(location, options)
439
+ options = options.dup
440
+ log = options.delete(:log)
441
+ unknown = options.keys - LS_REMOTE_ALLOWED_OPTS
442
+ raise ArgumentError, "Unknown options: #{unknown.join(', ')}" unless unknown.empty?
443
+
444
+ context = Git::ExecutionContext::Global.new(logger: log)
445
+ repository = location || '.'
446
+ output_lines = Git::Commands::LsRemote.new(context).call(repository, **options).stdout.split("\n")
447
+ Git::Parsers::LsRemote.parse_output(output_lines)
428
448
  end
429
449
 
430
450
  # Open a an existing Git working directory
@@ -474,17 +494,41 @@ module Git
474
494
  # commands are logged at the `:info` level. Additional logging is done
475
495
  # at the `:debug` level.
476
496
  #
477
- # @return [Git::Base] an object that can execute git commands in the context
497
+ # @return [Git::Repository] an object that can execute git commands in the context
478
498
  # of the opened working copy
479
499
  #
480
500
  def self.open(working_dir, options = {})
481
- Base.open(working_dir, options)
501
+ Git::Repository.open(working_dir, options)
502
+ end
503
+
504
+ # Thread-safe cache for git versions, keyed by binary path.
505
+ @git_version_cache_mutex = Mutex.new
506
+ @git_version_cache = {}
507
+
508
+ # @api private
509
+ def self.cached_git_version(binary_path, &block)
510
+ @git_version_cache_mutex.synchronize do
511
+ @git_version_cache[binary_path] ||= block.call
512
+ end
513
+ end
514
+
515
+ # @api private
516
+ def self.clear_git_version_cache
517
+ @git_version_cache_mutex.synchronize do
518
+ @git_version_cache.clear
519
+ end
482
520
  end
483
521
 
484
522
  # Return the version of a git binary as a {Git::Version}
485
523
  #
524
+ # @example Default binary
525
+ # Git.git_version #=> #<Git::Version 2.42.0>
526
+ #
527
+ # @example Explicit binary path
528
+ # Git.git_version('/opt/homebrew/bin/git') #=> #<Git::Version 2.42.0>
529
+ #
486
530
  # @param binary_path [String, nil] path to the git binary; defaults to
487
- # `Git::Base.config.binary_path`
531
+ # `Git::Config.instance.binary_path`
488
532
  #
489
533
  # @return [Git::Version] the parsed git version
490
534
  #
@@ -494,15 +538,9 @@ module Git
494
538
  #
495
539
  # @raise [Git::Error] if the binary is not found or fails to launch
496
540
  #
497
- # @example Default binary
498
- # Git.git_version #=> #<Git::Version 2.42.0>
499
- #
500
- # @example Explicit binary path
501
- # Git.git_version('/opt/homebrew/bin/git') #=> #<Git::Version 2.42.0>
502
- #
503
541
  def self.git_version(binary_path = nil)
504
- path = binary_path || Git::Base.config.binary_path
505
- Git::Lib.cached_git_version(path) { run_git_version(path) }
542
+ path = binary_path || Git::Config.instance.binary_path
543
+ cached_git_version(path) { run_git_version(path) }
506
544
  end
507
545
 
508
546
  # @api private
@@ -512,18 +550,83 @@ module Git
512
550
  end
513
551
  private_class_method :run_git_version
514
552
 
553
+ # @api private
554
+ def self.run_config_utility(name, value, global:)
555
+ context = Git::ExecutionContext::Global.new
556
+ options = global ? { global: true } : {}
557
+
558
+ return Git::Commands::ConfigOptionSyntax::Set.new(context).call(name, value, **options) if !name.nil? && !value.nil?
559
+ return run_config_get(context, name, options) if name
560
+
561
+ output = Git::Commands::ConfigOptionSyntax::List.new(context).call(**options).stdout
562
+ parse_config_list(output.split("\n"))
563
+ end
564
+ private_class_method :run_config_utility
565
+
566
+ def self.run_config_get(context, name, options)
567
+ result = Git::Commands::ConfigOptionSyntax::Get.new(context).call(name, **options)
568
+ raise Git::FailedError, result if result.status.exitstatus != 0
569
+
570
+ result.stdout
571
+ end
572
+ private_class_method :run_config_get
573
+
574
+ # @api private
575
+ def self.parse_config_list(lines)
576
+ lines.each_with_object({}) do |line, hsh|
577
+ key, value = line.split('=', 2)
578
+ hsh[key] = value || ''
579
+ end
580
+ end
581
+ private_class_method :parse_config_list
582
+
583
+ # @api private
584
+ def self.execution_context
585
+ Git::ExecutionContext::Global.new
586
+ end
587
+ private_class_method :execution_context
588
+
589
+ # Scopes that require an active repository and cannot be used at the Git module level
590
+ #
591
+ # @api private
592
+ #
593
+ REPOSITORY_SPECIFIC_SCOPES = %i[local worktree blob].freeze
594
+ private_constant :REPOSITORY_SPECIFIC_SCOPES
595
+
596
+ # Raises +ArgumentError+ when a repository-specific scope is requested.
597
+ #
598
+ # The +:local+, +:worktree+, and +:blob+ scopes require an active git
599
+ # repository and are therefore not valid at the Git module level.
600
+ #
601
+ # @api private
602
+ #
603
+ def self.assert_valid_scope!(**opts)
604
+ invalid = REPOSITORY_SPECIFIC_SCOPES.select { |s| opts[s] }
605
+ return if invalid.empty?
606
+
607
+ raise ArgumentError, "#{invalid.join(', ')} scope requires a repository"
608
+ end
609
+ private_class_method :assert_valid_scope!
610
+
515
611
  # Return the version of the git binary
516
612
  #
517
- # @example
518
- # Git.binary_version # => [2, 46, 0]
613
+ # @example Basic usage
614
+ # Git.binary_version # => [2, 46, 0]
615
+ #
616
+ # @param binary_path [String, nil] path to the git binary; defaults to
617
+ # `Git::Config.instance.binary_path`
519
618
  #
520
619
  # @return [Array<Integer>] the version of the git binary
521
620
  #
522
- # @deprecated Use {Git.git_version} instead, which returns a {Git::Version} (not an Array).
621
+ # @deprecated Use {Git.git_version} instead, which returns a
622
+ # {Git::Version} (not an Array)
623
+ #
523
624
  # For the legacy array shape, call: `Git.git_version.to_a`.
524
- # The optional binary_path argument is preserved: `Git.git_version(binary_path)`.
625
+ # The optional binary_path argument is preserved:
626
+ # `Git.git_version(binary_path)`.
525
627
  #
526
- def self.binary_version(binary_path = Git::Base.config.binary_path)
628
+ def self.binary_version(binary_path = nil)
629
+ binary_path ||= Git::Config.instance.binary_path
527
630
  Git::Deprecation.warn(
528
631
  'Git.binary_version is deprecated and will be removed in 6.0. ' \
529
632
  'Use Git.git_version instead, which returns a Git::Version ' \