クリアコードでは最近Gitリポジトリの管理ツールをgitoliteからGitLabに移行*1しました。
クリアコードではgit-utilsを使用してdiff付きのコミットメールを配信しているのですが、GitLabに移行した当初はgithub-post-receiverがGitLab 6.0に対応しきれていませんでした。 github-post-receiverをGitLabで使用する場合は、以下の2つの制限がありました。
この2つの制限の原因はgithub-post-receiverのGitLab対応が不十分であったことと複数サイトでの運用を考慮していなかったことでした。
そこで、この2つの制限を撤廃するための対応をしたのですが、一部互換性をなくす必要があったので、その説明と新しい設定ファイルの書き方の説明をします。
github-post-receiverで複数サイトに対応するための設定ファイルの書き方を説明します。設定ファイルは後方互換性を維持しています。
github-post-receiverにはconfig.yamlという設定ファイルが必要です。 古い書式は以下の通りです。
to: receiver@example.com error_to: admin@example.com exception_notifier: subject_label: "[git-utils]" sender: sender@example.com add_html: true owners: ranguba: to: groonga-commit@rubyforge.org repositories: examples: to: null@example.com groonga: to: groonga-commit@lists.sourceforge.jp
1つのサイトからのpost-receive-hooksしか受け付けないのであれば、古い書式のままでも問題ありません。 これは、古い書式の場合は全てのサイトで同一の設定を使用するようになっているためです。
複数のサイト*2を運用している場合は以下のようにdomainsキーを追加した設定ファイルを書けば、複数サイト運用時にgithub.comとGHEでリポジトリ名が重複していてもそれぞれのサイトごとにコミットメールの宛先などを設定することができます。
to: global-to@example.com error_to: error@example.com exception_notifier: subject_label: "[git-utils]" sender: sender@example.com add_html: false domains: github.com: add_html: true owners: clear-code: to: commit@clear-code.com ranguba: to: - groonga-commit@rubyforge.org - commit@clear-code.com repositories: examples: to: null@example.com groonga: to: groonga-commit@lists.sourceforge.jp ghe.example.com: owners: clear-code: to: - commit@example.com from: null@example.com repositories: test-project1: to: commit+test-project1@example.com test-project2: to: commit+test-project2@example.com support: to: support@example.com from: null+support@example.com ghe.example.co.jp: add_html: true owners: clear-code: to: - commit@example.co.jp from: null@example.co.jp repositories: test-project1: to: commit+test-project1@example.co.jp test-project2: to: commit+test-project2@example.co.jp support: to: support@example.co.jp from: null+support@example.co.jp
設定は、一番狭い(深い)指定にマッチしたものを使用します。 いくつか具体的に説明します。
詳しくはテストコードを参照してください。
以前のバージョンとの非互換を説明します。
github-post-receiverは、通知が来るとリポジトリをミラーリングします。このときにgithub-post-receiverが動作しているホスト上のディレクトリ構成を変更しました。 以下のようにリポジトリのURIからドメイン名も抽出してパスの一部として使うようになっています。 また、GitLabではowner_nameが"$gitlab"固定だったのをやめ、リポジトリのURIからowner_nameを抽出するようにしました。
Before: mirrors/#{owner_name}/#{repository_name} After : mirrors/#{domain}/#{owner_name}/#{repository_name}
以前からgithub-post-receiverを使用している場合は、事前にミラーしたリポジトリを移動しておくと無駄なミラーリングを行いません。 パスが変わるだけなので、事前にミラーしたリポジトリを移動しなかった場合でもディスクを余分に使用すること以外に害はありません。
git-utilsがGitLabのpost-receive-hooksと複数サイトの運用に対応したのでその説明をしました。 今後もより便利に使えるように、改善していきたいと考えているのでgit-utilsのissueに今後やりたいことをあげておきました。
お時間のある方は、取り組んでみるとよいかもしれません。
gettextという翻訳の仕組み*1はフリーソフトウェアではよく使われています。いくつか不便な点はありますが、長年使われている仕組みでツールが揃っていることが理由でしょう。不便な点の1つである、「バージョン管理システムとの相性の悪さ」を解消する案が浮かんだので紹介します。
gettextでは.poファイルに翻訳したテキストを書きます。翻訳したテキストは自動生成ではない情報なのでバージョン管理対象です。ということで、.poファイルはリポジトリーに入れます。
バージョン管理システムと相性が悪い原因は、.poファイルに翻訳したテキスト以外のいろいろな情報が入っていることです。中でも、「どこに翻訳対象のメッセージがあったか」は自動生成できる情報で、量も多く、通常であればバージョン管理しない情報です。しかし、バージョン管理したい.poファイルの中に含まれているので一緒にバージョン管理対象になってしまいます。これが相性が悪い点です。
ただ、「どこに翻訳対象のメッセージがあったか」は翻訳時には有用な情報なので単に.poファイルから消してしまうと翻訳時に不便です。翻訳時の利便を減らさずにこの相性の悪さを解決しようと試みるのがこの記事で説明する案です。
gettextとバージョン管理システムを一緒に使うと相性が悪いケースを具体的に確認しましょう。
まず、リポジトリーを用意します。
% mkdir -p /tmp/gettext-and-vcs % cd /tmp/gettext-and-vcs % git init
gettextを使ったプログラムを用意します。
hello.c:
1 2 3 4 5 6 7 8 9 |
#include <stdio.h> #include <libintl.h> int main(void) { puts(gettext("Hello")); return 0; } |
この中のgettext("Hello")
の「Hello」がgettextで翻訳するメッセージです。
リポジトリーに追加します。
% git add hello.c % git commit
次の作業の前に、gettextでどのように翻訳するかのざっくりとした流れを示します。
今は1.の「翻訳したい対象を用意する」を用意した段階です。
それでは、2.の「翻訳したい対象から翻訳対象のメッセージを抽出する」をやってみましょう。xgettextというツールで抽出し、抽出したメッセージをpo/hello.potに出力します*2。
% mkdir -p po/ % (cd po && xgettext --package-name Hello --package-version 1.0.0 --output hello.pot ../hello.c) % cat po/hello.pot # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: Hello 1.0.0\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2013-11-14 22:27+0900\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" #: ../hello.c:7 msgid "Hello" msgstr ""
最後の部分が大事なところです。
#: ../hello.c:7 msgid "Hello" msgstr ""
hello.cの7行目の「Hello」を翻訳対象のメッセージとして抽出しました。
次に、3.の「抽出した翻訳対象のメッセージから.poファイルを作る」をやりましょう。
% msginit --locale ja_JP.UTF-8 --input po/hello.pot --output-file po/ja.po ... (メールアドレスを入力) ... % cat po/ja.po # Japanese translations for Hello package # Hello パッケージに対する英訳. # Copyright (C) 2013 THE Hello'S COPYRIGHT HOLDER # This file is distributed under the same license as the Hello package. # Kouhei Sutou <kou@clear-code.com>, 2013. # msgid "" msgstr "" "Project-Id-Version: Hello 1.0.0\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2013-11-14 22:27+0900\n" "PO-Revision-Date: 2013-11-14 22:28+0900\n" "Last-Translator: Kouhei Sutou <kou@clear-code.com>\n" "Language-Team: Japanese\n" "Language: ja\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" #: ../hello.c:7 msgid "Hello" msgstr ""
翻訳対象のメッセージの前にはメタデータがいろいろ入っています。2.でできた.potファイルはバージョン管理しませんが、ここで作った.poファイルはバージョン管理対象です。
% echo '*.pot' >> .gitignore % git add .gitignore % git add po/ja.po % git commit
いよいよ翻訳です。4.の「.poファイルの中の翻訳対象のメッセージを翻訳する」になります。.poファイルを編集するためのエディターは、翻訳対象のメッセージがどのように使われているかをすぐに確認できる機能がある*3ので、それを使いながら翻訳します。このとき、翻訳対象のメッセージのすぐ上にある「../hello.c:7」という「どこに翻訳対象のメッセージがあったか」という情報を使います。本来であればバージョン管理対象としない情報ですが、翻訳時には便利なので.poファイルに入っている情報です。
% editor po/ja.po % git diff diff --git a/po/ja.po b/po/ja.po index 0a90c72..49e9f76 100644 --- a/po/ja.po +++ b/po/ja.po @@ -9,7 +9,7 @@ msgstr "" "Project-Id-Version: Hello 1.0.0\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2013-11-14 22:27+0900\n" -"PO-Revision-Date: 2013-11-14 22:28+0900\n" +"PO-Revision-Date: 2013-11-14 22:34+0900\n" "Last-Translator: Kouhei Sutou <kou@clear-code.com>\n" "Language-Team: Japanese\n" "Language: ja\n" @@ -20,4 +20,4 @@ msgstr "" #: ../hello.c:7 msgid "Hello" -msgstr "" +msgstr "こんにちは"
この変更*4はバージョン管理対象です。せっかくの翻訳を失いたくありません。
% git add po/ja.po % git commit
ここからは翻訳したメッセージを使うための作業です。
まず、.moファイルを作ります。5.の「.poファイルから.moファイルを作る」という作業です。.moファイルは.poファイルをコンパイルした実行に都合のよいファイルだと理解すれば十分です。locale/ja/LC_MESSAGES/hello.moに置きます*5。
% mkdir -p locale/ja/LC_MESSAGES/ % msgfmt --output-file locale/ja/LC_MESSAGES/hello.mo po/ja.po
なお、.moファイルは.poファイルから自動生成できるためバージョン管理対象外です。
% echo /locale/ >> .gitignore % git add .gitignore % git commit
いよいよ翻訳したメッセージを使います。6.の「.moファイルにあるデータを使って翻訳対象のメッセージを翻訳する」です。
% cc -o hello hello.c % LANG=ja_JP.UTF-8 ./hello Hello
あれ、英語のままですね。実は、元のプログラムは必要な関数呼び出しが足りません。
% editor hello.c % git diff diff --git a/hello.c b/hello.c index b210f06..9852314 100644 --- a/hello.c +++ b/hello.c @@ -1,9 +1,13 @@ #include <stdio.h> #include <libintl.h> +#include <locale.h> int main(void) { + setlocale(LC_ALL, ""); + bindtextdomain("hello", "locale"); + textdomain("hello"); puts(gettext("Hello")); return 0; }
実行してみましょう。
% cc -o hello hello.c % LANG=ja_JP.UTF-8 ./hello こんにちは
翻訳できたのでコミットします。
% git add hello.c % git commit
実行ファイル「hello」は自動生成のファイルなので無視しておきましょう。
% echo /hello >> .gitignore % git add .gitignore % git commit
それではもうひとつメッセージを追加しましょう。
% git diff diff --git a/hello.c b/hello.c index 9852314..862f1df 100644 --- a/hello.c +++ b/hello.c @@ -9,5 +9,6 @@ main(void) bindtextdomain("hello", "locale"); textdomain("hello"); puts(gettext("Hello")); + puts(gettext("World")); return 0; } % cc -o hello hello.c % LANG=ja_JP.UTF-8 ./hello こんにちは World
追加した方はまだ翻訳されていませんが、プログラムは動いているのでコミットしましょう。
% git add hello.c % git commit
それでは翻訳しましょう。
まず、.poファイルを更新します。
% (cd po && xgettext --package-name Hello --package-version 1.0.0 --output hello.pot ../hello.c) % msgmerge --update po/ja.po po/hello.pot ... 完了. % git diff | cat diff --git a/po/ja.po b/po/ja.po index 49e9f76..af14c60 100644 --- a/po/ja.po +++ b/po/ja.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Hello 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2013-11-14 22:27+0900\n" +"POT-Creation-Date: 2013-11-14 23:10+0900\n" "PO-Revision-Date: 2013-11-14 22:34+0900\n" "Last-Translator: Kouhei Sutou <kou@clear-code.com>\n" "Language-Team: Japanese\n" @@ -18,6 +18,10 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" -#: ../hello.c:7 +#: ../hello.c:11 msgid "Hello" msgstr "こんにちは" + +#: ../hello.c:12 +msgid "World" +msgstr ""
はい、注目!「World」用のエントリーが追加されただけではなく、「Hello」の出現位置情報(「../hello.c:7」)も更新されています。今はメッセージ数が少ないのであまり気になりませんが、通常はメッセージ数は数十や数百などもっと多くなります。そうすると出現位置の変更が数十や数百以上になります。例えば、Groongaのドキュメントで.poファイルを更新すると変更業は2000行くらいになります。メッセージを変更するたびにこのくらいの変更をバージョン管理するのはどうなんだろうと思うようになります。
ようやく本題です。バージョン管理システムとの相性の悪さを解消する案を紹介します。
まず状況を整理します。
この状況を解決する案を思いつくヒントになったのがgrosser/gettext_i18n_rails#103でのやりとりです。このやりとりで.poファイルに出現位置情報を含めないという使い方をしている人がいることを知りました。
このヒントから、バージョン管理する.poファイルには出現位置情報を含めず、バージョン管理しない作業用の.poファイルを導入する案を思いつきました。作業用の.poファイルには出現位置情報を含めます。そのため、この.poファイルを使えば翻訳時の利便は失われません。作業用の.poファイルの名前をja.edit.poとすると以下のような使い分けです。
これを実現するためには以下のような作業の流れにします。
% msgmerge --output po/ja.edit.po po/ja.po po/hello.pot ... 完了. % editor po/ja.edit.po # ← 出現位置情報付きなので今まで通りの利便性で翻訳できる % msgcat --no-location --output po/ja.po po/ja.edit.po % git diff diff --git a/po/ja.po b/po/ja.po index 49e9f76..81b1127 100644 --- a/po/ja.po +++ b/po/ja.po @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: Hello 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2013-11-14 22:27+0900\n" -"PO-Revision-Date: 2013-11-14 22:34+0900\n" +"POT-Creation-Date: 2013-11-14 23:10+0900\n" +"PO-Revision-Date: 2013-11-14 23:34+0900\n" "Last-Translator: Kouhei Sutou <kou@clear-code.com>\n" "Language-Team: Japanese\n" "Language: ja\n" @@ -18,6 +18,8 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" -#: ../hello.c:7 msgid "Hello" msgstr "こんにちは" + +msgid "World" +msgstr "世界"
出現位置情報がなくなったので大量の不必要な変更行をバージョン管理しなくてもよくなりました。作業用の.poファイルはバージョン管理対象外にしてコミットしましょう。
% echo '*.edit.po' >> .gitignore % git add .gitignore % git commit % git add po/ja.po % git commit
作業用の.poファイルとバージョン管理用の.poファイルを作る手間が増えるというデメリットはありますが、これは自動化できるので気にならないのではないかと予想しています。
ただ、翻訳中に「バージョン管理システムの機能を使ってdiffを見る」ことがしづらくなるデメリットが解消できるか微妙なところです。実際にやってみないとなんとも言えません。
gettextとバージョン管理システムは以下のように相性が悪いことを具体例と共に示しました。
これを解決する以下の案を紹介しました。
この案により、gettextとバージョン管理システムの相性問題は解決しますが、もしかしたら、作業時に別の不便なことが発生するかもしれません。そのため、実際にこの案を試してみてよさそうかどうかを確認する必要があります。gettext gemにこの案の実践を支援する機能を入れたいところです。
なお、ここで作成したリポジトリーはGitHubにあります。コミットメッセージの書き方や意図が伝わるコミットのしかたに興味のある人はのぞいてみてください。ライセンスはCC0(パブリックドメイン)です。
*1 gettextという仕組みの1つの実装がGNU gettextです。この記事では「gettext」を実装ではなく仕組みのことを指すために使います。
*2 cdしてからxgettextを実行しているのは、hello.cへのパスをこれから作る.poファイルからの相対パスにするためです。こうすると翻訳時にツールの助けを得られます。
*3 Emacs用の.poファイル編集モードであるpo-modeは「s」(たぶん、sourceからの連想)でメッセージの使用箇所を表示します。
*4 「PO-Revision-Date」の値はpo-modeが自動的に更新します。
*5 置き場所のルールがありますが、この記事の本質とは関係ないため省略します。
RubyはCのように変数宣言のための特別な構文はなく、変数に代入する式を書くとそれ以降その変数を使えるようになります。
1 2 3 |
1 + 1 # <- ここでは「message」変数を使えない message = "Hello" # <- ここから「message」変数を使える puts(message) # <- ここでは「message」変数を使える |
そのため、明示的に変数を宣言しようとすることはほとんどありません。そのように書かれているRubyのコードを見ると違和感を覚えるほどです。何か特別な意図があるのではないかと考えてしまいます。
1 2 3 4 5 |
message = nil # <- 変数を宣言するための代入式 1 + 1 message = "Hello" puts(message) |
しかし、このように変数を宣言するために代入式を使った方が読みやすくなる場合があります。どういう場合かを例と理由をつけて説明します。なお、どうしてこのようなことを書く気になったのかというと、コミットへのコメントサービスを実施したときのコメントのやりとりで話題になり、「こういう考えでやっている」ということを説明したからです*1。
なお、スコープを広げるためには変数宣言をする「必要」がありますが、今回は、どういうときに必要かという話はしません。こういうときに使うのが「オススメ」という話だけします。
「オススメ」のケースはデフォルト値を設定するケースです。このケースでは変数宣言のように最初に変数を初期化する必要はありません。しかし、次のように初期化した方が読みやすくなります。これは、一見、変数宣言のように見えますが、そうではなく、単にnil
を設定しているだけです。
1 2 3 4 |
variable = nil # ...variableを設定するかもしれない処理... variable ||= default_value variable |
この書き方をしない場合はif
を使って書いたりします。
1 2 3 4 5 6 7 |
if condition1 variable = get_value1 elsif condition2 variable = get_value2 else variable = default_value end |
単純な場合はこれでもよいのですが、次のように値を取得しようとしても失敗するかもしれない、という場合はデフォルト値の設定箇所が複数になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
if condition1 variable = get_value_if_success p variable # => nilかもしれない if variable.nil? variable = default_value # デフォルト値の設定箇所1 end elsif condition2 variable = get_value_from_network p variable # => nilかもしれない if variable.nil? variable = default_value # デフォルト値の設定箇所2 end else variable = default_value # デフォルト値の設定箇所3 end |
こうなると複雑なので次のような形にする方がオススメです。
1 2 3 4 5 6 7 8 |
variable = nil if condition1 variable = get_value_if_success elsif condition2 variable = get_value_from_network end variable ||= default_value # デフォルト値の設定 variable |
この形では「デフォルト値の設定」という処理を1回だけ書けば済むようになっているのがよいところです。また、その処理をif
の中ではなく必ず通るところに書いているおかげで「あぁ、この処理の塊では必ずデフォルト値を設定するんだな」感がでるのもよいところです。
if
でがんばる場合だと「抜けはないよね?抜けがあったとしたらそれは意図的なもので問題ないんだよね?」というのが気になります。
変数宣言をする必要のないRubyでも、変数宣言っぽいことをした方が読みやすくなるケースがあります。それは「デフォルト値を設定するケース」です。
ちょっとしたことでも、読みやすくしたり、意図を伝えやすく書けたりするものです。この話を書くきっかけになったのは、Rubyをよく知らない人がRubyのコードのコミットを見て「Rubyはよく知らないんだけど、変数宣言はいらないのに変数宣言っぽいことをしているのはなんで?」とコメントしたことでした。このような何気ない疑問からちょっとした工夫が明文化され、開発チームの資産になったりします。
間違ったところを指摘するためだけに他の人のコードをチェックするのではなく、コミットを見て、気になったことを気軽にコメントできる文化を作ってみてはいかがでしょうか。今まで気づいていなかった自分たちのよいところに気づけるかもしれませんよ。
参考: