2017-08-11 22:18:09 +00:00
|
|
|
"""
|
|
|
|
Template(; kwargs...) -> Template
|
|
|
|
|
2017-08-21 19:53:00 +00:00
|
|
|
Records common information used to generate a package. If you don't wish to manually
|
2017-08-21 21:19:40 +00:00
|
|
|
create a template, you can use [`interactive_template`](@ref) instead.
|
2017-08-11 22:18:09 +00:00
|
|
|
|
|
|
|
# Keyword Arguments
|
2017-08-16 22:01:52 +00:00
|
|
|
* `user::AbstractString="")`: GitHub username. If left unset, it will try to take the
|
|
|
|
value of a supplied git config's "github.username" key, then the global git config's
|
|
|
|
value. If neither is set, an `ArgumentError` is thrown.
|
|
|
|
**This is case-sensitive for some plugins, so take care to enter it correctly.**
|
|
|
|
* `host::AbstractString="github.com"`: URL to the code hosting service where your package
|
2017-08-18 07:08:11 +00:00
|
|
|
will reside. Note that while hosts other than GitHub won't cause errors, they are not
|
|
|
|
officially supported and they will cause certain plugins will produce incorrect output.
|
|
|
|
For example, [`AppVeyor`](@ref)'s badge image will point to a GitHub-specific URL,
|
|
|
|
regardless of the value of `host`.
|
2017-08-22 16:29:53 +00:00
|
|
|
* `license::Union{AbstractString, Void}="MIT"`: Name of the package license. If
|
2017-08-16 06:58:54 +00:00
|
|
|
no license is specified, no license is created. [`show_license`](@ref) can be used to
|
|
|
|
list all available licenses, or to print out a particular license's text.
|
2017-08-16 22:01:52 +00:00
|
|
|
* `authors::Union{AbstractString, Array}=""`: Names that appear on the license. Supply a
|
|
|
|
string for one author, and an array for multiple. Similarly to `user`, it will try to
|
|
|
|
take the value of a supplied git config's "user.name" key, then the global git config's
|
|
|
|
value, if it is left unset
|
2017-08-21 19:53:00 +00:00
|
|
|
* `years::Union{Int, AbstractString}=Dates.year(now())`: Copyright years on the license.
|
|
|
|
Can be supplied by a number, or a string such as "2016 - 2017".
|
2017-08-15 23:46:36 +00:00
|
|
|
* `dir::AbstractString=Pkg.dir()`: Directory in which the package will go.
|
2017-08-11 22:18:09 +00:00
|
|
|
* `julia_version::VersionNumber=VERSION`: Minimum allowed Julia version.
|
2017-08-18 07:08:03 +00:00
|
|
|
* `requirements::Vector{String}=String[]`: Package requirements. If there are duplicate
|
|
|
|
requirements with different versions, i.e. ["PkgTemplates", "PkgTemplates 0.1"],
|
|
|
|
an `ArgumentError` is thrown.
|
|
|
|
Each entry in this array will be copied into the `REQUIRE` file of packages generated
|
|
|
|
with this template.
|
2017-08-11 22:18:09 +00:00
|
|
|
* `git_config::Dict{String, String}=Dict{String, String}()`: Git configuration options.
|
2017-08-18 07:08:03 +00:00
|
|
|
* `plugins::Plugin[]`: A list of `Plugin`s that the package will include.
|
2017-08-16 23:04:25 +00:00
|
|
|
|
2017-08-18 07:08:03 +00:00
|
|
|
# Notes
|
|
|
|
When you create a `Template`, a temporary directory is created with
|
2017-08-16 23:04:25 +00:00
|
|
|
`mktempdir()`. This directory will be removed after you call [`generate`](@ref).
|
|
|
|
Creating multiple packages in succession with the same instance of a template will still
|
|
|
|
work, but there is a miniscule chance of another process sharing the temporary directory,
|
|
|
|
which could result in the created package repository containing untracked files that
|
|
|
|
don't belong.
|
2017-08-11 22:18:09 +00:00
|
|
|
"""
|
2017-08-14 18:12:37 +00:00
|
|
|
@auto_hash_equals struct Template
|
2017-08-15 16:10:05 +00:00
|
|
|
user::AbstractString
|
|
|
|
host::AbstractString
|
2017-08-11 22:18:09 +00:00
|
|
|
license::Union{AbstractString, Void}
|
|
|
|
authors::Union{AbstractString, Array}
|
|
|
|
years::AbstractString
|
2017-08-15 23:46:36 +00:00
|
|
|
dir::AbstractString
|
|
|
|
temp_dir::AbstractString
|
2017-08-11 22:18:09 +00:00
|
|
|
julia_version::VersionNumber
|
2017-08-18 07:08:03 +00:00
|
|
|
requirements::Vector{AbstractString}
|
2017-08-16 06:11:53 +00:00
|
|
|
git_config::Dict
|
2017-08-11 22:18:09 +00:00
|
|
|
plugins::Dict{DataType, Plugin}
|
|
|
|
|
2017-08-15 16:10:05 +00:00
|
|
|
function Template(;
|
2017-08-16 22:01:52 +00:00
|
|
|
user::AbstractString="",
|
2017-08-15 16:10:05 +00:00
|
|
|
host::AbstractString="https://github.com",
|
2017-08-22 16:29:53 +00:00
|
|
|
license::Union{AbstractString, Void}="MIT",
|
2017-08-16 22:01:52 +00:00
|
|
|
authors::Union{AbstractString, Array}="",
|
2017-08-21 19:53:00 +00:00
|
|
|
years::Union{Int, AbstractString}=Dates.year(now()),
|
2017-08-15 23:46:36 +00:00
|
|
|
dir::AbstractString=Pkg.dir(),
|
2017-08-11 22:18:09 +00:00
|
|
|
julia_version::VersionNumber=VERSION,
|
2017-08-18 07:08:03 +00:00
|
|
|
requirements::Vector{String}=String[],
|
2017-08-16 06:11:53 +00:00
|
|
|
git_config::Dict=Dict(),
|
2017-08-18 07:08:03 +00:00
|
|
|
plugins::Vector{P}=Plugin[],
|
2017-08-15 16:10:05 +00:00
|
|
|
) where P <: Plugin
|
2017-08-16 22:01:52 +00:00
|
|
|
# If no username was set, look for one in a supplied git config,
|
|
|
|
# and then in the global git config.
|
|
|
|
if isempty(user)
|
|
|
|
user = get(
|
|
|
|
git_config, "github.username",
|
|
|
|
LibGit2.getconfig("github.username", ""),
|
|
|
|
)
|
|
|
|
end
|
|
|
|
if isempty(user)
|
2017-08-15 16:10:05 +00:00
|
|
|
throw(ArgumentError("No GitHub username found, set one with user=username"))
|
|
|
|
end
|
|
|
|
|
|
|
|
host = URI(startswith(host, "https://") ? host : "https://$host").host
|
|
|
|
|
|
|
|
if license != nothing && !isfile(joinpath(LICENSE_DIR, license))
|
|
|
|
throw(ArgumentError("License '$license' is not available"))
|
2017-08-11 22:18:09 +00:00
|
|
|
end
|
2017-08-14 20:58:01 +00:00
|
|
|
|
2017-08-16 22:01:52 +00:00
|
|
|
# If no author was set, look for one in the supplied git config,
|
|
|
|
# and then in the global git config.
|
|
|
|
if isempty(authors)
|
2017-08-14 20:58:01 +00:00
|
|
|
authors = get(git_config, "user.name", LibGit2.getconfig("user.name", ""))
|
|
|
|
elseif isa(authors, Array)
|
2017-08-11 22:18:09 +00:00
|
|
|
authors = join(authors, ", ")
|
|
|
|
end
|
|
|
|
|
2017-08-15 16:10:05 +00:00
|
|
|
years = string(years)
|
2017-08-14 19:15:53 +00:00
|
|
|
|
2017-08-15 23:46:36 +00:00
|
|
|
temp_dir = mktempdir()
|
|
|
|
|
2017-08-18 07:08:03 +00:00
|
|
|
requirements_dedup = collect(Set(requirements))
|
|
|
|
diff = length(requirements) - length(requirements_dedup)
|
|
|
|
names = [tokens[1] for tokens in split.(requirements_dedup)]
|
|
|
|
if length(names) > length(Set(names))
|
|
|
|
throw(ArgumentError(
|
|
|
|
"requirements contains duplicate packages with conflicting versions"
|
|
|
|
))
|
|
|
|
elseif diff > 0
|
|
|
|
warn("Removed $(diff) duplicate$(diff == 1 ? "" : "s") from requirements")
|
|
|
|
end
|
|
|
|
|
2017-08-14 19:15:53 +00:00
|
|
|
plugin_dict = Dict{DataType, Plugin}(typeof(p) => p for p in plugins)
|
|
|
|
if (length(plugins) != length(plugin_dict))
|
|
|
|
warn("Plugin list contained duplicates, only the last of each type was kept")
|
|
|
|
end
|
2017-08-11 22:18:09 +00:00
|
|
|
|
|
|
|
new(
|
2017-08-15 23:46:36 +00:00
|
|
|
user, host, license, authors, years, dir, temp_dir,
|
2017-08-18 07:08:03 +00:00
|
|
|
julia_version, requirements_dedup, git_config, plugin_dict
|
2017-08-11 22:18:09 +00:00
|
|
|
)
|
|
|
|
end
|
|
|
|
end
|
2017-08-21 19:53:00 +00:00
|
|
|
|
|
|
|
"""
|
|
|
|
interactive_template() -> Template
|
|
|
|
|
2017-08-21 21:19:40 +00:00
|
|
|
Interactively create a [`Template`](@ref).
|
2017-08-21 19:53:00 +00:00
|
|
|
"""
|
2017-08-21 21:19:40 +00:00
|
|
|
function interactive_template()
|
|
|
|
info("Default values are shown in [brackets]")
|
2017-08-21 19:53:00 +00:00
|
|
|
# Getting the leaf types in a separate thread eliminates an awkward wait after
|
|
|
|
# "Select plugins" is printed.
|
|
|
|
plugin_types = @spawn leaves(Plugin)
|
|
|
|
kwargs = Dict{Symbol, Any}()
|
|
|
|
|
|
|
|
default_user = LibGit2.getconfig("github.username", "")
|
|
|
|
print("Enter your username [$(isempty(default_user) ? "REQUIRED" : default_user)]: ")
|
|
|
|
user = readline()
|
|
|
|
kwargs[:user] = if !isempty(user)
|
|
|
|
user
|
|
|
|
elseif !isempty(default_user)
|
|
|
|
default_user
|
|
|
|
else
|
|
|
|
throw(ArgumentError("Username is required"))
|
|
|
|
end
|
|
|
|
|
|
|
|
default_host = "github.com"
|
|
|
|
print("Enter the code hosting service [$default_host]: ")
|
|
|
|
host = readline()
|
|
|
|
kwargs[:host] = isempty(host) ? default_host : host
|
|
|
|
|
|
|
|
println("Select a license:")
|
|
|
|
io = IOBuffer()
|
|
|
|
show_license(; io=io)
|
|
|
|
licenses = [nothing => nothing, collect(LICENSES)...]
|
|
|
|
menu = RadioMenu(["None", split(String(take!(io)), "\n")...])
|
|
|
|
# If the user breaks out of the menu with C-c, the result is -1, the absolute value of
|
|
|
|
# which correponds to no license.
|
|
|
|
kwargs[:license] = licenses[abs(request(menu))].first
|
|
|
|
|
|
|
|
default_authors = LibGit2.getconfig("user.name", "")
|
|
|
|
default_str = isempty(default_authors) ? "None" : default_authors
|
|
|
|
print("Enter the package author(s) [$default_str]: ")
|
|
|
|
authors = readline()
|
|
|
|
kwargs[:authors] = isempty(authors) ? default_authors : authors
|
|
|
|
|
|
|
|
default_years = Dates.year(now())
|
|
|
|
print("Enter the copyright year(s) [$default_years]: ")
|
|
|
|
years = readline()
|
|
|
|
kwargs[:years] = isempty(years) ? default_years : years
|
|
|
|
|
|
|
|
default_dir = Pkg.dir()
|
|
|
|
print("Enter the path to the package directory [$default_dir]: ")
|
|
|
|
dir = readline()
|
|
|
|
kwargs[:dir] = isempty(dir) ? default_dir : dir
|
|
|
|
|
|
|
|
default_julia_version = VERSION
|
|
|
|
print("Enter the minimum Julia version [$default_julia_version]: ")
|
|
|
|
julia_version = readline()
|
|
|
|
kwargs[:julia_version] = if isempty(julia_version)
|
|
|
|
default_julia_version
|
|
|
|
else
|
|
|
|
VersionNumber(julia_version)
|
|
|
|
end
|
|
|
|
|
|
|
|
print("Enter any Julia package requirements, (separated by spaces) []: ")
|
|
|
|
requirements = String.(split(readline()))
|
|
|
|
|
|
|
|
git_config = Dict()
|
|
|
|
print("Enter any Git key-value pairs (one at a time, separated by spaces) [None]: ")
|
|
|
|
while true
|
|
|
|
tokens = split(readline())
|
|
|
|
isempty(tokens) && break
|
|
|
|
if haskey(git_config, tokens[1])
|
|
|
|
warn("Duplicate key '$(tokens[1])': Replacing old value '$(tokens[2])'")
|
|
|
|
end
|
|
|
|
git_config[tokens[1]] = tokens[2]
|
|
|
|
end
|
|
|
|
kwargs[:git_config] = git_config
|
|
|
|
|
|
|
|
println("Select plugins:")
|
2017-08-21 21:19:40 +00:00
|
|
|
# Only include plugin types which have an `interactive` method.
|
|
|
|
plugin_types = filter(t -> method_exists(interactive, (Type{t},)), fetch(plugin_types))
|
2017-08-21 19:53:00 +00:00
|
|
|
type_names = map(t -> split(string(t), ".")[end], plugin_types)
|
|
|
|
menu = MultiSelectMenu(String.(type_names); pagesize=length(type_names))
|
|
|
|
selected = collect(request(menu))
|
2017-08-21 21:19:40 +00:00
|
|
|
kwargs[:plugins] = map(t -> interactive(t), getindex(plugin_types, selected))
|
2017-08-21 19:53:00 +00:00
|
|
|
|
|
|
|
return Template(; kwargs...)
|
|
|
|
end
|
|
|
|
|
|
|
|
"""
|
|
|
|
leaves(t:Type) -> Vector{DataType}
|
|
|
|
|
|
|
|
Get all concrete subtypes of `t`.
|
|
|
|
"""
|
|
|
|
leaves(t::Type) = isleaftype(t) ? t : vcat(leaves.(subtypes(t))...)
|