Add Gemfile override DSL for version constraints (Phase 1)#9517
Merged
Conversation
Introduce a value object that holds a target/field/operation triple for the upcoming Gemfile `override` DSL. apply_to dispatches on the operation: a version spec string replaces the requirement absolutely, :ignore_upper strips < and <= while folding ~> into >=, and nil collapses to Gem::Requirement.default. The :ignore_upper logic is taken from Shopify/bundler-ignore-dependency. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reserve a slot on Definition for the upcoming Gemfile `override` DSL. This commit only stores the data; the DSL entry point and the resolver hookup come in later commits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduce Gemfile-level `override target, field: operation, ...` that collects Bundler::Override instances and forwards them to Definition via to_definition. Validation and resolver hookup come in later commits; this commit only wires the entry point. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switch remove_upper_bounds from a lower-bound allow-list to an upper-bound reject-list so that operators other than <, <=, and ~> are kept verbatim. The previous logic, inherited from Shopify/bundler-ignore-dependency, dropped != exclusions and could silently re-allow versions the user had explicitly pinned out (e.g. >= 1.0, != 1.5.0, < 2.0 collapsed to >= 1.0). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Version requirements are per-gem and have no meaning relative to the `:all` target. Raise ArgumentError at the DSL entry point and store nothing when this combination is given, even if other valid fields are mixed in the same call. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reject anything other than :all or a String for the target, fields outside `:version`, and operations that are not a String, nil, or one of the supported Symbol values (currently only :ignore_upper). Validation runs before any Override is recorded so that a multi-field call with an invalid entry leaves the DSL state untouched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two override statements that target the same gem and the same field make it ambiguous which operation should win. Raise ArgumentError when the new (target, field) pair already exists in the recorded overrides, leaving the previously recorded entry untouched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pass the Definition's overrides through to Resolver::Base and rewrite each dependency's requirement at the entry of to_dependency_hash so both direct and transitive deps are reshaped before PubGrub sees them. The hook is the same point Shopify/bundler-ignore-dependency uses, since prepare_dependencies and the @cached_dependencies closure both funnel through to_dependency_hash. Override#apply_to handles all three operation kinds, so :ignore_upper and nil also start working from this commit; integration coverage for those paths follows in later commits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Resolver-only hook reshapes deps just before PubGrub sees them, but it does not influence the Bundler::Dependency objects fed into Definition#expanded_dependencies. As a result the Resolver::Package built from each direct dep keeps the original requirement, so its prerelease policy and (in later commits) lockfile change detection ignore the override entirely. Apply the override to direct deps at expanded_dependencies as well so that Package metadata and convergence see the effective requirement; the Resolver hook remains responsible for transitive deps fetched from gemspecs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
converge_dependencies compared the original Gemfile dependency against the locked spec, so adding `override "foo", version: "= X"` to a previously installed bundle did not register as a change. additional_base_requirements_to_prevent_downgrades then kept forwarding the locked version as a >= base requirement, which intersected the override and made it a no-op. Pass overrides into Definition.new positionally so they are available before the constructor calls converge_dependencies, and compare each direct dep's effective requirement (after override) to the locked spec. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the Gemfile dependency does not request a prerelease, Resolver::Package's prerelease policy normally filters them out. Because Definition#expanded_dependencies now feeds the effective (override-applied) dep into Resolver::Base, an override that pins an exact prerelease propagates into the package's prerelease decision and the prerelease becomes selectable. Lock that contract in with an integration test on a has_prerelease 1.0 / 1.1.pre fixture. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Verify end-to-end that override(..., version: :ignore_upper) drops < / <= bounds and folds ~> into >= so a Gemfile-pinned 0.9.1 ceiling no longer prevents myrack 1.0.0 from being chosen. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Verify end-to-end that override(..., version: nil) collapses the requirement to Gem::Requirement.default for both direct deps (a Gemfile pin to 0.9.1 is removed) and transitive deps (a myrack_middleware-imposed = 0.9.1 floor is removed). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add an OVERRIDE section between GEMSPEC and SOURCE PRIORITY that covers the syntax, the three operations (version spec string, :ignore_upper, nil), and the lockfile-vs-resolution boundary. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
converge_dependencies only iterates @Dependencies (Gemfile-declared direct deps), so an override that targets a gem present only as a transitive dependency never registered as a change. With an existing lockfile, @dependency_changes stayed false, the resolver was skipped, and the override was a silent no-op. After the direct-dep loop, inspect @OVERRIDES for any String target that is locked but not a direct dep and force it onto @gems_to_unlock / @changed_dependencies so resolution runs and the Resolver-side override hook applies. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was the end-user or developer problem that led to this PR?
#9494, Supersedes #9454.
What is your fix for the problem, implemented in this PR?
Adds a Gemfile-level
overridedirective that rewrites a gem's version requirement before resolution. Phase 1 covers per-gemversion:only.Applies to direct and transitive deps and against an existing lockfile.
Gemfile.lockkeeps recording resolved versions, not the rewrite.Phase 2 will add
:alland metadata fields:Make sure the following tasks are checked