GitBucket 4.15.0をリリースしました

Scalaで実装されたオープンソースのGitサーバ、GitBucket 4.15.0をリリースしました。

https://github.com/takezoe/gitbucket/releases/tag/4.15.0

標準プラグインをバンドル

以下のプラグインがGitBucketのディストリビューションにバンドルされるようになりました。

これらのプラグインは管理画面で有効/無効を切り替えることができます。

f:id:takezoe:20170731123257p:plain

Notificationsプラグイン

イシューやプルリクエストのメール通知が gitbucket-notifications-pluginとしてプラグインに分離されました。機能も強化されており、リポジトリやイシュー/プルリクエスト毎にメールを受け取るかどうかをユーザが選択できるようになっています。

f:id:takezoe:20170731123317p:plain

f:id:takezoe:20170731123327p:plain

プラグインのホイットデプロイに対応

プラグインのインストール/アンインストールを、GitBucketを再起動しなくても行えるようになりました。プラグインをインストールする場合はGITBUCKET_HOME/pluginsディレクトリにプラグインのjarファイルを配置します。アンインストールする場合はjarファイルを削除すればOKです。

さらにGitBucketの起動オプションに--plugin_dirオプションが追加されました。これはプラグイン開発者向けの機能で、GitBucketはこのオプションで指定したディレクトリからもプラグインを読み込むようになります。たとえば以下のようにGitBucketを起動します。

$ java -jar gitbucket.war --plugin_dir=<YOUR_PLUGIN_PROJECT_DIR>/target/scala-2.12

プラグインのプロジェクト側で以下のようにしてsbtを実行します。

$ sbt ~package

するとプラグインのプロジェクト側でソースを変更しただけで自動的にjarファイルが生成され、GitBucketにも変更が反映されます。

Slickを3.2.1にバージョンアップ

Slick(GitBucketが使用しているデータベースアクセスライブラリ)を3.2.0から3.2.1にバージョンアップしました。

このバージョンのSlickは前バージョンであるSlick 3.2.0とバイナリ互換性がないため、現在リリースされている多くのGitBucketプラグインが動作しなくなってしまうものと思われます。GitBucketプラグイン開発者の皆様はGitBucket 4.15.0向けにプラグインの再ビルドをお願いできればと思います。

SSHアクセスでed25519キーをサポート

GitリポジトリへのSSHアクセスでed25519キーもサポートされるようになりました。

編集フォームでのMarkdownプレビュー

これまでイシューやコメントの作成フォームではMarkdownのプレビューが可能でしたが、編集画面ではプレビューを行うことができませんでした。このリリースでは編集フォームでもMarkdownのプレビューが可能になりました。

f:id:takezoe:20170731123345p:plain

今回のバージョンではこの他にも様々な改善やバグフィックスを行っています。詳細についてはIssueの一覧をご覧ください。

Scala福岡 2017でApache PredictionIOの紹介をしました

7月29日(土)に福岡のヌーラボさんオフィスで開催されたScala福岡2017に参加しました。

scala.connpass.com

セッションではApache PredictionIOの紹介と、PredictionIOやelasticsearch-hadoopなどのOSSScala警察として活動してきた中で見かけたScala的にいけてないコードあるある(とその対処法)についてお話させていただきました。

www.slideshare.net

www.slideshare.net

他の皆さんのセッション(2トラックだったのでもう片方のセッションを聴講できなかったのが残念でした!)も興味深いものが多かったですが、特にはてなさんのPlayのバージョンアップの話が大変心に残りました。Slick様…。また、かなり久々にお会いする方々やお世話になっている方々にもご挨拶できたりなど大変有意義な時間を過ごさせていただきました。(Project Mobster懐かしい!!)

次は9月のScala関西サミットですねー。準備しなくてはー。

MarkdownベースのドキュメンテーションツールParadoxを使ってみた

最近AkkaやAlpakkaなどのドキュメントで使われているLightbend製のParadoxというドキュメンテーションツールを試してみました。 *1

github.com

使い方はドキュメントを見ればわかると思いますが、要点だけまとめておきます。

はじめの一歩

Paradoxはsbtプラグインとして実装されているのでまずはproject/plugins.sbtに以下を追記します。

addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.2.12")

build.sbtに以下の設定を追加します。

enablePlugins(ParadoxPlugin)

paradoxTheme := Some(builtinParadoxTheme("generic"))

ドキュメントはsrc/main/paradoxディレクトリ配下に作成します。以下のような内容でindex.mdというファイルを作っておきます。

Paradoxのテスト
========
これはParadoxのテストページです。

ここまで設定したらsbt paradoxを実行するとtarget/paradox/site/mainディレクトリ配下にHTMLが生成されます。

f:id:takezoe:20170711160752p:plain

ディレクティブを使ってみる

Paradoxでは基本的にMarkdownでドキュメントを記述しますが、@(インライン)、@@(ブロック)、@@@(コンテナ)で特殊なディレクティブを使用することができます。ここでは特に重要そうなディレクティブを2つだけ紹介しておきます。

サイドメニューを表示する

index.mdの末尾に以下のような記述を入れておくとサイドメニューを表示することができます。

@@@ index

* [Getting started](getting-started.md)
* [News](news.md)
* [Documentation](documentation.md)
* [Community](community.md)

@@@

表示はこんな感じになります。

f:id:takezoe:20170711160839p:plain

ソースコードを引用する

Paradoxの非常に便利な機能の1つがこのソースコードの引用機能です。以下のような記述でソースコードの一部を埋め込むことができます。

@@snip [Hello World](../../main/scala/HelloWorld.scala) { #sample }

ソースコードには以下のようにコメントでParadoxから参照するためのタグを埋め込んでおきます。

// #sample
object HelloWorld extends App {
  println("Hello World!")
}
// #sample

ソースコードの引用機能を使うことでソースコードとドキュメント内のサンプルコードの二重管理を防ぐことができます。ドキュメントで使用できるということがテストケースを書くインセンティブになりますし、CIを回しておけばドキュメントに掲載されているコードはビルドが通るコードであることを保証できます。

その他の機能

Paradoxにはこの他にもドキュメント内のクロスリファレンスやScaladocへのリンクを簡単に張ることができたり、Java / Scalaなどソースコードをタブで切り替えられるような形で表示することもできます。シンプルながらフレームワークやライブラリなどのドキュメント用にツボを抑えたツールと言えるでしょう。

利用シーンは主にScalaに限定されますが、READMEだけでは使い方を書ききれなくなってきたらParadoxの利用を検討してみてもよいのではないかと思います。

*1:UnfilteredのWebサイトもParadoxを使っているそうです。sbt-siteとsbt-ghpagesを使ってGitHub Pagesにデプロイしているそう。
http://xuwei-k.hatenablog.com/entry/20161211/1481437560

PredictionIO Meetup #2 に参加してきました

f:id:takezoe:20170708134650j:plain

先日弊社のオフィスでPredictionIO Meetup #2が開催されるとのことだったので見物に行ってきました。

d-cube.connpass.com

コミッタの菅谷さんの発表資料が公開されていたので貼っておきます。

PythonはPySparkとPy4Jがとにかくつらいという話でした。今検討しているのは学習処理だけなので、予測サーバでもPythonを使えるようにするには別途検討が必要とのこと。将来的にはPredictionIOでSpark MLlibだけじゃなくscikit-learnなども使えるようになるかもしれません。

この他にも若者によるDeeplearning4JをPredictionIOで使う話だったり(結局まだ動かせていないらしいw)、レコメンドシステムを題材にしたPredictionIOのEvaluation機能の紹介などがありました。PredictionIOのイントロダクション的なところからPython対応といったマニアックな話、機械学習初心者でも勉強になる話など幅広い内容で面白かったです。

8月に第三回を開催するそうです。LTも募集するそうなので次回は自分も何か話をしてみようかなと思っています。

Play 2.6の新機能

Play 2.6のドキュメントにある「What’s new in Play 2.6」からScalaに関する部分をざっと日本語にしてみました。

細かい部分はマイグレーションガイドも見ないとダメそうですね。こちらはかなり分量があるのでさくっと日本語にするのは厳しそう…。

グローバルステートが非推奨に

アプリケーションではplay.api.Play.current / play.Play.application()でグローバルアプリケーションにアクセスできるけど非推奨。以下のようにして禁止することもできる。

play.allowGlobalApplication=false

こうしておくとPlay.currentが例外をスローするようになる。

Akka HTTPベースのバックエンド

Akka HTTPベースのバックエンドがデフォルトになった。Nettyのバックエンドも利用可能。Nettyを使用する場合は以下を参照:

HTTP/2サポート (Experimental)

PlayAkkaHttp2Supportモジュールを使うことでHTTP/2サポートを利用可能。

lazy val root = (project in file("."))
  .enablePlugins(PlayJava, PlayAkkaHttp2Support)

デフォルトではrunコマンドでは動作しない。詳細は以下を参照:

リクエストの属性

リクエストが属性を含むようになった。属性にリクエストに関する追加の情報を格納することができる。たとえばフィルタで値をセットしておき、後続のアクションでその値を参照するといったことができる。

属性はTypedMapというタイプセーフかつイミュータブルなマップに格納される。

// Create a TypedKey to store a User object
object Attrs {
  val User: TypedKey[User] = TypedKey[User].apply("user")
}

// Get the User object from the request
val user: User = req.attrs(Attrs.User)
// Put a User object into the request
val newReq = req.addAttr(Attrs.User, newUser)

これに伴ってリクエストタグは非推奨になった。リクエストタグはリクエスト属性に置き換えることが望ましい。

Route modifierタグ

routeファイルに、routeごとのカスタム動作を指定するための"modifiers"を記述できるようになった。たとえば"nocsrf"というタグを記述しておくとCSRFフィルタが適用されなくなる。

+ nocsrf # Don't CSRF protect this route 
POST /api/foo/bar ApiController.foobar

独自のmodifierを作成することもでき、+のあとに空白で区切っていくつでもタグを記述することができる。

modifierタグはHandlerDefリクエスト属性で参照可能(これはroutesファイルで定義された他のメタデータも含んでいる)。

import play.api.routing.{ HandlerDef, Router }
import play.api.mvc.RequestHeader

val handler = request.attrs(Router.Attrs.HandlerDef)
val modifiers = handler.modifiers

TwirlテンプレートでのDI

Twirlテンプレートで@thisと記述するとコンストラクタを作成できるようになった。これによってコントローラ経由ではなくテンプレートに直接DIが可能になった。

たとえば以下のテンプレートはコントローラでは使用しないTemplateRenderingComponentというコンポーネントをDIしている。@this@()より前に記述する必要がある点に注意。

@this(trc: TemplateRenderingComponent)
@(item: Item)

@{trc.render(item)}

デフォルトではすべての@thisが記述されたテンプレートクラスには自動的に@javax.inject.Inject()が付与される。この挙動はbuild.sbtで変更できる。

// Add one or more annotation(s):
TwirlKeys.constructorAnnotations += "@java.lang.Deprecated()"

// Or completely replace the default one with your own annotation(s):
TwirlKeys.constructorAnnotations := Seq("@com.google.inject.Inject()")

コントローラではテンプレートクラスをDIして使用する。

public MyController @Inject()(indexTemplate: views.html.IndexTemplate,
                              cc: ControllerComponents)
  extends AbstractController(cc) {

  def index = Action { implicit request =>
    Ok(indexTemplate())
  }
}

フィルタの改善

以下のフィルタはデフォルトで有効。

  • play.filters.csrf.CSRFFilter
  • play.filters.headers.SecurityHeadersFilter
  • play.filters.hosts.AllowedHostsFilter

application.confでは+=でフィルタを追加することができる。

play.filters.enabled+=MyFilter

テストなどの目的でフィルタを無効にしたい場合はこんな感じで。

play.filters.disabled+=MyFilter

詳細は以下を参照:

NOTE: CSRF.formFieldのようなCSRFフォームヘルパーを使用していない既存のプロジェクトはおそらくPUTおよびPOSTリクエストが"403 Forbidden"になるはず。この挙動をチェックするには<logger name="play.filters.csrf" value="TRACE"/>logback.xmlに追加する。また、もしlocalhost以外でPlayアプリケーションを実行している場合、AllowedHostsFilterでアクセス元のホスト名/IPアドレスを設定する必要がある。

gzipフィルタ

gzipフィルタを使っている場合、独自のフィルタを書く代わりにapplication.confで適用するContentTypeを指定できるようになった。

play.filters.gzip {

    contentType {

        # If non empty, then a response will only be compressed if its content type is in this list.
        whiteList = [ "text/*", "application/javascript", "application/json" ]

        # The black list is only used if the white list is empty.
        # Compress all responses except the ones whose content type is in this list.
        blackList = []
    }
}

JWTクッキー

セッションとFlashのクッキーにデフォルトでJSON Web Tokenを使用するようになった。

詳細は以下を参照:

Logging Marker API

play.Loggerplay.api.LoggerでSLF4Jのマーカーがサポートされた。

import play.api._
logger.info("some info message")(MarkerContext(someMarker))

これによってマーカーを暗黙的に渡すことが可能になる。たとえばLogstash Logback Encoderを使う場合、こんなトレイトを定義しておき、

trait RequestMarkerContext {

  implicit def requestHeaderToMarkerContext(request: RequestHeader): MarkerContext = {
    import net.logstash.logback.marker.LogstashMarker
    import net.logstash.logback.marker.Markers._

    val requestMarkers: LogstashMarker = append("host", request.host)
      .and(append("path", request.path))

    MarkerContext(requestMarkers)
  }

}

コントローラで使用すると、以下のような感じで異なるExecutionContextを使用するFutureにも持ち回ることができる。

def asyncIndex = Action.async { implicit request =>
  Future {
    methodInOtherExecutionContext()
  }(otherExecutionContext)
}

def methodInOtherExecutionContext()(implicit mc: MarkerContext): Result = {
  logger.debug("index: ") // same as above
  Ok("testing")
}

マーカーコンテキストは"tracer bullet"スタイルのロギング(明示的にログレベルを変更せずに特定のリクエストのログを出力したい場合)に便利。たとえば特定の条件の場合のみマーカーを追加することができる。

trait TracerMarker {
  import TracerMarker._

  implicit def requestHeaderToMarkerContext(implicit request: RequestHeader): MarkerContext = {
    val marker = org.slf4j.MarkerFactory.getDetachedMarker("dynamic") // base do-nothing marker...
    if (request.getQueryString("trace").nonEmpty) {
      marker.add(tracerMarker)
    }
    marker
  }
}

object TracerMarker {
  private val tracerMarker = org.slf4j.MarkerFactory.getMarker("TRACER")
}

class TracerBulletController @Inject()(cc: ControllerComponents)
  extends AbstractController(cc) with TracerMarker {
  private val logger = play.api.Logger("application")

  def index = Action { implicit request: Request[AnyContent] =>
    logger.trace("Only logged if queryString contains trace=true")

    Ok("hello world")
  }
}

logback.xmlで以下のTurboFilterでログをトリガーできる。

<turboFilter class="ch.qos.logback.classic.turbo.MarkerFilter">
  <Name>TRACER_FILTER</Name>
  <Marker>TRACER</Marker>
  <OnMatch>ACCEPT</OnMatch>
</turboFilter>

詳細は以下を参照。

また、マーカーを使ったロギングについては以下を参照:

Configurationの改善

play.api.Configurationにカスタム型で読み取るための新しいメソッドが追加された。任意の型に対応したimplicitなConfigLoaderを使用することができる。詳細は以下を参照:

セキュリティログ

Playのセキュリティに関するオペレーションにはsecurityマーカーが追加され、セキュリティチェックに失敗するとWARNレベルでログが出力されるようになった。これによって開発者がなぜリクエストが失敗したのかを把握できるようになった。Play 2.6ではセキュリティフィルタがデフォルトで有効になったため、これは重要なことである。

securityマーカーを通常のログと分けてトリガーしたりフィルタすることができる。たとえばsecurityマーカーがセットされているログを無効にするにはlogback.xmlに以下の設定を追加する。

<turboFilter class="ch.qos.logback.classic.turbo.MarkerFilter">
    <Marker>SECURITY</Marker>
    <OnMatch>DENY</OnMatch>
</turboFilter>

I18Nサポートの改善

PlayではMessagesApiLangクラスが国際化のために使用されており、フォームでのエラーメッセージの表示にも必要だったが、これまではいろいろ面倒なステップが必要だった。Play 2.6ではI18N APIがリファインされた。

MessagesActionBuilderが追加された。これはMessagesRequestMessageProviderを継承したWrappedRequest)を提供する。テンプレートには1つのimplicitパラメータを定義するだけでよく、コントローラはI18nSupportを継承する必要はない。

class FormController @Inject()(messagesAction: MessagesActionBuilder, components: ControllerComponents)
  extends AbstractController(components) {

  import play.api.data.Form
  import play.api.data.Forms._

  val userForm = Form(
    mapping(
      "name" -> text,
      "age" -> number
    )(UserData.apply)(UserData.unapply)
  )

  def index = messagesAction { implicit request: MessagesRequest[AnyContent] =>
    Ok(views.html.displayForm(userForm))
  }

  def post = ...
}

この場合のdisplayForm.scala.htmlはこんな感じ。

@(userForm: Form[UserData])(implicit request: MessagesRequestHeader)

@import helper._

@helper.form(action = routes.FormController.post()) {
  @CSRF.formField                     @* <- takes a RequestHeader    *@
  @helper.inputText(userForm("name")) @* <- takes a MessagesProvider *@
  @helper.inputText(userForm("age"))  @* <- takes a MessagesProvider *@
}

詳細以下を参照:

テストサポート

MessagesApiインスタンスの作成も改善されており、DefaultMessagesApi()DefaultLangs()をデフォルト引数で生成できるようになった。メッセージを指定したい場合は以下のようにできる。

val messagesApi: MessagesApi = {
    val env = new Environment(new File("."), this.getClass.getClassLoader, Mode.Dev)
    val config = Configuration.reference ++ Configuration.from(Map("play.i18n.langs" -> Seq("en", "fr", "fr-CH")))
    val langs = new DefaultLangsProvider(config).get
    new DefaultMessagesApi(testMessages, langs)
  }

Futureのタイムアウトとディレイのサポート

play.api.libs.concurrent.Futuresトレイトを使用する。

import play.api.libs.concurrent.Futures._

class MyController @Inject()(cc: ControllerComponents)(implicit futures: Futures) extends AbstractController(cc) {

  def index = Action.async {
    // withTimeout is an implicit type enrichment provided by importing Futures._
    intensiveComputation().withTimeout(1.seconds).map { i =>
      Ok("Got result: " + i)
    }.recover {
      case e: TimeoutException =>
        InternalServerError("timeout")
    }
  }
}

同様に指定した時間後にFutureを実行するdelayedメソッドも利用可能。詳細は以下を参照:

CustomExecutionContextとスレッドプールサイズ

CustomExecutionContextakka.actor.ActorSystemにデリゲートするカスタムExecutionContextを定義する。これはデフォルトのExecutionContextを使用するべきではない状況(たとえばデータベースアクセスやブロッキングI/Oなど)で便利。

Playのテンプレートプロジェクトを更新

PlayのダウンロードページにあるブロッキングAPI(AnormやJPAなど)を使用するサンプルテンプレートはカスタムExecutionContextを使うように変更された。たとえばJPAのサンプルではDatabaseExecutionContextを使用するようになっている。

JDBCのコネクションプールに関わるスレッドプールは、ThreadPoolExecutorを使用してコネクションプールのサイズにあわせて固定したいはず。HikariCPではコネクションプールのサイズは物理コア数の2倍+ディスクのスピンドル数に設定するべきとされている。dispatcherの設定はこんな感じ。

# db connections = ((physical_core_count * 2) + effective_spindle_count)
fixedConnectionPool = 9

database.dispatcher {
  executor = "thread-pool-executor"
  throughput = 1
  thread-pool-executor {
    fixed-pool-size = ${fixedConnectionPool}
  }
}

CustomExecutionContextの定義はこんな感じ。

@Singleton
class DatabaseExecutionContext @Inject()(system: ActorSystem)
   extends CustomExecutionContext(system, "database.dispatcher")

implicitパラメータで渡す。

class DatabaseService @Inject()(implicit executionContext: DatabaseExecutionContext) {
  ...
}

WSClientの改善

PlayのWSClientスタンドアロンで利用可能なplay-wsのラッパーになった。また、play-wsで使用されているライブラリはシャードされるようになったのでNettyがSpark、Playなどが使用しているNettyとコンフリクトすることがなくなった。

また、キャッシュ実装が存在する場合はHTTP Cachingがサポートされるようになった。

詳細は以下を参照:

Play JSONの改善

タプルのシリアライズ

タプルは配列にシリアライズされる。たとえば("foo", 2, "bar")["foo", 2, "bar"]レンダリングされる。

Scala.jsサポート

Play JSON 2.6.0はScala.jsもサポートしている。

libraryDependencies += "com.typesafe.play" %%% "play-json" % version

テストの改善

play.api.testパッケージにテストのためのいくつかのユーティリティクラスが追加された。DIされるコンポーネントを使用したファンクショナルテストを簡単に行うことができるようになった。

Injecting

今まではこんな感じで書いていたものが、

"test" in new WithApplication() {
  val executionContext = app.injector.instanceOf[ExecutionContext]
  ...
}

Injectingトレイトを使って以下のように記述できるようになった。

"test" in new WithApplication() with Injecting {
  val executionContext = inject[ExecutionContext]
  ...
}

StubControllerComponents

StubControllerComponentsFactoryControllerComponentsのスタブを生成することができ、コントローラーのユニットテストで以下のように使用できる。

val controller = new MyController(stubControllerComponents())

StubBodyParser

同様にStubBodyParserFactoryBodyParserのスタブを生成することができ、コントローラーのユニットテストで使用できる。

val stubParser = stubBodyParser(AnyContent("hello"))

ファイルアップロードの改善

ファイルアップロードはTemporaryFile(テンポラリファイルシステムに保存される)を使用する。理想的な挙動は不要になったTemporaryFileは可能な限り早く削除されることだが、Play 2.5ではファイナライザを使用してGC時に削除されていた。しかし特定の条件下ではGCはタイムリーに発生しないため、バックグラウンドのクリーンアップ処理はFinalizableReferenceQueueとPhantomReferenceを使用するように変更されていた。

TemporaryFileは再度修正され、TemporaryFileの参照はTemporaryFileCreatorトレイトから来るようになり、必要に応じて実装を差し替えることができるようになった。また、StandardCopyOption.ATOMIC_MOVEを使用するatomicMoveWithFallbackメソッドが追加された。

TemporaryFileReaper

Akkaスケジューラを使用して定期的にテンポラリファイルを削除できるplay.api.libs.Files.TemporaryFileReaperが追加された。デフォルトでは無効だがapplication.confで以下のようにすることで有効にできる。

play.temporaryFile {
  reaper {
    enabled = true
    initialDelay = "5 minutes"
    interval = "30 seconds"
    olderThan = "30 minutes"
  }
}

この設定は30分以上前のファイルを削除する。reaperはアプリケーションが起動してから5分後に開始し、30秒ごとにファイルシステムをチェックする。きちんと設定しないと時間のかかっているアップロード中のファイルを削除してしまう可能性があるので注意すること。

GitBucket 4.14をリリースしました

4.14にはリポジトリのフォークに失敗する場合があるバグがあり、これを修正した4.14.1をリリース済みです。こちらをご利用ください。 https://github.com/gitbucket/gitbucket/releases/tag/4.14.1

Scalaで実装されたオープンソースのGitサーバ、GitBucket 4.14をリリースしました。

https://github.com/takezoe/gitbucket/releases/tag/4.14

イシュー、プルリクエストに優先度を指定可能に

イシュー、プルリクエストに優先度を指定できるようになりました。優先度はリポジトリ毎にカスタマイズ可能で、指定した優先度によってフィルタリングやソートを行うことができます。

f:id:takezoe:20170701015316p:plain

f:id:takezoe:20170701015323p:plain

サイドバーの折りたたみ

サイドバーを折りたたんだ場合もアイコンを表示し、サイドバーを開かなくても画面遷移を行えるようになりました。

f:id:takezoe:20170701015458p:plain

f:id:takezoe:20170701015503p:plain

Webフックの改善

Webフックに以下の改善を行いました。

  • gollumイベント(Wiki関連のイベント)のサポート
  • ユーザ、グループ単位でのWebフックを設定可能に

f:id:takezoe:20170701015936p:plain

--max_file_sizeオプションの追加

アップロード可能なファイルサイズを指定するための--max_file_sizeオプションが追加されました。このオプションは組み込みJettyを使用して起動する場合に以下のように指定できます。

java -jar gitbucket.war --max_file_size=10485760

また、システムプロパティgitbucket.maxFileSizeでも同様に指定することができます。gitbucket.warをサーブレットコンテナにデプロイしている場合はこちらの方法で設定するとよいでしょう。

全ての設定を環境変数またはシステムプロパティで指定可能に

gitbucket.confおよびdatabase.confで設定可能なすべての項目を環境変数もしくはシステムプロパティで指定できるようになりました。たとえばldap.mail_attributeという設定項目の場合、GitBucketは以下の順番で設定値を取得します。

  1. システムプロパティgitbucket.ldap.mail_attribute
  2. 環境変数GITBUCKET_LDAP_MAIL_ATTRIBUTE
  3. gitbucket.confldap.mail_attributeプロパティ

これによってDockerコンテナなどの環境でGitBucketを使用する場合に設定の自動化が容易になります。

プラグインのための新しい拡張ポイント

イシューおよびプルリクエストに関するイベントをフックするための拡張ポイントを追加しました。

  • accountHooks
  • issueHooks
  • pullRequestHooks
  • issueSidebars

今回のバージョンではこの他にも様々な改善やバグフィックスを行っています。詳細についてはIssueの一覧をご覧ください。

型駆動開発の裏側 -IdrisのIDEモードを試してみる-

以前紹介したように、AtomにIdris用パッケージをインストールするとキーボードショートカットで様々な機能を利用できます。

takezoe.hatenablog.com

これがどのように実装されているのかが気になったので調べてみようと思い、Atomパッケージのソースコードを眺めてみました。

github.com

どうやらIdrisにはIDEモードというモードがあり、標準入出力を使用してS式でやり取りを行うIDE向けのプロトコルが存在するようです。ちょっと試してみました。

まずはIDEモードでプロセスを起動。

$ idris --ide-mode
000018(:protocol-version 1 0)

入出力は「メッセージの文字数を6桁の16進数にエンコードしたもの+S式のメッセージ」という形式で行われるようです。

こんな感じの作りかけのファイルを用意しておきます。

module Main

main : IO ()

このファイルを読み込みます。メッセージにはユニークなカウンタを含める必要があるようです。

000022((:load-file "/tmp/hello.idr") 1)

出力はこんな感じ。

0000a9(:output (:ok (:highlight-source ((((:filename "/tmp/hello.idr") (:start 1 8) (:end 1 12)) ((:namespace "Main") (:decor :module) (:source-file "/tmp/hello.idr")))))) 1)
00001e(:set-prompt "*/tmp/hello" 1)
000015(:return (:ok ()) 1)

CTRL+ALT+A相当の:add-clauseというメッセージを送信してみます。

00001B((:add-clause 3 "main") 2)

こんな感じで生成されたコードが返ってきます。

000025(:return (:ok "main = ?main_rhs") 2)

シンプルな手法ではありますが、Idris側でこのような機能を持っているので様々なエディタで同じ機能を簡単に実装できるようです。実際Atom以外にもvimEmacsSublime Text用のプラグインなども用意されています。ショートカットは異なりますが、使い慣れたエディタを使用して型駆動開発を行うことができますね。