diff --git a/Project.toml b/Project.toml index e80b8c4f..5278b307 100644 --- a/Project.toml +++ b/Project.toml @@ -8,6 +8,7 @@ CondaPkg = "992eb4ea-22a4-4c89-a5bb-47a3300528ab" PythonCall = "6099a3de-0909-46bc-b1f4-468b9a2dfc0d" TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76" UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" +YAML = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6" [compat] CondaPkg = "0.2" diff --git a/copier.yml b/copier.yml index 67717a4a..8e68b2ed 100644 --- a/copier.yml +++ b/copier.yml @@ -130,16 +130,4 @@ _skip_if_exists: _subdirectory: template _message_after_copy: | - Your package {{ PackageName }}.jl has been created successfully! 🎉 - - Next steps: Create git repository and push to Github. - $ cd - $ git init - $ git add . - $ pre-commit run -a # Try to fix possible pre-commit issues (failures are expected) - $ git add . - $ git commit -m "First commit" - $ pre-commit install # Future commits can't be directly to main unless you use -n - Create a repo on GitHub and push your code to it. - - Read the full guide: https://abelsiqueira.com/BestieTemplate.jl/stable/10-full-guide + All went well on copier's side. Going back to BestieTemplate. diff --git a/src/BestieTemplate.jl b/src/BestieTemplate.jl index e69f3c17..b5dc97bf 100644 --- a/src/BestieTemplate.jl +++ b/src/BestieTemplate.jl @@ -4,18 +4,40 @@ This package defines a copier template for Julia packages and a basic user interface aroud copier to use it from Julia. -The main functions are: [`generate`](@ref) and [`update`](@ref). +The main functions are: [`generate`](@ref), [`apply`](@ref), and [`update`](@ref). """ module BestieTemplate include("Copier.jl") using TOML: TOML +using YAML: YAML + +""" + _copy(src_path, dst_path, data; kwargs...) + +Internal function to run common code for new or existing packages. +""" +function _copy(src_path, dst_path, data; kwargs...) + # If the PackageName was not given or guessed from the Project.toml, use the sanitized path + if !haskey(data, "PackageName") + package_name = _sanitize_package_name(dst_path) + if package_name != "" + @info "Using sanitized path $package_name as package name" + data["PackageName"] = package_name + end + end + Copier.copy(src_path, dst_path, data; kwargs...) +end """ generate(dst_path[, data]; kwargs...) generate(src_path, dst_path[, data]; true, kwargs...) +Generates a new project at the path `dst_path` using the template. +If the `dst_path` already exists, this will throw an error, unless `dst_path = "."`. +For existing packages, use `BestieTemplate.apply` instead. + Runs the `copy` command of [copier](https://github.com/copier-org/copier) with the BestieTemplate template. If `src_path` is not informed, the GitHub URL of BestieTemplate.jl is used. @@ -27,7 +49,67 @@ The `data` argument is a dictionary of answers (values) to questions (keys) that The other keyword arguments are passed directly to the internal [`Copier.copy`](@ref). """ -function generate(src_path, dst_path, data::Dict = Dict(); warn_existing_pkg = true, kwargs...) +function generate(src_path, dst_path, data::Dict = Dict(); kwargs...) + if dst_path != "." && isdir(dst_path) && length(readdir(dst_path)) > 0 + error("$dst_path already exists. For existing packages, use `BestieTemplate.apply` instead.") + end + + _copy(src_path, dst_path, data; kwargs...) + + data = YAML.load_file(joinpath(dst_path, ".copier-answers.yml")) + package_name = data["PackageName"] + bestie_version = data["_commit"] + + println("""Your package $package_name.jl has been created successfully! 🎉 + + Next steps: Create git repository and push to Github. + \$ cd + \$ git init + \$ git add . + \$ pre-commit run -a # Try to fix possible pre-commit issues (failures are expected) + \$ git add . + \$ git commit -m "Generate repo with BestieTemplate $bestie_version" + \$ pre-commit install # Future commits can't be directly to main unless you use -n + Create a repo on GitHub and push your code to it. + + Read the full guide: https://abelsiqueira.com/BestieTemplate.jl/stable/10-full-guide + """) + + return nothing +end + +function generate(dst_path, data::Dict = Dict(); kwargs...) + generate("https://github.com/abelsiqueira/BestieTemplate.jl", dst_path, data; kwargs...) +end + +""" + apply(dst_path[, data]; kwargs...) + apply(src_path, dst_path[, data]; true, kwargs...) + +Applies the template to an existing project at path ``dst_path``. +If the `dst_path` does not exist, this will throw an error. +For new packages, use `BestieTemplate.generate` instead. + +Runs the `copy` command of [copier](https://github.com/copier-org/copier) with the BestieTemplate template. +If `src_path` is not informed, the GitHub URL of BestieTemplate.jl is used. + +The `data` argument is a dictionary of answers (values) to questions (keys) that can be used to bypass some of the interactive questions. + +## Keyword arguments + +- `warn_existing_pkg::Boolean = true`: Whether to check if you actually meant `update`. If you run `apply` and the `dst_path` contains a `.copier-answers.yml`, it means that the copy was already made, so you might have means `update` instead. When `true`, a warning is shown and execution is stopped. + +The other keyword arguments are passed directly to the internal [`Copier.copy`](@ref). +""" +function apply(src_path, dst_path, data::Dict = Dict(); warn_existing_pkg = true, kwargs...) + if !isdir(dst_path) + error("$dst_path does not exist. For new packages, use `BestieTemplate.generate` instead.") + end + if !isdir(joinpath(dst_path, ".git")) + error("""No folder $dst_path/.git found. Are you using git on the project? + To apply to existing packages, git is required to avoid data loss.""") + end + if warn_existing_pkg && isfile(joinpath(dst_path, ".copier-answers.yml")) @warn """There already exists a `.copier-answers.yml` file in the destination path. You might have meant to use `BestieTemplate.update` instead, which only fetches the non-applying updates. @@ -37,8 +119,8 @@ function generate(src_path, dst_path, data::Dict = Dict(); warn_existing_pkg = t return nothing end - # If there are answers in the destionation path, skip guessing the answers - if !isfile(joinpath(dst_path, ".copier-answers")) && isdir(dst_path) + # If there are answers in the destination path, skip guessing the answers + if !isfile(joinpath(dst_path, ".copier-answers")) existing_data = _read_data_from_existing_path(dst_path) for (key, value) in existing_data @info "Inferred $key=$value from destination path" @@ -48,21 +130,38 @@ function generate(src_path, dst_path, data::Dict = Dict(); warn_existing_pkg = t end data = merge(existing_data, data) end - # If the PackageName was not given or guessed from the Project.toml, use the sanitized path - if !haskey(data, "PackageName") - package_name = _sanitize_package_name(dst_path) - if package_name != "" - @info "Using sanitized path $package_name as package name" - data["PackageName"] = package_name - end - end - Copier.copy(src_path, dst_path, data; kwargs...) + + _copy(src_path, dst_path, data; kwargs...) + + data = YAML.load_file(joinpath(dst_path, ".copier-answers.yml")) + package_name = data["PackageName"] + bestie_version = data["_commit"] + + println("""BestieTemplate was applied to $package_name.jl! 🎉 + + Next steps: + + Review the modifications. + In particular README.md and docs/src/index.md tend to be heavily edited. + + \$ git switch -c apply-bestie # If you haven't created a branch + \$ git add . + \$ pre-commit run -a # Try to fix possible pre-commit issues (failures are expected) + \$ pre-commit run -a # Again. Now failures should not happen + \$ gid add . + \$ git commit -m "Apply BestieTemplate $bestie_version" + \$ pre-commit install + \$ git push -u origin apply-bestie + + Go to GitHub and create a Pull Request from apply-bestie to main. + Continue on the full guide: https://abelsiqueira.com/BestieTemplate.jl/stable/10-full-guide + """) return nothing end -function generate(dst_path, data::Dict = Dict(); kwargs...) - generate("https://github.com/abelsiqueira/BestieTemplate.jl", dst_path, data; kwargs...) +function apply(dst_path, data::Dict = Dict(); kwargs...) + apply("https://github.com/abelsiqueira/BestieTemplate.jl", dst_path, data; kwargs...) end """ diff --git a/test/runtests.jl b/test/runtests.jl index 639237a2..06ef3b9a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -50,6 +50,14 @@ template_options = Dict( "AddCopierCI" => false, ) +function _git_setup() + run(`git init`) + run(`git add .`) + run(`git config user.name "Test"`) + run(`git config user.email "test@test.com"`) + run(`git commit -q -m "First commit"`) +end + function test_diff_dir(dir1, dir2) ignore(line) = startswith("_commit")(line) || startswith("_src_path")(line) @testset "$(basename(dir1)) vs $(basename(dir2))" begin @@ -115,22 +123,14 @@ end mktempdir(TMPDIR; prefix = "cli_") do dir_copier_cli run(`copier copy --defaults --quiet $min_bash_args $template_url $dir_copier_cli`) cd(dir_copier_cli) do - run(`git init`) - run(`git add .`) - run(`git config user.name "Test"`) - run(`git config user.email "test@test.com"`) - run(`git commit -q -m "First commit"`) + _git_setup() end run(`copier update --defaults --quiet $bash_args $dir_copier_cli`) mktempdir(TMPDIR; prefix = "update_") do tmpdir BestieTemplate.generate(tmpdir, template_minimum_options; defaults = true, quiet = true) cd(tmpdir) do - run(`git init`) - run(`git add .`) - run(`git config user.name "Test"`) - run(`git config user.email "test@test.com"`) - run(`git commit -q -m "First commit"`) + _git_setup() BestieTemplate.update(template_options; defaults = true, quiet = true) end @@ -142,8 +142,51 @@ end @testset "Test that BestieTemplate.generate warns and exits for existing copy" begin mktempdir(TMPDIR; prefix = "cli_") do dir_copier_cli run(`copier copy --vcs-ref HEAD --quiet $bash_args $template_url $dir_copier_cli`) + cd(dir_copier_cli) do + _git_setup() + end - @test_logs (:warn,) BestieTemplate.generate(dir_copier_cli; quiet = true) + @test_logs (:warn,) BestieTemplate.apply(dir_copier_cli; quiet = true) + end +end + +@testset "Test that generate fails for existing non-empty paths" begin + mktempdir(TMPDIR) do dir + cd(dir) do + @testset "It fails if the dst_path exists and is non-empty" begin + mkdir("some_folder1") + open(joinpath("some_folder1", "README.md"), "w") do io + println(io, "Hi") + end + @test_throws Exception BestieTemplate.generate("some_folder1") + end + + @testset "It works if the dst_path is ." begin + mkdir("some_folder2") + cd("some_folder2") do + # Should not throw + BestieTemplate.generate( + template_path, + ".", + template_options; + quiet = true, + vcs_ref = "HEAD", + ) + end + end + + @testset "It works if the dst_path exists but is empty" begin + mkdir("some_folder3") + # Should not throw + BestieTemplate.generate( + template_path, + "some_folder3", + template_options; + quiet = true, + vcs_ref = "HEAD", + ) + end + end end end @@ -195,11 +238,14 @@ end end end -@testset "Test generating the template on an existing project" begin +@testset "Test applying the template on an existing project" begin mktempdir(TMPDIR; prefix = "existing_") do dir_existing cd(dir_existing) do Pkg.generate("NewPkg") - BestieTemplate.generate( + cd("NewPkg") do + _git_setup() + end + BestieTemplate.apply( template_path, "NewPkg/", Dict("AuthorName" => "T. Esther", "PackageOwner" => "test"); @@ -241,4 +287,19 @@ end end end end + + @testset "Test input validation of apply" begin + mktempdir(TMPDIR) do dir + cd(dir) do + @testset "It fails if the dst_path does not exist" begin + @test_throws Exception BestieTemplate.generate("some_folder1") + end + + @testset "It fails if the dst_path exists but does not contains .git" begin + mkdir("some_folder2") + @test_throws Exception BestieTemplate.generate("some_folder2") + end + end + end + end end