From 3fd6ae7729568f7668f90e463b03383593ce1181 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 18 Mar 2012 17:34:00 +0100 Subject: [PATCH 1/4] Refactor a couple methods in Bundler.definition --- lib/bundler/definition.rb | 44 +++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb index 874d2e1011b..5c49175d91b 100644 --- a/lib/bundler/definition.rb +++ b/lib/bundler/definition.rb @@ -68,23 +68,7 @@ def initialize(lockfile, dependencies, sources, unlock) @new_platform = !@platforms.include?(current_platform) @platforms |= [current_platform] - @path_changes = @sources.any? do |source| - next unless source.instance_of?(Source::Path) - - locked = @locked_sources.find do |ls| - ls.class == source.class && ls.path == source.path - end - - if locked - unlocking = locked.specs.any? do |spec| - @locked_specs.any? do |locked_spec| - locked_spec.source != locked - end - end - end - - !locked || unlocking || source.specs != locked.specs - end + @path_changes = converge_paths eager_unlock = expand_dependencies(@unlock[:gems]) @unlock[:gems] = @locked_specs.for(eager_unlock).map { |s| s.name } @@ -174,7 +158,7 @@ def specs_for(groups) def resolve @resolve ||= begin - if Bundler.settings[:frozen] || (!@unlocking && !@source_changes && !@dependency_changes && !@new_platform && !@path_changes) + if Bundler.settings[:frozen] || (!@unlocking && nothing_changed?) @locked_specs else last_resolve = converge_locked_specs @@ -352,6 +336,10 @@ def ensure_equivalent_gemfile_and_lockfile(explicit_flag = false) private + def nothing_changed? + !@source_changes && !@dependency_changes && !@new_platform && !@path_changes + end + def pretty_dep(dep, source = false) msg = "#{dep.name}" msg << " (#{dep.requirement})" unless dep.requirement == Gem::Requirement.default @@ -359,6 +347,26 @@ def pretty_dep(dep, source = false) msg end + def converge_paths + @sources.any? do |source| + next unless source.instance_of?(Source::Path) + + locked = @locked_sources.find do |ls| + ls.class == source.class && ls.path == source.path + end + + if locked + unlocking = locked.specs.any? do |spec| + @locked_specs.any? do |locked_spec| + locked_spec.source != locked + end + end + end + + !locked || unlocking || source.specs != locked.specs + end + end + def converge_sources changes = false From f043725b106fd46d6c68c6198d073d004cf168a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 18 Mar 2012 18:27:25 +0100 Subject: [PATCH 2/4] Encapsulate git source interaction in a git proxy. --- lib/bundler/source.rb | 212 ++++++++++++++++++++++++------------------ 1 file changed, 120 insertions(+), 92 deletions(-) diff --git a/lib/bundler/source.rb b/lib/bundler/source.rb index 5e058752e08..d53bf388bf4 100644 --- a/lib/bundler/source.rb +++ b/lib/bundler/source.rb @@ -479,6 +479,111 @@ def run_hooks(type, installer) end class Git < Path + # The GitProxy is responsible to iteract with git repositories. + # All actions required by the Git source is encapsualted in this + # object. + class GitProxy + attr_accessor :path, :uri, :ref, :revision + + def initialize(path, uri, ref, revision=nil, &allow) + @path = path + @uri = uri + @ref = ref + @revision = revision + @allow = allow || Proc.new { true } + end + + def revision + @revision ||= if allow? + in_path { git("rev-parse #{ref}").strip } + else + raise GitError, "The git source #{uri} is not yet checked out. Please run `bundle install` before trying to start your application" + end + end + + def checkout + if path.exist? + return if has_revision_cached? + Bundler.ui.info "Updating #{uri}" + in_path do + git %|fetch --force --quiet --tags #{uri_escaped} "refs/heads/*:refs/heads/*"| + end + else + Bundler.ui.info "Fetching #{uri}" + FileUtils.mkdir_p(path.dirname) + git %|clone #{uri_escaped} "#{path}" --bare --no-hardlinks| + end + end + + def copy_to(destination, submodules=false) + unless File.exist?(destination.join(".git")) + FileUtils.mkdir_p(destination.dirname) + FileUtils.rm_rf(destination) + git %|clone --no-checkout "#{path}" "#{destination}"| + File.chmod((0777 & ~File.umask), destination) + end + + Dir.chdir(destination) do + git %|fetch --force --quiet --tags "#{path}"| + git "reset --hard #{@revision}" + + if submodules + git "submodule init" + git "submodule update" + end + end + end + + private + + def git(command) + if allow? + out = %x{git #{command}} + + if $?.exitstatus != 0 + msg = "Git error: command `git #{command}` in directory #{Dir.pwd} has failed." + msg << "\nIf this error persists you could try removing the cache directory '#{path}'" if path.exist? + raise GitError, msg + end + out + else + raise GitError, "Bundler is trying to run a `git #{command}` at runtime. You probably need to run `bundle install`. However, " \ + "this error message could probably be more useful. Please submit a ticket at http://github.com/carlhuda/bundler/issues " \ + "with steps to reproduce as well as the following\n\nCALLER: #{caller.join("\n")}" + end + end + + def has_revision_cached? + return unless @revision + in_path { git("cat-file -e #{@revision}") } + true + rescue GitError + false + end + + # Escape the URI for git commands + def uri_escaped + if Bundler::WINDOWS + # Windows quoting requires double quotes only, with double quotes + # inside the string escaped by being doubled. + '"' + uri.gsub('"') {|s| '""'} + '"' + else + # Bash requires single quoted strings, with the single quotes escaped + # by ending the string, escaping the quote, and restarting the string. + "'" + uri.gsub("'") {|s| "'\\''"} + "'" + end + end + + def in_path(&blk) + checkout unless path.exist? + Dir.chdir(path, &blk) + end + + def allow? + @allow.call + end + end + attr_reader :uri, :ref, :options, :submodules def initialize(options) @@ -489,7 +594,6 @@ def initialize(options) @uri = options["uri"] @ref = options["ref"] || options["branch"] || options["tag"] || 'master' - @revision = options["revision"] @submodules = options["submodules"] @update = false @installed = nil @@ -530,7 +634,7 @@ def name File.basename(@uri, '.git') end - def path + def install_path @install_path ||= begin git_scope = "#{base_name}-#{shortref_for_path(revision)}" @@ -542,16 +646,17 @@ def path end end + alias :path :install_path + def unlock! - @revision = nil + git_proxy.revision = nil end # TODO: actually cache git specs def specs(*) - if allow_git_ops? && !@update - # Start by making sure the git cache is up to date - cache - checkout + if requires_checkout? && !@update + git_proxy.checkout + git_proxy.copy_to(install_path, submodules) @update = true end local_specs @@ -559,10 +664,9 @@ def specs(*) def install(spec) Bundler.ui.info "Using #{spec.name} (#{spec.version}) from #{to_s} " - - unless @installed + if requires_checkout? && !@installed Bundler.ui.debug " * Checking out revision: #{ref}" - checkout if allow_git_ops? + git_proxy.copy_to(install_path, submodules) @installed = true end generate_bin(spec) @@ -585,23 +689,11 @@ def cache_path end end end - private - def git(command) - if allow_git_ops? - out = %x{git #{command}} + private - if $?.exitstatus != 0 - msg = "Git error: command `git #{command}` in directory #{Dir.pwd} has failed." - msg << "\nIf this error persists you could try removing the cache directory '#{cache_path}'" if cached? - raise GitError, msg - end - out - else - raise GitError, "Bundler is trying to run a `git #{command}` at runtime. You probably need to run `bundle install`. However, " \ - "this error message could probably be more useful. Please submit a ticket at http://github.com/carlhuda/bundler/issues " \ - "with steps to reproduce as well as the following\n\nCALLER: #{caller.join("\n")}" - end + def requires_checkout? + allow_git_ops? end def base_name @@ -628,80 +720,16 @@ def uri_hash Digest::SHA1.hexdigest(input) end - # Escape the URI for git commands - def uri_escaped - if Bundler::WINDOWS - # Windows quoting requires double quotes only, with double quotes - # inside the string escaped by being doubled. - '"' + uri.gsub('"') {|s| '""'} + '"' - else - # Bash requires single quoted strings, with the single quotes escaped - # by ending the string, escaping the quote, and restarting the string. - "'" + uri.gsub("'") {|s| "'\\''"} + "'" - end - end - - def cache - if cached? - return if has_revision_cached? - Bundler.ui.info "Updating #{uri}" - in_cache do - git %|fetch --force --quiet --tags #{uri_escaped} "refs/heads/*:refs/heads/*"| - end - else - Bundler.ui.info "Fetching #{uri}" - FileUtils.mkdir_p(cache_path.dirname) - git %|clone #{uri_escaped} "#{cache_path}" --bare --no-hardlinks| - end - end - - def checkout - unless File.exist?(path.join(".git")) - FileUtils.mkdir_p(path.dirname) - FileUtils.rm_rf(path) - git %|clone --no-checkout "#{cache_path}" "#{path}"| - File.chmod((0777 & ~File.umask), path) - end - Dir.chdir(path) do - git %|fetch --force --quiet --tags "#{cache_path}"| - git "reset --hard #{revision}" - - if @submodules - git "submodule init" - git "submodule update" - end - end - end - - def has_revision_cached? - return unless @revision - in_cache { git %|cat-file -e #{@revision}| } - true - rescue GitError - false - end - def allow_git_ops? @allow_remote || @allow_cached end def revision - @revision ||= begin - if allow_git_ops? - in_cache { git("rev-parse #{ref}").strip } - else - raise GitError, "The git source #{uri} is not yet checked out. Please run `bundle install` before trying to start your application" - end - end - end - - def cached? - cache_path.exist? + git_proxy.revision end - def in_cache(&blk) - cache unless cached? - Dir.chdir(cache_path, &blk) + def git_proxy + @git_proxy ||= GitProxy.new(cache_path, uri, ref, options["revision"]){ allow_git_ops? } end end From b435db0f07d680c0fd3d47576394d50a5c7b96d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 19 Mar 2012 09:39:15 +0100 Subject: [PATCH 3/4] Initial implementation of local git repos. The proposal of this patch is to implement a functionality that allow developers to work against a git repository locally. This can be achieved by setting up a local override: bundle config rack.local ~/path/to/local/rack Now, instead of checking out a git repository, the local override will be used. This implies a few things: Similar to path, every time the local git repository change, changes will be automatically picked up by Bundler; This means a commit in the local git repo will update the revision in the Gemfile.lock to the local git repo revision. This requires the same attention as git submodules. Before pushing to remote, you need to ensure the local override was pushed, otherwise you may point to a commit that only exists in your local machine. Also, we are doing many checks to ensure a developer won't work with invalid references. Particularly, we force a developer to specify a branch in the Gemfile in order to use local overrides and, if your local repository is on another branch, we will abort. This allows a developer to change between topic or release branches without accidentally changing the reference in the Gemfile.lock. We also ensure that the current revision in the Gemfile.lock exists in the current local override. By doing this, we force the local override to fetch the latest changes in the remotes. There are a few things missing before this change can be merged in: 1) We could improve `bundle check` to validate the conditions above. With this in mind, a developer could add a pre-commit hook that invokes `bundle check` to ensure he isn't pushing an old or invalid reference. However, it will be up to the developer to ensure his local overrides are not dirty or that they were pushed to remote. 2) Currently, every time there is a local override, we are automatically by passing locked specs, regardless if there was a change or not. We need to improve this scenario in order to improve performance. 3) `bundle config foo bar` sets the configuration value for the current user (~). We need to be able to set project configuration and delete them as well. --- lib/bundler/definition.rb | 11 ++- lib/bundler/settings.rb | 10 +++ lib/bundler/source.rb | 107 ++++++++++++++++++++++++--- spec/install/git_spec.rb | 150 +++++++++++++++++++++++++++++++++++++- spec/support/helpers.rb | 2 +- 5 files changed, 265 insertions(+), 15 deletions(-) diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb index 5c49175d91b..892ff5ead28 100644 --- a/lib/bundler/definition.rb +++ b/lib/bundler/definition.rb @@ -75,6 +75,15 @@ def initialize(lockfile, dependencies, sources, unlock) @source_changes = converge_sources @dependency_changes = converge_dependencies + @local_changes = Bundler.settings.local_overrides.map do |k,v| + spec = @dependencies.find { |s| s.name == k } + source = spec && spec.source + if source && source.respond_to?(:local_override!) + source.local_override!(v) + true + end + end.any? + fixup_dependency_types! end @@ -337,7 +346,7 @@ def ensure_equivalent_gemfile_and_lockfile(explicit_flag = false) private def nothing_changed? - !@source_changes && !@dependency_changes && !@new_platform && !@path_changes + !@source_changes && !@dependency_changes && !@new_platform && !@path_changes && !@local_changes end def pretty_dep(dep, source = false) diff --git a/lib/bundler/settings.rb b/lib/bundler/settings.rb index 6392a648c9f..1f5faf9ab17 100644 --- a/lib/bundler/settings.rb +++ b/lib/bundler/settings.rb @@ -32,6 +32,16 @@ def all end end + def local_overrides + repos = {} + all.each do |k| + if k =~ /\.local$/ + repos[$`] = self[k] + end + end + repos + end + def locations(key) locations = {} diff --git a/lib/bundler/source.rb b/lib/bundler/source.rb index d53bf388bf4..23676dec4ea 100644 --- a/lib/bundler/source.rb +++ b/lib/bundler/source.rb @@ -494,10 +494,19 @@ def initialize(path, uri, ref, revision=nil, &allow) end def revision - @revision ||= if allow? - in_path { git("rev-parse #{ref}").strip } - else - raise GitError, "The git source #{uri} is not yet checked out. Please run `bundle install` before trying to start your application" + @revision ||= allowed_in_path { git("rev-parse #{ref}").strip } + end + + def branch + @branch ||= allowed_in_path do + git("branch") =~ /^\* (.*)$/ && $1.strip + end + end + + def contains?(commit) + allowed_in_path do + result = git_null("branch --contains #{commit}") + $? == 0 && result =~ /^\* (.*)$/ end end @@ -536,11 +545,24 @@ def copy_to(destination, submodules=false) private - def git(command) + # TODO: Do not rely on /dev/null. + # Given that open3 is not cross platform until Ruby 1.9.3, + # the best solution is to pipe to /dev/null if it exists. + # If it doesn't, everything will work fine, but the user + # will get the $stderr messages as well. + def git_null(command) + if !Bundler::WINDOWS && File.exist?("/dev/null") + git("#{command} 2>/dev/null", false) + else + git(command, false) + end + end + + def git(command, check_errors=true) if allow? out = %x{git #{command}} - if $?.exitstatus != 0 + if check_errors && $?.exitstatus != 0 msg = "Git error: command `git #{command}` in directory #{Dir.pwd} has failed." msg << "\nIf this error persists you could try removing the cache directory '#{path}'" if path.exist? raise GitError, msg @@ -574,13 +596,21 @@ def uri_escaped end end + def allow? + @allow.call + end + def in_path(&blk) checkout unless path.exist? Dir.chdir(path, &blk) end - def allow? - @allow.call + def allowed_in_path + if allow? + in_path { yield } + else + raise GitError, "The git source #{uri} is not yet checked out. Please run `bundle install` before trying to start your application" + end end end @@ -597,6 +627,7 @@ def initialize(options) @submodules = options["submodules"] @update = false @installed = nil + @local = false end def self.from_lock(options) @@ -626,8 +657,14 @@ def eql?(o) alias == eql? def to_s - sref = options["ref"] ? shortref_for_display(options["ref"]) : ref - "#{uri} (at #{sref})" + at = if local? + path + elsif options["ref"] + shortref_for_display(options["ref"]) + else + ref + end + "#{uri} (at #{at})" end def name @@ -652,6 +689,40 @@ def unlock! git_proxy.revision = nil end + def local_override!(path) + path = Pathname.new(path) + path = path.expand_path(Bundler.root) unless path.relative? + + unless options["branch"] + raise GitError, "Cannot use local override for #{name} at #{path} because " \ + ":branch is not specified in Gemfile. Specify a branch or check " \ + "`bundle config --delete` to remove the local override" + end + + unless path.exist? + raise GitError, "Cannot use local override for #{name} because #{path} " \ + "does not exist. Check `bundle config --delete` to remove the local override" + end + + @local = true + @local_specs = nil + @git_proxy = GitProxy.new(path, uri, ref) + @cache_path = @install_path = path + + if git_proxy.branch != options["branch"] + raise GitError, "Local override for #{name} at #{path} is using branch " \ + "#{git_proxy.branch} but Gemfile specifies #{options["branch"]}" + end + + rev = cached_revision + + if rev && rev != git_proxy.revision && !git_proxy.contains?(rev) + raise GitError, "The Gemfile lock is pointing to revision #{shortref_for_display(rev)} " \ + "but the current branch in your local override for #{name} does not contain such commit. " \ + "Please make sure your branch is up to date." + end + end + # TODO: actually cache git specs def specs(*) if requires_checkout? && !@update @@ -692,8 +763,12 @@ def cache_path private + def local? + @local + end + def requires_checkout? - allow_git_ops? + allow_git_ops? && !local? end def base_name @@ -724,12 +799,20 @@ def allow_git_ops? @allow_remote || @allow_cached end + def cached_revision + options["revision"] + end + def revision git_proxy.revision end + def cached? + cache_path.exist? + end + def git_proxy - @git_proxy ||= GitProxy.new(cache_path, uri, ref, options["revision"]){ allow_git_ops? } + @git_proxy ||= GitProxy.new(cache_path, uri, ref, cached_revision){ allow_git_ops? } end end diff --git a/spec/install/git_spec.rb b/spec/install/git_spec.rb index 41a23b42f23..1ac64f76b71 100644 --- a/spec/install/git_spec.rb +++ b/spec/install/git_spec.rb @@ -156,6 +156,155 @@ end end + describe "when specifying local" do + it "uses the local repository instead of checking a new one out" do + # We don't generate it because we actually don't need it + # build_git "rack", "0.8" + + build_git "rack", "0.8", :path => lib_path('local-rack') do |s| + s.write "lib/rack.rb", "puts :LOCAL" + end + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path('rack-0.8')}", :branch => "master" + G + + bundle %|config rack.local #{lib_path('local-rack')}| + bundle :install + out.should =~ /at #{lib_path('local-rack')}/ + + run "require 'rack'" + out.should == "LOCAL" + end + + it "chooses the local repository on runtime" do + build_git "rack", "0.8" + + FileUtils.cp_r("#{lib_path('rack-0.8')}/.", lib_path('local-rack')) + + update_git "rack", "0.8", :path => lib_path('local-rack') do |s| + s.write "lib/rack.rb", "puts :LOCAL" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path('rack-0.8')}", :branch => "master" + G + + bundle %|config rack.local #{lib_path('local-rack')}| + run "require 'rack'" + out.should == "LOCAL" + end + + it "updates specs on runtime" do + system_gems "nokogiri-1.4.2" + + build_git "rack", "0.8" + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path('rack-0.8')}", :branch => "master" + G + + lockfile0 = File.read(bundled_app("Gemfile.lock")) + + FileUtils.cp_r("#{lib_path('rack-0.8')}/.", lib_path('local-rack')) + update_git "rack", "0.8", :path => lib_path('local-rack') do |s| + s.add_dependency "nokogiri", "1.4.2" + end + + bundle %|config rack.local #{lib_path('local-rack')}| + run "require 'rack'" + + lockfile1 = File.read(bundled_app("Gemfile.lock")) + lockfile1.should_not == lockfile0 + end + + it "updates ref on install" do + build_git "rack", "0.8" + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path('rack-0.8')}", :branch => "master" + G + + lockfile0 = File.read(bundled_app("Gemfile.lock")) + + FileUtils.cp_r("#{lib_path('rack-0.8')}/.", lib_path('local-rack')) + update_git "rack", "0.8", :path => lib_path('local-rack') + + bundle %|config rack.local #{lib_path('local-rack')}| + bundle :install + + lockfile1 = File.read(bundled_app("Gemfile.lock")) + lockfile1.should_not == lockfile0 + end + + it "explodes if given path does not exist" do + build_git "rack", "0.8" + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path('rack-0.8')}", :branch => "master" + G + + bundle %|config rack.local #{lib_path('local-rack')}| + bundle :install + out.should =~ /Cannot use local override for rack-0.8 because #{Regexp.escape(lib_path('local-rack').to_s)} does not exist/ + end + + it "explodes if branch is not given" do + build_git "rack", "0.8" + FileUtils.cp_r("#{lib_path('rack-0.8')}/.", lib_path('local-rack')) + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path('rack-0.8')}" + G + + bundle %|config rack.local #{lib_path('local-rack')}| + bundle :install + out.should =~ /because :branch is not specified in Gemfile/ + end + + it "explodes on different branches" do + build_git "rack", "0.8" + + FileUtils.cp_r("#{lib_path('rack-0.8')}/.", lib_path('local-rack')) + + update_git "rack", "0.8", :path => lib_path('local-rack'), :branch => "another" do |s| + s.write "lib/rack.rb", "puts :LOCAL" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path('rack-0.8')}", :branch => "master" + G + + bundle %|config rack.local #{lib_path('local-rack')}| + bundle :install + out.should =~ /is using branch another but Gemfile specifies master/ + end + + it "explodes on invalid revision" do + build_git "rack", "0.8" + + build_git "rack", "0.8", :path => lib_path('local-rack') do |s| + s.write "lib/rack.rb", "puts :LOCAL" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path('rack-0.8')}", :branch => "master" + G + + bundle %|config rack.local #{lib_path('local-rack')}| + bundle :install + out.should =~ /The Gemfile lock is pointing to revision \w+/ + end + end + describe "specified inline" do # TODO: Figure out how to write this test so that it is not flaky depending # on the current network situation. @@ -573,7 +722,6 @@ install_gemfile <<-G source "file://#{gem_repo1}" - gem "valim", "= 1.0", :git => "#{lib_path('valim')}" G diff --git a/spec/support/helpers.rb b/spec/support/helpers.rb index c7f5c5bcdcc..c5500cf1571 100644 --- a/spec/support/helpers.rb +++ b/spec/support/helpers.rb @@ -56,7 +56,7 @@ def lib def bundle(cmd, options = {}) expect_err = options.delete(:expect_err) exitstatus = options.delete(:exitstatus) - options["no-color"] = true unless options.key?("no-color") || cmd.to_s[0..3] == "exec" + options["no-color"] = true unless options.key?("no-color") || %w(exec conf).include?(cmd.to_s[0..3]) bundle_bin = File.expand_path('../../../bin/bundle', __FILE__) From 09f1e0d93a41cb45644a4e96e46f57da7e14877a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 19 Mar 2012 10:36:13 +0100 Subject: [PATCH 4/4] Do not resolve if the local override did not change. --- lib/bundler/definition.rb | 60 +++++++++++++++++++++++++-------------- lib/bundler/source.rb | 8 ++++-- 2 files changed, 44 insertions(+), 24 deletions(-) diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb index 892ff5ead28..c5193f318df 100644 --- a/lib/bundler/definition.rb +++ b/lib/bundler/definition.rb @@ -74,15 +74,7 @@ def initialize(lockfile, dependencies, sources, unlock) @source_changes = converge_sources @dependency_changes = converge_dependencies - - @local_changes = Bundler.settings.local_overrides.map do |k,v| - spec = @dependencies.find { |s| s.name == k } - source = spec && spec.source - if source && source.respond_to?(:local_override!) - source.local_override!(v) - true - end - end.any? + @local_changes = converge_locals fixup_dependency_types! end @@ -356,23 +348,49 @@ def pretty_dep(dep, source = false) msg end - def converge_paths - @sources.any? do |source| - next unless source.instance_of?(Source::Path) + # Check if the specs of the given source changed + # according to the locked source. A block should be + # in order to specify how the locked version of + # the source should be found. + def specs_changed?(source, &block) + locked = @locked_sources.find(&block) - locked = @locked_sources.find do |ls| - ls.class == source.class && ls.path == source.path + if locked + unlocking = locked.specs.any? do |spec| + @locked_specs.any? do |locked_spec| + locked_spec.source != locked + end end + end - if locked - unlocking = locked.specs.any? do |spec| - @locked_specs.any? do |locked_spec| - locked_spec.source != locked - end - end + !locked || unlocking || source.specs != locked.specs + end + + # Get all locals and override their matching sources. + # Return true if any of the locals changed (for example, + # they point to a new revision) or depend on new specs. + def converge_locals + locals = [] + + Bundler.settings.local_overrides.map do |k,v| + spec = @dependencies.find { |s| s.name == k } + source = spec && spec.source + if source && source.respond_to?(:local_override!) + locals << [ source, source.local_override!(v) ] end + end - !locked || unlocking || source.specs != locked.specs + locals.any? do |source, changed| + changed || specs_changed?(source) { |o| source.class === o.class && source.uri == o.uri } + end + end + + def converge_paths + @sources.any? do |source| + next unless source.instance_of?(Source::Path) + specs_changed?(source) do |ls| + ls.class == source.class && ls.path == source.path + end end end diff --git a/lib/bundler/source.rb b/lib/bundler/source.rb index 23676dec4ea..195c7dc59ea 100644 --- a/lib/bundler/source.rb +++ b/lib/bundler/source.rb @@ -714,13 +714,15 @@ def local_override!(path) "#{git_proxy.branch} but Gemfile specifies #{options["branch"]}" end - rev = cached_revision + changed = cached_revision && cached_revision != git_proxy.revision - if rev && rev != git_proxy.revision && !git_proxy.contains?(rev) - raise GitError, "The Gemfile lock is pointing to revision #{shortref_for_display(rev)} " \ + if changed && !git_proxy.contains?(cached_revision) + raise GitError, "The Gemfile lock is pointing to revision #{shortref_for_display(cached_revision)} " \ "but the current branch in your local override for #{name} does not contain such commit. " \ "Please make sure your branch is up to date." end + + changed end # TODO: actually cache git specs