Update contributing guide
This commit is contained in:
parent
a265956339
commit
dfa9f123de
189
CONTRIBUTING.md
189
CONTRIBUTING.md
@ -2,126 +2,135 @@
|
|||||||
|
|
||||||
The best way to contribute to `PkgTemplates` is by adding new plugins.
|
The best way to contribute to `PkgTemplates` is by adding new plugins.
|
||||||
|
|
||||||
Plugins are pretty simple. They're defined as subtypes to `Plugin`, in their
|
There are two main types of plugins:
|
||||||
own file inside `src/plugins`. Let's create one, called `MyPlugin`, in
|
[`GenericPlugin`](https://invenia.github.io/PkgTemplates.jl/stable/pages/plugins.html#GenericPlugin-1)s
|
||||||
|
and
|
||||||
|
[`CustomPlugin`](https://invenia.github.io/PkgTemplates.jl/stable/pages/plugins.html#CustomPlugin-1)s.
|
||||||
|
|
||||||
|
## Writing a Generic Plugin
|
||||||
|
|
||||||
|
As the name suggests, generic plugins are simpler than custom ones, and as
|
||||||
|
such are extremely easy to implement. They have the ability to add patterns
|
||||||
|
the the generated `.gitignore`, as well as create a single configuration file.
|
||||||
|
We're going to define a new generic plugin `MyPlugin` in
|
||||||
`src/plugins/myplugin.jl`:
|
`src/plugins/myplugin.jl`:
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
@auto_hash_equals struct MyPlugin <: Plugin end
|
@auto_hash_equals struct MyPlugin <: GenericPlugin
|
||||||
```
|
gitignore::Vector{AbstractString}
|
||||||
|
src::Nullable{AbstractString}
|
||||||
The `@auto_hash_equals` macro means we don't have to implement `==` or `hash`
|
dest::AbstractString
|
||||||
ourselves ([ref](https://github.com/andrewcooke/AutoHashEquals.jl)).
|
badges::Vector{AbstractString}
|
||||||
|
view::Dict{String, Any}
|
||||||
All plugins need at least one attribute: `gitignore_files`. This is a
|
|
||||||
`Vector{AbstractString}`, of which each entry will be inserted in the
|
|
||||||
`.gitignore` of generated packages that use this plugin.
|
|
||||||
|
|
||||||
Maybe the service that `MyPlugin` is associated with creates a directory
|
|
||||||
called `secrets`, containing top secret data. In that case, `gitignore_files`
|
|
||||||
should contain that string:
|
|
||||||
|
|
||||||
```julia
|
|
||||||
@auto_hash_equals struct MyPlugin <: Plugin
|
|
||||||
gitignore_files::Vector{AbstractString}
|
|
||||||
|
|
||||||
function MyPlugin()
|
|
||||||
new(["/secrets"])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also add patterns like `*.key`, etc. to this array. Note that Windows
|
|
||||||
Git also recognizes `/` as a path separator in `.gitignore`, so there's no
|
|
||||||
need for `joinpath`.
|
|
||||||
|
|
||||||
Suppose that `MyPlugin` also has a configuration file at the root of the repo.
|
|
||||||
We're going to put a default `myplugin.yml` in `defaults`, but we also want
|
|
||||||
to let users supply their own, or choose to not use one at all:
|
|
||||||
|
|
||||||
```julia
|
|
||||||
@auto_hash_equals struct MyPlugin <: Plugin
|
|
||||||
gitignore_files::Vector{AbstractString}
|
|
||||||
config_file::Union{AbstractString, Void}
|
|
||||||
|
|
||||||
function MyPlugin(; config_file::Union{AbstractString, Void}="")
|
function MyPlugin(; config_file::Union{AbstractString, Void}="")
|
||||||
if config_file != nothing
|
if config_file != nothing
|
||||||
if isempty(config_file)
|
if isempty(config_file)
|
||||||
config_file = joinpath(DEFAULTS_DIR, "myplugin.yml")
|
config_file = joinpath(DEFAULTS_DIR, "myplugin.yml")
|
||||||
end
|
elseif !isfile(config_file)
|
||||||
if !isfile(config_file)
|
|
||||||
throw(ArgumentError("File $(abspath(config_file)) does not exist"))
|
throw(ArgumentError("File $(abspath(config_file)) does not exist"))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
new(["/secrets"], config_file)
|
new([], config_file, ".myplugin.yml", [], Dict{String, Any}())
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
Now to actually create this configuration file at package generation time,
|
That's all there is to it! Let's take a better look at what we've done:
|
||||||
we need a `gen_plugin` method. This method looks like this:
|
|
||||||
|
* The plugin has five attributes, these must be exactly as they are.
|
||||||
|
* `gitignore` is the array of patterns to add the the generated package's
|
||||||
|
`.gitignore`, we chose not to add any with this plugin.
|
||||||
|
* `src` is the location of the config file we're going to copy into the
|
||||||
|
generated package repository. If this is `nothing`, no config file will be
|
||||||
|
generated. This came from the `config_file` keyword argument, which
|
||||||
|
defaulted to an empty string. That's because we've placed a default
|
||||||
|
config file at `defaults/myplugin.yml`.
|
||||||
|
* `dest` is the path to our generated config file, relative to the root of
|
||||||
|
the package repository. In this example, the file will go in
|
||||||
|
`.myplugin.yml` at the root of the repository.
|
||||||
|
* `badges` is an array of Markdown-formatted badge strings to be displayed
|
||||||
|
on the package's README. We chose not to include any here. TODO talk about
|
||||||
|
`substitute`.
|
||||||
|
* `view` is a dictionary of additional replacements to `substitute`.
|
||||||
|
|
||||||
|
Plenty of services like
|
||||||
|
[`TravisCI`](https://invenia.github.io/PkgTemplates.jl/stable/pages/plugins.html#TravisCI-1)
|
||||||
|
and
|
||||||
|
[`CodeCov`](https://invenia.github.io/PkgTemplates.jl/stable/pages/plugins.html#CodeCov-1)
|
||||||
|
follow this format, so generic plugins should be able to get you pretty far.
|
||||||
|
|
||||||
|
## Writing a Custom Plugin
|
||||||
|
|
||||||
|
When a service doesn't follow the pattern demonstrated above, it's time to write a custom
|
||||||
|
plugin. These are still pretty simple, needing at most two additional methods. Let's create
|
||||||
|
a custom plugin called `Gamble` in `src/plugins/gamble.jl` that only generates a file if
|
||||||
|
you get lucky enough:
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
function gen_plugin(plugin::MyPlugin, template::Template, pkg_name::AbstractString)
|
@auto_hash_equals struct Gamble <: CustomPlugin
|
||||||
if plugin.config_file == nothing
|
gitignore:Vector{AbstractString}
|
||||||
|
src::AbstractString
|
||||||
|
success::Bool
|
||||||
|
|
||||||
|
function Gamble(config_file::AbstractString)
|
||||||
|
if !isfile(config_file)
|
||||||
|
throw(ArgumentError("File $(abspath(config_file)) does not exist"))
|
||||||
|
end
|
||||||
|
success = rand() > 0.8
|
||||||
|
println(success ? "Congratulations!" : "Maybe next time.")
|
||||||
|
new([], config_file, success)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function badges(plugin: Gamble, user::AbstractString, pkg_name::AbstractString)
|
||||||
|
if plugin.success
|
||||||
|
return ["[](https://pokerstars.net)"]
|
||||||
|
else
|
||||||
|
return String[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function gen_plugin(plugin::Gamble, template::Template, pkg_name::AbstractString)
|
||||||
|
if plugin.success
|
||||||
|
text = substitute(readstring(plugin.src), template, pkg_name)
|
||||||
|
gen_file(joinpath(t.temp_dir, ".gambler.yml"), text)
|
||||||
|
return [".gambler.yml"]
|
||||||
|
else
|
||||||
return String[]
|
return String[]
|
||||||
end
|
end
|
||||||
text = substitute(readstring(plugin.config_file), pkg_name, template)
|
|
||||||
gen_file(joinpath(template.temp_dir, pkg_name, ".myplugin.yml"))
|
|
||||||
return [".myplugin.yml"]
|
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
There are a few things to note here:
|
With that, we've got everything we need. Note that this plugin still has a `gitignore`
|
||||||
|
attribute; it's required for all plugins. Let's look at the extra methods we implemented:
|
||||||
|
|
||||||
* We use the `substitute` function on the config file's text.
|
#### `gen_plugin`
|
||||||
* More on that [later](#template-substitution).
|
|
||||||
* We use the `gen_file` function to create the file.
|
|
||||||
* It takes two arguments: the path to the file to be generated,
|
|
||||||
and the text to be written.
|
|
||||||
* We place our file in `template.temp_dir`.
|
|
||||||
* `template.temp_dir` is where all file generation takes place, files are
|
|
||||||
only moved to their final location at the end of package generation
|
|
||||||
to avoid leftovers in the case of an error.
|
|
||||||
* We return an array containing at most the name of our generated file.
|
|
||||||
* This array should contain all root-level files or directories that were
|
|
||||||
created. If we created `myplugin/foo` and `myplugin/bar`, we'd only need
|
|
||||||
to return `["myplugin/"]`. If nothing is created, then we return an
|
|
||||||
empty array.
|
|
||||||
|
|
||||||
We've got the essentials now, but perhaps `MyPlugin` has a web interface
|
We read the text from the plugin's source file, and then we run it through the `substitute`
|
||||||
that we want to access from the repo's homepage. We'll do this by adding a
|
function (more on that [later](#template-substitution)).
|
||||||
badge to the README:
|
|
||||||
|
|
||||||
```julia
|
Next, we use `gen_file` to write the text, with substitutions applied, to the destination
|
||||||
function badges(_::MyPlugin, user::AbstractString, pkg_name::AbstractString)
|
file in `t.temp_dir`. Generating our repository in a temp directory means we're not stuck
|
||||||
return [
|
with leftovers in the case of an error.
|
||||||
"[](https://myplugin.com/$user/$pkg_name.jl)"
|
|
||||||
]
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
This method should return an array of Markdown-formatted strings that display
|
This function returns an array of all the root-level files or directories
|
||||||
badges and link to somewhere relevant. Note that a plugin can have any number
|
that were created. If both `foo/bar` and `foo/baz` were created, we only need
|
||||||
of badges. The Markdown syntax is as follows:
|
to return `["foo/"]`.
|
||||||
|
|
||||||
```
|
#### `badges`
|
||||||
[](https://link.url)
|
|
||||||
```
|
|
||||||
|
|
||||||
Badges for just about everything can be found at
|
This function returns an array of Markdown-formatted badges to be displayed on
|
||||||
[Shields.io](https://shields.io/).
|
the package README. You can find badges and Markdown strings for just about
|
||||||
|
everything on [Shields.io](https://shields.io).
|
||||||
|
|
||||||
We're not done yet though, we need to add the plugin type to the list of
|
This will do the trick, but if we want our badge to appear at a specific
|
||||||
badge-enabled plugins. We want `MyPlugin`'s badge to be displayed on the far
|
position in the README, we need to edit `BADGE_ORDER` in
|
||||||
right side, so we're going to add `MyPlugin` to the end of `BADGE_ORDER` in
|
[`src/PkgTemplates.jl`(https://github.com/invenia/PkgTemplates.jl/blob/master/src/PkgTemplates.jl).
|
||||||
`src/PkgTemplates.jl`.
|
Say we want our badge to appear before all others, we'll add `Gamble` to the
|
||||||
|
beginning of the array.
|
||||||
|
|
||||||
```julia
|
That's all there is to it! We've just created a nifty custom plugin.
|
||||||
const BADGE_ORDER = [GitHubPages, TravisCI, AppVeyor, CodeCov, MyPlugin]
|
|
||||||
```
|
|
||||||
|
|
||||||
And we're done! We've just created a nifty new plugin.
|
|
||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
|
@ -93,6 +93,9 @@ Returns an array of generated file/directory names.
|
|||||||
"""
|
"""
|
||||||
function gen_readme(pkg_name::AbstractString, template::Template)
|
function gen_readme(pkg_name::AbstractString, template::Template)
|
||||||
text = "# $pkg_name\n"
|
text = "# $pkg_name\n"
|
||||||
|
remaining = copy(collect(keys(template.plugins)))
|
||||||
|
|
||||||
|
# Generate the ordered badges first, then add any remaining ones to the right.
|
||||||
for plugin_type in BADGE_ORDER
|
for plugin_type in BADGE_ORDER
|
||||||
if haskey(template.plugins, plugin_type)
|
if haskey(template.plugins, plugin_type)
|
||||||
text *= "\n"
|
text *= "\n"
|
||||||
@ -100,8 +103,16 @@ function gen_readme(pkg_name::AbstractString, template::Template)
|
|||||||
badges(template.plugins[plugin_type], template.user, pkg_name),
|
badges(template.plugins[plugin_type], template.user, pkg_name),
|
||||||
"\n",
|
"\n",
|
||||||
)
|
)
|
||||||
|
deleteat!(remaining, findin(remaining, plugin_type))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
for plugin_type in remaining
|
||||||
|
text *= "\n"
|
||||||
|
text *= join(
|
||||||
|
badges(template.plugins[plugin_type], template.user, pkg_name),
|
||||||
|
"\n",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
gen_file(joinpath(template.temp_dir, pkg_name, "README.md"), text)
|
gen_file(joinpath(template.temp_dir, pkg_name, "README.md"), text)
|
||||||
return ["README.md"]
|
return ["README.md"]
|
||||||
|
Loading…
Reference in New Issue
Block a user