Skip to content

Commit 4323674

Browse files
taralbasshsbt
authored andcommitted
[rubygems/rubygems] Don't treat a git-sourced gem install as complete if only the '.git' directory is present. This recovers cases where a git-sourced install can be left in a partially installed state.
ruby/rubygems@d132b7008d
1 parent 71e3408 commit 4323674

File tree

5 files changed

+177
-1
lines changed

5 files changed

+177
-1
lines changed

lib/bundler/source/git.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,11 @@ def requires_checkout?
360360
end
361361

362362
def locked_revision_checked_out?
363-
locked_revision && locked_revision == revision && install_path.exist?
363+
locked_revision && locked_revision == revision && installed?
364+
end
365+
366+
def installed?
367+
git_proxy.installed_to?(install_path)
364368
end
365369

366370
def base_name

lib/bundler/source/git/git_proxy.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,12 @@ def copy_to(destination, submodules = false)
147147
end
148148
end
149149

150+
def installed_to?(destination)
151+
# if copy_to is interrupted, it may leave a partially installed directory that
152+
# contains .git but no other files -- consider this not to be installed
153+
Dir.exist?(destination) && (Dir.children(destination) - [".git"]).any?
154+
end
155+
150156
private
151157

152158
def git_remote_fetch(args)

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,4 +211,45 @@
211211
subject.checkout
212212
end
213213
end
214+
215+
describe "#installed_to?" do
216+
let(:destination) { "install/dir" }
217+
let(:destination_dir_exists) { true }
218+
let(:children) { ["gem.gemspec", "README.me", ".git", "Rakefile"] }
219+
220+
before do
221+
allow(Dir).to receive(:exist?).with(destination).and_return(destination_dir_exists)
222+
allow(Dir).to receive(:children).with(destination).and_return(children)
223+
end
224+
225+
context "when destination dir exists with children other than just .git" do
226+
it "returns true" do
227+
expect(git_proxy.installed_to?(destination)).to be true
228+
end
229+
end
230+
231+
context "when destination dir does not exist" do
232+
let(:destination_dir_exists) { false }
233+
234+
it "returns false" do
235+
expect(git_proxy.installed_to?(destination)).to be false
236+
end
237+
end
238+
239+
context "when destination dir is empty" do
240+
let(:children) { [] }
241+
242+
it "returns false" do
243+
expect(git_proxy.installed_to?(destination)).to be false
244+
end
245+
end
246+
247+
context "when destination dir has only .git directory" do
248+
let(:children) { [".git"] }
249+
250+
it "returns false" do
251+
expect(git_proxy.installed_to?(destination)).to be false
252+
end
253+
end
254+
end
214255
end

spec/bundler/bundler/source/git_spec.rb

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,54 @@
7070
end
7171
end
7272
end
73+
74+
describe "#locked_revision_checked_out?" do
75+
let(:revision) { "abc" }
76+
let(:git_proxy_revision) { revision }
77+
let(:git_proxy_installed) { true }
78+
let(:git_proxy) { subject.send(:git_proxy) }
79+
let(:options) do
80+
{
81+
"uri" => uri,
82+
"revision" => revision,
83+
}
84+
end
85+
86+
before do
87+
allow(git_proxy).to receive(:revision).and_return(git_proxy_revision)
88+
allow(git_proxy).to receive(:installed_to?).with(subject.install_path).and_return(git_proxy_installed)
89+
end
90+
91+
context "when the locked revision is checked out" do
92+
it "returns true" do
93+
expect(subject.send(:locked_revision_checked_out?)).to be true
94+
end
95+
end
96+
97+
context "when no revision is provided" do
98+
let(:options) do
99+
{ "uri" => uri }
100+
end
101+
102+
it "returns falsey value" do
103+
expect(subject.send(:locked_revision_checked_out?)).to be_falsey
104+
end
105+
end
106+
107+
context "when the git proxy revision is different than the git revision" do
108+
let(:git_proxy_revision) { revision.next }
109+
110+
it "returns falsey value" do
111+
expect(subject.send(:locked_revision_checked_out?)).to be_falsey
112+
end
113+
end
114+
115+
context "when the gem hasn't been installed" do
116+
let(:git_proxy_installed) { false }
117+
118+
it "returns falsey value" do
119+
expect(subject.send(:locked_revision_checked_out?)).to be_falsey
120+
end
121+
end
122+
end
73123
end

spec/bundler/install/git_spec.rb

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,5 +214,80 @@
214214
expect(out).to include("Using foo 1.0 from #{lib_path("foo")} (at main@#{rev[0..6]})")
215215
expect(the_bundle).to include_gems "foo 1.0", source: "git@#{lib_path("foo")}"
216216
end
217+
218+
context "when install directory exists" do
219+
let(:checkout_confirmation_log_message) { "Checking out revision" }
220+
let(:using_foo_confirmation_log_message) { "Using foo 1.0 from #{lib_path("foo")} (at main@#{revision_for(lib_path("foo"))[0..6]})" }
221+
222+
context "and no contents besides .git directory are present" do
223+
it "reinstalls gem" do
224+
build_git "foo", "1.0", path: lib_path("foo")
225+
226+
gemfile = <<-G
227+
source "https://gem.repo1"
228+
gem "foo", :git => "#{lib_path("foo")}"
229+
G
230+
231+
install_gemfile gemfile, verbose: true
232+
233+
expect(out).to include(checkout_confirmation_log_message)
234+
expect(out).to include(using_foo_confirmation_log_message)
235+
expect(the_bundle).to include_gems "foo 1.0", source: "git@#{lib_path("foo")}"
236+
237+
# validate that the installed directory exists and has some expected contents
238+
install_directory = default_bundle_path("bundler/gems/foo-#{revision_for(lib_path("foo"))[0..11]}")
239+
dot_git_directory = install_directory.join(".git")
240+
lib_directory = install_directory.join("lib")
241+
gemspec = install_directory.join("foo.gemspec")
242+
expect([install_directory, dot_git_directory, lib_directory, gemspec]).to all exist
243+
244+
# remove all elements in the install directory except .git directory
245+
FileUtils.rm_rf(lib_directory)
246+
gemspec.delete
247+
248+
expect(dot_git_directory).to exist
249+
expect(lib_directory).not_to exist
250+
expect(gemspec).not_to exist
251+
252+
# rerun bundle install
253+
install_gemfile gemfile, verbose: true
254+
255+
expect(out).to include(checkout_confirmation_log_message)
256+
expect(out).to include(using_foo_confirmation_log_message)
257+
expect(the_bundle).to include_gems "foo 1.0", source: "git@#{lib_path("foo")}"
258+
259+
# validate that it reinstalls all components
260+
expect([install_directory, dot_git_directory, lib_directory, gemspec]).to all exist
261+
end
262+
end
263+
264+
context "and contents besides .git directory are present" do
265+
# we want to confirm that the change to try to detect partial installs and reinstall does not
266+
# result in repeatedly reinstalling the gem when it is fully installed
267+
it "does not reinstall gem" do
268+
build_git "foo", "1.0", path: lib_path("foo")
269+
270+
gemfile = <<-G
271+
source "https://gem.repo1"
272+
gem "foo", :git => "#{lib_path("foo")}"
273+
G
274+
275+
install_gemfile gemfile, verbose: true
276+
277+
expect(out).to include(checkout_confirmation_log_message)
278+
expect(out).to include(using_foo_confirmation_log_message)
279+
expect(the_bundle).to include_gems "foo 1.0", source: "git@#{lib_path("foo")}"
280+
281+
# rerun bundle install
282+
install_gemfile gemfile, verbose: true
283+
284+
# it isn't altogether straight-forward to validate that bundle didn't do soething on the second run, however,
285+
# the presence of the 2nd log message confirms install got past the point that it would have logged the above if
286+
# it was going to
287+
expect(out).not_to include(checkout_confirmation_log_message)
288+
expect(out).to include(using_foo_confirmation_log_message)
289+
end
290+
end
291+
end
217292
end
218293
end

0 commit comments

Comments
 (0)