git 5.0.5 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +69 -0
  3. data/CONTRIBUTING.md +209 -164
  4. data/LICENSE +1 -1
  5. data/README.md +205 -138
  6. data/git.gemspec +77 -17
  7. data/lib/git/command_line/base.rb +1 -2
  8. data/lib/git/commands/base.rb +1 -1
  9. data/lib/git/commands/cat_file/raw.rb +8 -7
  10. data/lib/git/configuring.rb +56 -0
  11. data/lib/git/log.rb +19 -0
  12. data/lib/git/repository/branching.rb +79 -5
  13. data/lib/git/repository/logging.rb +35 -11
  14. data/lib/git/repository/object_operations.rb +22 -1
  15. data/lib/git/version.rb +1 -1
  16. metadata +33 -122
  17. data/.claude/commands/address-copilot-reviews.md +0 -14
  18. data/.claude/settings.json +0 -16
  19. data/.claude/skills +0 -1
  20. data/.commitlintrc.yml +0 -38
  21. data/.dockerignore +0 -27
  22. data/.github/copilot-instructions.md +0 -30
  23. data/.github/hooks/bin-setup-on-worktree.json +0 -11
  24. data/.github/hooks/run-bin-setup-once.sh +0 -20
  25. data/.github/issue_template.md +0 -15
  26. data/.github/prompts/iteratively-address-copilot-reviews.prompt.md +0 -188
  27. data/.github/pull_request_template.md +0 -21
  28. data/.github/skills/breaking-change-analysis/SKILL.md +0 -99
  29. data/.github/skills/ci-cd-troubleshooting/SKILL.md +0 -264
  30. data/.github/skills/command-implementation/REFERENCE.md +0 -994
  31. data/.github/skills/command-implementation/SKILL.md +0 -230
  32. data/.github/skills/command-test-conventions/SKILL.md +0 -664
  33. data/.github/skills/command-yard-documentation/SKILL.md +0 -434
  34. data/.github/skills/dependency-management/SKILL.md +0 -72
  35. data/.github/skills/development-workflow/SKILL.md +0 -512
  36. data/.github/skills/facade-implementation/REFERENCE.md +0 -837
  37. data/.github/skills/facade-implementation/SKILL.md +0 -269
  38. data/.github/skills/facade-test-conventions/SKILL.md +0 -391
  39. data/.github/skills/facade-yard-documentation/SKILL.md +0 -435
  40. data/.github/skills/make-skill-template/SKILL.md +0 -226
  41. data/.github/skills/pr-readiness-review/SKILL.md +0 -205
  42. data/.github/skills/project-context/SKILL.md +0 -306
  43. data/.github/skills/pull-request-review/SKILL.md +0 -168
  44. data/.github/skills/rebase/SKILL.md +0 -148
  45. data/.github/skills/refactor-command-to-commandlineresult/SKILL.md +0 -131
  46. data/.github/skills/release-management/SKILL.md +0 -125
  47. data/.github/skills/resolve-feedback/SKILL.md +0 -288
  48. data/.github/skills/review-arguments-dsl/CHECKLIST.md +0 -788
  49. data/.github/skills/review-arguments-dsl/SKILL.md +0 -214
  50. data/.github/skills/review-cross-command-consistency/SKILL.md +0 -139
  51. data/.github/skills/reviewing-skills/SKILL.md +0 -214
  52. data/.github/skills/rspec-unit-testing-standards/SKILL.md +0 -685
  53. data/.github/skills/tdd-refactor-step/SKILL.md +0 -236
  54. data/.github/skills/test-debugging/SKILL.md +0 -161
  55. data/.github/skills/yard-documentation/SKILL.md +0 -981
  56. data/.github/skills/yard-documentation/element-rules.md +0 -162
  57. data/.github/skills-deprecated/README.md +0 -21
  58. data/.github/skills-deprecated/extract-command-from-lib/SKILL.md +0 -487
  59. data/.github/skills-deprecated/extract-facade-from-base-lib/KEYWORD_ARG_REMEDIATION.md +0 -22
  60. data/.github/skills-deprecated/extract-facade-from-base-lib/SKILL.md +0 -600
  61. data/.github/skills-deprecated/review-backward-compatibility/SKILL.md +0 -275
  62. data/.github/workflows/continuous_integration.yml +0 -343
  63. data/.github/workflows/enforce_conventional_commits.yml +0 -35
  64. data/.github/workflows/experimental_continuous_integration.yml +0 -59
  65. data/.github/workflows/release.yml +0 -52
  66. data/.github/workflows/warm_bundler_caches.yml +0 -82
  67. data/.gitignore +0 -30
  68. data/.husky/commit-msg +0 -1
  69. data/.husky/pre-commit +0 -13
  70. data/.release-please-config.json +0 -36
  71. data/.release-please-manifest.json +0 -3
  72. data/.rspec +0 -2
  73. data/.rubocop.yml +0 -37
  74. data/.rubocop_todo.yml +0 -30
  75. data/.yard-lint.yml +0 -75
  76. data/CLAUDE.md +0 -11
  77. data/Gemfile +0 -22
  78. data/Rakefile +0 -41
  79. data/docker/test/Dockerfile +0 -32
  80. data/docker/test/docker-compose.yml +0 -0
  81. data/package.json +0 -10
  82. data/redesign/1_architecture_existing.md +0 -102
  83. data/redesign/2_architecture_redesign.md +0 -449
  84. data/redesign/3_architecture_implementation.md +0 -1623
  85. data/redesign/Phase 4 - Step A.md +0 -366
  86. data/redesign/Phase 4 - Step B.md +0 -921
  87. data/redesign/Phase 4 - Step C.md +0 -833
  88. data/redesign/beta_release.md +0 -107
  89. data/redesign/branch_parse_refactor_plan.md +0 -163
  90. data/redesign/c1a-public-api-scope.tsv +0 -256
  91. data/redesign/c1c2_audit.md +0 -566
  92. data/redesign/c1c2_bucket6_lib_orphans.md +0 -626
  93. data/redesign/config_design.rb +0 -501
  94. data/redesign/index.md +0 -34
  95. data/redesign/info_object_migration_plan.md +0 -126
  96. data/redesign/integration_test_analysis.md +0 -521
  97. data/redesign/phase-4-step-b-test-audit.tsv +0 -485
  98. data/redesign/remote_refactor_plan.md +0 -164
  99. data/redesign/reverse_dependencies.sql +0 -44
  100. data/tasks/gem_tasks.rake +0 -14
  101. data/tasks/npm_tasks.rake +0 -7
  102. data/tasks/rspec.rake +0 -111
  103. data/tasks/rubocop.rake +0 -5
  104. data/tasks/test_gem.rake +0 -12
  105. data/tasks/yard.rake +0 -57
data/git.gemspec CHANGED
@@ -43,38 +43,98 @@ Gem::Specification.new do |spec|
43
43
 
44
44
  spec.add_dependency 'activesupport', '>= 5.0'
45
45
  spec.add_dependency 'addressable', '~> 2.8'
46
- spec.add_dependency 'process_executer', '~> 4.0'
46
+ spec.add_dependency 'process_executer', '~> 4.1'
47
47
  spec.add_dependency 'rchardet', '~> 1.9'
48
48
 
49
+ # Not every development dependency is installed on every runtime. Each predicate
50
+ # below names one reason for holding a gem back, which keeps the dependency list
51
+ # itself a flat, readable list. Deriving one predicate from another also keeps
52
+ # coupled gems from drifting apart when a condition changes.
53
+ #
54
+ # These are local variables rather than methods deliberately. The Gemfile uses
55
+ # `gemspec`, so Bundler evaluates this file on every `bundle exec`, and a top-level
56
+ # `def` -- including one written inside this block, since a block is not a definition
57
+ # scope -- would define a private method on Object in every one of those processes.
58
+
59
+ # JRuby (which reports RUBY_PLATFORM as 'java') and TruffleRuby build no C extensions
60
+ # and do not run the docs build.
61
+ mri = !(RUBY_PLATFORM == 'java' || RUBY_ENGINE == 'truffleruby')
62
+
63
+ # irb stopped being a default gem in Ruby 3.4. JRuby and TruffleRuby ship their own.
64
+ install_irb = mri
65
+
66
+ # Ruby 4.0.0 dropped fiddle from the default gems. On Windows, irb loads
67
+ # reline/io/windows.rb, which requires fiddle/import for the Win32 console API, so
68
+ # bin/console cannot start without it; every other platform takes reline's ANSI IO
69
+ # gate and never loads that file. Derived from install_irb because fiddle exists only
70
+ # to serve irb and, being a C extension, could not install where irb is not.
71
+ install_fiddle = install_irb && Gem.win_platform?
72
+
73
+ # The docs toolchain is supported on MRI only. redcarpet, YARD's Markdown renderer,
74
+ # is also a C extension and so could not install on JRuby regardless.
75
+ install_docs = mri
76
+
77
+ # yard-lint requires Ruby >= 3.3.
78
+ install_yard_lint = install_docs && Gem.ruby_version >= Gem::Version.new('3.3.0')
79
+
80
+ # i18n 1.15+ uses Fiber.[] (Ruby 3.2 Fiber storage), which TruffleRuby < 34.0.0 does
81
+ # not implement, so those runtimes hold at the last release that works there.
82
+ pin_old_i18n = RUBY_ENGINE == 'truffleruby' &&
83
+ Gem::Version.new(RUBY_ENGINE_VERSION) < Gem::Version.new('34.0.0')
84
+
49
85
  spec.add_development_dependency 'create_github_release', '~> 2.1'
86
+ spec.add_development_dependency 'fiddle', '~> 1.1' if install_fiddle
50
87
  spec.add_development_dependency 'fuubar', '~> 2.5'
88
+ spec.add_development_dependency 'i18n', '< 1.15' if pin_old_i18n
89
+ spec.add_development_dependency 'irb', '~> 1.16' if install_irb
51
90
  spec.add_development_dependency 'main_branch_shared_rubocop_config', '~> 0.1'
52
91
  spec.add_development_dependency 'parallel_tests', '~> 5.6'
53
92
  spec.add_development_dependency 'rake', '~> 13.3'
93
+ spec.add_development_dependency 'redcarpet', '~> 3.6' if install_docs
54
94
  spec.add_development_dependency 'rspec', '~> 3.13'
55
95
  spec.add_development_dependency 'rubocop', '~> 1.82'
56
96
  spec.add_development_dependency 'simplecov', '~> 1.0'
57
97
  spec.add_development_dependency 'simplecov-lcov', '~> 0.9'
58
98
  spec.add_development_dependency 'simplecov-rspec', '~> 1.1'
99
+ spec.add_development_dependency 'yard', '~> 0.9', '>= 0.9.28' if install_docs
100
+ spec.add_development_dependency 'yard_example_test', '~> 0.2', '>= 0.2.1' if install_docs
101
+ spec.add_development_dependency 'yard-lint', '~> 1.8' if install_yard_lint
59
102
 
60
- if RUBY_ENGINE == 'truffleruby' && Gem::Version.new(RUBY_ENGINE_VERSION) < Gem::Version.new('34.0.0')
61
- # i18n 1.15+ uses Fiber.[] (Ruby 3.2 Fiber storage) which TruffleRuby < 34.0.0 does not implement
62
- spec.add_development_dependency 'i18n', '< 1.15'
63
- end
64
-
65
- unless RUBY_PLATFORM == 'java' || RUBY_ENGINE == 'truffleruby'
66
- spec.add_development_dependency 'irb', '~> 1.16'
67
- spec.add_development_dependency 'redcarpet', '~> 3.6'
68
- spec.add_development_dependency 'yard', '~> 0.9', '>= 0.9.28'
69
- spec.add_development_dependency 'yard_example_test', '~> 0.2', '>= 0.2.1'
103
+ # Specify which files should be added to the gem when it is released.
104
+ #
105
+ # This is an allowlist rather than a denylist. A denylist admitted every new
106
+ # development-only path by default, so the gem shipped `.github/`, `tasks/`, the
107
+ # husky hooks, and -- the reason this became an allowlist -- the `.claude/skills`
108
+ # symlink. Extracting a symlink needs a privilege that Windows grants only under
109
+ # Developer Mode or an elevated shell, so installing the gem there either failed
110
+ # outright or, on RubyGems new enough to fall back to a copy, silently duplicated
111
+ # the whole skills tree into the installed gem.
112
+ #
113
+ # spec/unit/gemspec_spec.rb guards both directions: nothing in the list may be a
114
+ # symlink, and every tracked file under lib/ must be present, so the allowlist
115
+ # cannot quietly drop runtime code.
116
+ #
117
+ # doc_files must stay in sync with the extra files named in .yardopts -- those are
118
+ # what rubydoc.info renders for the published documentation, so a file listed there
119
+ # but absent from the gem becomes a broken link.
120
+ doc_files = %w[
121
+ AI_POLICY.md
122
+ CHANGELOG.md
123
+ CODE_OF_CONDUCT.md
124
+ CONTRIBUTING.md
125
+ GOVERNANCE.md
126
+ LICENSE
127
+ MAINTAINERS.md
128
+ README.md
129
+ UPGRADING.md
130
+ ]
70
131
 
71
- # yard-lint requires Ruby >= 3.3, so it is only installed on Ruby 3.3+.
72
- spec.add_development_dependency 'yard-lint', '~> 1.8' if Gem.ruby_version >= Gem::Version.new('3.3.0')
73
- end
132
+ # .yardopts drives the rubydoc.info build; the gemspec is included by convention.
133
+ build_files = %w[.yardopts git.gemspec]
74
134
 
75
- # Specify which files should be added to the gem when it is released.
76
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
77
135
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
78
- `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(tests|spec|features|bin)/}) }
136
+ `git ls-files -z`.split("\x0").select do |f|
137
+ f.start_with?('lib/') || doc_files.include?(f) || build_files.include?(f)
138
+ end
79
139
  end
80
140
  end
@@ -232,7 +232,7 @@ module Git
232
232
  #
233
233
  # @api private
234
234
  #
235
- # rubocop:disable Metrics/ParameterLists
235
+ # rubocop:disable-next Metrics/ParameterLists
236
236
  def command_line_result(command, result, processed_out, processed_err, timeout, raise_on_failure)
237
237
  Git::CommandLine::Result.new(command, result, processed_out, processed_err).tap do |processed_result|
238
238
  raise Git::TimeoutError.new(processed_result, timeout) if result.timed_out?
@@ -242,7 +242,6 @@ module Git
242
242
  raise Git::FailedError, processed_result if raise_on_failure && !result.success?
243
243
  end
244
244
  end
245
- # rubocop:enable Metrics/ParameterLists
246
245
  end
247
246
  end
248
247
  end
@@ -453,7 +453,7 @@ module Git
453
453
  #
454
454
  # @example Feed bound object names to a git batch command
455
455
  # bound = args_definition.bind(*args, **kwargs)
456
- # stdin_content = Array(bound.objects).map { |object| "#{object}\n" }.join
456
+ # stdin_content = Array(bound.object).map { |object| "#{object}\n" }.join
457
457
  # with_stdin(stdin_content) do |reader|
458
458
  # @execution_context.command_capturing('cat-file', '--batch-check', in: reader, raise_on_failure: false)
459
459
  # end
@@ -49,9 +49,10 @@ module Git
49
49
  # See https://git-scm.com/docs/git-cat-file#Documentation/git-cat-file.txt--s
50
50
  flag_option :s
51
51
 
52
- # Allow -t and -s to query broken or corrupt objects of unknown type;
53
- # rejected by git in any other mode enforced by constraint below
54
- # See https://git-scm.com/docs/git-cat-file#Documentation/git-cat-file.txt---allow-unknown-type
52
+ # Allow -t and -s to query broken or corrupt objects of unknown type.
53
+ # Git 2.28-2.49 rejects this flag in other modes; git 2.50+ accepts and
54
+ # ignores it everywhere (the unknown-type feature was removed; see issue 1709).
55
+ # See https://git-scm.com/docs/git-cat-file/2.49.0#Documentation/git-cat-file.txt---allow-unknown-type
55
56
  flag_option :allow_unknown_type
56
57
 
57
58
  # Map committer/author identities through the mailmap before reporting size
@@ -74,8 +75,6 @@ module Git
74
75
 
75
76
  # Object name: SHA, ref, `HEAD`, treeish path reference, etc.
76
77
  operand :object, required: true
77
-
78
- requires_one_of :t, :s, when: :allow_unknown_type
79
78
  end
80
79
 
81
80
  # Execute `git cat-file` for a single object.
@@ -114,7 +113,8 @@ module Git
114
113
  # @param options [Hash] command options
115
114
  #
116
115
  # @option options [Boolean, nil] :allow_unknown_type (nil) allow querying broken or corrupt objects of
117
- # unknown type
116
+ # unknown type on git 2.28-2.49; git 2.50 removed the unknown-type feature
117
+ # and accepts this flag as a no-op
118
118
  #
119
119
  # @option options [Boolean, nil] :use_mailmap (nil) remap identities via mailmap (`--use-mailmap`)
120
120
  #
@@ -138,7 +138,8 @@ module Git
138
138
  # @param options [Hash] command options
139
139
  #
140
140
  # @option options [Boolean, nil] :allow_unknown_type (nil) allow querying broken or corrupt objects of
141
- # unknown type
141
+ # unknown type on git 2.28-2.49; git 2.50 removed the unknown-type feature
142
+ # and accepts this flag as a no-op
142
143
  #
143
144
  # @option options [Boolean, nil] :use_mailmap (nil) remap identities via mailmap (`--use-mailmap`)
144
145
  #
@@ -88,6 +88,14 @@ module Git
88
88
  #
89
89
  # @raise [Git::FailedError] if git exits with an unexpected non-zero status
90
90
  #
91
+ # @note On Git for Windows, git's default regex engine matches *bytes* rather than
92
+ # characters, so a metacharacter such as `.` or a POSIX class such as
93
+ # `[[:alpha:]]` in `value_regex` never matches a whole multi-byte character. The
94
+ # failure is silent: nothing raises, and the outcome is indistinguishable from a
95
+ # `value_regex` that genuinely matches nothing. `git config` value patterns are
96
+ # POSIX extended regular expressions with no PCRE mode, so unlike
97
+ # {Git::Repository#grep} there is no alternate regex engine to select here.
98
+ #
91
99
  def config_get(name, value_regex = nil, **options)
92
100
  Private.assert_valid_opts!(CONFIG_GET_ALLOWED_OPTS, **options)
93
101
  assert_valid_scope!(**options)
@@ -139,6 +147,14 @@ module Git
139
147
  #
140
148
  # @raise [Git::FailedError] if git exits with an unexpected non-zero status
141
149
  #
150
+ # @note On Git for Windows, git's default regex engine matches *bytes* rather than
151
+ # characters, so a metacharacter such as `.` or a POSIX class such as
152
+ # `[[:alpha:]]` in `value_regex` never matches a whole multi-byte character. The
153
+ # failure is silent: nothing raises, and the outcome is indistinguishable from a
154
+ # `value_regex` that genuinely matches nothing. `git config` value patterns are
155
+ # POSIX extended regular expressions with no PCRE mode, so unlike
156
+ # {Git::Repository#grep} there is no alternate regex engine to select here.
157
+ #
142
158
  def config_get_all(name, value_regex = nil, **options)
143
159
  Private.assert_valid_opts!(CONFIG_GET_ALL_ALLOWED_OPTS, **options)
144
160
  assert_valid_scope!(**options)
@@ -238,6 +254,14 @@ module Git
238
254
  #
239
255
  # @raise [Git::FailedError] if git exits with an unexpected non-zero status
240
256
  #
257
+ # @note On Git for Windows, git's default regex engine matches *bytes* rather than
258
+ # characters, so a metacharacter such as `.` or a POSIX class such as
259
+ # `[[:alpha:]]` in `value_regex` never matches a whole multi-byte character. The
260
+ # failure is silent: nothing raises, and the outcome is indistinguishable from a
261
+ # `value_regex` that genuinely matches nothing. `git config` value patterns are
262
+ # POSIX extended regular expressions with no PCRE mode, so unlike
263
+ # {Git::Repository#grep} there is no alternate regex engine to select here.
264
+ #
241
265
  def config_get_regexp(name_regex, value_regex = nil, **options)
242
266
  Private.assert_valid_opts!(CONFIG_GET_REGEXP_ALLOWED_OPTS, **options)
243
267
  assert_valid_scope!(**options)
@@ -529,6 +553,22 @@ module Git
529
553
  #
530
554
  # @raise [Git::FailedError] if git exits with a non-zero exit status
531
555
  #
556
+ # @note On Git for Windows, git's default regex engine matches *bytes* rather than
557
+ # characters, so a metacharacter such as `.` or a POSIX class such as
558
+ # `[[:alpha:]]` in `value_regex` never matches a whole multi-byte character. The
559
+ # failure is silent: nothing raises, and the outcome is indistinguishable from a
560
+ # `value_regex` that genuinely matches nothing. `git config` value patterns are
561
+ # POSIX extended regular expressions with no PCRE mode, so unlike
562
+ # {Git::Repository#grep} there is no alternate regex engine to select here.
563
+ #
564
+ # @note A `value_regex` that selects nothing does not make this method a no-op.
565
+ # `git config --replace-all` *adds* `value` as an additional entry when no
566
+ # existing value matches, and exits zero. On Git for Windows, a `value_regex`
567
+ # whose metacharacters span non-ASCII text therefore leaves the value it was
568
+ # meant to replace in place and silently creates a duplicate entry beside it.
569
+ # Confirm the result with {#config_get_all}, or match on ASCII text, when the
570
+ # key must end up single-valued.
571
+ #
532
572
  def config_replace_all(name, value, value_regex = nil, **)
533
573
  Private.assert_valid_opts!(CONFIG_REPLACE_ALL_ALLOWED_OPTS, **)
534
574
  assert_valid_scope!(**)
@@ -621,6 +661,14 @@ module Git
621
661
  #
622
662
  # @raise [Git::FailedError] if git exits with a non-zero exit status
623
663
  #
664
+ # @note On Git for Windows, git's default regex engine matches *bytes* rather than
665
+ # characters, so a metacharacter such as `.` or a POSIX class such as
666
+ # `[[:alpha:]]` in `value_regex` never matches a whole multi-byte character. The
667
+ # failure is silent: nothing raises, and the outcome is indistinguishable from a
668
+ # `value_regex` that genuinely matches nothing. `git config` value patterns are
669
+ # POSIX extended regular expressions with no PCRE mode, so unlike
670
+ # {Git::Repository#grep} there is no alternate regex engine to select here.
671
+ #
624
672
  def config_unset(name, value_regex = nil, **)
625
673
  Private.assert_valid_opts!(CONFIG_UNSET_ALLOWED_OPTS, **)
626
674
  assert_valid_scope!(**)
@@ -666,6 +714,14 @@ module Git
666
714
  #
667
715
  # @raise [Git::FailedError] if git exits with a non-zero exit status
668
716
  #
717
+ # @note On Git for Windows, git's default regex engine matches *bytes* rather than
718
+ # characters, so a metacharacter such as `.` or a POSIX class such as
719
+ # `[[:alpha:]]` in `value_regex` never matches a whole multi-byte character. The
720
+ # failure is silent: nothing raises, and the outcome is indistinguishable from a
721
+ # `value_regex` that genuinely matches nothing. `git config` value patterns are
722
+ # POSIX extended regular expressions with no PCRE mode, so unlike
723
+ # {Git::Repository#grep} there is no alternate regex engine to select here.
724
+ #
669
725
  def config_unset_all(name, value_regex = nil, **)
670
726
  Private.assert_valid_opts!(CONFIG_UNSET_ALL_ALLOWED_OPTS, **)
671
727
  assert_valid_scope!(**)
data/lib/git/log.rb CHANGED
@@ -142,6 +142,25 @@ module Git
142
142
  #
143
143
  def grep(regex) = set_option(:grep, regex)
144
144
 
145
+ # Interprets {#grep} and {#author} patterns as Perl-compatible regular expressions
146
+ #
147
+ # Selects PCRE instead of git's default POSIX basic regular expressions for
148
+ # every pattern in the query. Requires a git built with PCRE support.
149
+ #
150
+ # @example Match a metacharacter against a non-ASCII character on Git for Windows
151
+ # repo.log.perl_regexp.grep('^.PFEL').execute
152
+ #
153
+ # @return [Git::Log] the current query builder
154
+ #
155
+ # @note On Git for Windows, git's default regex engine matches *bytes* rather
156
+ # than characters, so a metacharacter such as `.` or a POSIX class such as
157
+ # `[[:alpha:]]` never matches a whole multi-byte character. The match fails
158
+ # silently: git exits zero and the result is empty. PCRE does match characters
159
+ # on that platform, but it is a different dialect than git's default, so
160
+ # selecting it is a deliberate choice by the caller.
161
+ #
162
+ def perl_regexp = set_option(:perl_regexp, true)
163
+
145
164
  # Limits commits to those that touch the given path or paths
146
165
  #
147
166
  # @param path [String, Pathname, Array<String, Pathname>] path limiter input
@@ -41,7 +41,7 @@ module Git
41
41
 
42
42
  # Option keys accepted by {#checkout}
43
43
  #
44
- CHECKOUT_ALLOWED_OPTS = %i[force f new_branch b start_point].freeze
44
+ CHECKOUT_ALLOWED_OPTS = %i[force f new_branch b start_point orphan].freeze
45
45
  private_constant :CHECKOUT_ALLOWED_OPTS
46
46
 
47
47
  # Option keys accepted by {#checkout_index}
@@ -130,6 +130,9 @@ module Git
130
130
  # @example Create a new branch with a name different from the start point
131
131
  # repo.checkout('main', new_branch: 'new-feature')
132
132
  #
133
+ # @example Create and check out an unborn branch with no history
134
+ # repo.checkout('gh-pages', orphan: true)
135
+ #
133
136
  # @example Force checkout discarding local changes
134
137
  # repo.checkout('main', force: true)
135
138
  #
@@ -151,13 +154,25 @@ module Git
151
154
  #
152
155
  # @option opts [Boolean, nil] :f (nil) alias for `:force`
153
156
  #
157
+ # @option opts [Boolean, String, nil] :orphan (nil) when `true`, creates a
158
+ # new unborn branch named `branch` whose first commit has no parents
159
+ #
160
+ # When a `String`, creates an unborn branch with that name, using
161
+ # `branch` as the start point for the working tree and index.
162
+ #
163
+ # `false` and `nil` are both treated as unset. A blank branch name is
164
+ # rejected rather than ignored.
165
+ #
154
166
  # @option opts [String, nil] :start_point (nil) the commit or branch to
155
- # start the new branch from; used together with `new_branch: true`
167
+ # start the new branch from; used together with `new_branch: true` or
168
+ # `orphan: true`
156
169
  #
157
170
  # @return [String] git's stdout from the checkout
158
171
  #
159
172
  # @raise [ArgumentError] if unsupported options are provided
160
173
  #
174
+ # @raise [ArgumentError] if `:orphan` is given a blank or missing branch name
175
+ #
161
176
  # @raise [Git::FailedError] if git exits with a non-zero exit status
162
177
  #
163
178
  def checkout(branch = nil, opts = {})
@@ -680,19 +695,24 @@ module Git
680
695
  :unborn
681
696
  end
682
697
 
683
- # Translates legacy checkout options to the new command interface
698
+ # Translates {#checkout} options to the new command interface
684
699
  #
685
700
  # Legacy callers passed combinations like:
686
701
  # checkout('branch', new_branch: true, start_point: 'main')
687
702
  # which should map to:
688
703
  # checkout('main', b: 'branch')
689
704
  #
705
+ # `orphan: true` follows the same shape, naming the unborn branch:
706
+ # checkout('branch', orphan: true, start_point: 'main')
707
+ # maps to:
708
+ # checkout('main', orphan: 'branch')
709
+ #
690
710
  # @param branch [String, nil] the branch argument passed to {#checkout}
691
711
  #
692
712
  # @param checkout_options [Hash] the raw options passed to {#checkout}
693
713
  #
694
- # @return [Array] a two-element tuple `[target, options]` containing the
695
- # translated checkout arguments
714
+ # @return [Array((String, nil), Hash)] a two-element tuple
715
+ # `[target, options]` containing the translated checkout arguments
696
716
  #
697
717
  # `target` (`String` or `nil`) is the branch or commit to check out.
698
718
  # `options` is a `Hash` of keyword arguments for
@@ -701,15 +721,69 @@ module Git
701
721
  # @api private
702
722
  #
703
723
  def translate_checkout_opts(branch, checkout_options)
724
+ checkout_options = normalize_orphan_option(checkout_options)
725
+
704
726
  if checkout_options[:new_branch] == true || checkout_options[:b] == true
705
727
  [checkout_options[:start_point], checkout_options.except(:new_branch, :b, :start_point).merge(b: branch)]
706
728
  elsif checkout_options[:new_branch].is_a?(String)
707
729
  [branch, checkout_options.except(:new_branch).merge(b: checkout_options[:new_branch])]
730
+ elsif checkout_options[:orphan] == true
731
+ translate_orphan_opts(branch, checkout_options)
708
732
  else
709
733
  [branch, checkout_options]
710
734
  end
711
735
  end
712
736
 
737
+ # Normalizes the `:orphan` option, rejecting names that git would never see
738
+ #
739
+ # `:orphan` is a value option on the underlying command, so a literal
740
+ # `false` would be emitted as `--orphan false` and create a branch named
741
+ # "false". Flag options such as `:force` already ignore `false`; this
742
+ # gives `:orphan` the same behavior.
743
+ #
744
+ # A blank name is rejected rather than dropped: the argument DSL omits
745
+ # empty values, so `orphan: ''` would otherwise degrade silently into a
746
+ # plain checkout.
747
+ #
748
+ # @param checkout_options [Hash] the raw options passed to {#checkout}
749
+ #
750
+ # @return [Hash] the options with a `false` `:orphan` key removed
751
+ #
752
+ # @raise [ArgumentError] if `:orphan` is given a blank branch name
753
+ #
754
+ # @api private
755
+ #
756
+ def normalize_orphan_option(checkout_options)
757
+ orphan = checkout_options[:orphan]
758
+ return checkout_options.except(:orphan) if orphan == false
759
+ raise ArgumentError, 'orphan requires a non-empty branch name' if orphan.is_a?(String) && orphan.strip.empty?
760
+
761
+ checkout_options
762
+ end
763
+
764
+ # Translates `orphan: true` into the command's `:orphan` value option
765
+ #
766
+ # `orphan: true` names the unborn branch from the positional argument and
767
+ # takes its start point from `:start_point`, mirroring `new_branch: true`.
768
+ #
769
+ # @param branch [String, nil] the branch argument passed to {#checkout}
770
+ #
771
+ # @param checkout_options [Hash] the raw options passed to {#checkout}
772
+ #
773
+ # @return [Array((String, nil), Hash)] a two-element tuple
774
+ # `[target, options]` containing the translated checkout arguments
775
+ #
776
+ # @raise [ArgumentError] if `branch` is blank (`nil`, empty, or whitespace
777
+ # only), since the unborn branch would otherwise have no name
778
+ #
779
+ # @api private
780
+ #
781
+ def translate_orphan_opts(branch, checkout_options)
782
+ raise ArgumentError, 'orphan: true requires a branch name' if branch.to_s.strip.empty?
783
+
784
+ [checkout_options[:start_point], checkout_options.except(:start_point).merge(orphan: branch)]
785
+ end
786
+
713
787
  # Normalizes path specifications for Git commands
714
788
  #
715
789
  # @param pathspecs [String, Pathname, Array<String, Pathname>, nil]
@@ -18,7 +18,7 @@ module Git
18
18
  # @return [Array<Symbol>] the supported option keys
19
19
  #
20
20
  FULL_LOG_COMMITS_ALLOWED_OPTS = %i[
21
- count all cherry since until grep author between object path_limiter skip merges
21
+ count all cherry since until grep author between object path_limiter skip merges perl_regexp
22
22
  ].freeze
23
23
  private_constant :FULL_LOG_COMMITS_ALLOWED_OPTS
24
24
 
@@ -68,6 +68,13 @@ module Git
68
68
  #
69
69
  # @option opts [Boolean, nil] :merges (nil) include only merge commits
70
70
  #
71
+ # @option opts [Boolean, nil] :perl_regexp (nil) interpret the `:grep` and
72
+ # `:author` patterns as Perl-compatible regular expressions (PCRE) instead of
73
+ # git's default POSIX basic regular expressions
74
+ #
75
+ # Requires a git built with PCRE support; git otherwise fails with
76
+ # "cannot use Perl-compatible regexes...".
77
+ #
71
78
  # @return [Array<Hash>] the parsed raw log output for each commit
72
79
  #
73
80
  # @raise [ArgumentError] if unsupported options are provided
@@ -76,6 +83,14 @@ module Git
76
83
  #
77
84
  # @raise [Git::FailedError] if git exits with a non-zero exit status
78
85
  #
86
+ # @note On Git for Windows, git's default regex engine matches *bytes* rather
87
+ # than characters, so a metacharacter such as `.` or a POSIX class such as
88
+ # `[[:alpha:]]` never matches a whole multi-byte character in a `:grep` or
89
+ # `:author` pattern. The match fails silently: git exits zero and the result
90
+ # is empty. Pass `perl_regexp: true` to select PCRE, which does match
91
+ # characters. PCRE is a different dialect than git's default, so this is a
92
+ # deliberate choice by the caller rather than a transparent substitution.
93
+ #
79
94
  # @see https://git-scm.com/docs/git-log git-log
80
95
  #
81
96
  def full_log_commits(opts = {})
@@ -113,6 +128,14 @@ module Git
113
128
  module Private
114
129
  module_function
115
130
 
131
+ # Log option keys forwarded to {Git::Commands::Log#call} under the same name
132
+ #
133
+ # The remaining options are renamed or reshaped by {#log_base_call_options}.
134
+ #
135
+ # @return [Array<Symbol>] the pass-through option keys
136
+ #
137
+ PASSTHROUGH_LOG_OPTS = %i[all cherry since until grep author perl_regexp].freeze
138
+
116
139
  # Validates the :count log option
117
140
  #
118
141
  # @param opts [Hash] the log options
@@ -195,6 +218,9 @@ module Git
195
218
  # @option opts [String, nil] :author (nil) only include commits whose author
196
219
  # matches this pattern
197
220
  #
221
+ # @option opts [Boolean, nil] :perl_regexp (nil) interpret the `:grep` and
222
+ # `:author` patterns as Perl-compatible regular expressions
223
+ #
198
224
  # @option opts [Integer, nil] :count (nil) maximum number of commits to return
199
225
  #
200
226
  # @option opts [String, Pathname, Array<String, Pathname>, nil] :path_limiter (nil)
@@ -207,16 +233,14 @@ module Git
207
233
  # @return [Hash] keyword options for {Git::Commands::Log#call}
208
234
  #
209
235
  def log_base_call_options(opts, extra = {})
210
- {
211
- all: opts[:all],
212
- cherry: opts[:cherry],
213
- since: opts[:since],
214
- until: opts[:until],
215
- grep: opts[:grep],
216
- author: opts[:author],
217
- max_count: opts[:count],
218
- path: opts[:path_limiter] ? Array(opts[:path_limiter]) : nil
219
- }.merge(extra).compact
236
+ opts
237
+ .slice(*PASSTHROUGH_LOG_OPTS)
238
+ .merge(
239
+ max_count: opts[:count],
240
+ path: opts[:path_limiter] ? Array(opts[:path_limiter]) : nil
241
+ )
242
+ .merge(extra)
243
+ .compact
220
244
  end
221
245
 
222
246
  # Executes git log and parses the raw output
@@ -454,7 +454,7 @@ module Git
454
454
  end
455
455
 
456
456
  # Option keys accepted by {#grep}
457
- GREP_ALLOWED_OPTS = %i[ignore_case i invert_match v extended_regexp E object].freeze
457
+ GREP_ALLOWED_OPTS = %i[ignore_case i invert_match v extended_regexp E perl_regexp P object].freeze
458
458
  private_constant :GREP_ALLOWED_OPTS
459
459
 
460
460
  # Search tracked file contents in a git tree for a pattern
@@ -475,6 +475,9 @@ module Git
475
475
  # @example Case-insensitive search
476
476
  # repo.grep('todo', nil, ignore_case: true)
477
477
  #
478
+ # @example Match a metacharacter against a non-ASCII character on Git for Windows
479
+ # repo.grep('^.PFEL', nil, perl_regexp: true)
480
+ #
478
481
  # @param pattern [String] the pattern to search for
479
482
  #
480
483
  # @param path_limiter [String, Pathname, Array<String, Pathname>, nil]
@@ -499,6 +502,14 @@ module Git
499
502
  #
500
503
  # Alias: :E
501
504
  #
505
+ # @option opts [Boolean, nil] :perl_regexp (nil) use Perl-compatible regular
506
+ # expressions (PCRE) for the pattern
507
+ #
508
+ # Requires a git built with PCRE support; git otherwise fails with
509
+ # "cannot use Perl-compatible regexes...".
510
+ #
511
+ # Alias: :P
512
+ #
502
513
  # @return [Hash<String, Array<Array(Integer, String)>>] a hash mapping
503
514
  # each `"treeish:filename"` key to an array of `[line_number, text]`
504
515
  # pairs; returns an empty hash when no lines match
@@ -508,6 +519,16 @@ module Git
508
519
  # @raise [Git::FailedError] if git exits with a non-zero status and
509
520
  # stderr is non-empty (e.g. bad object reference)
510
521
  #
522
+ # @note On Git for Windows, git's default regex engine matches *bytes* rather
523
+ # than characters, so a metacharacter such as `.` or a POSIX class such as
524
+ # `[[:alpha:]]` never matches a whole multi-byte character. The failure is
525
+ # silent: nothing raises, and the empty hash returned is indistinguishable
526
+ # from a pattern that genuinely does not occur in the tree. Pass
527
+ # `perl_regexp: true` to select PCRE, which does match characters. PCRE is a
528
+ # different dialect than git's default POSIX basic/extended regular
529
+ # expressions, so this is a deliberate choice by the caller rather than a
530
+ # transparent substitution, and it requires a git built with PCRE support.
531
+ #
511
532
  # @see https://git-scm.com/docs/git-grep git-grep documentation
512
533
  #
513
534
  def grep(pattern, path_limiter = nil, opts = {})
data/lib/git/version.rb CHANGED
@@ -4,7 +4,7 @@ module Git
4
4
  # The current gem version
5
5
  #
6
6
  # @return [String] the current gem version
7
- VERSION = '5.0.5'
7
+ VERSION = '5.2.0'
8
8
 
9
9
  # Represents a git version with major, minor, and patch components
10
10
  #