Skip to content

Commit 099d1ce

Browse files
committed
Add sparse_checkout option to git source
1 parent ae1f702 commit 099d1ce

File tree

7 files changed

+128
-18
lines changed

7 files changed

+128
-18
lines changed

bundler/lib/bundler/dependency.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ def glob
5858
@glob = @options["glob"]
5959
end
6060

61+
def sparse_checkout
62+
return @sparse_checkout if defined?(@sparse_checkout)
63+
64+
@sparse_checkout = @options["sparse_checkout"]
65+
end
66+
6167
def platforms
6268
@platforms ||= Array(@options["platforms"])
6369
end

bundler/lib/bundler/dsl.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def self.evaluate(gemfile, lockfile, unlock)
1717
VALID_PLATFORMS = Bundler::CurrentRuby::PLATFORM_MAP.keys.freeze
1818

1919
VALID_KEYS = %w[group groups git path glob name branch ref tag require submodules
20-
platform platforms source install_if force_ruby_platform].freeze
20+
platform platforms source install_if force_ruby_platform sparse_checkout].freeze
2121

2222
GITHUB_PULL_REQUEST_URL = %r{\Ahttps://github\.com/([A-Za-z0-9_\-\.]+/[A-Za-z0-9_\-\.]+)/pull/(\d+)\z}
2323
GITLAB_MERGE_REQUEST_URL = %r{\Ahttps://gitlab\.com/([A-Za-z0-9_\-\./]+)/-/merge_requests/(\d+)\z}

bundler/lib/bundler/lockfile_parser.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def to_s
4747
PATH = "PATH"
4848
PLUGIN = "PLUGIN SOURCE"
4949
SPECS = " specs:"
50-
OPTIONS = /^ ([a-z]+): (.*)$/i
50+
OPTIONS = /^ ([a-z_]+): (.*)$/i
5151
SOURCE = [GIT, GEM, PATH, PLUGIN].freeze
5252

5353
SECTIONS_BY_VERSION_INTRODUCED = {

bundler/lib/bundler/man/gemfile.5.ronn

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,24 @@ Git repositories support a number of additional options.
352352
Specify `submodules: true` to cause bundler to expand any
353353
submodules included in the git repository
354354

355+
* `sparse_checkout`:
356+
Specify directories to include when checking out a git repository.
357+
Uses git's sparse-checkout feature (requires git 2.25+) to only
358+
download specified directories, reducing disk usage for large repos.
359+
360+
gem "foo", git: "https://github.com/org/monorepo.git",
361+
sparse_checkout: "packages/foo",
362+
glob: "packages/foo/*.gemspec"
363+
364+
Multiple directories can be specified as an array:
365+
366+
gem "bar", github: "org/mono",
367+
sparse_checkout: ["gems/bar", "shared/lib"],
368+
glob: "gems/bar/*.gemspec"
369+
370+
On git versions below 2.25, the entire repository will be cloned
371+
with a warning.
372+
355373
If a git repository contains multiple `.gemspecs`, each `.gemspec`
356374
represents a gem located at the same place in the file system as
357375
the `.gemspec`.

bundler/lib/bundler/source/git.rb

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ class Source
77
class Git < Path
88
autoload :GitProxy, File.expand_path("git/git_proxy", __dir__)
99

10-
attr_reader :uri, :ref, :branch, :options, :glob, :submodules
10+
attr_reader :uri, :ref, :branch, :options, :glob, :submodules, :sparse_checkout
1111

1212
def initialize(options)
1313
@options = options
@@ -25,6 +25,7 @@ def initialize(options)
2525
@branch = options["branch"]
2626
@ref = options["ref"] || options["branch"] || options["tag"]
2727
@submodules = options["submodules"]
28+
@sparse_checkout = options["sparse_checkout"]
2829
@name = options["name"]
2930
@version = options["version"].to_s.strip.gsub("-", ".pre.")
3031

@@ -57,27 +58,29 @@ def to_lock
5758
%w[ref branch tag submodules].each do |opt|
5859
out << " #{opt}: #{options[opt]}\n" if options[opt]
5960
end
61+
out << " sparse_checkout: #{@sparse_checkout}\n" if @sparse_checkout
6062
out << " glob: #{@glob}\n" unless default_glob?
6163
out << " specs:\n"
6264
end
6365

6466
def to_gemfile
65-
specifiers = %w[ref branch tag submodules glob].map do |opt|
67+
specifiers = %w[ref branch tag submodules glob sparse_checkout].map do |opt|
6668
"#{opt}: #{options[opt]}" if options[opt]
6769
end
6870

6971
uri_with_specifiers(specifiers)
7072
end
7173

7274
def hash
73-
[self.class, uri, ref, branch, name, glob, submodules].hash
75+
[self.class, uri, ref, branch, name, glob, submodules, sparse_checkout].hash
7476
end
7577

7678
def eql?(other)
7779
other.is_a?(Git) && uri == other.uri && ref == other.ref &&
7880
branch == other.branch && name == other.name &&
7981
glob == other.glob &&
80-
submodules == other.submodules
82+
submodules == other.submodules &&
83+
sparse_checkout == other.sparse_checkout
8184
end
8285

8386
alias_method :==, :eql?
@@ -86,7 +89,8 @@ def include?(other)
8689
other.is_a?(Git) && uri == other.uri &&
8790
name == other.name &&
8891
glob == other.glob &&
89-
submodules == other.submodules
92+
submodules == other.submodules &&
93+
sparse_checkout == other.sparse_checkout
9094
end
9195

9296
def to_s
@@ -126,17 +130,13 @@ def name
126130
# checkout of the git repository. When using local git
127131
# repos, this is set to the local repo.
128132
def install_path
129-
@install_path ||= begin
130-
git_scope = "#{base_name}-#{shortref_for_path(revision)}"
131-
132-
Bundler.install_path.join(git_scope)
133-
end
133+
@install_path ||= Bundler.install_path.join("#{base_name}-#{shortref_for_path(revision, sparse_checkout: @sparse_checkout)}")
134134
end
135135

136136
alias_method :path, :install_path
137137

138138
def extension_dir_name
139-
"#{base_name}-#{shortref_for_path(revision)}"
139+
"#{base_name}-#{shortref_for_path(revision, sparse_checkout: @sparse_checkout)}"
140140
end
141141

142142
def unlock!
@@ -246,7 +246,7 @@ def cache_path
246246
end
247247

248248
def app_cache_dirname
249-
"#{base_name}-#{shortref_for_path(locked_revision || revision)}"
249+
"#{base_name}-#{shortref_for_path(locked_revision || revision, sparse_checkout: @sparse_checkout)}"
250250
end
251251

252252
def revision
@@ -375,8 +375,10 @@ def shortref_for_display(ref)
375375
ref[0..6]
376376
end
377377

378-
def shortref_for_path(ref)
379-
ref[0..11]
378+
def shortref_for_path(ref, sparse_checkout: nil)
379+
scope = ref[0..11]
380+
scope += "-#{Bundler::Digest.sha1(sparse_checkout)[0..7]}" if sparse_checkout
381+
scope
380382
end
381383

382384
def glob_for_display
@@ -432,7 +434,9 @@ def load_gemspec(file)
432434
end
433435

434436
def git_scope
435-
"#{base_name}-#{uri_hash}"
437+
scope = "#{base_name}-#{uri_hash}"
438+
scope += "-#{Bundler::Digest.sha1(@sparse_checkout)[0..7]}" if @sparse_checkout
439+
scope
436440
end
437441

438442
def extension_cache_slug(_)

bundler/lib/bundler/source/git/git_proxy.rb

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def initialize(options)
5454
# All actions required by the Git source is encapsulated in this
5555
# object.
5656
class GitProxy
57-
attr_accessor :path, :uri, :branch, :tag, :ref, :explicit_ref
57+
attr_accessor :path, :uri, :branch, :tag, :ref, :explicit_ref, :sparse_checkout
5858
attr_writer :revision
5959

6060
def initialize(path, uri, options = {}, revision = nil, git = nil)
@@ -72,6 +72,7 @@ def initialize(path, uri, options = {}, revision = nil, git = nil)
7272
@revision = revision
7373
@git = git
7474
@commit_ref = nil
75+
@sparse_checkout = options["sparse_checkout"]
7576
end
7677

7778
def revision
@@ -130,6 +131,8 @@ def copy_to(destination, submodules = false)
130131
end
131132
end
132133

134+
setup_sparse_checkout(destination)
135+
133136
ref = @commit_ref || (locked_to_full_sha? && @revision)
134137
if ref
135138
git "config", "uploadpack.allowAnySHA1InWant", "true", dir: path.to_s if @commit_ref.nil? && needs_allow_any_sha1_in_want?
@@ -458,6 +461,21 @@ def supports_fetching_unreachable_refs?
458461
def supports_cloning_with_no_tags?
459462
@supports_cloning_with_no_tags ||= Gem::Version.new(version) >= Gem::Version.new("2.14.0-rc0")
460463
end
464+
465+
def supports_sparse_checkout?
466+
@supports_sparse_checkout ||= Gem::Version.new(version) >= Gem::Version.new("2.25.0")
467+
end
468+
469+
def setup_sparse_checkout(destination)
470+
return unless @sparse_checkout
471+
472+
unless supports_sparse_checkout?
473+
Bundler.ui.warn "Git #{version} doesn't support sparse-checkout (requires 2.25+). Cloning full repository."
474+
return
475+
end
476+
477+
git "sparse-checkout", "set", "--cone", @sparse_checkout, dir: destination
478+
end
461479
end
462480
end
463481
end

bundler/spec/bundler/source/git/git_proxy_spec.rb

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,4 +334,68 @@
334334
end
335335
end
336336
end
337+
338+
describe "#supports_sparse_checkout?" do
339+
it "returns true for git 2.25+" do
340+
allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.25.0")
341+
expect(git_proxy.send(:supports_sparse_checkout?)).to be true
342+
end
343+
344+
it "returns true for git 2.30+" do
345+
allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.30.0")
346+
expect(git_proxy.send(:supports_sparse_checkout?)).to be true
347+
end
348+
349+
it "returns false for git < 2.25" do
350+
allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.24.0")
351+
expect(git_proxy.send(:supports_sparse_checkout?)).to be false
352+
end
353+
354+
it "returns false for git 2.20" do
355+
allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.20.0")
356+
expect(git_proxy.send(:supports_sparse_checkout?)).to be false
357+
end
358+
end
359+
360+
describe "#setup_sparse_checkout" do
361+
let(:destination) { Pathname("destination") }
362+
363+
context "with sparse_checkout option" do
364+
let(:options) { { "sparse_checkout" => "packages/foo" } }
365+
366+
context "with git 2.25+" do
367+
it "runs sparse-checkout set with cone mode" do
368+
allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.30.0")
369+
expect(git_proxy).to receive(:git).with("sparse-checkout", "set", "--cone", "packages/foo", dir: destination)
370+
git_proxy.send(:setup_sparse_checkout, destination)
371+
end
372+
end
373+
374+
context "with git < 2.25" do
375+
it "skips sparse checkout and warns" do
376+
allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.20.0")
377+
expect(Bundler.ui).to receive(:warn).with(/doesn't support sparse-checkout/)
378+
expect(git_proxy).not_to receive(:git).with("sparse-checkout", any_args)
379+
git_proxy.send(:setup_sparse_checkout, destination)
380+
end
381+
end
382+
end
383+
384+
context "without sparse_checkout option" do
385+
let(:options) { {} }
386+
387+
it "does nothing" do
388+
expect(git_proxy).not_to receive(:git)
389+
git_proxy.send(:setup_sparse_checkout, destination)
390+
end
391+
end
392+
end
393+
394+
context "with sparse_checkout option" do
395+
let(:options) { { "sparse_checkout" => "packages/foo" } }
396+
397+
it "stores sparse_checkout from options" do
398+
expect(git_proxy.sparse_checkout).to eq("packages/foo")
399+
end
400+
end
337401
end

0 commit comments

Comments
 (0)