git 5.0.0.beta.1 → 5.0.0.beta.3

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 +90 -22
  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/context_helpers.rb +264 -0
  44. data/lib/git/repository/factories.rb +682 -0
  45. data/lib/git/repository/inspecting.rb +99 -0
  46. data/lib/git/repository/maintenance.rb +65 -0
  47. data/lib/git/repository/merging.rb +63 -1
  48. data/lib/git/repository/object_operations.rb +133 -35
  49. data/lib/git/repository/path_resolver.rb +1 -1
  50. data/lib/git/repository/remote_operations.rb +166 -21
  51. data/lib/git/repository/staging.rb +187 -23
  52. data/lib/git/repository/stashing.rb +39 -3
  53. data/lib/git/repository/status_operations.rb +21 -0
  54. data/lib/git/repository.rb +288 -128
  55. data/lib/git/stash.rb +2 -9
  56. data/lib/git/stashes.rb +2 -7
  57. data/lib/git/status.rb +12 -23
  58. data/lib/git/version.rb +2 -2
  59. data/lib/git/worktree.rb +2 -15
  60. data/lib/git/worktrees.rb +2 -15
  61. data/lib/git.rb +182 -77
  62. data/redesign/3_architecture_implementation.md +170 -112
  63. data/redesign/Phase 4 - Step A.md +366 -0
  64. data/redesign/beta_release.md +107 -0
  65. data/redesign/c1c2_audit.md +566 -0
  66. data/redesign/c1c2_bucket6_lib_orphans.md +626 -0
  67. data/redesign/config_design.rb +501 -0
  68. metadata +19 -6
  69. data/lib/git/base.rb +0 -1204
  70. data/lib/git/lib.rb +0 -2855
  71. data/lib/git/repository/configuring.rb +0 -156
data/lib/git/worktrees.rb CHANGED
@@ -1,18 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'git/base'
4
-
5
3
  module Git
6
4
  # Collection of all Git worktrees in a repository
7
5
  #
8
6
  # Wraps every linked and main worktree and provides enumeration and
9
7
  # path-based lookup.
10
8
  #
11
- # Accepts either a {Git::Repository} (new form) or a {Git::Base} (legacy
12
- # form) as the `base` argument. The `is_a?(Git::Base)` guard routes git
13
- # operations through the facade repository and will be removed when
14
- # {Git::Base} is deleted in Phase 4.
15
- #
16
9
  # @example Enumerate all worktrees
17
10
  # worktrees = repo.worktrees
18
11
  # worktrees.each { |wt| puts wt.dir }
@@ -24,7 +17,7 @@ module Git
24
17
 
25
18
  # Creates a new Worktrees collection populated from the given repository
26
19
  #
27
- # @param base [Git::Base, Git::Repository] the repository to enumerate
20
+ # @param base [Git::Repository] the repository to enumerate
28
21
  # worktrees from
29
22
  #
30
23
  # @return [void]
@@ -130,18 +123,12 @@ module Git
130
123
 
131
124
  private
132
125
 
133
- # Resolves the {Git::Repository} for this collection of worktrees
134
- #
135
- # Accepts either a {Git::Repository} (new form) or a {Git::Base} (legacy).
136
- # The `is_a?(Git::Base)` guard will be removed when {Git::Base} is deleted
137
- # in Phase 4.
138
- #
139
126
  # @return [Git::Repository] the repository used to enumerate worktrees
140
127
  #
141
128
  # @api private
142
129
  #
143
130
  def worktree_repository
144
- @base.is_a?(Git::Base) ? @base.facade_repository : @base
131
+ @base
145
132
  end
146
133
  end
147
134
  end
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,57 @@ 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.config_get(name)`, `Git.config_set(name, value)`, or `Git.config_list` 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.config_get(name), Git.config_set(name, value), or Git.config_list 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.config_get(name, global: true)`, `Git.config_set(name, value, global: true)`, or
118
+ # `Git.config_list(global: true)` instead.
98
119
  def global_config(name = nil, value = nil)
99
- self.class.global_config(name, value)
120
+ Git::Deprecation.warn(
121
+ 'Git#global_config is deprecated and will be removed in v6.0.0. ' \
122
+ 'Use Git.config_get(name, global: true), Git.config_set(name, value, global: true), ' \
123
+ 'or Git.config_list(global: true) instead.'
124
+ )
125
+ Git.global_config(name, value)
100
126
  end
101
127
 
102
128
  # Open a bare repository
@@ -130,11 +156,11 @@ module Git
130
156
  # are logged at the `:info` level. Additional logging is done at the `:debug`
131
157
  # level.
132
158
  #
133
- # @return [Git::Base] an object that can execute git commands in the context
159
+ # @return [Git::Repository] an object that can execute git commands in the context
134
160
  # of the bare repository.
135
161
  #
136
162
  def self.bare(git_dir, options = {})
137
- Base.bare(git_dir, options)
163
+ Git::Repository.bare(git_dir, options)
138
164
  end
139
165
 
140
166
  # Clone a repository into an empty or newly created directory
@@ -248,11 +274,11 @@ module Git
248
274
  # git_ssh: 'ssh -i /path/to/private_key'
249
275
  # )
250
276
  #
251
- # @return [Git::Base] an object that can execute git commands in the context
277
+ # @return [Git::Repository] an object that can execute git commands in the context
252
278
  # of the cloned local working copy or cloned repository.
253
279
  #
254
280
  def self.clone(repository_url, directory = nil, options = {})
255
- Base.clone(repository_url, directory, options)
281
+ Git::Repository.clone(repository_url, directory, options)
256
282
  end
257
283
 
258
284
  # Returns the name of the default branch of the given repository
@@ -293,7 +319,9 @@ module Git
293
319
  # @return [String] the name of the default branch
294
320
  #
295
321
  def self.default_branch(repository, options = {})
296
- Base.repository_default_branch(repository, options)
322
+ context = Git::ExecutionContext::Global.new(logger: options[:log])
323
+ output = Git::Commands::LsRemote.new(context).call(repository, 'HEAD', symref: true).stdout
324
+ Git::Parsers::LsRemote.parse_default_branch(output)
297
325
  end
298
326
 
299
327
  # Export the current HEAD (or a branch, if <tt>options[:branch]</tt>
@@ -317,17 +345,7 @@ module Git
317
345
  # g.config('user.name') # returns 'Scott Chacon'
318
346
  # g.config # returns whole config hash
319
347
  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
348
+ run_config_utility(name, value, global: true)
331
349
  end
332
350
 
333
351
  # Create an empty Git repository or reinitialize an existing Git repository
@@ -367,11 +385,15 @@ module Git
367
385
  # - If nil, disables SSH for this instance.
368
386
  # - If a non-empty string, uses that value for this instance.
369
387
  #
388
+ # @option options [String, :use_global_config] :binary_path path to the git
389
+ # binary; defaults to `Git::Config.instance.binary_path` when not specified.
390
+ # Raises `ArgumentError` if set to `nil`.
391
+ #
370
392
  # @option options [Logger] :log A logger to use for Git operations. Git
371
393
  # commands are logged at the `:info` level. Additional logging is done
372
394
  # at the `:debug` level.
373
395
  #
374
- # @return [Git::Base] an object that can execute git commands in the context
396
+ # @return [Git::Repository] an object that can execute git commands in the context
375
397
  # of the newly initialized repository
376
398
  #
377
399
  # @example Initialize a repository in the current directory
@@ -389,31 +411,23 @@ module Git
389
411
  # @see https://git-scm.com/docs/git-init git init
390
412
  #
391
413
  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)
414
+ Git::Repository.init(directory, options)
401
415
  end
402
416
 
403
- # Open the repository after initialization
417
+ # Option keys accepted by {.ls_remote}
418
+ #
419
+ # Parser-incompatible options such as `:get_url` and `:symref` are intentionally
420
+ # excluded because {Git::Parsers::LsRemote.parse_output} cannot handle the
421
+ # non-standard output formats those flags produce.
422
+ #
423
+ # @return [Array<Symbol>]
404
424
  #
405
- # @param directory [String] the directory containing the repository
406
- # @param options [Hash] the options hash
407
- # @return [Git::Base] the opened repository
408
425
  # @api private
409
426
  #
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
427
+ LS_REMOTE_ALLOWED_OPTS = %i[
428
+ branches b heads h tags t refs upload_pack quiet q exit_code sort server_option o timeout
429
+ ].freeze
430
+ private_constant :LS_REMOTE_ALLOWED_OPTS
417
431
 
418
432
  # returns a Hash containing information about the references
419
433
  # of the target repository
@@ -424,7 +438,15 @@ module Git
424
438
  # @param [String|NilClass] location the target repository location or nil for '.'
425
439
  # @return [{String=>Hash}] the available references of the target repo.
426
440
  def self.ls_remote(location = nil, options = {})
427
- Git::Lib.new.ls_remote(location, options)
441
+ options = options.dup
442
+ log = options.delete(:log)
443
+ unknown = options.keys - LS_REMOTE_ALLOWED_OPTS
444
+ raise ArgumentError, "Unknown options: #{unknown.join(', ')}" unless unknown.empty?
445
+
446
+ context = Git::ExecutionContext::Global.new(logger: log)
447
+ repository = location || '.'
448
+ output_lines = Git::Commands::LsRemote.new(context).call(repository, **options).stdout.split("\n")
449
+ Git::Parsers::LsRemote.parse_output(output_lines)
428
450
  end
429
451
 
430
452
  # Open a an existing Git working directory
@@ -474,17 +496,41 @@ module Git
474
496
  # commands are logged at the `:info` level. Additional logging is done
475
497
  # at the `:debug` level.
476
498
  #
477
- # @return [Git::Base] an object that can execute git commands in the context
499
+ # @return [Git::Repository] an object that can execute git commands in the context
478
500
  # of the opened working copy
479
501
  #
480
502
  def self.open(working_dir, options = {})
481
- Base.open(working_dir, options)
503
+ Git::Repository.open(working_dir, options)
504
+ end
505
+
506
+ # Thread-safe cache for git versions, keyed by binary path.
507
+ @git_version_cache_mutex = Mutex.new
508
+ @git_version_cache = {}
509
+
510
+ # @api private
511
+ def self.cached_git_version(binary_path, &block)
512
+ @git_version_cache_mutex.synchronize do
513
+ @git_version_cache[binary_path] ||= block.call
514
+ end
515
+ end
516
+
517
+ # @api private
518
+ def self.clear_git_version_cache
519
+ @git_version_cache_mutex.synchronize do
520
+ @git_version_cache.clear
521
+ end
482
522
  end
483
523
 
484
524
  # Return the version of a git binary as a {Git::Version}
485
525
  #
526
+ # @example Default binary
527
+ # Git.git_version #=> #<Git::Version 2.42.0>
528
+ #
529
+ # @example Explicit binary path
530
+ # Git.git_version('/opt/homebrew/bin/git') #=> #<Git::Version 2.42.0>
531
+ #
486
532
  # @param binary_path [String, nil] path to the git binary; defaults to
487
- # `Git::Base.config.binary_path`
533
+ # `Git::Config.instance.binary_path`
488
534
  #
489
535
  # @return [Git::Version] the parsed git version
490
536
  #
@@ -494,15 +540,9 @@ module Git
494
540
  #
495
541
  # @raise [Git::Error] if the binary is not found or fails to launch
496
542
  #
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
543
  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) }
544
+ path = binary_path || Git::Config.instance.binary_path
545
+ cached_git_version(path) { run_git_version(path) }
506
546
  end
507
547
 
508
548
  # @api private
@@ -512,18 +552,83 @@ module Git
512
552
  end
513
553
  private_class_method :run_git_version
514
554
 
555
+ # @api private
556
+ def self.run_config_utility(name, value, global:)
557
+ context = Git::ExecutionContext::Global.new
558
+ options = global ? { global: true } : {}
559
+
560
+ return Git::Commands::ConfigOptionSyntax::Set.new(context).call(name, value, **options) if !name.nil? && !value.nil?
561
+ return run_config_get(context, name, options) if name
562
+
563
+ output = Git::Commands::ConfigOptionSyntax::List.new(context).call(**options).stdout
564
+ parse_config_list(output.split("\n"))
565
+ end
566
+ private_class_method :run_config_utility
567
+
568
+ def self.run_config_get(context, name, options)
569
+ result = Git::Commands::ConfigOptionSyntax::Get.new(context).call(name, **options)
570
+ raise Git::FailedError, result if result.status.exitstatus != 0
571
+
572
+ result.stdout
573
+ end
574
+ private_class_method :run_config_get
575
+
576
+ # @api private
577
+ def self.parse_config_list(lines)
578
+ lines.each_with_object({}) do |line, hsh|
579
+ key, value = line.split('=', 2)
580
+ hsh[key] = value || ''
581
+ end
582
+ end
583
+ private_class_method :parse_config_list
584
+
585
+ # @api private
586
+ def self.execution_context
587
+ Git::ExecutionContext::Global.new
588
+ end
589
+ private_class_method :execution_context
590
+
591
+ # Scopes that require an active repository and cannot be used at the Git module level
592
+ #
593
+ # @api private
594
+ #
595
+ REPOSITORY_SPECIFIC_SCOPES = %i[local worktree blob].freeze
596
+ private_constant :REPOSITORY_SPECIFIC_SCOPES
597
+
598
+ # Raises +ArgumentError+ when a repository-specific scope is requested.
599
+ #
600
+ # The +:local+, +:worktree+, and +:blob+ scopes require an active git
601
+ # repository and are therefore not valid at the Git module level.
602
+ #
603
+ # @api private
604
+ #
605
+ def self.assert_valid_scope!(**opts)
606
+ invalid = REPOSITORY_SPECIFIC_SCOPES.select { |s| opts[s] }
607
+ return if invalid.empty?
608
+
609
+ raise ArgumentError, "#{invalid.join(', ')} scope requires a repository"
610
+ end
611
+ private_class_method :assert_valid_scope!
612
+
515
613
  # Return the version of the git binary
516
614
  #
517
- # @example
518
- # Git.binary_version # => [2, 46, 0]
615
+ # @example Basic usage
616
+ # Git.binary_version # => [2, 46, 0]
617
+ #
618
+ # @param binary_path [String, nil] path to the git binary; defaults to
619
+ # `Git::Config.instance.binary_path`
519
620
  #
520
621
  # @return [Array<Integer>] the version of the git binary
521
622
  #
522
- # @deprecated Use {Git.git_version} instead, which returns a {Git::Version} (not an Array).
623
+ # @deprecated Use {Git.git_version} instead, which returns a
624
+ # {Git::Version} (not an Array)
625
+ #
523
626
  # For the legacy array shape, call: `Git.git_version.to_a`.
524
- # The optional binary_path argument is preserved: `Git.git_version(binary_path)`.
627
+ # The optional binary_path argument is preserved:
628
+ # `Git.git_version(binary_path)`.
525
629
  #
526
- def self.binary_version(binary_path = Git::Base.config.binary_path)
630
+ def self.binary_version(binary_path = nil)
631
+ binary_path ||= Git::Config.instance.binary_path
527
632
  Git::Deprecation.warn(
528
633
  'Git.binary_version is deprecated and will be removed in 6.0. ' \
529
634
  'Use Git.git_version instead, which returns a Git::Version ' \