diff --git a/bundler/lib/bundler/dependency.rb b/bundler/lib/bundler/dependency.rb index cb9c7a76eab8..35aa71902737 100644 --- a/bundler/lib/bundler/dependency.rb +++ b/bundler/lib/bundler/dependency.rb @@ -58,6 +58,12 @@ def glob @glob = @options["glob"] end + def sparse_checkout + return @sparse_checkout if defined?(@sparse_checkout) + + @sparse_checkout = @options["sparse_checkout"] + end + def platforms @platforms ||= Array(@options["platforms"]) end diff --git a/bundler/lib/bundler/dsl.rb b/bundler/lib/bundler/dsl.rb index 6f06c4e91879..76a8d24b13ee 100644 --- a/bundler/lib/bundler/dsl.rb +++ b/bundler/lib/bundler/dsl.rb @@ -17,7 +17,7 @@ def self.evaluate(gemfile, lockfile, unlock) VALID_PLATFORMS = Bundler::CurrentRuby::PLATFORM_MAP.keys.freeze VALID_KEYS = %w[group groups git path glob name branch ref tag require submodules - platform platforms source install_if force_ruby_platform].freeze + platform platforms source install_if force_ruby_platform sparse_checkout].freeze GITHUB_PULL_REQUEST_URL = %r{\Ahttps://github\.com/([A-Za-z0-9_\-\.]+/[A-Za-z0-9_\-\.]+)/pull/(\d+)\z} GITLAB_MERGE_REQUEST_URL = %r{\Ahttps://gitlab\.com/([A-Za-z0-9_\-\./]+)/-/merge_requests/(\d+)\z} diff --git a/bundler/lib/bundler/lockfile_parser.rb b/bundler/lib/bundler/lockfile_parser.rb index ac0ce1ef3d0a..b3bde007c04c 100644 --- a/bundler/lib/bundler/lockfile_parser.rb +++ b/bundler/lib/bundler/lockfile_parser.rb @@ -47,7 +47,7 @@ def to_s PATH = "PATH" PLUGIN = "PLUGIN SOURCE" SPECS = " specs:" - OPTIONS = /^ ([a-z]+): (.*)$/i + OPTIONS = /^ ([a-z_]+): (.*)$/i SOURCE = [GIT, GEM, PATH, PLUGIN].freeze SECTIONS_BY_VERSION_INTRODUCED = { diff --git a/bundler/lib/bundler/man/bundle-add.1 b/bundler/lib/bundler/man/bundle-add.1 index 4474969db608..f5975c70230c 100644 --- a/bundler/lib/bundler/man/bundle-add.1 +++ b/bundler/lib/bundler/man/bundle-add.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-ADD" "1" "September 2025" "" +.TH "BUNDLE\-ADD" "1" "February 2026" "" .SH "NAME" \fBbundle\-add\fR \- Add gem to the Gemfile and run bundle install .SH "SYNOPSIS" diff --git a/bundler/lib/bundler/man/bundle-binstubs.1 b/bundler/lib/bundler/man/bundle-binstubs.1 index b8c153696bba..314d4ef61308 100644 --- a/bundler/lib/bundler/man/bundle-binstubs.1 +++ b/bundler/lib/bundler/man/bundle-binstubs.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-BINSTUBS" "1" "September 2025" "" +.TH "BUNDLE\-BINSTUBS" "1" "February 2026" "" .SH "NAME" \fBbundle\-binstubs\fR \- Install the binstubs of the listed gems .SH "SYNOPSIS" diff --git a/bundler/lib/bundler/man/bundle-cache.1 b/bundler/lib/bundler/man/bundle-cache.1 index c1dafbf07052..0d20662b7b69 100644 --- a/bundler/lib/bundler/man/bundle-cache.1 +++ b/bundler/lib/bundler/man/bundle-cache.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-CACHE" "1" "September 2025" "" +.TH "BUNDLE\-CACHE" "1" "February 2026" "" .SH "NAME" \fBbundle\-cache\fR \- Package your needed \fB\.gem\fR files into your application .SH "SYNOPSIS" diff --git a/bundler/lib/bundler/man/bundle-check.1 b/bundler/lib/bundler/man/bundle-check.1 index f83af1eb5577..154fb06818f4 100644 --- a/bundler/lib/bundler/man/bundle-check.1 +++ b/bundler/lib/bundler/man/bundle-check.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-CHECK" "1" "September 2025" "" +.TH "BUNDLE\-CHECK" "1" "February 2026" "" .SH "NAME" \fBbundle\-check\fR \- Verifies if dependencies are satisfied by installed gems .SH "SYNOPSIS" diff --git a/bundler/lib/bundler/man/bundle-clean.1 b/bundler/lib/bundler/man/bundle-clean.1 index c4d148c5df83..47fb8f92aa2c 100644 --- a/bundler/lib/bundler/man/bundle-clean.1 +++ b/bundler/lib/bundler/man/bundle-clean.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-CLEAN" "1" "September 2025" "" +.TH "BUNDLE\-CLEAN" "1" "February 2026" "" .SH "NAME" \fBbundle\-clean\fR \- Cleans up unused gems in your bundler directory .SH "SYNOPSIS" diff --git a/bundler/lib/bundler/man/bundle-config.1 b/bundler/lib/bundler/man/bundle-config.1 index 05c13e2d0f32..bdab2a9730da 100644 --- a/bundler/lib/bundler/man/bundle-config.1 +++ b/bundler/lib/bundler/man/bundle-config.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-CONFIG" "1" "September 2025" "" +.TH "BUNDLE\-CONFIG" "1" "February 2026" "" .SH "NAME" \fBbundle\-config\fR \- Set bundler configuration options .SH "SYNOPSIS" diff --git a/bundler/lib/bundler/man/bundle-console.1 b/bundler/lib/bundler/man/bundle-console.1 index 5ab15668be7b..3e3db532e434 100644 --- a/bundler/lib/bundler/man/bundle-console.1 +++ b/bundler/lib/bundler/man/bundle-console.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-CONSOLE" "1" "September 2025" "" +.TH "BUNDLE\-CONSOLE" "1" "February 2026" "" .SH "NAME" \fBbundle\-console\fR \- Open an IRB session with the bundle pre\-loaded .SH "SYNOPSIS" diff --git a/bundler/lib/bundler/man/bundle-doctor.1 b/bundler/lib/bundler/man/bundle-doctor.1 index a0329dfc4813..f5b2be400722 100644 --- a/bundler/lib/bundler/man/bundle-doctor.1 +++ b/bundler/lib/bundler/man/bundle-doctor.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-DOCTOR" "1" "September 2025" "" +.TH "BUNDLE\-DOCTOR" "1" "February 2026" "" .SH "NAME" \fBbundle\-doctor\fR \- Checks the bundle for common problems .SH "SYNOPSIS" diff --git a/bundler/lib/bundler/man/bundle-env.1 b/bundler/lib/bundler/man/bundle-env.1 index eee3ca05d0c6..a6d11a3d3c08 100644 --- a/bundler/lib/bundler/man/bundle-env.1 +++ b/bundler/lib/bundler/man/bundle-env.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-ENV" "1" "September 2025" "" +.TH "BUNDLE\-ENV" "1" "February 2026" "" .SH "NAME" \fBbundle\-env\fR \- Print information about the environment Bundler is running under .SH "SYNOPSIS" diff --git a/bundler/lib/bundler/man/bundle-exec.1 b/bundler/lib/bundler/man/bundle-exec.1 index 24c84889b5bf..62c9245b003a 100644 --- a/bundler/lib/bundler/man/bundle-exec.1 +++ b/bundler/lib/bundler/man/bundle-exec.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-EXEC" "1" "September 2025" "" +.TH "BUNDLE\-EXEC" "1" "February 2026" "" .SH "NAME" \fBbundle\-exec\fR \- Execute a command in the context of the bundle .SH "SYNOPSIS" diff --git a/bundler/lib/bundler/man/bundle-fund.1 b/bundler/lib/bundler/man/bundle-fund.1 index fe24a25ca12c..a09a4ed97cfc 100644 --- a/bundler/lib/bundler/man/bundle-fund.1 +++ b/bundler/lib/bundler/man/bundle-fund.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-FUND" "1" "September 2025" "" +.TH "BUNDLE\-FUND" "1" "February 2026" "" .SH "NAME" \fBbundle\-fund\fR \- Lists information about gems seeking funding assistance .SH "SYNOPSIS" diff --git a/bundler/lib/bundler/man/bundle-gem.1 b/bundler/lib/bundler/man/bundle-gem.1 index 85c0f57674a4..33cacbe5323b 100644 --- a/bundler/lib/bundler/man/bundle-gem.1 +++ b/bundler/lib/bundler/man/bundle-gem.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-GEM" "1" "September 2025" "" +.TH "BUNDLE\-GEM" "1" "February 2026" "" .SH "NAME" \fBbundle\-gem\fR \- Generate a project skeleton for creating a rubygem .SH "SYNOPSIS" diff --git a/bundler/lib/bundler/man/bundle-help.1 b/bundler/lib/bundler/man/bundle-help.1 index 05fd5a7c4844..3a97e56a1fcb 100644 --- a/bundler/lib/bundler/man/bundle-help.1 +++ b/bundler/lib/bundler/man/bundle-help.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-HELP" "1" "September 2025" "" +.TH "BUNDLE\-HELP" "1" "February 2026" "" .SH "NAME" \fBbundle\-help\fR \- Displays detailed help for each subcommand .SH "SYNOPSIS" diff --git a/bundler/lib/bundler/man/bundle-info.1 b/bundler/lib/bundler/man/bundle-info.1 index 96c7d876f64a..39ff74a7d975 100644 --- a/bundler/lib/bundler/man/bundle-info.1 +++ b/bundler/lib/bundler/man/bundle-info.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-INFO" "1" "September 2025" "" +.TH "BUNDLE\-INFO" "1" "February 2026" "" .SH "NAME" \fBbundle\-info\fR \- Show information for the given gem in your bundle .SH "SYNOPSIS" diff --git a/bundler/lib/bundler/man/bundle-init.1 b/bundler/lib/bundler/man/bundle-init.1 index 83dad5c050bf..a053ac4c4c35 100644 --- a/bundler/lib/bundler/man/bundle-init.1 +++ b/bundler/lib/bundler/man/bundle-init.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-INIT" "1" "September 2025" "" +.TH "BUNDLE\-INIT" "1" "February 2026" "" .SH "NAME" \fBbundle\-init\fR \- Generates a Gemfile into the current working directory .SH "SYNOPSIS" diff --git a/bundler/lib/bundler/man/bundle-install.1 b/bundler/lib/bundler/man/bundle-install.1 index 68530f3ebb58..21997fb29091 100644 --- a/bundler/lib/bundler/man/bundle-install.1 +++ b/bundler/lib/bundler/man/bundle-install.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-INSTALL" "1" "September 2025" "" +.TH "BUNDLE\-INSTALL" "1" "February 2026" "" .SH "NAME" \fBbundle\-install\fR \- Install the dependencies specified in your Gemfile .SH "SYNOPSIS" diff --git a/bundler/lib/bundler/man/bundle-issue.1 b/bundler/lib/bundler/man/bundle-issue.1 index 394d6c546910..f5033d6e076c 100644 --- a/bundler/lib/bundler/man/bundle-issue.1 +++ b/bundler/lib/bundler/man/bundle-issue.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-ISSUE" "1" "September 2025" "" +.TH "BUNDLE\-ISSUE" "1" "February 2026" "" .SH "NAME" \fBbundle\-issue\fR \- Get help reporting Bundler issues .SH "SYNOPSIS" diff --git a/bundler/lib/bundler/man/bundle-licenses.1 b/bundler/lib/bundler/man/bundle-licenses.1 index 2931e42dd7e8..39dcf0bd38b9 100644 --- a/bundler/lib/bundler/man/bundle-licenses.1 +++ b/bundler/lib/bundler/man/bundle-licenses.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-LICENSES" "1" "September 2025" "" +.TH "BUNDLE\-LICENSES" "1" "February 2026" "" .SH "NAME" \fBbundle\-licenses\fR \- Print the license of all gems in the bundle .SH "SYNOPSIS" diff --git a/bundler/lib/bundler/man/bundle-list.1 b/bundler/lib/bundler/man/bundle-list.1 index d8bcf20585a4..34d25ae12c26 100644 --- a/bundler/lib/bundler/man/bundle-list.1 +++ b/bundler/lib/bundler/man/bundle-list.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-LIST" "1" "September 2025" "" +.TH "BUNDLE\-LIST" "1" "February 2026" "" .SH "NAME" \fBbundle\-list\fR \- List all the gems in the bundle .SH "SYNOPSIS" diff --git a/bundler/lib/bundler/man/bundle-lock.1 b/bundler/lib/bundler/man/bundle-lock.1 index 478d17353507..eb5c8732cc01 100644 --- a/bundler/lib/bundler/man/bundle-lock.1 +++ b/bundler/lib/bundler/man/bundle-lock.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-LOCK" "1" "September 2025" "" +.TH "BUNDLE\-LOCK" "1" "February 2026" "" .SH "NAME" \fBbundle\-lock\fR \- Creates / Updates a lockfile without installing .SH "SYNOPSIS" diff --git a/bundler/lib/bundler/man/bundle-open.1 b/bundler/lib/bundler/man/bundle-open.1 index 2f13b1329fc1..1d9463d12b1b 100644 --- a/bundler/lib/bundler/man/bundle-open.1 +++ b/bundler/lib/bundler/man/bundle-open.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-OPEN" "1" "September 2025" "" +.TH "BUNDLE\-OPEN" "1" "February 2026" "" .SH "NAME" \fBbundle\-open\fR \- Opens the source directory for a gem in your bundle .SH "SYNOPSIS" diff --git a/bundler/lib/bundler/man/bundle-outdated.1 b/bundler/lib/bundler/man/bundle-outdated.1 index 7e10d202bedf..28d361754a3d 100644 --- a/bundler/lib/bundler/man/bundle-outdated.1 +++ b/bundler/lib/bundler/man/bundle-outdated.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-OUTDATED" "1" "September 2025" "" +.TH "BUNDLE\-OUTDATED" "1" "February 2026" "" .SH "NAME" \fBbundle\-outdated\fR \- List installed gems with newer versions available .SH "SYNOPSIS" diff --git a/bundler/lib/bundler/man/bundle-platform.1 b/bundler/lib/bundler/man/bundle-platform.1 index 6a3a08c3a93a..cb53cd192dcf 100644 --- a/bundler/lib/bundler/man/bundle-platform.1 +++ b/bundler/lib/bundler/man/bundle-platform.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-PLATFORM" "1" "September 2025" "" +.TH "BUNDLE\-PLATFORM" "1" "February 2026" "" .SH "NAME" \fBbundle\-platform\fR \- Displays platform compatibility information .SH "SYNOPSIS" diff --git a/bundler/lib/bundler/man/bundle-plugin.1 b/bundler/lib/bundler/man/bundle-plugin.1 index 25da56247598..5c019b305d80 100644 --- a/bundler/lib/bundler/man/bundle-plugin.1 +++ b/bundler/lib/bundler/man/bundle-plugin.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-PLUGIN" "1" "September 2025" "" +.TH "BUNDLE\-PLUGIN" "1" "February 2026" "" .SH "NAME" \fBbundle\-plugin\fR \- Manage Bundler plugins .SH "SYNOPSIS" diff --git a/bundler/lib/bundler/man/bundle-pristine.1 b/bundler/lib/bundler/man/bundle-pristine.1 index a8316b5cca1e..0973d63e4edd 100644 --- a/bundler/lib/bundler/man/bundle-pristine.1 +++ b/bundler/lib/bundler/man/bundle-pristine.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-PRISTINE" "1" "September 2025" "" +.TH "BUNDLE\-PRISTINE" "1" "February 2026" "" .SH "NAME" \fBbundle\-pristine\fR \- Restores installed gems to their pristine condition .SH "SYNOPSIS" diff --git a/bundler/lib/bundler/man/bundle-remove.1 b/bundler/lib/bundler/man/bundle-remove.1 index 4dc7a03b9f5d..3398d6cd330c 100644 --- a/bundler/lib/bundler/man/bundle-remove.1 +++ b/bundler/lib/bundler/man/bundle-remove.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-REMOVE" "1" "September 2025" "" +.TH "BUNDLE\-REMOVE" "1" "February 2026" "" .SH "NAME" \fBbundle\-remove\fR \- Removes gems from the Gemfile .SH "SYNOPSIS" diff --git a/bundler/lib/bundler/man/bundle-show.1 b/bundler/lib/bundler/man/bundle-show.1 index 901460962cfe..05ff3205766f 100644 --- a/bundler/lib/bundler/man/bundle-show.1 +++ b/bundler/lib/bundler/man/bundle-show.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-SHOW" "1" "September 2025" "" +.TH "BUNDLE\-SHOW" "1" "February 2026" "" .SH "NAME" \fBbundle\-show\fR \- Shows all the gems in your bundle, or the path to a gem .SH "SYNOPSIS" diff --git a/bundler/lib/bundler/man/bundle-update.1 b/bundler/lib/bundler/man/bundle-update.1 index 2f9932dc1e5e..db078d74fc84 100644 --- a/bundler/lib/bundler/man/bundle-update.1 +++ b/bundler/lib/bundler/man/bundle-update.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-UPDATE" "1" "September 2025" "" +.TH "BUNDLE\-UPDATE" "1" "February 2026" "" .SH "NAME" \fBbundle\-update\fR \- Update your gems to the latest available versions .SH "SYNOPSIS" diff --git a/bundler/lib/bundler/man/bundle-version.1 b/bundler/lib/bundler/man/bundle-version.1 index 6462ec795832..8f9088451bb0 100644 --- a/bundler/lib/bundler/man/bundle-version.1 +++ b/bundler/lib/bundler/man/bundle-version.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-VERSION" "1" "September 2025" "" +.TH "BUNDLE\-VERSION" "1" "February 2026" "" .SH "NAME" \fBbundle\-version\fR \- Prints Bundler version information .SH "SYNOPSIS" diff --git a/bundler/lib/bundler/man/bundle.1 b/bundler/lib/bundler/man/bundle.1 index 9f7feb4133d7..8613924602a0 100644 --- a/bundler/lib/bundler/man/bundle.1 +++ b/bundler/lib/bundler/man/bundle.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE" "1" "September 2025" "" +.TH "BUNDLE" "1" "February 2026" "" .SH "NAME" \fBbundle\fR \- Ruby Dependency Management .SH "SYNOPSIS" diff --git a/bundler/lib/bundler/man/gemfile.5 b/bundler/lib/bundler/man/gemfile.5 index a8c055a0c1a4..59e41623e84b 100644 --- a/bundler/lib/bundler/man/gemfile.5 +++ b/bundler/lib/bundler/man/gemfile.5 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "GEMFILE" "5" "September 2025" "" +.TH "GEMFILE" "5" "February 2026" "" .SH "NAME" \fBGemfile\fR \- A format for describing gem dependencies for Ruby programs .SH "SYNOPSIS" @@ -293,6 +293,13 @@ gem "rails", git: "https://github\.com/rails/rails\.git", ref: "4aded" .TP \fBsubmodules\fR For reference, a git submodule \fIhttps://git\-scm\.com/book/en/v2/Git\-Tools\-Submodules\fR lets you have another git repository within a subfolder of your repository\. Specify \fBsubmodules: true\fR to cause bundler to expand any submodules included in the git repository +.TP +\fBsparse_checkout\fR +Specify a directory to include when checking out a git repository\. Uses git's sparse\-checkout feature (requires git 2\.25+) to only download the specified directory, reducing disk usage for large repos\. +.IP +gem "foo", git: "https://github\.com/org/monorepo\.git", sparse_checkout: "packages/foo", glob: "packages/foo/*\.gemspec" +.IP +On git versions below 2\.25, the entire repository will be cloned with a warning\. .P If a git repository contains multiple \fB\.gemspecs\fR, each \fB\.gemspec\fR represents a gem located at the same place in the file system as the \fB\.gemspec\fR\. .IP "" 4 diff --git a/bundler/lib/bundler/man/gemfile.5.ronn b/bundler/lib/bundler/man/gemfile.5.ronn index 18d7bb826e44..5d508be19eef 100644 --- a/bundler/lib/bundler/man/gemfile.5.ronn +++ b/bundler/lib/bundler/man/gemfile.5.ronn @@ -352,6 +352,18 @@ Git repositories support a number of additional options. Specify `submodules: true` to cause bundler to expand any submodules included in the git repository + * `sparse_checkout`: + Specify a directory to include when checking out a git repository. + Uses git's sparse-checkout feature (requires git 2.25+) to only + download the specified directory, reducing disk usage for large repos. + + gem "foo", git: "https://github.com/org/monorepo.git", + sparse_checkout: "packages/foo", + glob: "packages/foo/*.gemspec" + + On git versions below 2.25, the entire repository will be cloned + with a warning. + If a git repository contains multiple `.gemspecs`, each `.gemspec` represents a gem located at the same place in the file system as the `.gemspec`. diff --git a/bundler/lib/bundler/source/git.rb b/bundler/lib/bundler/source/git.rb index bb669ebba39d..5ebfe4f58bd2 100644 --- a/bundler/lib/bundler/source/git.rb +++ b/bundler/lib/bundler/source/git.rb @@ -7,7 +7,7 @@ class Source class Git < Path autoload :GitProxy, File.expand_path("git/git_proxy", __dir__) - attr_reader :uri, :ref, :branch, :options, :glob, :submodules + attr_reader :uri, :ref, :branch, :options, :glob, :submodules, :sparse_checkout def initialize(options) @options = options @@ -25,6 +25,7 @@ def initialize(options) @branch = options["branch"] @ref = options["ref"] || options["branch"] || options["tag"] @submodules = options["submodules"] + @sparse_checkout = options["sparse_checkout"] @name = options["name"] @version = options["version"].to_s.strip.gsub("-", ".pre.") @@ -57,12 +58,13 @@ def to_lock %w[ref branch tag submodules].each do |opt| out << " #{opt}: #{options[opt]}\n" if options[opt] end + out << " sparse_checkout: #{@sparse_checkout}\n" if @sparse_checkout out << " glob: #{@glob}\n" unless default_glob? out << " specs:\n" end def to_gemfile - specifiers = %w[ref branch tag submodules glob].map do |opt| + specifiers = %w[ref branch tag submodules glob sparse_checkout].map do |opt| "#{opt}: #{options[opt]}" if options[opt] end @@ -70,14 +72,15 @@ def to_gemfile end def hash - [self.class, uri, ref, branch, name, glob, submodules].hash + [self.class, uri, ref, branch, name, glob, submodules, sparse_checkout].hash end def eql?(other) other.is_a?(Git) && uri == other.uri && ref == other.ref && branch == other.branch && name == other.name && glob == other.glob && - submodules == other.submodules + submodules == other.submodules && + sparse_checkout == other.sparse_checkout end alias_method :==, :eql? @@ -86,7 +89,8 @@ def include?(other) other.is_a?(Git) && uri == other.uri && name == other.name && glob == other.glob && - submodules == other.submodules + submodules == other.submodules && + sparse_checkout == other.sparse_checkout end def to_s @@ -126,17 +130,13 @@ def name # checkout of the git repository. When using local git # repos, this is set to the local repo. def install_path - @install_path ||= begin - git_scope = "#{base_name}-#{shortref_for_path(revision)}" - - Bundler.install_path.join(git_scope) - end + @install_path ||= Bundler.install_path.join("#{base_name}-#{shortref_for_path(revision, sparse_checkout: @sparse_checkout)}") end alias_method :path, :install_path def extension_dir_name - "#{base_name}-#{shortref_for_path(revision)}" + "#{base_name}-#{shortref_for_path(revision, sparse_checkout: @sparse_checkout)}" end def unlock! @@ -246,7 +246,7 @@ def cache_path end def app_cache_dirname - "#{base_name}-#{shortref_for_path(locked_revision || revision)}" + "#{base_name}-#{shortref_for_path(locked_revision || revision, sparse_checkout: @sparse_checkout)}" end def revision @@ -375,8 +375,10 @@ def shortref_for_display(ref) ref[0..6] end - def shortref_for_path(ref) - ref[0..11] + def shortref_for_path(ref, sparse_checkout: nil) + scope = ref[0..11] + scope += "-#{Bundler::Digest.sha1(sparse_checkout)[0..7]}" if sparse_checkout + scope end def glob_for_display @@ -432,7 +434,9 @@ def load_gemspec(file) end def git_scope - "#{base_name}-#{uri_hash}" + scope = "#{base_name}-#{uri_hash}" + scope += "-#{Bundler::Digest.sha1(@sparse_checkout)[0..7]}" if @sparse_checkout + scope end def extension_cache_slug(_) diff --git a/bundler/lib/bundler/source/git/git_proxy.rb b/bundler/lib/bundler/source/git/git_proxy.rb index fe05e9d57b27..4bffb2b44f2c 100644 --- a/bundler/lib/bundler/source/git/git_proxy.rb +++ b/bundler/lib/bundler/source/git/git_proxy.rb @@ -54,7 +54,7 @@ def initialize(options) # All actions required by the Git source is encapsulated in this # object. class GitProxy - attr_accessor :path, :uri, :branch, :tag, :ref, :explicit_ref + attr_accessor :path, :uri, :branch, :tag, :ref, :explicit_ref, :sparse_checkout attr_writer :revision def initialize(path, uri, options = {}, revision = nil, git = nil) @@ -72,6 +72,7 @@ def initialize(path, uri, options = {}, revision = nil, git = nil) @revision = revision @git = git @commit_ref = nil + @sparse_checkout = options["sparse_checkout"] end def revision @@ -106,6 +107,7 @@ def checkout extra_fetch_needed = clone_needs_extra_fetch? unshallow_needed = clone_needs_unshallow? + return unless extra_fetch_needed || unshallow_needed git_remote_fetch(unshallow_needed ? ["--unshallow"] : depth_args) @@ -120,7 +122,13 @@ def copy_to(destination, submodules = false) SharedHelpers.filesystem_access(destination) do |p| FileUtils.rm_rf(p) end - git "clone", "--no-checkout", "--quiet", path.to_s, destination.to_s + if @sparse_checkout && supports_partial_clone? + Bundler.ui.debug "Using partial clone with sparse checkout for #{@sparse_checkout}" + git(*build_sparse_checkout_clone_args(destination)) + else + git "clone", "--no-checkout", "--quiet", path.to_s, destination.to_s + end + File.chmod((File.stat(destination).mode | 0o777) & ~File.umask, destination) rescue Errno::EEXIST => e file_path = e.message[%r{.*?((?:[a-zA-Z]:)?/.*)}, 1] @@ -130,6 +138,8 @@ def copy_to(destination, submodules = false) end end + setup_sparse_checkout(destination) + ref = @commit_ref || (locked_to_full_sha? && @revision) if ref git "config", "uploadpack.allowAnySHA1InWant", "true", dir: path.to_s if @commit_ref.nil? && needs_allow_any_sha1_in_want? @@ -156,6 +166,7 @@ def installed_to?(destination) private def git_remote_fetch(args) + args = [*partial_clone_filter_args, *args] command = ["fetch", "--force", "--quiet", "--no-tags", *args, "--", configured_uri, refspec].compact command_with_no_credentials = check_allowed(command) @@ -412,15 +423,16 @@ def capture3_args_for(cmd, dir) end def extra_clone_args - args = depth_args - return [] if args.empty? + filter_args = partial_clone_filter_args + args = depth_args.dup + return filter_args if args.empty? args += ["--single-branch"] args.unshift("--no-tags") if supports_cloning_with_no_tags? + args.unshift(*filter_args) # If there's a locked revision, no need to clone any specific branch - # or tag, since we will end up checking out that locked revision - # anyways. + # or tag, since we will end up checking out that locked revision anyways. return args if @revision args += ["--branch", branch_option] if branch_option @@ -458,6 +470,41 @@ def supports_fetching_unreachable_refs? def supports_cloning_with_no_tags? @supports_cloning_with_no_tags ||= Gem::Version.new(version) >= Gem::Version.new("2.14.0-rc0") end + + def supports_sparse_checkout? + @supports_sparse_checkout ||= Gem::Version.new(version) >= Gem::Version.new("2.25.0") + end + + def supports_partial_clone? + @supports_partial_clone ||= Gem::Version.new(version) >= Gem::Version.new("2.17.0") + end + + def partial_clone_filter_args + return [] unless @sparse_checkout && supports_partial_clone? + ["--filter=blob:none"] + end + + def build_sparse_checkout_clone_args(destination) + args = ["clone", *partial_clone_filter_args, "--no-checkout", "--quiet"] + args.concat(depth_args) unless depth_args.empty? + args << "--single-branch" + args << "--no-tags" if supports_cloning_with_no_tags? + args << "--branch" << branch_option if branch_option + args.concat([configured_uri, destination.to_s]) + args + end + + def setup_sparse_checkout(destination) + return unless @sparse_checkout + + unless supports_sparse_checkout? + Bundler.ui.warn "Git #{version} doesn't support sparse-checkout (requires 2.25+). Cloning full repository." + return + end + + Bundler.ui.debug "Setting sparse checkout to only include #{@sparse_checkout}" + git "sparse-checkout", "set", "--cone", @sparse_checkout, dir: destination + end end end end diff --git a/bundler/spec/bundler/source/git/git_proxy_spec.rb b/bundler/spec/bundler/source/git/git_proxy_spec.rb index b2b7ab5c5400..a11f0ee34100 100644 --- a/bundler/spec/bundler/source/git/git_proxy_spec.rb +++ b/bundler/spec/bundler/source/git/git_proxy_spec.rb @@ -334,4 +334,68 @@ end end end + + describe "#supports_sparse_checkout?" do + it "returns true for git 2.25+" do + allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.25.0") + expect(git_proxy.send(:supports_sparse_checkout?)).to be true + end + + it "returns true for git 2.30+" do + allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.30.0") + expect(git_proxy.send(:supports_sparse_checkout?)).to be true + end + + it "returns false for git < 2.25" do + allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.24.0") + expect(git_proxy.send(:supports_sparse_checkout?)).to be false + end + + it "returns false for git 2.20" do + allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.20.0") + expect(git_proxy.send(:supports_sparse_checkout?)).to be false + end + end + + describe "#setup_sparse_checkout" do + let(:destination) { Pathname("destination") } + + context "with sparse_checkout option" do + let(:options) { { "sparse_checkout" => "packages/foo" } } + + context "with git 2.25+" do + it "runs sparse-checkout set with cone mode" do + allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.30.0") + expect(git_proxy).to receive(:git).with("sparse-checkout", "set", "--cone", "packages/foo", dir: destination) + git_proxy.send(:setup_sparse_checkout, destination) + end + end + + context "with git < 2.25" do + it "skips sparse checkout and warns" do + allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.20.0") + expect(Bundler.ui).to receive(:warn).with(/doesn't support sparse-checkout/) + expect(git_proxy).not_to receive(:git).with("sparse-checkout", any_args) + git_proxy.send(:setup_sparse_checkout, destination) + end + end + end + + context "without sparse_checkout option" do + let(:options) { {} } + + it "does nothing" do + expect(git_proxy).not_to receive(:git) + git_proxy.send(:setup_sparse_checkout, destination) + end + end + end + + context "with sparse_checkout option" do + let(:options) { { "sparse_checkout" => "packages/foo" } } + + it "stores sparse_checkout from options" do + expect(git_proxy.sparse_checkout).to eq("packages/foo") + end + end end diff --git a/bundler/spec/install/gemfile/git_sparse_checkout_spec.rb b/bundler/spec/install/gemfile/git_sparse_checkout_spec.rb new file mode 100644 index 000000000000..6ef363b2e70c --- /dev/null +++ b/bundler/spec/install/gemfile/git_sparse_checkout_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with git sources and sparse_checkout" do + describe "with sparse_checkout option" do + it "only checks out the specified directory" do + build_lib "foo", "1.0", path: lib_path("monorepo/packages/foo") + build_lib "bar", "2.0", path: lib_path("monorepo/packages/bar") + build_git "monorepo", path: lib_path("monorepo"), gemspec: false + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", git: "#{lib_path("monorepo")}", + sparse_checkout: "packages/foo", + glob: "packages/foo/*.gemspec" + G + + expect(the_bundle).to include_gems "foo 1.0" + + # Verify only sparse_checkout dir exists (when git 2.25+) + gem_path = Dir[default_bundle_path("bundler/gems/monorepo-*")].first + git_version = Gem::Version.new(`git --version`.match(/\d+\.\d+\.\d+/)[0]) + if git_version >= Gem::Version.new("2.25.0") + expect(Dir.exist?("#{gem_path}/packages/foo")).to be true + expect(Dir.exist?("#{gem_path}/packages/bar")).to be false + end + end + end + + describe "lockfile round-trip" do + it "preserves sparse_checkout in lockfile" do + build_lib "foo", "1.0", path: lib_path("monorepo/packages/foo") + build_git "monorepo", path: lib_path("monorepo"), gemspec: false + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", git: "#{lib_path("monorepo")}", + sparse_checkout: "packages/foo", + glob: "packages/foo/*.gemspec" + G + + lockfile_content = File.read(bundled_app_lock) + expect(lockfile_content).to include("sparse_checkout: packages/foo") + + # Re-run bundle install with existing lockfile + bundle :install + expect(the_bundle).to include_gems "foo 1.0" + end + end + + describe "multiple gems from same repo with different sparse_checkouts" do + it "creates separate sources for each sparse_checkout" do + build_lib "foo", "1.0", path: lib_path("monorepo/packages/foo") + build_lib "bar", "2.0", path: lib_path("monorepo/packages/bar") + build_git "monorepo", path: lib_path("monorepo"), gemspec: false + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", git: "#{lib_path("monorepo")}", + sparse_checkout: "packages/foo", + glob: "packages/foo/*.gemspec" + gem "bar", git: "#{lib_path("monorepo")}", + sparse_checkout: "packages/bar", + glob: "packages/bar/*.gemspec" + G + + expect(the_bundle).to include_gems "foo 1.0", "bar 2.0" + + # Different sparse_checkouts = different cache directories + cache_dirs = Dir[default_bundle_path("cache/bundler/git/monorepo-*")] + expect(cache_dirs.size).to eq(2) + end + end +end