Skip to content

Instantly share code, notes, and snippets.

@viktorklang
Last active June 9, 2017 07:27
Show Gist options
  • Save viktorklang/a09aad920c1a4072cfe6 to your computer and use it in GitHub Desktop.
Save viktorklang/a09aad920c1a4072cfe6 to your computer and use it in GitHub Desktop.
Gistard — an sbt autoplugin for depending on Gists — such as Gistard itself
/*
Copyright 2015 Viktor Klang
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Documentation
*
* Install:
*
* `wget -P <sbt-project-base-dir>/project https://gist.githubusercontent.com/viktorklang/a09aad920c1a4072cfe6/raw/Gistard.scala`
*
* Or visit the above address with your web browser of choice and save the file in your `<sbt-project-basedir>/project` folder as `Gistard.scala` (important)
*
*
* Configuration:
*
* **It is recommended to configure Gistard to update itself using Gistard** so add this to your `build.sbt`:
*
* If you want to make sure you are always on the latest version, omit the revision as below:
*
* GistardKeys.gistardDependencies += gist("viktorklang", "a09aad920c1a4072cfe6", "", "Gistard.scala", baseDirectory.value / "project")
*
* If you want to update to a specific revision, use this instead:
*
* GistardKeys.gistardDependencies += gist("viktorklang", "a09aad920c1a4072cfe6", "<insert revision here>", "Gistard.scala", baseDirectory.value / "project")
*
* To see available revisions, have a look here: https://gist.github.com/viktorklang/a09aad920c1a4072cfe6/revisions
*
*
* `Gistard` has three tasks for maintaining the dependencies, `gistardUpdate` which downloads the gist dependencies if they are not already downloaded,
* `gistardClean` which deletes all gist dependencies, and `gistardReload` which downloads the gist dependencies and replaces the existing versions.
*
* `Gistard` by default hooks `gistardUpdate` into the `update` task and `gistardClean` into the `clean` task.
*
*
* Usage:
*
* So you've found a Gist that you want to automatically pull into your `sbt` project, fantastic!
*
* Example:
*
* You were browsing `viktorklang`s Gists when you saw `GistardVerify` (https://gist.github.com/viktorklang/3fdb2ec2709b79fdc7a4),
* and now you want to add that snippet to your `build.sbt` using `Gistard`.
*
* In the URL, the "viktorklang" part will be the value of the `owner` field of the Gist,
* and "3fdb2ec2709b79fdc7a4" is the `id` field value.
* You note that the file you want to depend on is named `GistardVerify.scala`,
* this will be the `fileName` field value.
*
* First you decide if you want to depend on a specific revision of that Gist, you can see them at:
* https://gist.github.com/viktorklang/3fdb2ec2709b79fdc7a4/revisions
*
* You decide that you want to have "the latest" revision (empty String for revision below),
* and that you want to place it in `main/scala/gistard/verify` so you add the following to your `build.sbt`:
*
* GistardKeys.gistardDependencies += gist(owner = "viktorklang",
* id = "3fdb2ec2709b79fdc7a4",
* revision = "",
* fileName = "GistardVerify.scala",
* localDirectory = scalaSource.value / "gistard" / "verify")
*
* Then you either restart `sbt` or run the `reload` command to see this change take effect and all that is left to do is to
* run the `update` task to have `Gistard` download the `GistardVerify.scala` file.
*
*
*
*
**/
package gistard
import sbt._
import sbt.Keys._
import scala.collection.immutable
import java.net.URL
import java.io.{File => JFile}
/**
* Gistard is a GitHub Gist hosted sbt autoplugin that allows you to depend on GitHub Gist files as source dependencies.
* Gistard itself is a GitHub Gist and can use Gistard to update itself.
**/
object Gistard {
/**
* Gist represents a specific revision of a specific file in a specific Gist for a specific owner.
* If the revision is empty, it will fetch the latest revision.
**/
final case class Gist(owner: String, id: String, revision: String, fileName: String, localDirectory: File) {
if (fileName.isEmpty) throw new IllegalArgumentException("Gist file must be non-empty!")
if (id.isEmpty) throw new IllegalArgumentException("Gist ID must be non-empty!")
if (!revision.isEmpty && Hash.toHex(Hash.fromHex(revision)) != revision)
throw new IllegalArgumentException("Gist revision must be hexadecimal or empty!")
if (owner.isEmpty) throw new IllegalArgumentException("Gist owner must be non-empty!")
override def toString = "Gist[owner=%s,id=%s,revision=%s,fileName=%s,localDirectory=%s]".format(owner, id, revision, fileName, localDirectory)
}
/**
* APIVersion is the API for Github,
* it is possible, if needed, to provide your own implementation of this,
* see the `gistardAPIVersion` SettingKey.
**/
trait APIVersion {
/**
* @return the URL to the contents of the given Gist
**/
def contentURLFor(gist: Gist): URL
/**
* @return the URL to the summary of the given Gist
**/
def summaryURLFor(gist: Gist): URL
/**
* Downloads all Gists given as the `gistDependencies` parameter,
* doesn't update already existing files unless `force` is set to true.
**/
def update(gistDependencies: Set[Gist], force: Boolean, log: Logger): Seq[File] =
gistDependencies.flatMap(update(_, force, log))(scala.collection.breakOut)
/**
* Downloads all Gists given as the `gist` parameter,
* doesn't update already existing files unless `force` is set to true.
**/
def update(gist: Gist, force: Boolean, log: Logger): Option[File] = {
val file = gist.localDirectory / gist.fileName
if (file.isDirectory) {
log.error("Gistard: Can't download %s since the file already exists but is a directory: %s".format(gist, file))
None
} else if (force || !file.exists) {
log.info("Gistard: downloading %s as %s".format(gist, file))
IO.withTemporaryFile("gistard", gist.fileName) {
temp =>
IO.download(contentURLFor(gist), temp)
if (file.exists) IO.delete(file)
IO.move(temp, file)
Some(file)
}
} else {
log.info("Gistard: Using cached version of %s instead of downloading".format(file))
Some(file)
}
}
/**
* Deletes all Gists given as the `gistDependencies` parameter
**/
def clean(gistDependencies: Set[Gist], log: Logger): Set[File] =
gistDependencies.flatMap(clean(_, log))
/**
* Deletes the Gist given as the `gist` parameter.
**/
def clean(gist: Gist, log: Logger): Option[File] = {
val file = gist.localDirectory / gist.fileName
if (!file.exists) None
else if (!file.isFile) {
log.error("Gistard: Could not delete %s since it is a directory and not a file".format(file))
None
} else if (!file.delete()) {
log.error("Gistard: Could not delete: %s".format(file))
None
} else {
log.info("Gistard: Deleting %s".format(file))
Some(file)
}
}
}
object GithubV3 extends APIVersion {
override def contentURLFor(gist: Gist): URL =
if (gist.revision.isEmpty)
new URL("https://gist.githubusercontent.com/%s/%s/raw/%s".format(gist.owner, gist.id, gist.fileName))
else
new URL("https://gist.githubusercontent.com/%s/%s/raw/%s/%s".format(gist.owner, gist.id, gist.revision, gist.fileName))
override def summaryURLFor(gist: Gist): URL =
if (gist.revision.isEmpty)
new URL("https://api.github.com/gists/%d".format(gist.id))
else
new URL("https://api.github.com/gists/%d/%s".format(gist.id, gist.revision))
}
}
object GistardPlugin extends AutoPlugin {
import Gistard._
object autoImport {
object GistardKeys {
val gistardUpdate = TaskKey[Seq[JFile]]("gistard-update", "Downloads all Gist dependencies that aren't already downloaded")
val gistardClean = TaskKey[Set[JFile]]("gistard-clean", "Deletes all Gist Dependencies")
val gistardReload = TaskKey[Seq[JFile]]("gistard-reload", "Downloads all Gist dependencies")
val gistardDependencies = SettingKey[Set[Gist]]("gistard-dependencies", "All Gist Dependencies")
val gistardAPIVersion = SettingKey[APIVersion]("gistard-api-version", "What version of the Github API to use")
}
/**
* Auto-imported convenience method to create a new `Gist` from the given parameters.
**/
def gist(owner: String, id: String, revision: String, fileName: String, localDirectory: File): Gist =
Gist(owner, id, revision, fileName, localDirectory)
}
import autoImport._
import GistardKeys._
override def requires = sbt.plugins.JvmPlugin
override def trigger = allRequirements
override lazy val projectSettings = Seq(
gistardAPIVersion := GithubV3,
gistardDependencies := Set(),
gistardUpdate := (gistardAPIVersion).value.update(gistardDependencies.value, false, streams.value.log),
gistardClean := (gistardAPIVersion).value.clean(gistardDependencies.value, streams.value.log),
gistardReload := (gistardAPIVersion).value.update(gistardDependencies.value, true, streams.value.log),
update <<= update dependsOn(gistardUpdate),
clean <<= clean dependsOn(gistardClean)
)
}
@viktorklang
Copy link
Author

Thanks @sschaef, I've updated Gistard (https://gist.github.com/viktorklang/a09aad920c1a4072cfe6/3cd3eb4131e6302d3a365d7919e810443012b331), so if you have enabled Gistard auto updating, you should get it when you do gistardReload.

@Blaisorblade
Copy link

Problem: cleaning with auto-updating enabled deletes Gistard itself.

> clean
[info] Gistard: Deleting /Users/pgiarrusso/.sbt/0.13/plugins/Gistard.scala

Restarting sbt now triggers a build failure, because the auto-update config references the now-deleted Gistard.

That was with the following in /Users/pgiarrusso/.sbt/0.13/gistard.sbt:

gistard.GistardPlugin.autoImport.GistardKeys.gistardDependencies += gistard.GistardPlugin.autoImport.gist("viktorklang", "a09aad920c1a4072cfe6", "", "Gistard.scala", new File(System.getProperty("user.home")) / ".sbt" / "0.13" / "plugins")

(This looks awkward, but autoimports don't work in ~/.sbt — there was a ticket for that).

And with Gistard installed as:

cd ~/.sbt/0.13/plugins
wget https://gist.githubusercontent.com/viktorklang/a09aad920c1a4072cfe6/raw/Gistard.scala

Would that problem not show-up with a per-project installation?

@viktorklang
Copy link
Author

@Blaisorblade Omg, I just saw your comment, 1 year late. :(
I haven't seen that problem in a per-project installation, could most definitely be related to having it in ~/.sbt

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment