Skip to content

Commit 4d8a6e3

Browse files
committed
Add Builder#sync
1 parent 938550f commit 4d8a6e3

File tree

3 files changed

+212
-1
lines changed

3 files changed

+212
-1
lines changed

Diff for: lib/omnibus/builder.rb

+108-1
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515
#
1616

1717
require 'fileutils'
18-
require 'ostruct'
1918
require 'mixlib/shellout'
19+
require 'ostruct'
20+
require 'pathname'
2021

2122
module Omnibus
2223
class Builder
@@ -493,6 +494,80 @@ def link(source, destination, options = {})
493494
end
494495
expose :link
495496

497+
#
498+
# Copy the files from +source+ to +destination+, while removing any files
499+
# in +destination+ that are not present in +source+.
500+
#
501+
# You can pass the option +:exclude+ option to ignore files and folders that
502+
# match the given pattern(s). Note the exclude pattern behaves on paths
503+
# relative to the given source. If you want to exclude a nested directory,
504+
# you will need to use something like +**/directory+.
505+
#
506+
# @example
507+
# sync "#{project_dir}/**/*.rb", "#{install_dir}/ruby_files"
508+
#
509+
# @example
510+
# sync project_dir, "#{install_dir}/files", exclude: '.git'
511+
#
512+
# @param [String] source
513+
# the path on disk to sync from
514+
# @param [String] destination
515+
# the path on disk to sync to
516+
#
517+
# @option options [String, Array<String>] :exclude
518+
# a file, folder, or globbing pattern of files to ignore when syncing
519+
#
520+
# @return (see #command)
521+
#
522+
def sync(source, destination, options = {})
523+
build_commands << BuildCommand.new("sync `#{source}' to `#{destination}'") do
524+
Dir.chdir(software.install_dir) do
525+
# The source must be a destination in the sync command
526+
unless File.directory?(source)
527+
raise ArgumentError, "`source' must be a directory, but was a " \
528+
"`#{File.ftype(source)}'! If you just want to sync a file, use " \
529+
"the `copy' method instead."
530+
end
531+
532+
# Reject any files that match the excludes pattern
533+
excludes = Array(options[:exclude]).map do |exclude|
534+
[exclude, "#{exclude}/*"]
535+
end.flatten
536+
537+
source_files = all_files(source)
538+
source_files = source_files.reject do |source_file|
539+
basename = relative_path_for(source_file, source)
540+
excludes.any? { |exclude| File.fnmatch?(exclude, basename, File::FNM_DOTMATCH) }
541+
end
542+
543+
# Ensure the destination directory exists
544+
FileUtils.mkdir_p(destination) unless File.directory?(destination)
545+
546+
# Copy over the filtered source files
547+
FileUtils.cp_r(source_files, destination)
548+
549+
# Remove any files in the destination that are not in the source files
550+
destination_files = all_files(destination)
551+
552+
# Calculate the relative paths of files so we can compare to the
553+
# source.
554+
relative_source_files = source_files.map do |file|
555+
relative_path_for(file, source)
556+
end
557+
relative_destination_files = destination_files.map do |file|
558+
relative_path_for(file, destination)
559+
end
560+
561+
# Remove any extra files that are present in the destination, but are
562+
# not in the source list
563+
extra_files = relative_destination_files - relative_source_files
564+
extra_files.each do |file|
565+
FileUtils.rm_rf(File.join(destination, file))
566+
end
567+
end
568+
end
569+
end
570+
496571
#
497572
# @!endgroup
498573
# --------------------------------------------------
@@ -744,6 +819,38 @@ def find_file(path, source)
744819
[candidate_paths, file]
745820
end
746821

822+
#
823+
# Get all the regular files and directories at the given path. It is assumed
824+
# this path is a fully-qualified path and/or executed from a proper relative
825+
# path.
826+
#
827+
# @param [String] path
828+
# the path to get all files from
829+
#
830+
# @return [Array<String>]
831+
# the list of all files
832+
#
833+
def all_files(path)
834+
Dir.glob("#{path}/**/*", File::FNM_DOTMATCH).reject do |file|
835+
basename = File.basename(file)
836+
IGNORED_FILES.include?(basename)
837+
end
838+
end
839+
840+
#
841+
# The relative path of the given +path+ to the +parent+.
842+
#
843+
# @param [String] path
844+
# the path to get relative with
845+
# @param [String] parent
846+
# the parent where the path is contained (hopefully)
847+
#
848+
# @return [String]
849+
#
850+
def relative_path_for(path, parent)
851+
Pathname.new(path).relative_path_from(Pathname.new(parent)).to_s
852+
end
853+
747854
#
748855
# The log key for this class, overriden to incorporate the software name.
749856
#

Diff for: spec/functional/builder_spec.rb

+103
Original file line numberDiff line numberDiff line change
@@ -436,5 +436,108 @@ def fake_embedded_bin(name)
436436
expect(File.symlink?("#{destination}/file_b")).to be_truthy
437437
end
438438
end
439+
440+
describe '#sync' do
441+
let(:source) do
442+
source = File.join(tmp_path, 'source')
443+
FileUtils.mkdir_p(source)
444+
445+
FileUtils.touch(File.join(source, 'file_a'))
446+
FileUtils.touch(File.join(source, 'file_b'))
447+
FileUtils.touch(File.join(source, 'file_c'))
448+
449+
FileUtils.mkdir_p(File.join(source, 'folder'))
450+
FileUtils.touch(File.join(source, 'folder', 'file_d'))
451+
FileUtils.touch(File.join(source, 'folder', 'file_e'))
452+
453+
FileUtils.mkdir_p(File.join(source, '.dot_folder'))
454+
FileUtils.touch(File.join(source, '.dot_folder', 'file_f'))
455+
456+
FileUtils.touch(File.join(source, '.file_g'))
457+
source
458+
end
459+
460+
let(:destination) { File.join(tmp_path, 'destination') }
461+
462+
context 'when the destination is empty' do
463+
it 'syncs the directories' do
464+
subject.sync(source, destination)
465+
subject.build
466+
467+
expect(File.file?("#{destination}/file_a")).to be_truthy
468+
expect(File.file?("#{destination}/file_b")).to be_truthy
469+
expect(File.file?("#{destination}/file_c")).to be_truthy
470+
expect(File.file?("#{destination}/folder/file_d")).to be_truthy
471+
expect(File.file?("#{destination}/folder/file_e")).to be_truthy
472+
expect(File.file?("#{destination}/.dot_folder/file_f")).to be_truthy
473+
expect(File.file?("#{destination}/.file_g")).to be_truthy
474+
end
475+
end
476+
477+
context 'when the directory exists' do
478+
before { FileUtils.mkdir_p(destination) }
479+
480+
it 'deletes existing files and folders' do
481+
FileUtils.mkdir_p("#{destination}/existing_folder")
482+
FileUtils.mkdir_p("#{destination}/.existing_folder")
483+
FileUtils.touch("#{destination}/existing_file")
484+
FileUtils.touch("#{destination}/.existing_file")
485+
486+
subject.sync(source, destination)
487+
subject.build
488+
489+
expect(File.file?("#{destination}/file_a")).to be_truthy
490+
expect(File.file?("#{destination}/file_b")).to be_truthy
491+
expect(File.file?("#{destination}/file_c")).to be_truthy
492+
expect(File.file?("#{destination}/folder/file_d")).to be_truthy
493+
expect(File.file?("#{destination}/folder/file_e")).to be_truthy
494+
expect(File.file?("#{destination}/.dot_folder/file_f")).to be_truthy
495+
expect(File.file?("#{destination}/.file_g")).to be_truthy
496+
497+
expect(File.exist?("#{destination}/existing_folder")).to be_falsey
498+
expect(File.exist?("#{destination}/.existing_folder")).to be_falsey
499+
expect(File.exist?("#{destination}/existing_file")).to be_falsey
500+
expect(File.exist?("#{destination}/.existing_file")).to be_falsey
501+
end
502+
end
503+
504+
context 'when :exclude is given' do
505+
it 'does not copy files and folders that match the pattern' do
506+
subject.sync(source, destination, exclude: '.dot_folder')
507+
subject.build
508+
509+
expect(File.file?("#{destination}/file_a")).to be_truthy
510+
expect(File.file?("#{destination}/file_b")).to be_truthy
511+
expect(File.file?("#{destination}/file_c")).to be_truthy
512+
expect(File.file?("#{destination}/folder/file_d")).to be_truthy
513+
expect(File.file?("#{destination}/folder/file_e")).to be_truthy
514+
expect(File.exist?("#{destination}/.dot_folder")).to be_falsey
515+
expect(File.file?("#{destination}/.dot_folder/file_f")).to be_falsey
516+
expect(File.file?("#{destination}/.file_g")).to be_truthy
517+
end
518+
519+
it 'removes existing files and folders in destination' do
520+
FileUtils.mkdir_p("#{destination}/existing_folder")
521+
FileUtils.touch("#{destination}/existing_file")
522+
FileUtils.mkdir_p("#{destination}/.dot_folder")
523+
FileUtils.touch("#{destination}/.dot_folder/file_f")
524+
525+
subject.sync(source, destination, exclude: '.dot_folder')
526+
subject.build
527+
528+
expect(File.file?("#{destination}/file_a")).to be_truthy
529+
expect(File.file?("#{destination}/file_b")).to be_truthy
530+
expect(File.file?("#{destination}/file_c")).to be_truthy
531+
expect(File.file?("#{destination}/folder/file_d")).to be_truthy
532+
expect(File.file?("#{destination}/folder/file_e")).to be_truthy
533+
expect(File.exist?("#{destination}/.dot_folder")).to be_falsey
534+
expect(File.file?("#{destination}/.dot_folder/file_f")).to be_falsey
535+
expect(File.file?("#{destination}/.file_g")).to be_truthy
536+
537+
expect(File.exist?("#{destination}/existing_folder")).to be_falsey
538+
expect(File.exist?("#{destination}/existing_file")).to be_falsey
539+
end
540+
end
541+
end
439542
end
440543
end

Diff for: spec/unit/builder_spec.rb

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ module Omnibus
3939
it_behaves_like 'a cleanroom setter', :copy, %|copy 'file', 'file2'|
4040
it_behaves_like 'a cleanroom setter', :move, %|move 'file', 'file2'|
4141
it_behaves_like 'a cleanroom setter', :link, %|link 'file', 'file2'|
42+
it_behaves_like 'a cleanroom setter', :sync, %|link 'a/', 'b/'|
4243
it_behaves_like 'a cleanroom getter', :project_root, %|puts project_root|
4344
it_behaves_like 'a cleanroom getter', :windows_safe_path, %|puts windows_safe_path('foo')|
4445

0 commit comments

Comments
 (0)