git 5.0.1 → 5.0.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.
@@ -593,11 +593,10 @@ module Git
593
593
  # @return [void]
594
594
  #
595
595
  def apply_file_mode(type, mode)
596
- case type
597
- when 'new'
596
+ if type == 'new'
598
597
  @current_file[:dst_mode] = mode
599
598
  @current_file[:src_path] = nil
600
- when 'deleted'
599
+ else
601
600
  @current_file[:src_mode] = mode
602
601
  @current_file[:dst_path] = nil
603
602
  end
@@ -32,7 +32,7 @@ module Git
32
32
  def parse(output)
33
33
  output.each_line.with_object(Hash.new { |h, k| h[k] = [] }) do |line, hsh|
34
34
  filename, line_num, text = line.chomp.split("\0", 3)
35
- next unless text && line_num&.match?(/\A\d+\z/)
35
+ next unless text && line_num.match?(/\A\d+\z/)
36
36
 
37
37
  hsh[filename] << [line_num.to_i, text]
38
38
  end
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git/commands/rev_parse'
4
+ require 'git/errors'
5
+ require 'git/execution_context'
6
+ require 'git/execution_context/global'
7
+
8
+ module Git
9
+ # Resolves and normalizes the filesystem paths that locate a Git repository
10
+ #
11
+ # `PathResolver` is the single home for the path-resolution logic used by the
12
+ # `Git::Repository` factory class methods ({Git::Repository.open} and
13
+ # {Git::Repository.bare}). It computes the absolute working-directory,
14
+ # repository (`.git`), and index paths from the caller-supplied values,
15
+ # following the same rules Git itself uses (including gitdir-pointer files for
16
+ # submodules and linked worktrees).
17
+ #
18
+ # @api private
19
+ #
20
+ module PathResolver
21
+ module_function
22
+
23
+ # Resolve and normalize the paths that locate a Git repository
24
+ #
25
+ # Returns a new hash containing the resolved absolute paths for:
26
+ # * `:working_directory` — the working tree root (`nil` for bare repos)
27
+ # * `:repository` — the `.git` directory
28
+ # * `:index` — the index file
29
+ #
30
+ # This method does not mutate any inputs.
31
+ #
32
+ # @example Resolve paths for a working tree
33
+ # Git::PathResolver.resolve_paths(working_directory: '/repo')
34
+ # #=> { working_directory: '/repo', repository: '/repo/.git', index: '/repo/.git/index' }
35
+ #
36
+ # @param working_directory [String, nil] the working directory path
37
+ #
38
+ # @param repository [String, nil] the repository (`.git`) directory path
39
+ #
40
+ # @param index [String, nil] the index file path
41
+ #
42
+ # @param bare [Boolean] whether this is a bare repository
43
+ #
44
+ # @return [Hash{Symbol => (String, nil)}] a hash with `:working_directory`,
45
+ # `:repository`, and `:index` keys
46
+ #
47
+ def resolve_paths(working_directory: nil, repository: nil, index: nil, bare: false)
48
+ working_dir = resolve_working_directory(working_directory, bare: bare)
49
+ # For bare repos, use working_directory as the default repository location
50
+ repo_path = resolve_repository(repository, working_dir, bare: bare, bare_default: working_directory)
51
+ index_path = resolve_index(index, repo_path)
52
+
53
+ {
54
+ working_directory: working_dir,
55
+ repository: repo_path,
56
+ index: index_path
57
+ }
58
+ end
59
+
60
+ # Find the root of the working tree that contains `working_dir`
61
+ #
62
+ # Runs `git rev-parse --show-toplevel` from `working_dir` to locate the
63
+ # top-level directory of the working tree.
64
+ #
65
+ # @example Find the worktree root from a subdirectory
66
+ # Git::PathResolver.root_of_worktree('/repo/subdir') #=> '/repo'
67
+ #
68
+ # @param working_dir [String] a path inside the working tree
69
+ #
70
+ # @param binary_path [String, :use_global_config] path to the git binary
71
+ #
72
+ # Controls which git binary is invoked during root detection. Defaults to
73
+ # `:use_global_config`, which resolves to `Git.config.binary_path`.
74
+ #
75
+ # @param git_ssh [String, nil, :use_global_config] the SSH wrapper path
76
+ #
77
+ # Forwarded as `GIT_SSH`. Defaults to `:use_global_config`.
78
+ #
79
+ # @return [String] the absolute path to the root of the working tree
80
+ #
81
+ # @raise [ArgumentError] if `working_dir` does not exist, is not a
82
+ # directory, or is not inside a git working tree
83
+ #
84
+ # Also raised if the git binary cannot be found.
85
+ #
86
+ def root_of_worktree(working_dir, binary_path: :use_global_config, git_ssh: :use_global_config)
87
+ raise ArgumentError, "'#{working_dir}' does not exist or is not a directory" unless Dir.exist?(working_dir)
88
+
89
+ execute_rev_parse_toplevel(working_dir, binary_path: binary_path, git_ssh: git_ssh)
90
+ end
91
+
92
+ # Run `git rev-parse --show-toplevel` from `working_dir` and return stdout
93
+ #
94
+ # @param working_dir [String] a path inside the working tree
95
+ #
96
+ # @param binary_path [String, :use_global_config] path to the git binary
97
+ #
98
+ # @param git_ssh [String, nil, :use_global_config] the SSH wrapper path
99
+ #
100
+ # @return [String] the top-level directory reported by git
101
+ #
102
+ # @raise [ArgumentError] if the git binary is not found or `working_dir` is
103
+ # not inside a git working tree
104
+ #
105
+ # @api private
106
+ #
107
+ def execute_rev_parse_toplevel(working_dir, binary_path: :use_global_config, git_ssh: :use_global_config)
108
+ execution_context = Git::ExecutionContext::Global.new(binary_path: binary_path, git_ssh: git_ssh)
109
+ Git::Commands::RevParse.new(execution_context).call(
110
+ show_toplevel: true, chdir: File.expand_path(working_dir)
111
+ ).stdout
112
+ rescue Errno::ENOENT
113
+ raise ArgumentError, 'Failed to find the root of the worktree: git binary not found'
114
+ rescue Git::FailedError
115
+ raise ArgumentError, "'#{working_dir}' is not in a git working tree"
116
+ end
117
+ private_class_method :execute_rev_parse_toplevel
118
+
119
+ # Resolve the working directory path
120
+ #
121
+ # @param path [String, nil] the working directory path or `nil`
122
+ #
123
+ # @param bare [Boolean] whether this is a bare repository
124
+ #
125
+ # @return [String, nil] the absolute path, or `nil` for bare repos
126
+ #
127
+ # @api private
128
+ #
129
+ def resolve_working_directory(path, bare:)
130
+ return nil if bare
131
+
132
+ File.expand_path(path || Dir.pwd)
133
+ end
134
+ private_class_method :resolve_working_directory
135
+
136
+ # Resolve the repository (`.git`) directory path
137
+ #
138
+ # Handles the gitdir-pointer file case for submodules and linked worktrees.
139
+ #
140
+ # @param path [String, nil] the repository path or `nil`
141
+ #
142
+ # @param working_dir [String, nil] the working directory used for relative
143
+ # path resolution
144
+ #
145
+ # @param bare [Boolean] whether this is a bare repository
146
+ #
147
+ # @param bare_default [String, nil] for bare repos, used as the default when
148
+ # `path` is `nil`
149
+ #
150
+ # @return [String] the absolute path to the repository
151
+ #
152
+ # @api private
153
+ #
154
+ def resolve_repository(path, working_dir, bare:, bare_default: nil)
155
+ initial_path = if bare
156
+ File.expand_path(path || bare_default || Dir.pwd)
157
+ else
158
+ File.expand_path(path || '.git', working_dir)
159
+ end
160
+
161
+ resolve_gitdir_pointer(initial_path)
162
+ end
163
+ private_class_method :resolve_repository
164
+
165
+ # Resolve gitdir-pointer files used by submodules and linked worktrees
166
+ #
167
+ # If `path` points to a file containing `"gitdir: <path>"`, returns the
168
+ # resolved target path. Otherwise returns `path` unchanged.
169
+ #
170
+ # @param path [String] the path to check
171
+ #
172
+ # @return [String] the resolved absolute path
173
+ #
174
+ # Relative pointer targets are resolved from the directory containing the
175
+ # pointer file itself, matching git's pointer-file semantics.
176
+ #
177
+ # @api private
178
+ #
179
+ def resolve_gitdir_pointer(path)
180
+ return path unless File.file?(path)
181
+
182
+ gitdir_content = File.read(path).strip
183
+ return path unless gitdir_content.start_with?('gitdir: ')
184
+
185
+ gitdir_path = gitdir_content.sub(/\Agitdir: /, '')
186
+ File.expand_path(gitdir_path, File.dirname(path))
187
+ end
188
+ private_class_method :resolve_gitdir_pointer
189
+
190
+ # Resolve the index file path
191
+ #
192
+ # @param path [String, nil] the index path or `nil`
193
+ #
194
+ # @param repository [String] the repository directory used for relative
195
+ # path resolution
196
+ #
197
+ # @return [String] the absolute path to the index file
198
+ #
199
+ # @api private
200
+ #
201
+ def resolve_index(path, repository)
202
+ File.expand_path(path || 'index', repository)
203
+ end
204
+ private_class_method :resolve_index
205
+ end
206
+ end
@@ -9,7 +9,6 @@ require 'git/repository/branching'
9
9
  require 'git/repository/context_helpers'
10
10
  require 'git/repository/committing'
11
11
  require 'git/repository/diffing'
12
- require 'git/repository/factories'
13
12
  require 'git/repository/inspecting'
14
13
  require 'git/repository/logging'
15
14
  require 'git/repository/maintenance'
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.1'
7
+ VERSION = '5.0.3'
8
8
 
9
9
  # Represents a git version with major, minor, and patch components
10
10
  #
data/lib/git.rb CHANGED
@@ -55,6 +55,7 @@ require 'git/config'
55
55
  require 'git/config_entry_info'
56
56
  require 'git/parsers/config_entry'
57
57
  require 'git/configuring'
58
+ require 'git/factories'
58
59
  require 'git/diff'
59
60
  require 'git/diff_file_numstat_info'
60
61
  require 'git/diff_file_patch_info'
@@ -99,7 +100,7 @@ require 'git/worktrees'
99
100
  #
100
101
  module Git
101
102
  extend Git::Configuring
102
- extend Git::Repository::Factories
103
+ extend Git::Factories
103
104
 
104
105
  # Minimum git version required by this gem
105
106
  #
data/tasks/rspec.rake CHANGED
@@ -1,48 +1,111 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rspec/core/rake_task'
3
+ # Skip parallel test execution when the PARALLEL_TESTS environment variable is set to a falsy value
4
+ def parallel_tests_disabled_via_env?
5
+ parallel_tests_env = ENV.fetch('PARALLEL_TESTS', 'true').strip.downcase
6
+ %w[false 0 no off].include?(parallel_tests_env)
7
+ end
8
+
9
+ # Use parallel test execution for MRI where it cuts build times by 30-48%.
10
+ # JRuby and TruffleRuby are slower with parallel_tests because each worker
11
+ # process pays JVM/Truffle startup and warm-up overhead independently,
12
+ # resulting in 18-28% slower builds vs. serial execution.
13
+ def parallel_tests?
14
+ RUBY_ENGINE == 'ruby' && !parallel_tests_disabled_via_env?
15
+ end
4
16
 
5
- # Run all specs
6
- RSpec::Core::RakeTask.new(:spec) do |t|
7
- t.pattern = 'spec/**/*_spec.rb'
17
+ # SPEC lets a developer target one or more specific files, e.g.
18
+ # `SPEC=spec/unit/git/version_spec.rb rake spec:unit`, running just those files
19
+ # instead of the whole directory. Both rspec and parallel_rspec recurse a bare
20
+ # directory argument using their own default *_spec.rb pattern, so no explicit
21
+ # --pattern flag is needed for the non-SPEC case.
22
+ #
23
+ # Matches are filtered to dir so that each task only ever runs the specs it owns,
24
+ # no matter how broad SPEC is. This keeps every task self-consistent (a unit spec
25
+ # can never be run by spec:integration under integration's runner and env) and lets
26
+ # a single cross-layer glob drive `rake spec`, e.g.
27
+ # `SPEC=spec/**/git/commands/add_spec.rb rake spec` runs the unit file under
28
+ # spec:unit and the integration file under spec:integration. A SPEC that matches
29
+ # files elsewhere in spec/ but none under dir yields no targets, and the caller
30
+ # skips the task rather than falling back to running all of dir.
31
+ def spec_targets(dir)
32
+ spec_files = ENV.fetch('SPEC', nil)&.strip
33
+ return [dir] if spec_files.nil? || spec_files.empty?
34
+
35
+ targets = FileList[spec_files].sort
36
+ raise "SPEC=#{spec_files.inspect} did not match any files" if targets.empty?
37
+
38
+ targets.select { |target| target.delete_prefix('./').start_with?(dir) }
8
39
  end
9
40
 
10
- # Run only unit specs (mocked, fast)
11
- RSpec::Core::RakeTask.new('spec:unit') do |t|
12
- t.pattern = 'spec/unit/**/*_spec.rb'
13
- # On JRuby, use 'documentation' formatter so each test name is printed before
14
- # it runs. When the suite hangs, the last printed line identifies the blocking
15
- # test. Mirrors the same diagnostic added to spec:integration in PR #1274.
16
- t.rspec_opts = '--format documentation' if RUBY_ENGINE == 'jruby'
41
+ # True when targets name exactly one spec file (as opposed to a directory, or
42
+ # several files).
43
+ def single_spec_file?(targets)
44
+ targets.one? && File.file?(targets.first)
17
45
  end
18
46
 
19
- # Run only integration specs (real git, slower)
20
- RSpec::Core::RakeTask.new('spec:integration') do |t|
21
- t.pattern = 'spec/integration/**/*_spec.rb'
22
- # On JRuby, override the default Fuubar formatter with 'documentation' so that
23
- # each test name is printed before it runs. This makes CI logs useful when the
24
- # suite hangs: the last printed test is the one that caused the hang.
25
- t.rspec_opts = '--format documentation' if RUBY_ENGINE == 'jruby'
47
+ # The runner for targets: parallel_rspec only when the task runs in parallel and
48
+ # there is more than one spec file to spread across workers.
49
+ #
50
+ # A single file always uses plain rspec. Worker startup is pure overhead with nothing
51
+ # to divide, and parallel_tests sets TEST_ENV_NUMBER in every worker, which makes
52
+ # spec_helper fall back from the documentation formatter to plain dots -- the opposite
53
+ # of what a run narrowed to one file wants.
54
+ def spec_runner(targets, parallel:)
55
+ parallel && !single_spec_file?(targets) ? 'parallel_rspec' : 'rspec'
26
56
  end
27
57
 
28
- # Run all specs in parallel
29
- desc 'Run all specs in parallel'
30
- task 'spec:parallel' do
31
- sh 'bundle exec parallel_rspec spec/'
58
+ # Run the SPEC-filtered targets for dir, or announce a skip when SPEC selected nothing
59
+ # under dir. Silently doing nothing would read as a passing run of the whole directory.
60
+ def run_spec_task(name, dir, env, parallel:)
61
+ targets = spec_targets(dir)
62
+
63
+ if targets.empty?
64
+ puts "Skipping #{name}: SPEC=#{ENV.fetch('SPEC', nil)} matched no files under #{dir}"
65
+ else
66
+ sh env, 'bundle', 'exec', spec_runner(targets, parallel: parallel), *targets
67
+ end
32
68
  end
33
69
 
34
- # Run only unit specs in parallel
35
- desc 'Run unit specs in parallel'
36
- task 'spec:unit:parallel' do
37
- sh 'bundle exec parallel_rspec spec/unit/'
70
+ def define_parallel_spec_task(name, dir, env: {})
71
+ task name do
72
+ run_spec_task(name, dir, env, parallel: true)
73
+ end
38
74
  end
39
75
 
40
- # Run only integration specs in parallel
41
- desc 'Run integration specs in parallel'
42
- task 'spec:integration:parallel' do
43
- sh 'bundle exec parallel_rspec spec/integration/'
76
+ def define_serial_spec_task(name, dir, env: {})
77
+ task name do
78
+ run_spec_task(name, dir, env, parallel: false)
79
+ end
44
80
  end
45
81
 
82
+ # Define a spec task that runs in parallel (via parallel_tests) when parallel_tests? is
83
+ # true, and serially (via a direct `bundle exec rspec` call) otherwise. env, when given,
84
+ # is passed to `sh` and scoped to just the spawned test subprocess(es).
85
+ def define_spec_task(name, dir, desc_text, env: {})
86
+ desc desc_text
87
+ parallel_tests? ? define_parallel_spec_task(name, dir, env: env) : define_serial_spec_task(name, dir, env: env)
88
+ end
89
+
90
+ # Run only unit specs. These run in a few seconds, so parallel_tests startup
91
+ # overhead isn't worth it - always run serially.
92
+ desc 'Run unit RSpec tests (mocked, fast; SPEC=<glob> to run specific files)'
93
+ define_serial_spec_task('spec:unit', 'spec/unit/')
94
+
95
+ # Run only integration specs. Coverage is forced off (regardless of the COVERAGE
96
+ # env var): integration tests intentionally aren't exhaustive, so tracking or
97
+ # reporting coverage for them would misleadingly suggest that low integration
98
+ # coverage is a problem to fix.
99
+ define_spec_task(
100
+ 'spec:integration', 'spec/integration/',
101
+ 'Run integration RSpec tests (real git, slower; SPEC=<glob> to run specific files)',
102
+ env: { 'COVERAGE' => 'false' }
103
+ )
104
+
105
+ # Run all specs, keeping unit and integration output clearly separated
106
+ desc 'Run unit and integration RSpec tests (SPEC=<glob> to run specific files)'
107
+ task spec: %w[spec:unit spec:integration]
108
+
46
109
  CLEAN << 'coverage'
47
110
  CLEAN << '.rspec_status'
48
111
  CLEAN << 'rspec-report.xml'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: git
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.0.1
4
+ version: 5.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Scott Chacon and others
@@ -169,14 +169,14 @@ dependencies:
169
169
  requirements:
170
170
  - - "~>"
171
171
  - !ruby/object:Gem::Version
172
- version: '0.22'
172
+ version: '1.0'
173
173
  type: :development
174
174
  prerelease: false
175
175
  version_requirements: !ruby/object:Gem::Requirement
176
176
  requirements:
177
177
  - - "~>"
178
178
  - !ruby/object:Gem::Version
179
- version: '0.22'
179
+ version: '1.0'
180
180
  - !ruby/object:Gem::Dependency
181
181
  name: simplecov-lcov
182
182
  requirement: !ruby/object:Gem::Requirement
@@ -197,14 +197,14 @@ dependencies:
197
197
  requirements:
198
198
  - - "~>"
199
199
  - !ruby/object:Gem::Version
200
- version: '0.4'
200
+ version: '1.1'
201
201
  type: :development
202
202
  prerelease: false
203
203
  version_requirements: !ruby/object:Gem::Requirement
204
204
  requirements:
205
205
  - - "~>"
206
206
  - !ruby/object:Gem::Version
207
- version: '0.4'
207
+ version: '1.1'
208
208
  - !ruby/object:Gem::Dependency
209
209
  name: irb
210
210
  requirement: !ruby/object:Gem::Requirement
@@ -298,6 +298,9 @@ executables: []
298
298
  extensions: []
299
299
  extra_rdoc_files: []
300
300
  files:
301
+ - ".claude/commands/address-copilot-reviews.md"
302
+ - ".claude/settings.json"
303
+ - ".claude/skills"
301
304
  - ".commitlintrc.yml"
302
305
  - ".dockerignore"
303
306
  - ".github/copilot-instructions.md"
@@ -356,6 +359,7 @@ files:
356
359
  - ".yardopts"
357
360
  - AI_POLICY.md
358
361
  - CHANGELOG.md
362
+ - CLAUDE.md
359
363
  - CODE_OF_CONDUCT.md
360
364
  - CONTRIBUTING.md
361
365
  - GOVERNANCE.md
@@ -547,6 +551,7 @@ files:
547
551
  - lib/git/execution_context.rb
548
552
  - lib/git/execution_context/global.rb
549
553
  - lib/git/execution_context/repository.rb
554
+ - lib/git/factories.rb
550
555
  - lib/git/file_ref.rb
551
556
  - lib/git/fsck_object.rb
552
557
  - lib/git/fsck_result.rb
@@ -563,6 +568,7 @@ files:
563
568
  - lib/git/parsers/remote.rb
564
569
  - lib/git/parsers/stash.rb
565
570
  - lib/git/parsers/tag.rb
571
+ - lib/git/path_resolver.rb
566
572
  - lib/git/remote.rb
567
573
  - lib/git/remote_info.rb
568
574
  - lib/git/repository.rb
@@ -570,13 +576,11 @@ files:
570
576
  - lib/git/repository/committing.rb
571
577
  - lib/git/repository/context_helpers.rb
572
578
  - lib/git/repository/diffing.rb
573
- - lib/git/repository/factories.rb
574
579
  - lib/git/repository/inspecting.rb
575
580
  - lib/git/repository/logging.rb
576
581
  - lib/git/repository/maintenance.rb
577
582
  - lib/git/repository/merging.rb
578
583
  - lib/git/repository/object_operations.rb
579
- - lib/git/repository/path_resolver.rb
580
584
  - lib/git/repository/remote_operations.rb
581
585
  - lib/git/repository/shared_private.rb
582
586
  - lib/git/repository/staging.rb
@@ -626,8 +630,8 @@ licenses:
626
630
  metadata:
627
631
  homepage_uri: http://github.com/ruby-git/ruby-git
628
632
  source_code_uri: http://github.com/ruby-git/ruby-git
629
- changelog_uri: https://rubydoc.info/gems/git/5.0.1/file/CHANGELOG.md
630
- documentation_uri: https://rubydoc.info/gems/git/5.0.1
633
+ changelog_uri: https://rubydoc.info/gems/git/5.0.3/file/CHANGELOG.md
634
+ documentation_uri: https://rubydoc.info/gems/git/5.0.3
631
635
  rubygems_mfa_required: 'true'
632
636
  rdoc_options: []
633
637
  require_paths: