From d023138f7f47c4fc1c911cbdd35a652767d120a8 Mon Sep 17 00:00:00 2001 From: Benoit de Chezelles Date: Mon, 8 Jan 2018 01:25:21 +0100 Subject: [PATCH] Allow to init a crystal app/lib in an empty directory (#4691) --- spec/compiler/crystal/tools/init_spec.cr | 53 +++++++++++---- src/compiler/crystal/command.cr | 7 +- src/compiler/crystal/tools/init.cr | 87 ++++++++++++++++++++---- 3 files changed, 116 insertions(+), 31 deletions(-) diff --git a/spec/compiler/crystal/tools/init_spec.cr b/spec/compiler/crystal/tools/init_spec.cr index 40e32651f077..2fe62a4fcd83 100644 --- a/spec/compiler/crystal/tools/init_spec.cr +++ b/spec/compiler/crystal/tools/init_spec.cr @@ -7,9 +7,11 @@ require "yaml" PROJECT_ROOT_DIR = "#{__DIR__}/../../../.." -private def exec_init(project_name, project_dir = nil, type = "lib") +private def exec_init(project_name, project_dir = nil, type = "lib", force = false, skip_existing = false) args = [type, project_name] args << project_dir if project_dir + args << "--force" if force + args << "--skip-existing" if skip_existing config = Crystal::Init.parse_args(args) config.silent = true @@ -186,18 +188,11 @@ end end describe "Init invocation" do - it "prints error if a directory or a file is already present" do + it "prints error if a file is already present" do within_temporary_directory do - existing_dir = "existing-dir" - Dir.mkdir(existing_dir) - - expect_raises(Crystal::Init::Error, "File or directory #{existing_dir} already exists") do - exec_init(existing_dir) - end - existing_file = "existing-file" File.touch(existing_file) - expect_raises(Crystal::Init::Error, "File or directory #{existing_file} already exists") do + expect_raises(Crystal::Init::Error, "#{existing_file.inspect} is a file") do exec_init(existing_file) end end @@ -209,12 +204,46 @@ end project_dir = "project_dir" Dir.mkdir(project_name) + File.write("README.md", "content before init") exec_init(project_name, project_dir) - expect_raises(Crystal::Init::Error, "File or directory #{project_dir} already exists") do - exec_init(project_name, project_dir) + File.read("README.md").should eq("content before init") + File.exists?(File.join(project_dir, "README.md")).should be_true + end + end + + it "errors if files will be overwritten by a generated file" do + within_temporary_directory do + File.touch("README.md") + + ex = expect_raises(Crystal::Init::FilesConflictError) do + exec_init("my_lib", ".") end + ex.conflicting_files.should contain("./README.md") + end + end + + it "doesn't error if files will be overwritten by a generated file and --force is used" do + within_temporary_directory do + File.write("README.md", "content before init") + File.exists?("README.md").should be_true + + exec_init("my_lib", ".", force: true) + + File.read("README.md").should_not eq("content before init") + File.exists?("LICENSE").should be_true + end + end + + it "doesn't error when asked to skip existing files" do + within_temporary_directory do + File.write("README.md", "content before init") + + exec_init("my_lib", ".", skip_existing: true) + + File.read("README.md").should eq("content before init") + File.exists?("LICENSE").should be_true end end end diff --git a/src/compiler/crystal/command.cr b/src/compiler/crystal/command.cr index 616541a611d5..6a789f0ce991 100644 --- a/src/compiler/crystal/command.cr +++ b/src/compiler/crystal/command.cr @@ -156,12 +156,7 @@ class Crystal::Command end private def init - begin - Init.run(options) - rescue ex : Init::Error - STDERR.puts ex - exit 1 - end + Init.run(options) end private def build diff --git a/src/compiler/crystal/tools/init.cr b/src/compiler/crystal/tools/init.cr index 0238f77160dd..5b0d771f6203 100644 --- a/src/compiler/crystal/tools/init.cr +++ b/src/compiler/crystal/tools/init.cr @@ -13,9 +13,30 @@ module Crystal end end + class FilesConflictError < Error + getter conflicting_files : Array(String) + + def initialize(@conflicting_files) + super("Some files would be overwritten: #{conflicting_files.join(", ")}") + end + end + def self.run(args) - config = parse_args(args) - InitProject.new(config).run + begin + config = parse_args(args) + InitProject.new(config).run + rescue ex : Init::FilesConflictError + STDERR.puts "Cannot initialize Crystal project, the following files would be overwritten:" + ex.conflicting_files.each do |path| + STDERR.puts " #{"file".colorize(:red)} #{path} #{"already exist".colorize(:red)}" + end + STDERR.puts "You can use --force to overwrite those files," + STDERR.puts "or --skip-existing to skip existing files and generate the others." + exit 1 + rescue ex : Init::Error + STDERR.puts "Cannot initialize Crystal project: #{ex}" + exit 1 + end end def self.parse_args(args) @@ -36,11 +57,19 @@ module Crystal USAGE - opts.on("--help", "show this help") do + opts.on("-h", "--help", "show this help") do puts opts exit end + opts.on("-f", "--force", "force overwrite existing files") do + config.force = true + end + + opts.on("-s", "--skip-existing", "skip existing files") do + config.skip_existing = true + end + opts.unknown_args do |args, after_dash| config.skeleton_type = fetch_skeleton_type(opts, args) config.name = fetch_name(opts, args) @@ -48,6 +77,10 @@ module Crystal end end + if config.force && config.skip_existing + raise Error.new "Cannot use --force and --skip-existing together" + end + config.author = fetch_author config.email = fetch_email config.github_name = fetch_github_name @@ -84,9 +117,11 @@ module Crystal def self.fetch_directory(args, project_name) directory = args.empty? ? project_name : args.shift - if Dir.exists?(directory) || File.exists?(directory) - raise Error.new "File or directory #{directory} already exists" + + if File.file?(directory) + raise Error.new "#{directory.inspect} is a file" end + directory end @@ -113,6 +148,8 @@ module Crystal property email : String property github_name : String property silent : Bool + property force : Bool + property skip_existing : Bool def initialize( @skeleton_type = "none", @@ -121,7 +158,9 @@ module Crystal @author = "none", @email = "none", @github_name = "none", - @silent = false + @silent = false, + @force = false, + @skip_existing = false ) end end @@ -143,13 +182,19 @@ module Crystal end def render + overwriting = File.exists?(full_path) + Dir.mkdir_p(File.dirname(full_path)) File.write(full_path, to_s) - puts log_message unless config.silent + puts log_message(overwriting) unless config.silent end - def log_message - " #{"create".colorize(:light_green)} #{full_path}" + def log_message(overwriting = false) + if overwriting + " #{"overwrite".colorize(:light_green)} #{full_path}" + else + " #{"create".colorize(:light_green)} #{full_path}" + end end def module_name @@ -161,18 +206,34 @@ module Crystal class InitProject getter config : Config + getter views : Array(View)? def initialize(@config : Config) end - def run - views.each do |view| - view.new(config).render + def overwrite_checks + overwriting_files = views.compact_map do |view| + path = view.full_path + File.exists?(path) ? path : nil end + + if overwriting_files.any? + if config.skip_existing + views.reject! { |view| File.exists?(view.full_path) } + else + raise FilesConflictError.new overwriting_files + end + end + end + + def run + overwrite_checks unless config.force + + views.each &.render end def views - View.views + @views ||= View.views.map(&.new(config)) end end