Skip to content

Commit

Permalink
Allow to init a crystal app/lib in an empty directory (#4691)
Browse files Browse the repository at this point in the history
  • Loading branch information
bew authored and RX14 committed Jan 8, 2018
1 parent f7a931c commit d023138
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 31 deletions.
53 changes: 41 additions & 12 deletions spec/compiler/crystal/tools/init_spec.cr
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
7 changes: 1 addition & 6 deletions src/compiler/crystal/command.cr
Expand Up @@ -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
Expand Down
87 changes: 74 additions & 13 deletions src/compiler/crystal/tools/init.cr
Expand Up @@ -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)
Expand All @@ -36,18 +57,30 @@ 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)
config.dir = fetch_directory(args, config.name)
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
Expand Down Expand Up @@ -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

Expand All @@ -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",
Expand All @@ -121,7 +158,9 @@ module Crystal
@author = "none",
@email = "none",
@github_name = "none",
@silent = false
@silent = false,
@force = false,
@skip_existing = false
)
end
end
Expand All @@ -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
Expand All @@ -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

Expand Down

0 comments on commit d023138

Please sign in to comment.