Skip to content

Commit

Permalink
Add Builder#sync
Browse files Browse the repository at this point in the history
  • Loading branch information
sethvargo committed Jul 23, 2014
1 parent 938550f commit 4d8a6e3
Show file tree
Hide file tree
Showing 3 changed files with 212 additions and 1 deletion.
109 changes: 108 additions & 1 deletion lib/omnibus/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
#

require 'fileutils'
require 'ostruct'
require 'mixlib/shellout'
require 'ostruct'
require 'pathname'

module Omnibus
class Builder
Expand Down Expand Up @@ -493,6 +494,80 @@ def link(source, destination, options = {})
end
expose :link

#
# Copy the files from +source+ to +destination+, while removing any files
# in +destination+ that are not present in +source+.
#
# You can pass the option +:exclude+ option to ignore files and folders that
# match the given pattern(s). Note the exclude pattern behaves on paths
# relative to the given source. If you want to exclude a nested directory,
# you will need to use something like +**/directory+.
#
# @example
# sync "#{project_dir}/**/*.rb", "#{install_dir}/ruby_files"
#
# @example
# sync project_dir, "#{install_dir}/files", exclude: '.git'
#
# @param [String] source
# the path on disk to sync from
# @param [String] destination
# the path on disk to sync to
#
# @option options [String, Array<String>] :exclude
# a file, folder, or globbing pattern of files to ignore when syncing
#
# @return (see #command)
#
def sync(source, destination, options = {})
build_commands << BuildCommand.new("sync `#{source}' to `#{destination}'") do
Dir.chdir(software.install_dir) do
# The source must be a destination in the sync command
unless File.directory?(source)
raise ArgumentError, "`source' must be a directory, but was a " \
"`#{File.ftype(source)}'! If you just want to sync a file, use " \
"the `copy' method instead."
end

# Reject any files that match the excludes pattern
excludes = Array(options[:exclude]).map do |exclude|
[exclude, "#{exclude}/*"]
end.flatten

source_files = all_files(source)
source_files = source_files.reject do |source_file|
basename = relative_path_for(source_file, source)
excludes.any? { |exclude| File.fnmatch?(exclude, basename, File::FNM_DOTMATCH) }
end

# Ensure the destination directory exists
FileUtils.mkdir_p(destination) unless File.directory?(destination)

# Copy over the filtered source files
FileUtils.cp_r(source_files, destination)

# Remove any files in the destination that are not in the source files
destination_files = all_files(destination)

# Calculate the relative paths of files so we can compare to the
# source.
relative_source_files = source_files.map do |file|
relative_path_for(file, source)
end
relative_destination_files = destination_files.map do |file|
relative_path_for(file, destination)
end

# Remove any extra files that are present in the destination, but are
# not in the source list
extra_files = relative_destination_files - relative_source_files
extra_files.each do |file|
FileUtils.rm_rf(File.join(destination, file))
end
end
end
end

#
# @!endgroup
# --------------------------------------------------
Expand Down Expand Up @@ -744,6 +819,38 @@ def find_file(path, source)
[candidate_paths, file]
end

#
# Get all the regular files and directories at the given path. It is assumed
# this path is a fully-qualified path and/or executed from a proper relative
# path.
#
# @param [String] path
# the path to get all files from
#
# @return [Array<String>]
# the list of all files
#
def all_files(path)
Dir.glob("#{path}/**/*", File::FNM_DOTMATCH).reject do |file|
basename = File.basename(file)
IGNORED_FILES.include?(basename)
end
end

#
# The relative path of the given +path+ to the +parent+.
#
# @param [String] path
# the path to get relative with
# @param [String] parent
# the parent where the path is contained (hopefully)
#
# @return [String]
#
def relative_path_for(path, parent)
Pathname.new(path).relative_path_from(Pathname.new(parent)).to_s
end

#
# The log key for this class, overriden to incorporate the software name.
#
Expand Down
103 changes: 103 additions & 0 deletions spec/functional/builder_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -436,5 +436,108 @@ def fake_embedded_bin(name)
expect(File.symlink?("#{destination}/file_b")).to be_truthy
end
end

describe '#sync' do
let(:source) do
source = File.join(tmp_path, 'source')
FileUtils.mkdir_p(source)

FileUtils.touch(File.join(source, 'file_a'))
FileUtils.touch(File.join(source, 'file_b'))
FileUtils.touch(File.join(source, 'file_c'))

FileUtils.mkdir_p(File.join(source, 'folder'))
FileUtils.touch(File.join(source, 'folder', 'file_d'))
FileUtils.touch(File.join(source, 'folder', 'file_e'))

FileUtils.mkdir_p(File.join(source, '.dot_folder'))
FileUtils.touch(File.join(source, '.dot_folder', 'file_f'))

FileUtils.touch(File.join(source, '.file_g'))
source
end

let(:destination) { File.join(tmp_path, 'destination') }

context 'when the destination is empty' do
it 'syncs the directories' do
subject.sync(source, destination)
subject.build

expect(File.file?("#{destination}/file_a")).to be_truthy
expect(File.file?("#{destination}/file_b")).to be_truthy
expect(File.file?("#{destination}/file_c")).to be_truthy
expect(File.file?("#{destination}/folder/file_d")).to be_truthy
expect(File.file?("#{destination}/folder/file_e")).to be_truthy
expect(File.file?("#{destination}/.dot_folder/file_f")).to be_truthy
expect(File.file?("#{destination}/.file_g")).to be_truthy
end
end

context 'when the directory exists' do
before { FileUtils.mkdir_p(destination) }

it 'deletes existing files and folders' do
FileUtils.mkdir_p("#{destination}/existing_folder")
FileUtils.mkdir_p("#{destination}/.existing_folder")
FileUtils.touch("#{destination}/existing_file")
FileUtils.touch("#{destination}/.existing_file")

subject.sync(source, destination)
subject.build

expect(File.file?("#{destination}/file_a")).to be_truthy
expect(File.file?("#{destination}/file_b")).to be_truthy
expect(File.file?("#{destination}/file_c")).to be_truthy
expect(File.file?("#{destination}/folder/file_d")).to be_truthy
expect(File.file?("#{destination}/folder/file_e")).to be_truthy
expect(File.file?("#{destination}/.dot_folder/file_f")).to be_truthy
expect(File.file?("#{destination}/.file_g")).to be_truthy

expect(File.exist?("#{destination}/existing_folder")).to be_falsey
expect(File.exist?("#{destination}/.existing_folder")).to be_falsey
expect(File.exist?("#{destination}/existing_file")).to be_falsey
expect(File.exist?("#{destination}/.existing_file")).to be_falsey
end
end

context 'when :exclude is given' do
it 'does not copy files and folders that match the pattern' do
subject.sync(source, destination, exclude: '.dot_folder')
subject.build

expect(File.file?("#{destination}/file_a")).to be_truthy
expect(File.file?("#{destination}/file_b")).to be_truthy
expect(File.file?("#{destination}/file_c")).to be_truthy
expect(File.file?("#{destination}/folder/file_d")).to be_truthy
expect(File.file?("#{destination}/folder/file_e")).to be_truthy
expect(File.exist?("#{destination}/.dot_folder")).to be_falsey
expect(File.file?("#{destination}/.dot_folder/file_f")).to be_falsey
expect(File.file?("#{destination}/.file_g")).to be_truthy
end

it 'removes existing files and folders in destination' do
FileUtils.mkdir_p("#{destination}/existing_folder")
FileUtils.touch("#{destination}/existing_file")
FileUtils.mkdir_p("#{destination}/.dot_folder")
FileUtils.touch("#{destination}/.dot_folder/file_f")

subject.sync(source, destination, exclude: '.dot_folder')
subject.build

expect(File.file?("#{destination}/file_a")).to be_truthy
expect(File.file?("#{destination}/file_b")).to be_truthy
expect(File.file?("#{destination}/file_c")).to be_truthy
expect(File.file?("#{destination}/folder/file_d")).to be_truthy
expect(File.file?("#{destination}/folder/file_e")).to be_truthy
expect(File.exist?("#{destination}/.dot_folder")).to be_falsey
expect(File.file?("#{destination}/.dot_folder/file_f")).to be_falsey
expect(File.file?("#{destination}/.file_g")).to be_truthy

expect(File.exist?("#{destination}/existing_folder")).to be_falsey
expect(File.exist?("#{destination}/existing_file")).to be_falsey
end
end
end
end
end
1 change: 1 addition & 0 deletions spec/unit/builder_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ module Omnibus
it_behaves_like 'a cleanroom setter', :copy, %|copy 'file', 'file2'|
it_behaves_like 'a cleanroom setter', :move, %|move 'file', 'file2'|
it_behaves_like 'a cleanroom setter', :link, %|link 'file', 'file2'|
it_behaves_like 'a cleanroom setter', :sync, %|link 'a/', 'b/'|
it_behaves_like 'a cleanroom getter', :project_root, %|puts project_root|
it_behaves_like 'a cleanroom getter', :windows_safe_path, %|puts windows_safe_path('foo')|

Expand Down

0 comments on commit 4d8a6e3

Please sign in to comment.