bundler 4.1.0.beta1 → 4.1.0.beta3

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/lib/bundler/build_metadata.rb +2 -2
  3. data/lib/bundler/cli/common.rb +4 -0
  4. data/lib/bundler/cli/doctor/ssl.rb +4 -8
  5. data/lib/bundler/cli/info.rb +1 -1
  6. data/lib/bundler/cli/lock.rb +19 -4
  7. data/lib/bundler/cli/open.rb +7 -1
  8. data/lib/bundler/cli/outdated.rb +8 -10
  9. data/lib/bundler/cli/platform.rb +15 -1
  10. data/lib/bundler/cli/show.rb +5 -1
  11. data/lib/bundler/cli/update.rb +1 -1
  12. data/lib/bundler/cli.rb +22 -7
  13. data/lib/bundler/constants.rb +1 -1
  14. data/lib/bundler/definition.rb +102 -16
  15. data/lib/bundler/dsl.rb +76 -5
  16. data/lib/bundler/fetcher/downloader.rb +14 -4
  17. data/lib/bundler/fetcher.rb +4 -1
  18. data/lib/bundler/gem_helper.rb +1 -1
  19. data/lib/bundler/installer/parallel_installer.rb +2 -1
  20. data/lib/bundler/installer.rb +5 -1
  21. data/lib/bundler/lazy_specification.rb +1 -1
  22. data/lib/bundler/lockfile_generator.rb +6 -0
  23. data/lib/bundler/lockfile_parser.rb +18 -2
  24. data/lib/bundler/man/bundle-info.1 +4 -1
  25. data/lib/bundler/man/bundle-info.1.ronn +4 -0
  26. data/lib/bundler/man/bundle-lock.1 +2 -2
  27. data/lib/bundler/man/bundle-lock.1.ronn +4 -3
  28. data/lib/bundler/man/bundle-open.1 +4 -1
  29. data/lib/bundler/man/bundle-open.1.ronn +4 -1
  30. data/lib/bundler/man/bundle-outdated.1 +1 -1
  31. data/lib/bundler/man/bundle-outdated.1.ronn +1 -1
  32. data/lib/bundler/man/bundle-show.1 +4 -1
  33. data/lib/bundler/man/bundle-show.1.ronn +4 -0
  34. data/lib/bundler/man/bundle-update.1 +3 -3
  35. data/lib/bundler/man/bundle-update.1.ronn +5 -3
  36. data/lib/bundler/man/gemfile.5 +43 -2
  37. data/lib/bundler/man/gemfile.5.ronn +69 -4
  38. data/lib/bundler/match_platform.rb +12 -3
  39. data/lib/bundler/materialization.rb +3 -1
  40. data/lib/bundler/override.rb +83 -2
  41. data/lib/bundler/resolver.rb +28 -4
  42. data/lib/bundler/rubygems_ext.rb +15 -4
  43. data/lib/bundler/self_manager.rb +15 -9
  44. data/lib/bundler/settings.rb +10 -26
  45. data/lib/bundler/source/git/git_proxy.rb +39 -2
  46. data/lib/bundler/source/git.rb +34 -7
  47. data/lib/bundler/source/rubygems.rb +2 -2
  48. data/lib/bundler/source/rubygems_aggregate.rb +1 -1
  49. data/lib/bundler/source_map.rb +7 -3
  50. data/lib/bundler/spec_set.rb +8 -4
  51. data/lib/bundler/templates/newgem/ext/newgem/Cargo.toml.tt +1 -1
  52. data/lib/bundler/vendor/fileutils/lib/fileutils.rb +54 -29
  53. data/lib/bundler/version.rb +1 -1
  54. data/lib/bundler.rb +1 -3
  55. data/lib/rubygems/compact_index_client/http_fetcher.rb +5 -3
  56. data/lib/rubygems/compact_index_client/parser.rb +1 -0
  57. data/lib/rubygems/vendor/uri/lib/uri/common.rb +1 -1
  58. data/lib/rubygems/vendor/uri/lib/uri/generic.rb +15 -3
  59. data/lib/rubygems/vendor/uri/lib/uri/version.rb +1 -1
  60. data/lib/rubygems/yaml_serializer.rb +131 -103
  61. metadata +391 -390
@@ -359,6 +359,20 @@ Git repositories support a number of additional options.
359
359
  Specify `submodules: true` to cause bundler to expand any
360
360
  submodules included in the git repository
361
361
 
362
+ * `sparse_checkout`:
363
+ Specify a directory, or an array of directories, to include when
364
+ checking out a git repository. Uses git's sparse-checkout feature
365
+ (requires git 2.25+) to only check out the specified directories,
366
+ reducing disk usage for large repos. Unless `glob` is given, the
367
+ `.gemspec` is looked up within those directories.
368
+
369
+ gem "foo", git: "https://github.com/org/monorepo.git",
370
+ sparse_checkout: "packages/foo"
371
+
372
+ Files at the top level of the repository are always checked out.
373
+ On git versions below 2.25, the entire repository will be checked
374
+ out with a warning.
375
+
362
376
  If a git repository contains multiple `.gemspecs`, each `.gemspec`
363
377
  represents a gem located at the same place in the file system as
364
378
  the `.gemspec`.
@@ -587,8 +601,7 @@ one of:
587
601
  A per-gem override on the same field takes precedence over an `:all` override.
588
602
  `:all + version:` is rejected: version requirements only make sense per gem.
589
603
 
590
- Multiple `override` calls for distinct targets are allowed; declaring the
591
- same `target` and `field` twice is an error.
604
+ Declaring the same `target` and `field` twice is an error.
592
605
 
593
606
  source "https://rubygems.org"
594
607
 
@@ -610,11 +623,63 @@ same `target` and `field` twice is an error.
610
623
  gem "rails", "~> 7.0"
611
624
 
612
625
  The override only affects resolution and the install-time Ruby/RubyGems
613
- compatibility checks; `Gemfile.lock` continues to reflect the resolved
614
- versions, not the rewritten requirements. When resolution still fails,
626
+ compatibility checks; the gems in `Gemfile.lock` continue to reflect the
627
+ resolved versions, not the rewritten requirements. The overrides themselves are
628
+ recorded in the lockfile's `OVERRIDES` section, one line per field, sorted:
629
+
630
+ OVERRIDES
631
+ "rails", version: ">= 8.0"
632
+ :all, required_ruby_version: :ignore_upper
633
+
634
+ Requirements are recorded in normalized form, so `">=8.0"` and `">= 8.0"` are
635
+ the same override. A frozen install fails when the Gemfile's overrides differ
636
+ from the recorded ones. Adding or removing a `:all` or metadata override
637
+ rewrites the section and keeps the locked versions. The exception is a locked
638
+ gem that is not installed and whose metadata rejects the current Ruby. Bundler
639
+ re-resolves it, as it does without the override. Run `bundle update` to
640
+ re-resolve under the override. When resolution still fails,
615
641
  Bundler appends the active overrides (with their Gemfile location) to the
616
642
  error message so it is clear which override shaped the constraint set.
617
643
 
644
+ ### DROPPING OR REPLACING A DEPENDENCY
645
+
646
+ `with:` and `in:` rewrite a single dependency of a single gem:
647
+
648
+ override <target>, with: nil, in: <gem>
649
+ override <target>, with: <replacement>, in: <gem>, version: <requirement>
650
+
651
+ `with: nil` removes the dependency of `<gem>` on `<target>`, so `<target>` is
652
+ not installed unless something else still requires it. `with: <replacement>`
653
+ makes `<gem>` depend on `<replacement>` instead of `<target>`. The requirement
654
+ `<gem>` put on `<target>` is discarded, and the optional `version:` gives the
655
+ replacement its own. Both forms apply whether `<gem>` is a direct or a
656
+ transitive dependency.
657
+
658
+ `in:` is required, and so is `with:` once `in:` is given. To rewrite the
659
+ same dependency of several gems, write one `override` per gem. It is an error
660
+ to declare the same `<target>` and `in:` twice, to drop a gem that the
661
+ Gemfile requires directly, or to replace a gem that another gem still
662
+ requires, since both would then be in the bundle.
663
+
664
+ # jekyll only loads em-websocket for `jekyll serve --livereload`.
665
+ override "em-websocket", with: nil, in: "jekyll"
666
+
667
+ # Use an API-compatible gem that provides the same files.
668
+ override "foo", with: "foo-lite", in: "bar"
669
+
670
+ Bundler does not check what a gem loads at runtime. If `<gem>` requires
671
+ `<target>` unconditionally, dropping it makes that require raise `LoadError`,
672
+ as with jekyll's dependency on jekyll-sass-converter or the adapters that
673
+ faraday 1.x requires on load. A replacement only works when it provides the
674
+ files `<gem>` requires, and it does not take over the name of `<target>`:
675
+ `Gem.loaded_specs` has no entry for `<target>`, and `gem "<target>"` fails.
676
+
677
+ `Gemfile.lock` keeps listing `<target>` under `<gem>` as the gem publishes it,
678
+ while the locked gems leave `<target>` out or include `<replacement>`, and the
679
+ `OVERRIDES` section records the rewrite so tools that read the lockfile can
680
+ apply it. A frozen install accepts that lockfile as long as the Gemfile still
681
+ has the override. Removing the override makes Bundler resolve `<target>` again.
682
+
618
683
  ## SOURCE PRIORITY
619
684
 
620
685
  When attempting to locate a gem to satisfy a gem requirement,
@@ -27,9 +27,18 @@ module Bundler
27
27
  end
28
28
 
29
29
  def self.prefer_content_addressable(matching)
30
- addressable, non_addressable = matching.partition {|s| Gem::ContentAddress.content_addressed?(s, validate_ruby_abi: false) }
31
- return matching if addressable.empty?
32
-
30
+ first = matching.index {|s| Gem::ContentAddress.content_addressed?(s, validate_ruby_abi: false) }
31
+ return matching unless first
32
+
33
+ addressable = [matching[first]]
34
+ non_addressable = matching.take(first)
35
+ matching.drop(first + 1).each do |s|
36
+ if Gem::ContentAddress.content_addressed?(s, validate_ruby_abi: false)
37
+ addressable << s
38
+ else
39
+ non_addressable << s
40
+ end
41
+ end
33
42
  compatible = addressable.select(&:matches_current_metadata?)
34
43
  return compatible if compatible.any?
35
44
 
@@ -30,7 +30,9 @@ module Bundler
30
30
  end
31
31
 
32
32
  def dependencies
33
- (materialized_spec || specs.first).runtime_dependencies.map {|d| [d, platform] }
33
+ spec = materialized_spec || specs.first
34
+ overrides = specs.first.overrides if specs.first.is_a?(LazySpecification)
35
+ Override.rewrite_dependencies(overrides, spec.name, spec.runtime_dependencies).map {|d| [d, platform] }
34
36
  end
35
37
 
36
38
  def materialized_spec
@@ -9,6 +9,20 @@ module Bundler
9
9
  overrides.find {|o| o.target == :all && o.field == field }
10
10
  end
11
11
 
12
+ # Drops or replaces the dependencies of the `in:` gem, passed as `from`,
13
+ # that a `with:`/`in:` override targets. The requirement `from` put on the
14
+ # replaced gem is discarded.
15
+ def self.rewrite_dependencies(overrides, from, dependencies)
16
+ return dependencies if overrides.nil? || overrides.none? {|o| o.from == from }
17
+
18
+ dependencies.filter_map do |dep|
19
+ override = overrides.find {|o| o.from == from && o.target == dep.name }
20
+ next dep unless override
21
+ next unless override.operation
22
+ Gem::Dependency.new(override.operation, *Array(override.requirement))
23
+ end
24
+ end
25
+
12
26
  # Attach the given overrides onto every LazySpecification in `specs` so
13
27
  # downstream consumers (LazySpecification#choose_compatible, the install-
14
28
  # time compatibility check, etc.) can read the override list off the spec
@@ -19,12 +33,14 @@ module Bundler
19
33
  specs.each {|s| s.overrides = overrides if s.is_a?(LazySpecification) }
20
34
  end
21
35
 
22
- attr_reader :target, :field, :operation, :source_location
36
+ attr_reader :target, :field, :operation, :from, :requirement, :source_location
23
37
 
24
- def initialize(target, field, operation, source_location: nil)
38
+ def initialize(target, field, operation, from: nil, requirement: nil, source_location: nil)
25
39
  @target = target
26
40
  @field = field
27
41
  @operation = operation
42
+ @from = from
43
+ @requirement = requirement
28
44
  @source_location = source_location
29
45
  end
30
46
 
@@ -33,7 +49,72 @@ module Bundler
33
49
  "#{File.basename(@source_location.path)}:#{@source_location.lineno}"
34
50
  end
35
51
 
52
+ def to_s
53
+ location = source_location_label
54
+ "override #{to_lock}#{" (declared at #{location})" if location}"
55
+ end
56
+
57
+ # The override as recorded in the OVERRIDES section of Gemfile.lock: the
58
+ # `override` arguments with requirements normalized, so equivalent
59
+ # spellings ("<=3", "<= 3") record the same line.
60
+ def to_lock
61
+ target = @target == :all ? ":all" : @target.inspect
62
+ fields = if @from
63
+ ["with: #{@operation.inspect}", "in: #{@from.inspect}", *("version: #{self.class.normalize(@requirement).inspect}" if @requirement)]
64
+ else
65
+ ["#{@field}: #{self.class.normalize(@operation).inspect}"]
66
+ end
67
+ [target, *fields].join(", ")
68
+ end
69
+
70
+ LOCK_VALUE = /"[^"\\]*"|:[a-z_]+|nil/
71
+ LOCK_FIELD = /, ([a-z_]+): (#{LOCK_VALUE})/
72
+ LOCK_LINE = /\A(:all|"[a-zA-Z0-9._-]+")((?:#{LOCK_FIELD})+)\z/
73
+ private_constant :LOCK_VALUE, :LOCK_FIELD, :LOCK_LINE
74
+
75
+ # Parses a line written by #to_lock. The DSL only accepts valid gem names,
76
+ # and requirements are normalized, so no recorded value contains a quote
77
+ # or a backslash and the line can be read without evaluating it.
78
+ def self.from_lock(line)
79
+ match = LOCK_LINE.match(line)
80
+ raise ArgumentError, "invalid override in lockfile: #{line.inspect}" unless match
81
+
82
+ target = match[1] == ":all" ? :all : match[1][1..-2]
83
+ fields = match[2].scan(LOCK_FIELD).map {|key, value| [key.to_sym, parse_lock_value(value)] }
84
+ keys = fields.map(&:first)
85
+ raise ArgumentError, "invalid override in lockfile: #{line.inspect}" unless keys.uniq.size == keys.size
86
+ fields = fields.to_h
87
+
88
+ if fields.key?(:in)
89
+ unless (keys - [:in, :with, :version]).empty? && fields.key?(:with) && fields[:in].is_a?(String)
90
+ raise ArgumentError, "invalid override in lockfile: #{line.inspect}"
91
+ end
92
+ new(target, :with, fields[:with], from: fields[:in], requirement: normalize(fields[:version]))
93
+ else
94
+ field, operation = fields.first
95
+ unless fields.size == 1 && Dsl::SUPPORTED_OVERRIDE_FIELDS.include?(field)
96
+ raise ArgumentError, "invalid override in lockfile: #{line.inspect}"
97
+ end
98
+ new(target, field, normalize(operation))
99
+ end
100
+ end
101
+
102
+ def self.parse_lock_value(value)
103
+ case value
104
+ when "nil" then nil
105
+ when /\A:/ then value[1..].to_sym
106
+ else value[1..-2]
107
+ end
108
+ end
109
+ private_class_method :parse_lock_value
110
+
111
+ def self.normalize(operation)
112
+ operation.is_a?(String) ? Gem::Requirement.new(operation).to_s : operation
113
+ end
114
+
36
115
  def apply_to(requirement)
116
+ raise ArgumentError, "#{self} rewrites a dependency and has no requirement to apply" if @from
117
+
37
118
  case operation
38
119
  when nil
39
120
  Gem::Requirement.default
@@ -71,6 +71,7 @@ module Bundler
71
71
  @cached_dependencies = Hash.new do |dependencies, package|
72
72
  dependencies[package] = Hash.new do |versions, version|
73
73
  deps = version.dependencies.reject {|d| d.name == package.name }
74
+ deps = Override.rewrite_dependencies(@base.overrides, package.name, deps)
74
75
  deps = apply_metadata_overrides(deps, package.name)
75
76
  versions[version] = to_dependency_hash(deps, @packages)
76
77
  end
@@ -88,6 +89,7 @@ module Bundler
88
89
  solver = Gem::PubGrub::VersionSolver.new(source: self, root: root, strategy: Strategy.new(self), logger: logger)
89
90
  result = solver.solve
90
91
  resolved_specs = result.flat_map {|package, version| version.to_specs(package, @most_specific_locked_platform) }
92
+ check_replaced_gems(resolved_specs)
91
93
  Override.attach(resolved_specs, @base.overrides)
92
94
  spec_set = SpecSet.new(resolved_specs).specs_with_additional_variants_from(@base.locked_specs)
93
95
  @cooldown_skipped = cooldown_skipped_summary(spec_set)
@@ -137,15 +139,37 @@ module Bundler
137
139
  raise SolveFailure.new(explanation)
138
140
  end
139
141
 
142
+ # A replacement is only useful when it provides the files of the gem it
143
+ # replaces, so the two cannot be loaded side by side.
144
+ def check_replaced_gems(specs)
145
+ names = specs.map(&:name)
146
+
147
+ @base.overrides.each do |override|
148
+ next unless override.from && override.operation
149
+ next unless names.include?(override.target) && names.include?(override.operation)
150
+ next unless specs.any? {|s| s.name == override.from && s.dependencies.any? {|d| d.name == override.target } }
151
+
152
+ dependents = specs.select do |spec|
153
+ Override.rewrite_dependencies(@base.overrides, spec.name, spec.dependencies).any? {|d| d.name == override.target }
154
+ end.map(&:name).uniq
155
+
156
+ lines = ["#{override} would leave both #{override.target} and #{override.operation} in the bundle because #{override.target} is still required by:"]
157
+ lines << " the Gemfile" if @base.requirements.any? {|d| d.name == override.target }
158
+ lines.concat(dependents.map {|name| " #{name}" })
159
+ if dependents.any?
160
+ lines << "Replace it for each of them, for example:"
161
+ lines << " #{Override.new(override.target, :with, override.operation, from: dependents.first)}"
162
+ end
163
+ raise GemfileError, lines.join("\n")
164
+ end
165
+ end
166
+
140
167
  def override_diagnostic_summary
141
168
  return nil if @base.overrides.empty?
142
169
 
143
170
  lines = ["Bundler applied the following overrides while resolving:"]
144
171
  @base.overrides.each do |override|
145
- target = override.target == :all ? ":all" : override.target.inspect
146
- location = override.source_location_label
147
- lines << " override #{target}, #{override.field}: #{override.operation.inspect}" \
148
- "#{location ? " (declared at #{location})" : ""}"
172
+ lines << " #{override}"
149
173
  end
150
174
  "\n\n#{lines.join("\n")}"
151
175
  end
@@ -86,11 +86,22 @@ module Gem
86
86
  end
87
87
  end
88
88
 
89
- def open_file_with_lock(path, &block)
89
+ def open_file_with_lock(path)
90
90
  file_lock = "#{path}.lock"
91
- open_file_with_flock(file_lock, &block)
92
- ensure
93
- FileUtils.rm_f file_lock
91
+
92
+ loop do
93
+ open_file_with_flock(file_lock) do |io|
94
+ # The previous holder removed the file while we were waiting on it,
95
+ # so leave this block and try again on the file now at the lock path
96
+ next unless File.identical?(io, file_lock)
97
+
98
+ begin
99
+ return yield io
100
+ ensure
101
+ FileUtils.rm_f file_lock
102
+ end
103
+ end
104
+ end
94
105
  end
95
106
  end
96
107
  end
@@ -30,8 +30,8 @@ module Bundler
30
30
  install_and_restart_with(restart_version)
31
31
  end
32
32
 
33
- def update_bundler_and_restart_with_it_if_needed(target)
34
- spec = resolve_update_version_from(target)
33
+ def update_bundler_and_restart_with_it_if_needed(target, pre: false)
34
+ spec = resolve_update_version_from(target, pre: pre)
35
35
  return unless spec
36
36
 
37
37
  version = spec.version
@@ -108,9 +108,9 @@ module Bundler
108
108
  lockfile_version
109
109
  end
110
110
 
111
- def resolve_update_version_from(target)
111
+ def resolve_update_version_from(target, pre: false)
112
112
  requirement = Gem::Requirement.new(target)
113
- update_candidate = find_latest_matching_spec(requirement)
113
+ update_candidate = find_latest_matching_spec(requirement, pre: pre)
114
114
 
115
115
  if update_candidate.nil?
116
116
  raise InvalidOption, "The `bundle update --bundler` target version (#{target}) does not exist"
@@ -137,18 +137,24 @@ module Bundler
137
137
  end
138
138
  end
139
139
 
140
- def find_latest_matching_spec(requirement)
140
+ def find_latest_matching_spec(requirement, pre: false)
141
141
  Bundler.configure
142
- local_result = find_latest_matching_spec_from_collection(local_specs, requirement)
142
+ # A bare `bundle update --bundler` must stay on releases, like `gem update
143
+ # --system`, so only `--pre` or a prerelease requirement opts into one.
144
+ allow_prerelease = pre || requirement.prerelease?
145
+
146
+ local_result = find_latest_matching_spec_from_collection(local_specs, requirement, allow_prerelease)
143
147
  return local_result if local_result && requirement.specific?
144
148
 
145
- remote_result = find_latest_matching_spec_from_collection(remote_specs, requirement)
149
+ remote_result = find_latest_matching_spec_from_collection(remote_specs, requirement, allow_prerelease)
146
150
  return remote_result if local_result.nil?
147
151
 
148
- [local_result, remote_result].max
152
+ [local_result, remote_result].compact.max
149
153
  end
150
154
 
151
- def find_latest_matching_spec_from_collection(specs, requirement)
155
+ def find_latest_matching_spec_from_collection(specs, requirement, allow_prerelease)
156
+ specs = specs.reject {|spec| spec.version.prerelease? } unless allow_prerelease
157
+
152
158
  specs.sort.reverse_each.find {|spec| requirement.satisfied_by?(spec.version) }
153
159
  end
154
160
 
@@ -896,32 +896,16 @@ module Bundler
896
896
  "#{prefix}#{uri}#{suffix}"
897
897
  end
898
898
 
899
- # This is a hot method, so avoid respond_to? checks on every invocation
900
- if :read.respond_to?(:name)
901
- def self.key_to_s(key)
902
- case key
903
- when String
904
- key
905
- when Symbol
906
- key.name
907
- when Gem::URI::HTTP
908
- key.to_s
909
- else
910
- raise ArgumentError, "Invalid key: #{key.inspect}"
911
- end
912
- end
913
- else
914
- def self.key_to_s(key)
915
- case key
916
- when String
917
- key
918
- when Symbol
919
- key.to_s
920
- when Gem::URI::HTTP
921
- key.to_s
922
- else
923
- raise ArgumentError, "Invalid key: #{key.inspect}"
924
- end
899
+ def self.key_to_s(key)
900
+ case key
901
+ when String
902
+ key
903
+ when Symbol
904
+ key.name
905
+ when Gem::URI::HTTP
906
+ key.to_s
907
+ else
908
+ raise ArgumentError, "Invalid key: #{key.inspect}"
925
909
  end
926
910
  end
927
911
  end
@@ -54,7 +54,7 @@ module Bundler
54
54
  # All actions required by the Git source is encapsulated in this
55
55
  # object.
56
56
  class GitProxy
57
- attr_accessor :path, :uri, :branch, :tag, :ref, :explicit_ref
57
+ attr_accessor :path, :uri, :branch, :tag, :ref, :explicit_ref, :sparse_checkout
58
58
  attr_writer :revision
59
59
 
60
60
  def self.version
@@ -95,6 +95,7 @@ module Bundler
95
95
  @revision = revision
96
96
  @git = git
97
97
  @commit_ref = nil
98
+ @sparse_checkout = options["sparse_checkout"]
98
99
  end
99
100
 
100
101
  def revision
@@ -159,6 +160,8 @@ module Bundler
159
160
  end
160
161
  end
161
162
 
163
+ setup_sparse_checkout(destination)
164
+
162
165
  ref = @commit_ref || (locked_to_full_sha? && @revision)
163
166
  if ref
164
167
  git "config", "uploadpack.allowAnySHA1InWant", "true", dir: path.to_s if @commit_ref.nil? && needs_allow_any_sha1_in_want?
@@ -507,7 +510,15 @@ module Bundler
507
510
 
508
511
  return ["git", *opts, *cmd] unless dir
509
512
 
510
- ["git", "-C", dir.to_s, *opts, *cmd]
513
+ # With safe.bareRepository=explicit, git refuses to discover a bare
514
+ # repository from -C, so the cache clone is named with --git-dir.
515
+ # Working trees, like a local override, still go through -C.
516
+ location = bare_repo?(dir) ? "--git-dir" : "-C"
517
+ ["git", location, dir.to_s, *opts, *cmd]
518
+ end
519
+
520
+ def bare_repo?(dir)
521
+ File.exist?(File.join(dir, "objects")) && File.exist?(File.join(dir, "HEAD"))
511
522
  end
512
523
 
513
524
  def extra_clone_args
@@ -565,6 +576,32 @@ module Bundler
565
576
  def supports_cloning_with_no_tags?
566
577
  @supports_cloning_with_no_tags ||= Gem::Version.new(version) >= Gem::Version.new("2.14.0-rc0")
567
578
  end
579
+
580
+ def supports_sparse_checkout?
581
+ @supports_sparse_checkout ||= Gem::Version.new(version) >= Gem::Version.new("2.25.0")
582
+ end
583
+
584
+ def supports_sparse_checkout_set_with_cone?
585
+ @supports_sparse_checkout_set_with_cone ||= Gem::Version.new(version) >= Gem::Version.new("2.35.0")
586
+ end
587
+
588
+ def setup_sparse_checkout(destination)
589
+ return unless @sparse_checkout
590
+
591
+ unless supports_sparse_checkout?
592
+ Bundler.ui.warn "Git #{version} doesn't support sparse-checkout (requires 2.25+). Checking out the full repository."
593
+ return
594
+ end
595
+
596
+ Bundler.ui.debug "Setting sparse checkout to only include #{@sparse_checkout.join(", ")}"
597
+ if supports_sparse_checkout_set_with_cone?
598
+ git "sparse-checkout", "set", "--cone", *@sparse_checkout, dir: destination
599
+ else
600
+ # Before git 2.35, `set` keeps an unknown --cone as a pattern instead of rejecting it.
601
+ git "sparse-checkout", "init", "--cone", dir: destination
602
+ git "sparse-checkout", "set", *@sparse_checkout, dir: destination
603
+ end
604
+ end
568
605
  end
569
606
  end
570
607
  end
@@ -7,12 +7,13 @@ module Bundler
7
7
  class Git < Path
8
8
  autoload :GitProxy, File.expand_path("git/git_proxy", __dir__)
9
9
 
10
- attr_reader :uri, :ref, :branch, :options, :glob, :submodules
10
+ attr_reader :uri, :ref, :branch, :options, :glob, :submodules, :sparse_checkout
11
11
 
12
12
  def initialize(options)
13
13
  @options = options
14
14
  @checksum_store = Checksum::Store.new
15
- @glob = options["glob"] || DEFAULT_GLOB
15
+ @sparse_checkout = normalize_sparse_checkout(options)
16
+ @glob = options["glob"] || sparse_checkout_glob || DEFAULT_GLOB
16
17
 
17
18
  @allow_cached = false
18
19
  @allow_remote = false
@@ -58,12 +59,13 @@ module Bundler
58
59
  %w[ref branch tag submodules].each do |opt|
59
60
  out << " #{opt}: #{options[opt]}\n" if options[opt]
60
61
  end
62
+ @sparse_checkout&.each {|dir| out << " sparse_checkout: #{dir}\n" }
61
63
  out << " glob: #{@glob}\n" unless default_glob?
62
64
  out << " specs:\n"
63
65
  end
64
66
 
65
67
  def to_gemfile
66
- specifiers = %w[ref branch tag submodules glob].map do |opt|
68
+ specifiers = %w[ref branch tag submodules glob sparse_checkout].map do |opt|
67
69
  "#{opt}: #{options[opt]}" if options[opt]
68
70
  end
69
71
 
@@ -71,14 +73,15 @@ module Bundler
71
73
  end
72
74
 
73
75
  def hash
74
- [self.class, uri, ref, branch, name, glob, submodules].hash
76
+ [self.class, uri, ref, branch, name, glob, submodules, sparse_checkout].hash
75
77
  end
76
78
 
77
79
  def eql?(other)
78
80
  other.is_a?(Git) && uri == other.uri && ref == other.ref &&
79
81
  branch == other.branch && name == other.name &&
80
82
  glob == other.glob &&
81
- submodules == other.submodules
83
+ submodules == other.submodules &&
84
+ sparse_checkout == other.sparse_checkout
82
85
  end
83
86
 
84
87
  alias_method :==, :eql?
@@ -87,7 +90,8 @@ module Bundler
87
90
  other.is_a?(Git) && uri == other.uri &&
88
91
  name == other.name &&
89
92
  glob == other.glob &&
90
- submodules == other.submodules
93
+ submodules == other.submodules &&
94
+ sparse_checkout == other.sparse_checkout
91
95
  end
92
96
 
93
97
  def to_s
@@ -389,7 +393,30 @@ module Bundler
389
393
  end
390
394
 
391
395
  def shortref_for_path(ref)
392
- ref[0..11]
396
+ shortref = ref[0..11]
397
+ return shortref unless @sparse_checkout
398
+
399
+ # Checkouts of the same revision with different cones hold different files
400
+ "#{shortref}-#{Bundler::Digest.sha1(@sparse_checkout.join("\n"))[0..7]}"
401
+ end
402
+
403
+ def normalize_sparse_checkout(options)
404
+ return unless options["sparse_checkout"]
405
+
406
+ dirs = Array(options["sparse_checkout"])
407
+ unless dirs.any? && dirs.all? {|dir| dir.is_a?(String) && !dir.empty? }
408
+ raise GemfileError, "The :sparse_checkout option must be a directory or an array of directories, " \
409
+ "but #{options["sparse_checkout"].inspect} was given"
410
+ end
411
+
412
+ options["sparse_checkout"] = dirs
413
+ end
414
+
415
+ def sparse_checkout_glob
416
+ return unless @sparse_checkout
417
+
418
+ dirs = @sparse_checkout.size == 1 ? @sparse_checkout.first : "{#{@sparse_checkout.join(",")}}"
419
+ "#{dirs}/#{DEFAULT_GLOB}"
393
420
  end
394
421
 
395
422
  def glob_for_display
@@ -77,11 +77,11 @@ module Bundler
77
77
  end
78
78
 
79
79
  def hash
80
- @remotes.hash
80
+ credless_remotes.hash
81
81
  end
82
82
 
83
83
  def eql?(other)
84
- other.is_a?(Rubygems) && other.credless_remotes == credless_remotes
84
+ equal?(other) || (other.is_a?(Rubygems) && other.credless_remotes == credless_remotes)
85
85
  end
86
86
 
87
87
  alias_method :==, :eql?
@@ -29,7 +29,7 @@ module Bundler
29
29
 
30
30
  def build_index
31
31
  Index.build do |idx|
32
- dependency_names = source_map.pinned_spec_names
32
+ dependency_names = source_map.pinned_spec_names | source_map.replacement_names
33
33
 
34
34
  sources.all_sources.each do |source|
35
35
  next if @excluded_sources.include?(source)
@@ -2,12 +2,15 @@
2
2
 
3
3
  module Bundler
4
4
  class SourceMap
5
- attr_reader :sources, :dependencies, :locked_specs
5
+ attr_reader :sources, :dependencies, :locked_specs, :replacement_names
6
6
 
7
- def initialize(sources, dependencies, locked_specs)
7
+ def initialize(sources, dependencies, locked_specs, replacement_names = [])
8
8
  @sources = sources
9
9
  @dependencies = dependencies
10
10
  @locked_specs = locked_specs
11
+ # No gemspec names a gem that a `with:`/`in:` override substitutes, so
12
+ # the dependency API never fetches it unless asked.
13
+ @replacement_names = replacement_names
11
14
  end
12
15
 
13
16
  def pinned_spec_names(skip = nil)
@@ -22,6 +25,7 @@ module Bundler
22
25
  end
23
26
 
24
27
  unmet_deps = explicit_sources.map do |source|
28
+ source.add_dependency_names(replacement_names)
25
29
  (source.spec_names - pinned_spec_names).each do |indirect_dependency_name|
26
30
  previous_source = requirements[indirect_dependency_name]
27
31
  if previous_source.nil?
@@ -39,7 +43,7 @@ module Bundler
39
43
  source.unmet_deps
40
44
  end
41
45
 
42
- sources.default_source.add_dependency_names(unmet_deps.flatten - requirements.keys)
46
+ sources.default_source.add_dependency_names(unmet_deps.flatten + replacement_names - requirements.keys)
43
47
 
44
48
  requirements
45
49
  end