diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb index d0470f3f3426af..0d37f89772fb3c 100644 --- a/lib/bundler/definition.rb +++ b/lib/bundler/definition.rb @@ -711,9 +711,10 @@ def materialize(dependencies) "available locally before rerunning Bundler." else "Your bundle is locked to #{locked_gem} from #{locked_gem.source}, but that version can " \ - "no longer be found in that source. That means the author of #{locked_gem} has removed it. " \ - "You'll need to update your bundle to a version other than #{locked_gem} that hasn't been " \ - "removed in order to install." + "no longer be found in that source. That means either the author of #{locked_gem} has removed it, " \ + "or you no longer have access to that source. You'll need to update your bundle to a version other " \ + "than #{locked_gem} that hasn't been removed, or check your credentials and access rights for " \ + "#{locked_gem.source}, in order to install." end raise GemNotFound, message diff --git a/lib/bundler/source/git/git_proxy.rb b/lib/bundler/source/git/git_proxy.rb index 8094dcaa9d94ff..78ab747215ca7d 100644 --- a/lib/bundler/source/git/git_proxy.rb +++ b/lib/bundler/source/git/git_proxy.rb @@ -144,6 +144,12 @@ def copy_to(destination, submodules = false) FileUtils.rm_rf(p) end git "clone", "--no-checkout", "--quiet", path.to_s, destination.to_s + # The copy is cloned from the local bare cache, which holds no Git LFS + # objects, so point origin back at the real remote and let git-lfs derive + # its endpoint from there when checking out. Use the credential-filtered + # URI to avoid persisting secrets in the copy's .git/config; auth is left + # to git's credential helper. + git "remote", "set-url", "origin", credential_filtered_uri, dir: destination File.chmod((File.stat(destination).mode | 0o777) & ~File.umask, destination) rescue Errno::EEXIST => e file_path = e.message[%r{.*?((?:[a-zA-Z]:)?/.*)}, 1] diff --git a/spec/bundler/bundler/source/git/git_proxy_spec.rb b/spec/bundler/bundler/source/git/git_proxy_spec.rb index 1f10ca4b0776fb..e2d3bbb6e7efaf 100644 --- a/spec/bundler/bundler/source/git/git_proxy_spec.rb +++ b/spec/bundler/bundler/source/git/git_proxy_spec.rb @@ -98,6 +98,45 @@ end end + describe "#copy_to" do + let(:revision) { "abc123" } + let(:destination) { tmp("git-proxy-copy") } + + before do + # The bare cache (`path`) is the clone source, so stub it away and only + # exercise the post-clone wiring of the working copy at `destination`. + allow(File).to receive(:stat).and_call_original + allow(File).to receive(:stat).with(destination).and_return(double("File::Stat", mode: 0o755)) + allow(File).to receive(:chmod) + allow(git_proxy).to receive(:capture).and_return(["", "", clone_result]) + end + + it "points the working copy's origin back at the real remote" do + expect(git_proxy).to receive(:capture).with(["remote", "set-url", "origin", uri], destination).and_return(["", "", clone_result]) + git_proxy.copy_to(destination) + end + + it "does not persist credentials in the working copy's origin" do + Bundler.settings.temporary(uri => "u:p") do + credentialed_uri = "https://u:p@github.com/ruby/rubygems.git" + expect(git_proxy).not_to receive(:capture).with(["remote", "set-url", "origin", credentialed_uri], destination) + expect(git_proxy).to receive(:capture).with(["remote", "set-url", "origin", uri], destination).and_return(["", "", clone_result]) + git_proxy.copy_to(destination) + end + end + + context "when the remote URI embeds credentials" do + let(:uri) { "https://user:secret@github.com/ruby/rubygems.git" } + + it "strips the password before writing origin" do + filtered_uri = "https://user@github.com/ruby/rubygems.git" + expect(git_proxy).not_to receive(:capture).with(["remote", "set-url", "origin", uri], destination) + expect(git_proxy).to receive(:capture).with(["remote", "set-url", "origin", filtered_uri], destination).and_return(["", "", clone_result]) + git_proxy.copy_to(destination) + end + end + end + describe "#version" do context "with a normal version number" do before do diff --git a/spec/bundler/install/gemfile/git_spec.rb b/spec/bundler/install/gemfile/git_spec.rb index b2a82caf017b91..f2138b5fda2088 100644 --- a/spec/bundler/install/gemfile/git_spec.rb +++ b/spec/bundler/install/gemfile/git_spec.rb @@ -31,6 +31,14 @@ expect(out).to eq("WIN") end + it "points the installed copy's origin at the real remote, not the local cache" do + install_base_gemfile + + installed = Pathname.glob(default_bundle_path("bundler/gems/foo-1.0-*")).first + origin = git("config --get remote.origin.url", installed).strip + expect(origin).to eq(lib_path("foo-1.0").to_s) + end + it "does not (yet?) enforce CHECKSUMS" do build_git "foo" revision = revision_for(lib_path("foo-1.0")) diff --git a/spec/bundler/install/yanked_spec.rb b/spec/bundler/install/yanked_spec.rb index c92af7bfb09526..fb9316cced8a4c 100644 --- a/spec/bundler/install/yanked_spec.rb +++ b/spec/bundler/install/yanked_spec.rb @@ -26,6 +26,8 @@ G expect(err).to include("Your bundle is locked to foo (10.0.0)") + expect(err).to include("either the author of foo (10.0.0) has removed it, or you no longer have access to that source") + expect(err).to include("check your credentials and access rights") end context "when a platform specific yanked version is included in the lockfile, and a generic variant is available remotely" do diff --git a/test/json/json_minefield_parser_test.rb b/test/json/json_minefield_parser_test.rb index 17619b2db1dbaa..71590325573edf 100644 --- a/test/json/json_minefield_parser_test.rb +++ b/test/json/json_minefield_parser_test.rb @@ -78,7 +78,7 @@ def define_test(name, &block) end fixtures.each do |path| - payload = File.read(path) + payload = File.binread(path) name = File.basename(path, '.json') if COMMENT_TESTS.include?(name) @@ -111,4 +111,4 @@ def define_test(name, &block) raise "Unexpected minefield test: #{name}" end end -end \ No newline at end of file +end