Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Func to fixup existing pkg #483

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions src/PkgTemplates.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
@doc read(joinpath(dirname(@__DIR__), "README.md"), String)
module PkgTemplates
@doc read(joinpath(dirname(@__DIR__), "README.md"), String) module PkgTemplates

using Base: active_project, contractuser

Expand All @@ -15,8 +14,7 @@

using Mocking

export
Template,
export Template,
AppVeyor,
BlueStyleBadge,
CirrusCI,
Expand Down Expand Up @@ -46,7 +44,8 @@
SrcDir,
TagBot,
Tests,
TravisCI
TravisCI,
fixup

"""
Plugins are PkgTemplates' source of customization and extensibility.
Expand All @@ -56,10 +55,19 @@
"""
abstract type Plugin end

"""
isfixable(::Plugin, pkg_dir) -> Bool

Determines whether or not the plugin can be updated on an existing project via
[`fixup`](@ref).
"""
isfixable(::Plugin, pkg_dir) = false

Check warning on line 64 in src/PkgTemplates.jl

View check run for this annotation

Codecov / codecov/patch

src/PkgTemplates.jl#L64

Added line #L64 was not covered by tests

include("template.jl")
include("plugin.jl")
include("show.jl")
include("interactive.jl")
include("fixup.jl")
include("deprecated.jl")

# Run some function with a project activated at the given path.
Expand Down
4 changes: 2 additions & 2 deletions src/deprecated.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@deprecate generate(t::Template, pkg::AbstractString) t(pkg)
@deprecate generate(pkg::AbstractString, t::Template) t(pkg)
@deprecate interactive_template() Template(; interactive=true)
@deprecate generate_interactive(pkg::AbstractString) Template(; interactive=true)(pkg)
@deprecate interactive_template() Template(; interactive = true)
@deprecate generate_interactive(pkg::AbstractString) Template(; interactive = true)(pkg)
@deprecate GitHubPages(; kwargs...) Documenter{TravisCI}(; kwargs...)
@deprecate GitLabPages(; kwargs...) Documenter{GitLabCI}(; kwargs...)
56 changes: 56 additions & 0 deletions src/fixup.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""
fixup(tpl::Template, pkg_dir)

Fixes up the package at `pkg_dir` according to the template `tpl`. Returns the path to the fixed package and the path to the backup folder.

## Example

```julia
using PkgTemplates

# Original package:
t = Template(user="my-username", dir="~")
pkg_dir = t("MyPkg.jl")

# Fixup the package (with Documenter plugin):
t = Template(
user="my-username", dir="~",
authors="Acme Corp",
plugins=[
Documenter{GitHubActions}(),
]
)
pkg_dir, backup = fixup(t, pkg_dir)
```
"""
function fixup(tpl::Template, pkg_dir)

Check warning on line 26 in src/fixup.jl

View check run for this annotation

Codecov / codecov/patch

src/fixup.jl#L26

Added line #L26 was not covered by tests

# Assertions:
pkg_dir = realpath(pkg_dir)
ispath(pkg_dir) || throw(ArgumentError("Not a directory."))
isdir(joinpath(pkg_dir, "src")) || throw(ArgumentError("No `src/` directory."))

Check warning on line 31 in src/fixup.jl

View check run for this annotation

Codecov / codecov/patch

src/fixup.jl#L29-L31

Added lines #L29 - L31 were not covered by tests

# Back up in temporary directory:
backup = joinpath(tempdir(), splitpath(pkg_dir)[end])
if !isdir(backup)
@info "Fixing up the package at $pkg_dir might require overwriting files.\nThe current state of the package is backed up at $backup. Hit ENTER to continue."
readline()
run(`cp -r $pkg_dir $backup`)

Check warning on line 38 in src/fixup.jl

View check run for this annotation

Codecov / codecov/patch

src/fixup.jl#L34-L38

Added lines #L34 - L38 were not covered by tests
else
@warn "Existing backup for $pkg_dir found at $backup. Skipping backup. If you are sure that you want to apply the fix-up again, hit ENTER to continue."
readline()

Check warning on line 41 in src/fixup.jl

View check run for this annotation

Codecov / codecov/patch

src/fixup.jl#L40-L41

Added lines #L40 - L41 were not covered by tests
end

# Fix all plugins that are fixable:
fixable = filter(p -> isfixable(p, pkg_dir), tpl.plugins)
foreach((prehook, hook, posthook)) do h
@info "Running $(nameof(h))s"
foreach(sort(fixable; by = p -> priority(p, h), rev = true)) do p
h(p, tpl, pkg_dir)

Check warning on line 49 in src/fixup.jl

View check run for this annotation

Codecov / codecov/patch

src/fixup.jl#L45-L49

Added lines #L45 - L49 were not covered by tests
end
end
@info "Fixed up package at $pkg_dir. The old state of the package is backed up at $backup."

Check warning on line 52 in src/fixup.jl

View check run for this annotation

Codecov / codecov/patch

src/fixup.jl#L52

Added line #L52 was not covered by tests
# TODO: some magic to add badges to an existing Readme?!

return pkg_dir, backup

Check warning on line 55 in src/fixup.jl

View check run for this annotation

Codecov / codecov/patch

src/fixup.jl#L55

Added line #L55 was not covered by tests
end
37 changes: 21 additions & 16 deletions src/interactive.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
Shortcut for `Template(; interactive=true)(pkg)`.
If no package name is supplied, you will be prompted for one.
"""
function generate(pkg::AbstractString=prompt(Template, String, :pkg))
t = Template(; interactive=true)
function generate(pkg::AbstractString = prompt(Template, String, :pkg))
t = Template(; interactive = true)

Check warning on line 8 in src/interactive.jl

View check run for this annotation

Codecov / codecov/patch

src/interactive.jl#L7-L8

Added lines #L7 - L8 were not covered by tests
t(pkg)
return t
end
Expand All @@ -17,7 +17,7 @@
related functions only if you want completely custom behaviour.
"""
function interactive(T::Type)
pairs = Vector{Pair{Symbol, Type}}(interactive_pairs(T))
pairs = Vector{Pair{Symbol,Type}}(interactive_pairs(T))

# There must be at least 2 MultiSelectMenu options.
# If there are none, return immediately.
Expand All @@ -34,13 +34,13 @@
"$k"
end
end
menu = MultiSelectMenu(opts; pagesize=length(pairs))
menu = MultiSelectMenu(opts; pagesize = length(pairs))
customize = sort!(collect(request(menu)))

# If the "None" option was selected, don't customize anything.
just_one && lastindex(pairs) in customize && return T()

kwargs = Dict{Symbol, Any}()
kwargs = Dict{Symbol,Any}()
foreach(pairs[customize]) do (name, F)
kwargs[name] = prompt(T, F, name)
end
Expand All @@ -64,7 +64,7 @@
r"Array{(.*?),1}" => s"Vector{\1}",
r"Union{Nothing, (.*?)}" => s"Union{\1, Nothing}",
]
return reduce((s, p) -> replace(s, p), replacements; init=s)
return reduce((s, p) -> replace(s, p), replacements; init = s)
end

"""
Expand All @@ -73,12 +73,12 @@
Provide some extra tips to users on how to structure their input for the type `T`,
for example if multiple delimited values are expected.
"""
input_tips(::Type{Vector{T}}) where T = [input_tips(T)..., "comma-delimited"]
input_tips(::Type{Union{T, Nothing}}) where T = [input_tips(T)..., input_tips(Nothing)...]
input_tips(::Type{Vector{T}}) where {T} = [input_tips(T)..., "comma-delimited"]
input_tips(::Type{Union{T,Nothing}}) where {T} = [input_tips(T)..., input_tips(Nothing)...]
input_tips(::Type{Nothing}) = ["'nothing' for nothing"]
input_tips(::Type{Secret}) = ["name only"]
# Show expected input type as a tip if it's anything other than `String`
input_tips(::Type{T}) where T = String[string(T)]
input_tips(::Type{T}) where {T} = String[string(T)]
input_tips(::Type{String}) = String[]
input_tips(::Type{<:Signed}) = ["Int"] # Specific Int type likely not important

Expand All @@ -91,19 +91,23 @@
convert_input(::Type, T::Type{<:Real}, s::AbstractString) = parse(T, s)
convert_input(::Type, T::Type, s::AbstractString) = T(s)

function convert_input(P::Type, ::Type{Union{T, Nothing}}, s::AbstractString) where T
function convert_input(P::Type, ::Type{Union{T,Nothing}}, s::AbstractString) where {T}
# This is kind of sketchy because technically, there might be some other input
# whose value we want to instantiate with the string "nothing",
# but I think that would be a pretty rare occurrence.
# If that really happens, they can just override this method.
return s == "nothing" ? nothing : convert_input(P, T, s)
end

function convert_input(P::Type, ::Type{Union{T, Symbol, Nothing}}, s::AbstractString) where T
function convert_input(
P::Type,
::Type{Union{T,Symbol,Nothing}},
s::AbstractString,
) where {T}
# Assume inputs starting with ':' char are intended as Symbols, if a plugin accept symbols.
# i.e. assume the set of valid Symbols the plugin expects can be spelt starting with ':'.
return if startswith(s, ":")
Symbol(chop(s, head=1, tail=0)) # remove ':'
Symbol(chop(s, head = 1, tail = 0)) # remove ':'
else
convert_input(P, Union{T,Nothing}, s)
end
Expand Down Expand Up @@ -140,7 +144,7 @@
prompt(P::Type, T::Type, name::Symbol) = prompt(P, T, Val(name))

# The trailing `nothing` is a hack for `fallback_prompt` to use, ignore it.
function prompt(P::Type, ::Type{T}, ::Val{name}, ::Nothing=nothing) where {T, name}
function prompt(P::Type, ::Type{T}, ::Val{name}, ::Nothing = nothing) where {T,name}
default = defaultkw(P, name)
tips = join([input_tips(T); "default: $(input_string(default))"], ", ")
input = Base.prompt(pretty_message("Enter value for '$name' ($tips)"))
Expand Down Expand Up @@ -170,8 +174,9 @@
end

# Compute all the concrete subtypes of T.
concretes_rec(T::Type) = isabstracttype(T) ? vcat(map(concretes_rec, subtypes(T))...) : Any[T]
concretes(T::Type) = sort!(concretes_rec(T); by=nameof)
concretes_rec(T::Type) =
isabstracttype(T) ? vcat(map(concretes_rec, subtypes(T))...) : Any[T]
concretes(T::Type) = sort!(concretes_rec(T); by = nameof)

# Compute name => type pairs for T's interactive options.
function interactive_pairs(T::Type)
Expand All @@ -181,7 +186,7 @@
prepend!(pairs, reverse(customizable(T)))
uniqueby!(first, pairs)
filter!(p -> last(p) !== NotCustomizable, pairs)
sort!(pairs; by=first)
sort!(pairs; by = first)

return pairs
end
Expand Down
34 changes: 25 additions & 9 deletions src/plugin.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const DEFAULT_PRIORITY = 1000
const DEFAULT_TEMPLATE_DIR = Ref{String}(joinpath(dirname(dirname(pathof(PkgTemplates))), "templates"))
const DEFAULT_TEMPLATE_DIR =
Ref{String}(joinpath(dirname(dirname(pathof(PkgTemplates))), "templates"))

"""
@plugin struct ... end
Expand Down Expand Up @@ -64,7 +65,11 @@

msg = "Run `using PkgTemplates: @with_kw_noshow` before using this macro"
@assert isdefined(__module__, Symbol("@with_kw_noshow")) msg
block = :(begin @with_kw_noshow $ex end)
block = :(
begin
@with_kw_noshow $ex
end
)

foreach(filter(arg -> arg isa Expr, ex.args[3].args)) do field
@assert field.head === :(=) "Field must have a default value"
Expand All @@ -77,7 +82,7 @@
return esc(block)
end

function Base.:(==)(a::T, b::T) where T <: Plugin
function Base.:(==)(a::T, b::T) where {T<:Plugin}
return all(n -> getfield(a, n) == getfield(b, n), fieldnames(T))
end

Expand Down Expand Up @@ -122,7 +127,7 @@

By default, an empty `Dict` is returned.
"""
view(::Plugin, ::Template, ::AbstractString) = Dict{String, Any}()
view(::Plugin, ::Template, ::AbstractString) = Dict{String,Any}()

"""
user_view(::Plugin, ::Template, pkg::AbstractString) -> Dict{String, Any}
Expand All @@ -132,7 +137,7 @@
Values returned by this function will override those from [`view`](@ref)
when the keys are the same.
"""
user_view(::Plugin, ::Template, ::AbstractString) = Dict{String, Any}()
user_view(::Plugin, ::Template, ::AbstractString) = Dict{String,Any}()

"""
combined_view(::Plugin, ::Template, pkg::AbstractString) -> Dict{String, Any}
Expand Down Expand Up @@ -210,6 +215,17 @@
"""
function destination end

"""
isfixable(p::FilePlugin) -> Bool

Determines whether or not [`fixup`](@ref) should update the files created by `p`.

By default, returns `true` if the [`destination(p)`](@ref) file does not exist.
Subtype of [`FilePlugin`](@ref) should implement their own method if they require
different behaviour.
"""
isfixable(p::FilePlugin, pkg_dir) = !isfile(joinpath(pkg_dir, destination(p)))

Check warning on line 227 in src/plugin.jl

View check run for this annotation

Codecov / codecov/patch

src/plugin.jl#L227

Added line #L227 was not covered by tests

"""
Badge(hover::AbstractString, image::AbstractString, link::AbstractString)

Expand Down Expand Up @@ -285,7 +301,7 @@
"""
posthook(::Plugin, ::Template, ::AbstractString) = nothing

function validate(p::T, ::Template) where T <: FilePlugin
function validate(p::T, ::Template) where {T<:FilePlugin}
src = source(p)
src === nothing && return
isfile(src) || throw(ArgumentError("$(nameof(T)): The file $src does not exist"))
Expand Down Expand Up @@ -322,7 +338,7 @@
`tags` should be a tuple of two strings, which are the opening and closing delimiters,
or `nothing` to use the default delimiters.
"""
function render_file(file::AbstractString, view::Dict{<:AbstractString}, tags=nothing)
function render_file(file::AbstractString, view::Dict{<:AbstractString}, tags = nothing)
return render_text(read(file, String), view, tags)
end

Expand All @@ -333,8 +349,8 @@
`tags` should be a tuple of two strings, which are the opening and closing delimiters,
or `nothing` to use the default delimiters.
"""
function render_text(text::AbstractString, view::Dict{<:AbstractString}, tags=nothing)
return tags === nothing ? render(text, view) : render(text, view; tags=tags)
function render_text(text::AbstractString, view::Dict{<:AbstractString}, tags = nothing)
return tags === nothing ? render(text, view) : render(text, view; tags = tags)
end

"""
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/badges.jl
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ function badges(::PkgEvalBadge)
return Badge(
"PkgEval",
"https://JuliaCI.github.io/NanosoldierReports/pkgeval_badges/{{{PKG1}}}/{{{PKG}}}.svg",
"https://JuliaCI.github.io/NanosoldierReports/pkgeval_badges/{{{PKG1}}}/{{{PKG}}}.html"
"https://JuliaCI.github.io/NanosoldierReports/pkgeval_badges/{{{PKG1}}}/{{{PKG}}}.html",
)
end

Expand Down
6 changes: 3 additions & 3 deletions src/plugins/ci.jl
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ function view(p::GitHubActions, t::Template, pkg::AbstractString)
p.osx && push!(os, "macOS-latest")
p.windows && push!(os, "windows-latest")
arch = filter(a -> getfield(p, Symbol(a)), ["x64", "x86"])
excludes = Dict{String, String}[]
excludes = Dict{String,String}[]
p.osx && p.x86 && push!(excludes, Dict("E_OS" => "macOS-latest", "E_ARCH" => "x86"))

v = Dict(
Expand Down Expand Up @@ -149,7 +149,7 @@ function view(p::TravisCI, t::Template, pkg::AbstractString)
versions = collect_versions(t, p.extra_versions)
allow_failures = filter(in(versions), ALLOWED_FAILURES)

excludes = Dict{String, String}[]
excludes = Dict{String,String}[]
p.x86 && p.osx && push!(excludes, Dict("E_OS" => "osx", "E_ARCH" => "x86"))
if p.arm64
p.osx && push!(excludes, Dict("E_OS" => "osx", "E_ARCH" => "arm64"))
Expand Down Expand Up @@ -416,7 +416,7 @@ function collect_versions(t::Template, versions::Vector)
return sort(unique(vs))
end

const AllCI = Union{AppVeyor, GitHubActions, TravisCI, CirrusCI, GitLabCI, DroneCI}
const AllCI = Union{AppVeyor,GitHubActions,TravisCI,CirrusCI,GitLabCI,DroneCI}

"""
is_ci(::Plugin) -> Bool
Expand Down
12 changes: 9 additions & 3 deletions src/plugins/codeowners.jl
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,16 @@ end

function PkgTemplates.validate(p::CodeOwners, ::Template)
for (pattern, subowners) in p.owners
contains(pattern, r"\s") && throw(ArgumentError(("Pattern ($pattern) must not contain whitespace")))
contains(pattern, r"\s") &&
throw(ArgumentError(("Pattern ($pattern) must not contain whitespace")))
for subowner in subowners
contains(subowner, r"\s") && throw(ArgumentError("Owner name ($subowner) must not contain whitespace"))
'@' ∈ subowner || throw(ArgumentError("Owner name ($subowner) must be `@user` or `[email protected]`"))
contains(subowner, r"\s") &&
throw(ArgumentError("Owner name ($subowner) must not contain whitespace"))
'@' ∈ subowner || throw(
ArgumentError(
"Owner name ($subowner) must be `@user` or `[email protected]`",
),
)
end
end
end
Loading
Loading