diff --git a/lib/omnibus/git_cache.rb b/lib/omnibus/git_cache.rb index c4c33272c..842b6085a 100644 --- a/lib/omnibus/git_cache.rb +++ b/lib/omnibus/git_cache.rb @@ -32,7 +32,7 @@ class GitCache # will not have the generated content, so these snapshots would be # incompatible with the current omnibus codebase. Incrementing the serial # number ensures these old shapshots will not be used in subsequent builds. - SERIAL_NUMBER = 3 + SERIAL_NUMBER = 4 REQUIRED_GIT_FILES = %w{ HEAD @@ -126,11 +126,12 @@ def incremental create_cache_path remove_git_dirs + hardlinks = find_hardlinks git_cmd("add -A -f") begin - git_cmd(%Q{commit -q -m "Backup of #{tag}"}) + git_cmd("commit -q -F -", input: "Backup of #{tag}\n\n#{FFI_Yajl::Encoder.encode(hardlinks, pretty: true)}") rescue CommandFailed => e raise unless e.message.include?("nothing to commit") end @@ -159,6 +160,7 @@ def restore def restore_from_cache git_cmd("checkout -f restore_here") + restore_hardlinks ensure git_cmd("tag -d restore_here") end @@ -184,6 +186,43 @@ def remove_git_dirs true end + # Discover any hardlinked files in the install_dir + # + # @return [Hash{String => Array}] + def find_hardlinks + hardlink_sources = {} + hardlinks = {} + Omnibus::FileSyncer.all_files_under(install_dir).each do |path| + stat = File.stat(path) + if stat.ftype.to_sym == :file && stat.nlink > 1 + key = [stat.dev, stat.ino] + if source = hardlink_sources[key] + hardlinks[source] ||= [] + hardlinks[source] << path + else + hardlink_sources[key] = path + end + end + end + hardlinks + end + + # Restores any hardlinking from the commit message body. Body is assumed to + # be the JSON encoded return value from #find_hardlinks. + # + # @return true + def restore_hardlinks + body = git_cmd("log --format=%b -n 1").stdout + hardlinks = FFI_Yajl::Parser.parse(body) + + hardlinks.each do |source, dest| + dest.each do |path| + FileUtils.ln(source, path, force: true) + end + end + true + end + private # diff --git a/spec/unit/git_cache_spec.rb b/spec/unit/git_cache_spec.rb index 0d188fd55..f3c738cf2 100644 --- a/spec/unit/git_cache_spec.rb +++ b/spec/unit/git_cache_spec.rb @@ -105,10 +105,83 @@ module Omnibus end end + describe "#find_hardlinks" do + let(:regular_file_stat) do + stat = double(File::Stat) + allow(stat).to receive(:ftype).and_return(:file) + allow(stat).to receive(:nlink).and_return(1) + stat + end + + let(:hardlinked_file_stat) do + stat = double(File::Stat) + allow(stat).to receive(:ftype).and_return(:file) + allow(stat).to receive(:nlink).and_return(2) + allow(stat).to receive(:dev).and_return(5) + allow(stat).to receive(:ino).and_return(25) + stat + end + + before do + allow(File).to receive(:stat).and_return(regular_file_stat) + allow(File).to receive(:stat).with("foo").and_return(hardlinked_file_stat) + allow(File).to receive(:stat).with("bar").and_return(hardlinked_file_stat) + + allow(Omnibus::FileSyncer).to receive(:all_files_under).and_return( + %w{ foo bar baz quux } + ) + end + + it "returns some hardlinks" do + expect(ipc.find_hardlinks).to eq({ "foo" => ["bar"] }) + end + end + + describe "#restore_hardlinks" do + let(:hardlinks) do + { + "/opt/demo/bin/file1" => [ + "/opt/demo/bin/file2", + "/opt/demo/bin/file3", + ], + } + end + + let(:git_log_output) { FFI_Yajl::Encoder.encode(hardlinks) } + + let(:log_cmd) do + cmd_double = double(Mixlib::ShellOut) + allow(cmd_double).to receive(:stdout).and_return(git_log_output) + cmd_double + end + + before(:each) do + allow(ipc).to receive(:git_cmd) + .with("log --format=%b -n 1").and_return(log_cmd) + allow(FileUtils).to receive(:ln) + end + + it "checks the commit message" do + expect(ipc).to receive(:git_cmd) + .with("log --format=%b -n 1") + + ipc.restore_hardlinks + end + + it "recreates hardlinks" do + expect(FileUtils).to receive(:ln) + .with("/opt/demo/bin/file1", "/opt/demo/bin/file2", force: true) + expect(FileUtils).to receive(:ln) + .with("/opt/demo/bin/file1", "/opt/demo/bin/file3", force: true) + ipc.restore_hardlinks + end + end + describe "#incremental" do before(:each) do allow(ipc).to receive(:git_cmd) allow(ipc).to receive(:create_cache_path) + allow(ipc).to receive(:find_hardlinks).and_return({}) end it "creates the cache path" do @@ -125,7 +198,7 @@ module Omnibus it "commits the backup for the software" do expect(ipc).to receive(:git_cmd) - .with(%Q{commit -q -m "Backup of #{ipc.tag}"}) + .with("commit -q -F -", input: "Backup of #{ipc.tag}\n\n{\n\n}\n") ipc.incremental end @@ -176,6 +249,7 @@ module Omnibus allow(ipc).to receive(:git_cmd) .with(%Q{tag -f restore_here "#{ipc.tag}"}) allow(ipc).to receive(:create_cache_path) + allow(ipc).to receive(:restore_hardlinks) end it "creates the cache path" do