Code Monkey home page Code Monkey logo

sbt-header's Introduction

sbt-header

License

sbt-header is an sbt plugin for creating or updating file headers, e.g. copyright headers.

Getting started

In order to add the sbt-header plugin to your build, add the following line to project/plugins.sbt:

addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.6.0") // Check the latest version above or look at the release tags

Then in your build.sbt configure the following settings:

organizationName := "Heiko Seeberger"
startYear := Some(2015)
licenses += ("Apache-2.0", new URL("https://www.apache.org/licenses/LICENSE-2.0.txt"))

This configuration will apply Apache License 2.0 headers to Scala and Java files. sbt-header provides two tasks: headerCreate and headerCheck, which are described in the following sub sections. For more information on how to customize sbt-header, please refer to the Configuration section.

Creating headers

In order to create or update file headers, execute the headerCreate task:

> headerCreate
[info] Headers created for 2 files:
[info]   /Users/heiko/projects/sbt-header/sbt-header-test/test.scala
[info]   /Users/heiko/projects/sbt-header/sbt-header-test/test2.scala

The task is incremental, meaning that it will not look at files that have not seen changes since the last time the task was run.

Checking headers

In order to check whether all files have headers (for example for CI), execute the headerCheck task:

> headerCheck
[error] (compile:checkHeaders) There are files without headers!
[error]   /Users/heiko/projects/sbt-header/sbt-header-test/test.scala
[error]   /Users/heiko/projects/sbt-header/sbt-header-test/test2.scala

headerCheck will not modify any files but will cause the build to fail if there are files without a license header.

Requirements

  • Java 8 or higher
  • sbt 1.0.0 or higher

Configuration

By default sbt-header tries to infer the license header you want to use from the organizationName, startYear and licenses settings. For this to work, sbt-header requires the licenses setting to contain exactly one entry. The first component of that entry has to be the SPDX license identifier of one of the supported licenses.

Setting the license to use explicitly

If you can not setup your build in a way that sbt-header can detect the license you want to use (see above), you can set the license to use explicitly:

headerLicense := Some(HeaderLicense.MIT("2015", "Heiko Seeberger"))

This will also be given precedence if a license has been auto detected from project settings.

Build in licenses

The most common licenses have been pre-canned in License. They can either be detected using their SPDX identifier or by setting them explicitly.

License SPDX identifier
Apache License, Version 2.0 Apache-2.0
BSD 2 Clause BSD-2-Clause
BSD 3 Clause BSD-3-Clause
GNU General Public License v3 or later GPL-3.0-or-later
GNU General Public License v3 only GPL-3.0-only
GNU General Public License v3 (deprecated) GPL-3.0
GNU Lesser General Public License v3 or later LGPL-3.0-or-later
GNU Lesser General Public License v3 only LGPL-3.0-only
GNU Lesser General Public License v3 (deprecated) LGPL-3.0
GNU Affero General Public License v3 or later AGPL-3.0-or-later
GNU Affero General Public License v3 only AGPL-3.0-only
GNU Affero General Public License v3 (deprecated) AGPL-3.0
MIT License MIT
Mozilla Public License, v. 2.0 MPL-2.0

Using the short SPDX license identifier syntax

If you want to use the following syntax:

  /*
   * Copyright 2015 Heiko Seeberger
   *
   * SPDX-License-Identifier: BSD-3-Clause
   */

You have two possibilites:

  • If you are using auto-detection, you just need to add the following to your build.sbt
headerLicenseStyle := HeaderLicenseStyle.SpdxSyntax
  • On the other hand, if you are defining your license explicitly, you'll have to pass the style when defining the headerLicense attribute:
headerLicense := Some(HeaderLicense.MIT("2015", "Heiko Seeberger", HeaderLicenseStyle.SpdxSyntax))

Using a custom license text

If you don't want to use one of the built-in licenses, you can define a custom license text using the Custom case class:

headerLicense := Some(HeaderLicense.Custom(
  """|Copyright (c) Awesome Company 2015
     |
     |This is the custom License of Awesome Company
     |""".stripMargin
))

Note that you don't need to add comment markers like // or /*. The comment style is configured on a per file type basis (see next section).

Configuring comment styles

Comment styles are configured on a per file type basis. The default is to apply C Style block comments to Scala and Java files. No other comment styles are predefined. If you want to create comments for example for your XML files, you have to add the corresponding mapping manually (see below). The build-in comment styles are defined in CommentStyle:

Name Description
cStyleBlockComment C style block comments (blocks starting with "/*" and ending with "*/")
cppStyleLineComment C++ style line comments (lines prefixed with "//")
hashLineComment Hash line comments (lines prefixed with "#")
twirlStyleComment Twirl style comment (blocks starting with "@*" and ending with "*@")
twirlStyleBlockComment Twirl style block comments (comment blocks with a frame made of "*")

To override the configuration for Scala/Java files or add a configuration for some other file type, use the headerMapping setting:

headerMappings := headerMappings.value + (HeaderFileType.scala -> HeaderCommentStyle.cppStyleLineComment)

Custom comment creators

You can customize how content gets created by providing your own CommentCreator. For example, this would be a (crude) way to preserve the copyright year in existing headers but still update the rest:

CommentStyle.cStyleBlockComment.copy(commentCreator = new CommentCreator() {
  val Pattern = "(?s).*?(\\d{4}(-\\d{4})?).*".r
  def findYear(header: String): Option[String] = header match {
   case Pattern(years, _) => Some(years)
    case _                 => None
  }
  override def apply(text: String, existingText: Option[String]): String = {
    val newText = CommentStyle.cStyleBlockComment.commentCreator.apply(text, existingText)
    existingText
      .flatMap(findYear)
      .map(year => newText.replace("2017", year))
      .getOrElse(newText)
  }
})

Excluding files

To exclude some files, use the sbt's file filters:

excludeFilter.in(headerSources) := HiddenFileFilter || "*Excluded.scala"
excludeFilter.in(headerResources) := HiddenFileFilter || "*.xml"

Empty line between header and body

If an empty line header should be added between the header and the body of a file (defaults to true):

headerEmptyLine := false

Using an auto plugin

If your build uses an auto plugin for common settings, make sure to add HeaderPlugin to requires:

import de.heikoseeberger.sbtheader.HeaderPlugin

object Build extends AutoPlugin {
  override def requires = ... && HeaderPlugin
  ...
}

Adding headers to files in other configurations

By default sbt-header takes Compile and Test configurations into account. If you need more, just add them:

headerSettings(It, MultiJvm)

Automation

If you want to automate header creation/update on compile, enable the AutomateHeaderPlugin:

lazy val myProject = project
  .in(file("."))
  .enablePlugins(AutomateHeaderPlugin)

By default automation takes Compile and Test configurations into account. If you need more, just add them:

automateHeaderSettings(It, MultiJvm)

Integration with other plugins

This plugin by default only handles managedSources and managedResources in Compile and Test. For this reason you need to tell sbt-header if it should also add headers to additional files managed by other plugins.

sbt-twirl / play projects

To use sbt-header in a project using sbt-twirl (for example a Play web project), the Twirl templates have to be added to the sources handled by sbt-header. Add the following to your build definition:

import de.heikoseeberger.sbtheader.FileType
import play.twirl.sbt.Import.TwirlKeys

headerMappings := headerMappings.value + (FileType("html") -> HeaderCommentStyle.twirlStyleBlockComment)

headerSources.in(Compile) ++= sources.in(Compile, TwirlKeys.compileTemplates).value

sbt-header supports two comment styles for Twirl templates. twirlStyleBlockComment will produce simple twirl block comments, while twirlStyleFramedBlockComment will produce framed twirl comments.

twirlStyleBlockComment comment style:

@*
 * This is a simple twirl block comment
 *@

twirlStyleFramedBlockComment comment style:

@**********************************
 * This is a framed twirl comment *
 **********************************@

sbt-boilerplate

In order to use sbt-header with sbt-boilerplate plugin add the following to your build definition:

def addBoilerplate(confs: Configuration*) = confs.foldLeft(List.empty[Setting[_]]) { (acc, conf) =>
  acc ++ Seq(
    headerSources in conf ++= (((sourceDirectory in conf).value / "boilerplate") ** "*.template").get),
    headerMappings        += (FileType("template") -> HeaderCommentStyle.cStyleBlockComment)
  )
}

addBoilerplate(Compile, Test)

This adds src/{conf}/boilerplate/**.scala in the list of files handled by sbt-headers for conf, where conf is either Compile or Test.

Migrating from 1.x

This section contains migration notes from version 1.x of sbt-header to version 2.x. The latest release of the 1.x line is 1.8.0. You can find the documentation of that release in the corresponding git tag.

Changed task names and settings keys

The names of all tasks and settings have been changed from 1.x to 2.x. Furthermore types of settings have changed. The following tables give an overview of the changes:

Changed task names:

Old Name New Name
createHeaders headerCreate
checkHeaders headerCheck

Changed settings:

Old Name : Old Type New Name: New Type
headers : Map[String, (Regex, String)] headerMappings : Map[FileType, CommentStyle]
- headerLicense : Option[License]
exclude : Seq[String] removed in favor of sbt include/excude filters

createFrom method

sbt-header 1.x featured some default header mappings as well as the createFrom method, which could be used to easily define header mappings:

headers := createFrom(Apache2_0, "2015", "Heiko Seeberger")

This method has been removed and the default mappings for Scala and Java files has been added as default mapping to the headerMappings setting.

Custom licenses

In sbt-header 1.x when you needed to use a custom license this would typically look like this:

headers := Map(
  "scala" -> (
    HeaderPattern.cStyleBlockComment,
    """|/*
       | * Copyright 2015 Awesome Company
       | */
       |""".stripMargin
  )
)

In sbt-header 2.x, licenses are defined as instances of de.hseeberger.sbtheader.License. Further more, the license is only defined once and not per file type. So the above in 2.x is equivalent to:

headerLicense := Some(HeaderLicense.Custom(
    """|Copyright 2015 Awesome Company
       |""".stripMargin
))

Note that you only need to define the license text, but not the comment markers. The latter are configured via the headerMappings setting. The configuration above will use the default mappings which apply C style block comments to Java and Scala files. If you have mappings for additional file types, please add these to the headerMappings setting.

Dropped features

In sbt-header 1.x it was possible to define different licenses for different files types, e.g.:

headers := Map(
  "scala" -> Apache2_0("2015", "Heiko Seeberger"),
  "java" -> MIT("2015", "Heiko Seeberger")
)

Since we believe most of the projects out there will only ever have one license, we dropped this feature without replacement. In sbt-header 2.x users have to define a single license for the whole project using the headerLicense setting (or let sbt-header infer it from the licenses project setting, see above) and a mapping from file type to comment style using the headerMappings setting.

Contribution policy

Contributions via GitHub pull requests are gladly accepted from their original author. Along with any pull requests, please state that the contribution is your original work and that you license the work to the project under the project's open source license. Whether or not you state this explicitly, by submitting any copyrighted material via pull request, email, or other means you agree to license the material under the project's open source license and warrant that you have the legal authority to do so.

License

This code is open source software licensed under the Apache 2.0 License.

sbt-header's People

Contributors

aaabramov avatar abestel avatar alejandrohdezma avatar britter avatar derekwyatt avatar dwijnand avatar esamson avatar fommil avatar gitter-badger avatar hseeberger avatar huntc avatar ihostage avatar jan0sch avatar jeffreyolchovy avatar jhoncamargo avatar johanandren avatar jonas avatar krchniam avatar mkurz avatar nrinaudo avatar pdalpra avatar phdoerfler avatar philippus avatar raboof avatar richdougherty avatar scala-steward avatar sethtisue avatar sirthias avatar sullis avatar tlmak0 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

sbt-header's Issues

Move stuff out of HeaderPlugin.scala

Currently there is a lot of stuff inside HeaderPlugin:

  • AutomateHeaderPlugin
  • CommentStyleMapping
  • HeaderKey
  • HeaderPattern
  • HeaderPlugin

Decide what really belongs to HeaderPlugin and move the rest out of the file

Model comment styles as ADT

In Lincense, we match over the comment style, which is defined as a string. Since there is only a number of supported comment styles, we should model them as an ADT.

Support for file exclusion

Is there a way to exclude particular files from getting headers? The use-case is when a project needs to include a file taken from another project, which already has a header.

Remove/rename exclude setting

exclude is to generic as a name for a sbt setting and is likely to clash with other plugin settings. Further more, sources collections in sbt can be filtered using the build in mechanisms of sbt. Maybe we don't need the exclude setting at all.

ability to ignore some files

We have some files that have been copied from another project and have additional copyright / license information that must be displayed. Is it possible to ignore such files?

I'm thinking of a workaround whereby those files simply have two header sections.

Make optional first line (e.g. shebang) a property of file type

Currently the HeaderCreator know about shebang lines and how to preserve them. However the fact that there may be a first line is really a property of a file type and not of the header creator. In order to implement #90 we need to make this more generic. So I propose to introduce a file type:

final case class FileType(extension: String, firstLinePattern: Option[Regex] = None)

The headerMappings setting would have to be changed to:

headerMappings = settingsKey[Map[FileType, CommentStyle]]

This will also contribute to #75

MalformedInputException

when I upgraded sbt-header to the latest in ensime-server I got this exception.

[error] (monkeys/compile:createHeaders) java.nio.charset.MalformedInputException: Input length = 1

should be easy to reproduce since everything is available on github.

Downgrading as workaround...

integration with git history

It would be great to have an option to generate the copyright years and names based on the github history. That section in the template notice could then become a placeholder, to be auto-generated.

This would be prone to errors, so it is likely that overrides (not captured by the history) and blacklists (dupes and bots) would need to be provided. This could perhaps be provided on a per file basis using a syntax in the comments much like emacs' local variables.

Refactor Licenses

  • Make Licenses trait an object
  • Rename Licenses to License
  • Make License implementations objects with an apply method

Restructure documentation

The documentation has evolved over the time sat-headers exists. We should make the documentation more accessible starting with the most easy configuration and explaining customization (like defining custom header texts) later on.

Derive configuration from project meta data

At the moment, for using sbt-header I always have to add something like:

headers := Map(
  "scala" -> Apache2_0("2016", "Benedikt Ritter"),
  "conf" -> Apache2_0("2016", "Benedikt Ritter", "#")
)

This feels redundant, since I've to copy the some block for all projects. So it would be nice if the plugin could derive it's configuration from the project meta data:

  • use startYear for the yyyy parameter
  • use organizationName for the copyrightOwner parameter
  • use the value of licenses if there is only one entry to chose the appropriate License implementation
  • add a file to header style mapping, so that the C block comment style is applied to Scala, Groovy, and Java file, for example

I'm happy to contribute a PR, but I'd like to discuss how to implement this change first.

Got error java.lang.NoSuchMethodError: java.nio.file.Files.readAllLines(Ljava/nio/file/Path

I get a java.lang.NoSuchMethodError when I try to run on a project on OS-X.
I'm using

SBT
sbt version 0.13.5.

JVM
java version "1.7.0_45"
Java(TM) SE Runtime Environment (build 1.7.0_45-b18)
Java HotSpot(TM) 64-Bit Server VM (build 24.45-b08, mixed mode)

Scala
scalaVersion := "2.10.4"

java.lang.NoSuchMethodError: java.nio.file.Files.readAllLines(Ljava/nio/file/Path;)Ljava/util/List; at de.heikoseeberger.sbtheader.SbtHeader$.de$heikoseeberger$sbtheader$SbtHeader$$createHeader(SbtHeader.scala:78) at de.heikoseeberger.sbtheader.SbtHeader$$anonfun$4$$anonfun$apply$1.apply(SbtHeader.scala:69) at de.heikoseeberger.sbtheader.SbtHeader$$anonfun$4$$anonfun$apply$1.apply(SbtHeader.scala:69) at scala.collection.TraversableLike$$anonfun$flatMap$1.apply(TraversableLike.scala:251) at scala.collection.TraversableLike$$anonfun$flatMap$1.apply(TraversableLike.scala:251) at scala.collection.immutable.List.foreach(List.scala:318) at scala.collection.TraversableLike$class.flatMap(TraversableLike.scala:251) at scala.collection.AbstractTraversable.flatMap(Traversable.scala:105) at de.heikoseeberger.sbtheader.SbtHeader$$anonfun$4.apply(SbtHeader.scala:69) at de.heikoseeberger.sbtheader.SbtHeader$$anonfun$4.apply(SbtHeader.scala:69) at scala.collection.TraversableLike$$anonfun$flatMap$1.apply(TraversableLike.scala:251) at scala.collection.TraversableLike$$anonfun$flatMap$1.apply(TraversableLike.scala:251) at scala.collection.immutable.List.foreach(List.scala:318) at scala.collection.TraversableLike$class.flatMap(TraversableLike.scala:251) at scala.collection.AbstractTraversable.flatMap(Traversable.scala:105) at de.heikoseeberger.sbtheader.SbtHeader$.de$heikoseeberger$sbtheader$SbtHeader$$createHeadersTask(SbtHeader.scala:69) at de.heikoseeberger.sbtheader.SbtHeader$$anonfun$toBeScopedSettings$3.apply(SbtHeader.scala:53) at de.heikoseeberger.sbtheader.SbtHeader$$anonfun$toBeScopedSettings$3.apply(SbtHeader.scala:53) at scala.Function1$$anonfun$compose$1.apply(Function1.scala:47) at sbt.$tilde$greater$$anonfun$$u2219$1.apply(TypeFunctions.scala:42) at sbt.std.Transform$$anon$4.work(System.scala:64) at sbt.Execute$$anonfun$submit$1$$anonfun$apply$1.apply(Execute.scala:237) at sbt.Execute$$anonfun$submit$1$$anonfun$apply$1.apply(Execute.scala:237) at sbt.ErrorHandling$.wideConvert(ErrorHandling.scala:18) at sbt.Execute.work(Execute.scala:244) at sbt.Execute$$anonfun$submit$1.apply(Execute.scala:237) at sbt.Execute$$anonfun$submit$1.apply(Execute.scala:237) at sbt.ConcurrentRestrictions$$anon$4$$anonfun$1.apply(ConcurrentRestrictions.scala:160) at sbt.CompletionService$$anon$2.call(CompletionService.scala:30) at java.util.concurrent.FutureTask.run(FutureTask.java:262) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471) at java.util.concurrent.FutureTask.run(FutureTask.java:262) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) at java.lang.Thread.run(Thread.java:744) [error] (compile:createHeaders) java.lang.NoSuchMethodError: java.nio.file.Files.readAllLines(Ljava/nio/file/Path;)Ljava/util/List;

HeaderCheck - Stack trace suppress - SBT Multi module project

Hi,
I'm trying to use Sbt-Header with a sbt multimodule project, but i'm having a java.lang.RuntimeException: There are files without headers! but the files(.scala,.xml) actually contains header.
The stacktrace is:

	at de.heikoseeberger.sbtheader.HeaderPlugin$.de$heikoseeberger$sbtheader$HeaderPlugin$$checkHeadersTask(HeaderPlugin.scala:179)
	at de.heikoseeberger.sbtheader.HeaderPlugin$$anonfun$de$heikoseeberger$sbtheader$HeaderPlugin$$toBeScopedSettings$4.apply(HeaderPlugin.scala:115)
	at de.heikoseeberger.sbtheader.HeaderPlugin$$anonfun$de$heikoseeberger$sbtheader$HeaderPlugin$$toBeScopedSettings$4.apply(HeaderPlugin.scala:115)
	at scala.Function1$$anonfun$compose$1.apply(Function1.scala:47)
	at sbt.$tilde$greater$$anonfun$$u2219$1.apply(TypeFunctions.scala:40)
	at sbt.std.Transform$$anon$4.work(System.scala:63)
	at sbt.Execute$$anonfun$submit$1$$anonfun$apply$1.apply(Execute.scala:228)
	at sbt.Execute$$anonfun$submit$1$$anonfun$apply$1.apply(Execute.scala:228)
	at sbt.ErrorHandling$.wideConvert(ErrorHandling.scala:17)
	at sbt.Execute.work(Execute.scala:237)
	at sbt.Execute$$anonfun$submit$1.apply(Execute.scala:228)
	at sbt.Execute$$anonfun$submit$1.apply(Execute.scala:228)
	at sbt.ConcurrentRestrictions$$anon$4$$anonfun$1.apply(ConcurrentRestrictions.scala:159)
	at sbt.CompletionService$$anon$2.call(CompletionService.scala:28)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
	at java.lang.Thread.run(Thread.java:745)```
any idea? thanks.

Using sbt-header in scala sbt build

Hi there,
I'm trying to use this nice plugin with a full scala sbt definition (build.scala, not build.sbt).

In order to be able to define the headers map, I had to import the following:

import de.heikoseeberger.sbtheader.SbtHeader.autoImport._
import de.heikoseeberger.sbtheader.SbtHeader.HeaderPattern

I then define the headers map in my project settings:

  lazy val core: Project =
    Project(id, file(base))
      .settings(
        // SbtHeader settings
        headers := Map(
          "scala" -> (
            (HeaderPattern.cStyleBlockComment,
              """|/*
                 | * Test header
                 | */
                 |
              """.stripMargin
            )
          )
        )
      )

I am then able to create the headers interactively in sbt:

$ sbt
> createHeaders

However, I cannot get this task to be automatically executed with the compile and test tasks.

Could you please add an example on how to use your plugin correctly in a build.scala build definition?

Document how the sbt-header-test project works

I'm working on a PR for #30 but I have no idea how the test project works. When I start sbt in the sbt-header-test folder I get:

[error]
[error]      while compiling: /Users/bene/workspace/projects/sbt-header/src/main/scala/de/heikoseeberger/sbtheader/package.scala
[error]         during phase: jvm
[error]      library version: version 2.10.6
[error]     compiler version: version 2.10.6
[error]   reconstructed args: -language:_ -deprecation -unchecked -classpath /Users/bene/workspace/projects/sbt-header/target/scala-2.10/sbt-0.13/classes:/Users/bene/.sbt/0.13/plugins/target/scala-2.10/sbt-0.13/classes:/Users/bene/.ivy2/cache/scala_2.10/sbt_0.13/de.heikoseeberger/sbt-fresh/jars/sbt-fresh-1.1.0.jar:/Users/bene/.ivy2/cache/org.eclipse.jgit/org.eclipse.jgit/jars/org.eclipse.jgit-4.2.0.201601211800-r.jar:/Users/bene/.ivy2/cache/com.jcraft/jsch/jars/jsch-0.1.53.jar:/Users/bene/.ivy2/cache/com.googlecode.javaewah/JavaEWAH/bundles/JavaEWAH-0.7.9.jar:/Users/bene/.ivy2/cache/org.apache.httpcomponents/httpclient/jars/httpclient-4.3.6.jar:/Users/bene/.ivy2/cache/org.apache.httpcomponents/httpcore/jars/httpcore-4.3.3.jar:/Users/bene/.ivy2/cache/commons-logging/commons-logging/jars/commons-logging-1.1.3.jar:/Users/bene/.ivy2/cache/commons-codec/commons-codec/jars/commons-codec-1.6.jar:/Users/bene/.ivy2/cache/org.slf4j/slf4j-api/jars/slf4j-api-1.7.2.jar:/Users/bene/.ivy2/cache/org.scala-sbt/sbt/jars/sbt-0.13.11.jar:/Users/bene/.ivy2/cache/org.scala-sbt/main/jars/main-0.13.11.jar:/Users/bene/.ivy2/cache/org.scala-sbt/actions/jars/actions-0.13.11.jar:/Users/bene/.ivy2/cache/org.scala-sbt/classpath/jars/classpath-0.13.11.jar:/Users/bene/.sbt/boot/scala-2.10.6/lib/scala-compiler.jar:/Users/bene/.sbt/boot/scala-2.10.6/lib/scala-reflect.jar:/Users/bene/.ivy2/cache/org.scala-sbt/interface/jars/interface-0.13.11.jar:/Users/bene/.ivy2/cache/org.scala-sbt/io/jars/io-0.13.11.jar:/Users/bene/.ivy2/cache/org.scala-sbt/control/jars/control-0.13.11.jar:/Users/bene/.ivy2/cache/org.scala-sbt/launcher-interface/jars/launcher-interface-1.0.0-M1.jar:/Users/bene/.ivy2/cache/org.scala-sbt/completion/jars/completion-0.13.11.jar:/Users/bene/.ivy2/cache/org.scala-sbt/collections/jars/collections-0.13.11.jar:/Users/bene/.ivy2/cache/jline/jline/jars/jline-2.13.jar:/Users/bene/.ivy2/cache/org.fusesource.jansi/jansi/jars/jansi-1.11.jar:/Users/bene/.ivy2/cache/org.scala-sbt/api/jars/api-0.13.11.jar:/Users/bene/.ivy2/cache/org.scala-sbt/classfile/jars/classfile-0.13.11.jar:/Users/bene/.ivy2/cache/org.scala-sbt/logging/jars/logging-0.13.11.jar:/Users/bene/.ivy2/cache/org.scala-sbt/process/jars/process-0.13.11.jar:/Users/bene/.ivy2/cache/org.scala-sbt/compiler-integration/jars/compiler-integration-0.13.11.jar:/Users/bene/.ivy2/cache/org.scala-sbt/incremental-compiler/jars/incremental-compiler-0.13.11.jar:/Users/bene/.ivy2/cache/org.scala-sbt/relation/jars/relation-0.13.11.jar:/Users/bene/.ivy2/cache/org.scala-sbt/compile/jars/compile-0.13.11.jar:/Users/bene/.ivy2/cache/org.scala-sbt/persist/jars/persist-0.13.11.jar:/Users/bene/.ivy2/cache/org.scala-tools.sbinary/sbinary_2.10/jars/sbinary_2.10-0.4.2.jar:/Users/bene/.ivy2/cache/org.scala-sbt/compiler-ivy-integration/jars/compiler-ivy-integration-0.13.11.jar:/Users/bene/.ivy2/cache/org.scala-sbt/ivy/jars/ivy-0.13.11.jar:/Users/bene/.ivy2/cache/org.scala-sbt/cross/jars/cross-0.13.11.jar:/Users/bene/.ivy2/cache/org.scala-sbt.ivy/ivy/jars/ivy-2.3.0-sbt-2cc8d2761242b072cedb0a04cb39435c4fa24f9a.jar:/Users/bene/.ivy2/cache/org.scala-sbt/serialization_2.10/jars/serialization_2.10-0.1.2.jar:/Users/bene/.ivy2/cache/org.scala-lang.modules/scala-pickling_2.10/jars/scala-pickling_2.10-0.10.1.jar:/Users/bene/.ivy2/cache/org.scalamacros/quasiquotes_2.10/jars/quasiquotes_2.10-2.0.1.jar:/Users/bene/.ivy2/cache/org.json4s/json4s-core_2.10/jars/json4s-core_2.10-3.2.10.jar:/Users/bene/.ivy2/cache/org.json4s/json4s-ast_2.10/jars/json4s-ast_2.10-3.2.10.jar:/Users/bene/.ivy2/cache/com.thoughtworks.paranamer/paranamer/jars/paranamer-2.6.jar:/Users/bene/.ivy2/cache/org.spire-math/jawn-parser_2.10/jars/jawn-parser_2.10-0.6.0.jar:/Users/bene/.ivy2/cache/org.spire-math/json4s-support_2.10/jars/json4s-support_2.10-0.6.0.jar:/Users/bene/.ivy2/cache/org.scala-sbt/run/jars/run-0.13.11.jar:/Users/bene/.ivy2/cache/org.scala-sbt/task-system/jars/task-system-0.13.11.jar:/Users/bene/.ivy2/cache/org.scala-sbt/tasks/jars/tasks-0.13.11.jar:/Users/bene/.ivy2/cache/org.scala-sbt/tracking/jars/tracking-0.13.11.jar:/Users/bene/.ivy2/cache/org.scala-sbt/cache/jars/cache-0.13.11.jar:/Users/bene/.ivy2/cache/org.scala-sbt/testing/jars/testing-0.13.11.jar:/Users/bene/.ivy2/cache/org.scala-sbt/test-agent/jars/test-agent-0.13.11.jar:/Users/bene/.ivy2/cache/org.scala-sbt/test-interface/jars/test-interface-1.0.jar:/Users/bene/.ivy2/cache/org.scala-sbt/main-settings/jars/main-settings-0.13.11.jar:/Users/bene/.ivy2/cache/org.scala-sbt/apply-macro/jars/apply-macro-0.13.11.jar:/Users/bene/.ivy2/cache/org.scala-sbt/command/jars/command-0.13.11.jar:/Users/bene/.ivy2/cache/org.scala-sbt/logic/jars/logic-0.13.11.jar:/Users/bene/.ivy2/cache/org.scala-sbt/compiler-interface/jars/compiler-interface-0.13.11.jar -target:jvm-1.7 -bootclasspath /Library/Java/JavaVirtualMachines/jdk1.8.0_91.jdk/Contents/Home/jre/lib/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_91.jdk/Contents/Home/jre/lib/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_91.jdk/Contents/Home/jre/lib/sunrsasign.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_91.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_91.jdk/Contents/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_91.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_91.jdk/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_91.jdk/Contents/Home/jre/classes:/Users/bene/.sbt/boot/scala-2.10.6/lib/scala-library.jar
[error]
[error]   last tree to typer: Literal(Constant(java.io.File))
[error]               symbol: null
[error]    symbol definition: null
[error]                  tpe: Class(classOf[java.io.File])
[error]        symbol owners:
[error]       context owners: class package$FileOps -> package sbtheader
[error]
[error] == Enclosing template or block ==
[error]
[error] Template( // val <local FileOps>: <notype> in class package$FileOps, tree.tpe=de.heikoseeberger.sbtheader.package$FileOps
[error]   "java.lang.Object" // parents
[error]   ValDef(
[error]     private
[error]     "_"
[error]     <tpt>
[error]     <empty>
[error]   )
[error]   // 6 statements
[error]   ValDef( // private[this] val file: java.io.File in class package$FileOps
[error]     private <local> <paramaccessor> <triedcooking>
[error]     "file "
[error]     <tpt> // tree.tpe=java.io.File
[error]     <empty>
[error]   )
[error]   DefDef( // val file(): java.io.File in class package$FileOps
[error]     <method> <stable> <accessor> <paramaccessor> <triedcooking>
[error]     "file"
[error]     []
[error]     List(Nil)
[error]     <tpt> // tree.tpe=java.io.File
[error]     package$FileOps.this."file " // private[this] val file: java.io.File in class package$FileOps, tree.tpe=java.io.File
[error]   )
[error]   DefDef( // def extension(): Option in class package$FileOps
[error]     <method> <triedcooking>
[error]     "extension"
[error]     []
[error]     List(Nil)
[error]     <tpt> // tree.tpe=Option
[error]     Apply( // final def extension$extension($this: java.io.File): Option in object package$FileOps, tree.tpe=Option
[error]       "de"."heikoseeberger"."sbtheader"."package$FileOps"."extension$extension" // final def extension$extension($this: java.io.File): Option in object package$FileOps, tree.tpe=($this: java.io.File)Option
[error]       Apply( // val file(): java.io.File in class package$FileOps, tree.tpe=java.io.File
[error]         package$FileOps.this."file" // val file(): java.io.File in class package$FileOps, tree.tpe=()java.io.File
[error]         Nil
[error]       )
[error]     )
[error]   )
[error]   DefDef( // override def hashCode(): Int in class package$FileOps
[error]     <method> override <synthetic>
[error]     "hashCode"
[error]     []
[error]     List(Nil)
[error]     <tpt> // tree.tpe=Int
[error]     Apply( // final def hashCode$extension($this: java.io.File): Int in object package$FileOps, tree.tpe=Int
[error]       "de"."heikoseeberger"."sbtheader"."package$FileOps"."hashCode$extension" // final def hashCode$extension($this: java.io.File): Int in object package$FileOps, tree.tpe=($this: java.io.File)Int
[error]       Apply( // val file(): java.io.File in class package$FileOps, tree.tpe=java.io.File
[error]         package$FileOps.this."file" // val file(): java.io.File in class package$FileOps, tree.tpe=()java.io.File
[error]         Nil
[error]       )
[error]     )
[error]   )
[error]   DefDef( // override def equals(x$1: Object): Boolean in class package$FileOps
[error]     <method> override <synthetic>
[error]     "equals"
[error]     []
[error]     // 1 parameter list
[error]     ValDef( // x$1: Object
[error]       <param> <synthetic> <triedcooking>
[error]       "x$1"
[error]       <tpt> // tree.tpe=Object
[error]       <empty>
[error]     )
[error]     <tpt> // tree.tpe=Boolean
[error]     Apply( // final def equals$extension($this: java.io.File,x$1: Object): Boolean in object package$FileOps, tree.tpe=Boolean
[error]       "de"."heikoseeberger"."sbtheader"."package$FileOps"."equals$extension" // final def equals$extension($this: java.io.File,x$1: Object): Boolean in object package$FileOps, tree.tpe=($this: java.io.File, x$1: Object)Boolean
[error]       // 2 arguments
[error]       Apply( // val file(): java.io.File in class package$FileOps, tree.tpe=java.io.File
[error]         package$FileOps.this."file" // val file(): java.io.File in class package$FileOps, tree.tpe=()java.io.File
[error]         Nil
[error]       )
[error]       "x$1" // x$1: Object, tree.tpe=Object
[error]     )
[error]   )
[error]   DefDef( // def <init>(file: java.io.File): de.heikoseeberger.sbtheader.package$FileOps in class package$FileOps
[error]     <method> <triedcooking>
[error]     "<init>"
[error]     []
[error]     // 1 parameter list
[error]     ValDef( // file: java.io.File
[error]       <param> <paramaccessor> <triedcooking>
[error]       "file"
[error]       <tpt> // tree.tpe=java.io.File
[error]       <empty>
[error]     )
[error]     <tpt> // tree.tpe=de.heikoseeberger.sbtheader.package$FileOps
[error]     Block( // tree.tpe=Unit
[error]       // 2 statements
[error]       Assign( // tree.tpe=Unit
[error]         package$FileOps.this."file " // private[this] val file: java.io.File in class package$FileOps, tree.tpe=java.io.File
[error]         "file" // file: java.io.File, tree.tpe=java.io.File
[error]       )
[error]       Apply( // def <init>(): Object in class Object, tree.tpe=Object
[error]         package$FileOps.super."<init>" // def <init>(): Object in class Object, tree.tpe=()Object
[error]         Nil
[error]       )
[error]       ()
[error]     )
[error]   )
[error] )
[error]
[error] == Expanded type of tree ==
[error]
[error] ConstantType(value = Constant(java.io.File))
[error]
[error] uncaught exception during compilation: java.lang.AssertionError
java.lang.AssertionError: assertion failed: List(object package$FileOps, object package$FileOps)
    at scala.reflect.internal.Symbols$Symbol.suchThat(Symbols.scala:1678)
    at scala.reflect.internal.Symbols$ClassSymbol.companionModule0(Symbols.scala:2988)
    at scala.reflect.internal.Symbols$ClassSymbol.companionModule(Symbols.scala:2991)
    at scala.tools.nsc.backend.jvm.GenASM$JPlainBuilder.genClass(GenASM.scala:1371)
    at scala.tools.nsc.backend.jvm.GenASM$AsmPhase.run(GenASM.scala:120)
    at scala.tools.nsc.Global$Run.compileUnitsInternal(Global.scala:1583)
    at scala.tools.nsc.Global$Run.compileUnits(Global.scala:1557)
    at scala.tools.nsc.Global$Run.compileSources(Global.scala:1553)
    at scala.tools.nsc.Global$Run.compile(Global.scala:1662)
    at xsbt.CachedCompiler0.run(CompilerInterface.scala:116)
    at xsbt.CachedCompiler0.run(CompilerInterface.scala:95)
    at xsbt.CompilerInterface.run(CompilerInterface.scala:26)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at sbt.compiler.AnalyzingCompiler.call(AnalyzingCompiler.scala:101)
    at sbt.compiler.AnalyzingCompiler.compile(AnalyzingCompiler.scala:47)
    at sbt.compiler.AnalyzingCompiler.compile(AnalyzingCompiler.scala:41)
    at sbt.compiler.MixedAnalyzingCompiler$$anonfun$compileScala$1$1.apply$mcV$sp(MixedAnalyzingCompiler.scala:50)
    at sbt.compiler.MixedAnalyzingCompiler$$anonfun$compileScala$1$1.apply(MixedAnalyzingCompiler.scala:50)
    at sbt.compiler.MixedAnalyzingCompiler$$anonfun$compileScala$1$1.apply(MixedAnalyzingCompiler.scala:50)
    at sbt.compiler.MixedAnalyzingCompiler.timed(MixedAnalyzingCompiler.scala:74)
    at sbt.compiler.MixedAnalyzingCompiler.compileScala$1(MixedAnalyzingCompiler.scala:49)
    at sbt.compiler.MixedAnalyzingCompiler.compile(MixedAnalyzingCompiler.scala:64)
    at sbt.compiler.IC$$anonfun$compileInternal$1.apply(IncrementalCompiler.scala:160)
    at sbt.compiler.IC$$anonfun$compileInternal$1.apply(IncrementalCompiler.scala:160)
    at sbt.inc.IncrementalCompile$$anonfun$doCompile$1.apply(Compile.scala:66)
    at sbt.inc.IncrementalCompile$$anonfun$doCompile$1.apply(Compile.scala:64)
    at sbt.inc.IncrementalCommon.cycle(IncrementalCommon.scala:32)
    at sbt.inc.Incremental$$anonfun$1.apply(Incremental.scala:68)
    at sbt.inc.Incremental$$anonfun$1.apply(Incremental.scala:67)
    at sbt.inc.Incremental$.manageClassfiles(Incremental.scala:95)
    at sbt.inc.Incremental$.compile(Incremental.scala:67)
    at sbt.inc.IncrementalCompile$.apply(Compile.scala:54)
    at sbt.compiler.IC$.compileInternal(IncrementalCompiler.scala:160)
    at sbt.compiler.IC$.incrementalCompile(IncrementalCompiler.scala:138)
    at sbt.Compiler$.compile(Compiler.scala:152)
    at sbt.Compiler$.compile(Compiler.scala:138)
    at sbt.Defaults$.sbt$Defaults$$compileIncrementalTaskImpl(Defaults.scala:860)
    at sbt.Defaults$$anonfun$compileIncrementalTask$1.apply(Defaults.scala:851)
    at sbt.Defaults$$anonfun$compileIncrementalTask$1.apply(Defaults.scala:849)
    at scala.Function1$$anonfun$compose$1.apply(Function1.scala:47)
    at sbt.$tilde$greater$$anonfun$$u2219$1.apply(TypeFunctions.scala:40)
    at sbt.std.Transform$$anon$4.work(System.scala:63)
    at sbt.Execute$$anonfun$submit$1$$anonfun$apply$1.apply(Execute.scala:228)
    at sbt.Execute$$anonfun$submit$1$$anonfun$apply$1.apply(Execute.scala:228)
    at sbt.ErrorHandling$.wideConvert(ErrorHandling.scala:17)
    at sbt.Execute.work(Execute.scala:237)
    at sbt.Execute$$anonfun$submit$1.apply(Execute.scala:228)
    at sbt.Execute$$anonfun$submit$1.apply(Execute.scala:228)
    at sbt.ConcurrentRestrictions$$anon$4$$anonfun$1.apply(ConcurrentRestrictions.scala:159)
    at sbt.CompletionService$$anon$2.call(CompletionService.scala:28)
    at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
    at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)
[error] ({file:/Users/bene/workspace/projects/sbt-header/}sbtHeader/compile:compileIncremental) java.lang.AssertionError: assertion failed: List(object package$FileOps, object package$FileOps)

Header detection is broken on Windows

See #67: The AppVeyor build fails, because sat-header detects that headers are missing on each file. It looks like the regex defined in HeaderPattern don't work. This may be caused by a bug in sbt-header. Currently I suspect that the line ending configuration of the git client on AppVeyor may cause the problem. Needs investigation, I probably will have to setup a Windows VM for this ๐Ÿ™ˆ

Importing the whole sbtheader package leads to compile errors

[error] /Users/pdalpra/Work/Gatling/plugins/gatling-build-plugin/src/main/scala/io/gatling/build/LicenseHeadersPlugin.scala:16: value Seq in package sbtheader cannot be accessed in object de.heikoseeberger.sbtheader.package
[error]   val baseSettings = Seq(
[error]                      ^
[error] one error found

Maybe those type aliases can be made public or move to something else than a package object ?

build on JDK6

inclusion of this plugin leads to:

java.lang.UnsupportedClassVersionError: de/heikoseeberger/sbtheader/AutomateHeaderPlugin$ : Unsupported major.minor version 51.0

it would be good to support JDK6 as it is still the official JDK for Scala and is therefore used in many CIs. (I actually use it locally for my OSS projects to avoid discovering the pain only when merging)

Separate creation of a comment block from license texts

Since the license trait takes care of creating the header block using a comment style, it is not easily possible for users to reuse license texts with custom comment styles. We should separate the license texts from the creation of header blocks.

any idea when next release of sbt-header will be available?

I curious because my sbt-fresh PR depends on some functionality from master branch. I'd used self published Bintray artefact for that PR and it doesn't looks like nice approach.
Instead of using that self-publishing artefact I would like to use official release or snapshot version.

Is it any bugs or issues at sbt-header which is delaying a release? - might be I'll able to help with some of theses.

Add headers to managed sources

The current plugin "only" adds / updates headers on unmanaged sources.

It would be nice if it dealt with managed sources as well, at least optionally - I'm using sbt-boilerplate to generate some boilerplate code, and would love for the generated code to have proper headers. I can probably already get something working, by playing with unmanagedSources in createHeaders and updating the supported extensions, but it would be cleaner to have a setting like supportManagedSources.

If this is seen as interesting, I'd be happy to have a go at a PR.

Releasing 1.6.1

I'd love to use #30, but the latest release (1.6.0) doesn't seem to include the functionality. Would it be possible to cut a new release? \cc @fommil (as you contributed the feature, maybe you have also released it somewhere? :))

File type -> CommentStyle default mappings

It should not be necessary to specify which comment style to apply to common file types such as *.scala or *.java files. We should define a default value for headerMappings setting

Leverage the type system better

Currently everything seems to be a tuple of map of Strings. This is hard to understand. Define some types or at least type aliases.

Change settings to comment styles per file type but only one license.

Follow up to #85. Currently we define the License per file type using the headers setting:

headers := Map(
  scala -> Apache2_0("2015", "Heiko Seeberger")
  conf -> Apache2_0("2015", "Heiko Seeberger", HashLineCommentStyle)
)

This is pretty redundant because usually one will only use one license per project. We should change the way settings are defined to:

headerLicense: SettingsKey[License]
headerMappings: SettingsKey[String, CommentStyle]

This way the above configuration will become:

headerLicense := Apache2_0("2015", "Heiko Seeberger")

headerMapping := Map(
  scala -> CStyleCommentStyle
  conf -> HashLineCommentStyle
)

After this we can implement auto detection for license from project settings as well as sensible default values for headerMappings.

Add support for unmanagedSourceDirectories over unmanagedSources

At the moment the plugin is based on unmanagedSources which defaults to *.scala and *.java only. However the plugin allows us to specify headers by extension.

I believe the better solution would be to modify the plugin to use the unmanagedSourceDirectories key and find all the matching files in those directories based on the headerSettings map passed in, rather than getting the user to both specify the extension in the map, and then update their includeFilter in (Compile, unmanagedSources ) to include custom extensions too.

This is useful when dealing with plugins which use different file formats, but still can take copyright headers (such as .proto files and .txt files for i18n)

Preserve copyright years

Rather than just writing the new header into files, it would be better to "merge" the old and new headers, taking the current year and overall format from the configured header, and the copyright year ranges from the old.

Headers for build definition

First of all, thanks for this software! Can I use this plugin to add headers for files from project directory and build.sbt? I already created mapping for sbt files, but project definition stays untouched. Thanks!

use of scope ThisBuild doesn't seem to work

in a multiModule project you can use things like

scalaVersion in ThisBuild := [some-version]

which will apply to all submodules in the project

however, headers in ThisBuild:= does not seem to work.

Task to verify the presence of headers?

Is there a task to check whether headers are present and up to date? That'd be handy for Travis CI and similar services, to catch pull requests that don't have proper headers in new files.

CI build on windows

Currently the project is only build on Travis which runs builds in an Unix environment. #56 and #58 seem to be OS specific, so it would be good to have a CI build running on windows. AppVeyor looks like the solution to go with.

Add XML comment style

sbt-header currently does not support XML block comments. We should add an XML comment style. Note that XML has header line which needs to be preserved. So the code which detects shebangs in HeaderCreator needs to be made more generic. I propose to make the header line an optional property of CommentStyle. The HashbangLineComment style would detect shebangs, while the XmlBlockComment style would detect XML declarations. Other comment styles would have no header line detection.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.