パスワード受け渡し用の Heroku アプリ、 Keydrop というのを作りました

keydrop

Keydrop は、遠隔にいる方からパスワードを (比較的) 安全に受け取るための Heroku アプリケーションです。

必要な時だけ起動して、使い終わったら消す、という使い方を想定しています。

Keydrop がやること

Keydrop はパスワードの

  • 暗号化
  • バリデーション

を行います。実装は PHP です。

暗号化

以下を使います。

  • 送信時: HTTPS https://*.herokuapp.com/...
  • 保存時: htpasswd apr1 形式でハッシュ化、もしくは RSA 公開鍵で暗号化
  • 受信時: PostgreSQL SSL 接続

バリデーション

デフォルトでは、パスワードに以下の制約を設けています。

  • 10 文字以上
  • 最低 1 文字の数字 (0 - 9)
  • 最低 1 文字の半角大文字アルファベット (A - Z)
  • 最低 1 文字の半角記号 (#$%@&*!...)

準備

  1. Heroku のアカウント作成。2段階認証を忘れずに
  2. Heroku のコマンドラインアプリケーションをインストール
  3. PostgreSQL をインストール。Heroku のデータベースに接続するためクライアントだけを使用します。サーバのセットアップは不要です
    • 例: $ brew install postgresql
  4. 公開鍵で暗号化する場合は OpenSSL をインストール

セットアップ

以下のボタンをクリックしてください。

Deploy

以下の設定画面が表示されます。

config

設定: App name

空のままがおすすめです。ランダムなドメイン名が設定されます。

設定: ACCEPT_PATHS

ランダムなパス名を設定してください。コンマで区切って複数のパスを設定できます。

例:

/K7FElrHrPmUnmBdw,/Mfj2Wl2YF6rFcMQ6

URL は以下のような形式になります。

https://${app-name}.herokuapp.com${ACCEPT_PATH}

ACCEPT_PATHS に合致しない URL は 404 になります。

ユーザグループを区別したい場合などに、複数のパス名を設定して、それぞれの URL を伝えてください。

設定: ENCRYPTION_TYPE

暗号化の方式。デフォルトは htpasswd です。

公開鍵で暗号化したい場合は、publickey に設定してください。

設定: PUBLICKEY

ENCRYPTION_TYPE に htpasswd を設定している場合は、変更する必要はありません。

デフォルト値は、GitHub にコミットされている鍵でテスト用です。公開鍵暗号を使う場合は、必ず修正してください。

公開鍵の生成

bin/genkey.sh を使うか、以下のようなコマンドで生成してください。

$ openssl genrsa -aes256 -out private.pem 2048
$ openssl rsa -pubout -in private.pem -out public.pem

URL を伝える

パスワードを送ってほしい相手に、以下を伝えます。

以下の URL にアクセスし、xxxxxxx という ID でパスワードを入力してご連絡ください。

  • https://${app-name}.herokuapp.com${ACCEPT_PATH}

実際のログイン ID ではなく、受け渡し用の仮の ID を使っても構いません。

同じ ID で複数のパスワードがポストされた場合は、2度送信したかどうかを確認してください。他人が別のパスワードを送っている可能性を検討してください。ID が重複しても、ユーザには何も知らせません。

結果を受け取る

./bin/dump.sh を使用します。結果をブラウザで表示する方法はありません。

$ ./bin/dump.sh random-appname-12345
--> Connecting to postgresql-dbname-54321
1       2017-08-30 03:06:57+00  htpasswd        /vEswu2ech4ta   test-1  test-1:$apr1$4.Pz0U/W$TppMU9SjE/W0yg1FAPiMS/
2       2017-08-30 03:08:19+00  publickey       /vEswu2ech4ta   test-2  nJ97fbhHIkZAMISp/zXVETvitUl8Qlbi1pyOTtoF3ybI9EDrqenPFb4WMOISrTn8sW+Qu5xvNsjaMEIC3j0Md+hmtEzlLmVK+Nb9bq989I9TnmjgdtFE9klyKkhb5J7r+7SKqBgzfmu7kAoREYBtg05hvNb3mJXGbAruybElbZlxNgf06b5f6W/kkHtGcJaV49oNHKBEmg03ceMip2wP5H6tk/BS6O4FTrEKvpYsn4+Kh6+7JMioCVQEXz3NvpH0BIkmnGncXBZTdtPihju7srb0uEHe0sys66PPBZGZQWbisBdr9knJ5WTfnh2iWLOGv2NgOwfgXQZyMdizINALDw==

公開鍵暗号の復号もできます。

$ ./bin/dump.sh random-appname-12345 ./keys/insecure-private.pem
--> Connecting to postgresql-dbname-54321
1       2017-08-30 03:06:57+00  htpasswd        /vEswu2ech4ta   test-1  test-1:$apr1$4.Pz0U/W$TppMU9SjE/W0yg1FAPiMS/
2       2017-08-30 03:08:19+00  publickey       /vEswu2ech4ta   test-2  testTEST1234!!!

または、 heroku pg:sql で直接 DB にアクセスしてください。

$ heroku pg:psql --app random-appname-12345
--> Connecting to postgresql-dbname-54321
psql (9.6.1, server 9.6.4)
SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off)
Type "help" for help.

random-appname-12345::DATABASE=> select * from keys;
 id |   type    |     path      | username |                                                                                                                                                                           key                                                                                                                                                                            |       created_at
 ----+-----------+---------------+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------
   1 | htpasswd  | /vEswu2ech4ta | test-1   | test-1:$apr1$4.Pz0U/W$TppMU9SjE/W0yg1FAPiMS/                                                                                                                                                                                                                                                                                                             | 2017-08-30 03:06:57+00
   2 | publickey | /vEswu2ech4ta | test-2   | nJ97fbhHIkZAMISp/zXVETvitUl8Qlbi1pyOTtoF3ybI9EDrqenPFb4WMOISrTn8sW+Qu5xvNsjaMEIC3j0Md+hmtEzlLmVK+Nb9bq989I9TnmjgdtFE9klyKkhb5J7r+7SKqBgzfmu7kAoREYBtg05hvNb3mJXGbAruybElbZlxNgf06b5f6W/kkHtGcJaV49oNHKBEmg03ceMip2wP5H6tk/BS6O4FTrEKvpYsn4+Kh6+7JMioCVQEXz3NvpH0BIkmnGncXBZTdtPihju7srb0uEHe0sys66PPBZGZQWbisBdr9knJ5WTfnh2iWLOGv2NgOwfgXQZyMdizINALDw== | 2017-08-30 03:08:19+00
(2 rows)

公開鍵暗号を手動で復号する場合は、以下を行います。

  1. Base64 デコード $ base64 -D input_file
  2. OpenSSL で復号 $ openssl rsautl -decrypt -inkey privatekey_file -in base64decoded_file

片付け

受け渡しが終わったら、Heroku の管理画面からアプリを削除します。

Heroku の料金体系

Keydrop を使っていて、無料の範囲を超えることは普通はないと思います。

無料のプランでは、30 分アクセスがないとアプリがスリープ状態になり、アクセスが来ると起動します(表示に少し時間がかかります)。月合計で 550 時間 (22日分) まで、クレジットカードを登録すると 1,000 時間 (41日分) まで稼働させることができます。

(2017年9月現在)

qiita.com

www.heroku.com

devcenter.heroku.com

マインクラフト内に記憶の宮殿を建てる

マインクラフトを使って、記憶の宮殿を作る試みを行っています。

いまは主に英単語を覚えようとしています。

動画

記憶の宮殿とは

記憶の宮殿は、非常に古くからある記憶術です。

2,500 年ほど前、古代ギリシャの詩人シモニデスが、記憶の宮殿を発明したと言われています。

ある宴会で天井が崩れ落ち、たった一人生き延びたシモニデスは、出席者がどこに座っていたかを完璧に思い出せることに気づきます。

シモニデスは目を閉じて、記憶の中で崩壊した建物を再び組み立てた。驚異的な再現力により、彼は悲劇の晩餐でどの客人がどこに座っていたのかも思い出すことが出来た。部屋の見取り図を意識して憶えようとしたわけではなかったのに、記憶の中にずっと残っていたのだ。このシンプルな発見から、いわゆる「記憶術」の基盤となるテクニックが編み出されたと言われている。

エドは、宴席に並んでいるのが客人ではなく別の人や物であったとしても、例えばギリシャの偉大な劇作家が生まれた順番に座っていたとしても、自分の詩の言葉だったとしても、あるいはその日にやらなければならない仕事の内容だったとしても、空間記憶を使って憶えれば、たいていのことは、並んでいる順番のまま脳に刻み込むことができると確信していた。(中略)

「ジョシュ、わかってほしいのは、人間は本当に場所を憶えることに長けているってことだ」

ごく平凡な記憶力の私が1年で全米記憶力チャンピオンになれた理由

ごく平凡な記憶力の私が1年で全米記憶力チャンピオンになれた理由

詳しくは Wikipedia 記憶術 場所法を見てください。下のプレゼンテーション動画もおすすめです。

これをマインクラフトを使ってやると楽しいので試してみませんか、というお誘いが、本記事の内容です。

既存の試みとの比較

こんなナイスなアイディアを思いつくのは世界で自分だけだろう、などと一瞬思いましたが、ググったら全くそんなことはなくて、何年も前からたくさんの人がこのアイディアについて書いたり動画を作ったりしていました。

自分の試している方法に、多少なりともオリジナリティがあるとしたら、

  • 屋外を使う
  • 看板を使う
  • パターンを使う

とかかなと思います。

大建築を作るのも楽しそうですが、基本的には屋外を使っています。 屋外のいいところは、いくらでも自動で生成されるところです。

PC版のマインクラフトは地球の約8倍の広さがあるそうなので、実質、無限に土地を使えます。 マインクラフトが生成する風景は、壁に囲まれた屋内よりもダイナミックで記憶に残りやすいように思います。夕焼けや雲の流れ、天候の変化、歩き回る動物が、単調になるのを防いでくれます。

パターン

試行錯誤してみた中で、特にうまく機能しそうだった、いくつかのパターンを紹介します。

Circle

Circle パターンは、12個の単語のグループを作り、円形に配置します。

アルファベット順、時計回りに、看板で円を作ります。

例えば、conf* で始まる12個の単語。

confection 砂糖菓子 / confederate 連合国・共犯者・南部連合 / confer 相談する
confide 打ち明ける / confine 閉じ込める / confiscate 没収する
conflagration 大火事 / conflate まとめる / confluence 川の合流点
conform 適合する / confound 混乱させる / confrontation 対立

看板の後ろに、単語を思い出す手がかりになるようなオブジェクトを配置します。ちょうど、ストーンヘンジのような見た目になります。

単語はアルファベット順に並んでいるので、一部の単語が思い出せれば、間にある単語のスペルの範囲がわかります。 東西南北4つの単語を憶えていれば、間2つを思い出すのは、そんなに難しくありません。

寝る前や入浴時に、頭の中でサークルを歩き回ります。

12個の単語でストーリーを作るのも、いいのではないかと思います。上の conf* の単語だったら、砂糖菓子職人が共犯者と一緒に金庫破りを計画し仲間割れしていく話とか。

8個、16 個、20 個のサークルも作っていますが、12 個 〜 16 個がちょうどいい感じでした。

12個

16個

20個

12 + 8 の二重のサークル。外側の円と内側の円で関連を出せるのはいいんですが、ちょっと複雑かもしれません。

サークルに単語が入りきらない場合は、あまり大事でない単語をサークルの外側に、放射状に配置します。関連する単語も外側に置いたりしてみています。

blunt と同じ「ぶっきらぼうな」という意味の単語 curt をサークルの外側に足したら、隣の blurt と韻を踏んでいて嬉しかったの図です。

  • blunt なまくらな、ぶっきらぼうな
  • curt ぶっきらぼうな
  • blurt 口を滑らせる

こういう小さい発見をつないで言葉のネットワークを広げていくところが、ゲームっぽい。

Line

Line パターンは、本や映像などの、ある程度長いコンテンツから、知らない単語を含むフレーズを抜き出して一列に並べます。

語られている内容のあらすじが、意味を思い出す助けになります。

TVドラマ glee

小説

Moon Palace

Moon Palace

TED

全てのフレーズを完全に暗記するというよりは(そんなことできる気がしないので)、フレーズを見たときに意味が思い浮かべばいいかというつもりでやっています。

また、副次的な利点として、内容が頭に入る、というのがあります。 自分は小説を読んでもぼんやりとした印象しか残っていないことがほとんどですが、 Line を歩いていると、あらすじを説明できる程度には物語が頭に入ります。普通に読むよりもずっと、物語の構造が把握できるような気がします (気のせいかもしれませんが)。

Scene

Scene パターンは、複数の単語を組み合わせて、ドラマチックな場面を作ります。

preach 説教する / plead 嘆願する / heed 傾聴する

clout 権威・一撃 / conceal 隠し持つ / clot (血などの)どろっとした固まり

repulsion revulsion 嫌悪感
impulsion 衝動 / compulsion 衝動・上からの強制力 / convulsion 痙攣
complaint 抗議・不平 / composure 沈着・平静
expulsion 排除・駆逐 / expel 追い出す・発射する / compel 強制する・強要する
plunge into 飛び込む

Circle よりも自由で、その分、想像力が必要になります。あまりうまくはやれていませんが。

Circle で粗筋を作り、周囲に放射状に Scene を配置し、物語のディテールを埋めていく、みたいなこともできるかもしれません。

辞書について

Spotlight 辞書

単語の意味は、看板に書いていません。

最初は、裏側にもう1枚看板を貼って意味を書いてみたりしたのですが、面倒くさいのと、Mac 版のマインクラフトは日本語が書けず、また、簡単に Spotlight 辞書で検索できるのでやめてしまいました。

Spotlight にはショートカットキーを割り当てて(デフォルトは コントロール+スペース)、ゲーム中にいつでもショートカットキーで呼び出せるようにしています。 Spotlight に単語を入力した後、辞書で開くショートカットは Command + L です。

マインクラフトがフルスクリーンだとショートカットが効かないので、ウィンドウを最大にして実行しています。

Wisdom2 (iOS)

他に、物書堂の Wisdom2 (iOS)を使っています。2,900 円。

素直なインタフェースで使いやすいアプリです。

Circle パターンで単語のグループを作るために、パターン検索を使います。 例えば「bl で始まって t で終わる単語」(blast, bleat, bleast, blight...) を bl*t で検索します。知らない単語がだいたい12個くらいに収まりそうなパターンを探すのが、楽しみになります。

オンラインだと、OneLook dictionary があります。ただ、これだと(自分程度の語彙力では)ちょっとレベルが高すぎでした。

ゲームの話

元々マインクラフトは、地下の「ゲームらしさ」に比べて、地上は、好きなように使えば?と放置されている感じがあって、そこに記憶の宮殿がうまくはまる気がしています。

語彙のネットワークを広げていく作業は、地下の洞窟探検によく似ています。コツコツと掘り進めていくと、突然視界が開けて、よく知っている場所に出ることがあります。あ、この洞窟はここにつながってるのか!という発見があります。

例えば、tin* で始まる単語を調べていて、tinker 鋳掛屋(いかけや)という言葉を初めて知りました。

Tinkers

大阪では夫婦が仲良く出かけることを鋳掛屋と言った、とか、これだけでも十分面白いんですが、鋳掛屋について調べてから何日か経って、朝ふとんの中でぼんやり tin* のサークルを思い出していて、突然 tinker ってティンカーベルの tinker なのか!と思ったんですね。

そういえば子供の頃に、ティンカーベルが鍋や釜を直す仕事というのが、なんか妖精のイメージと合わなくてヘンなの、と思ってたのも思い出した。

これを思い出した瞬間、 tin* のサークルに血が通う感じがしました。tinker の隣は、tinkle 鈴がチリンチリン鳴る、などの意味で、Tinker Bell のネーミングに tinkle がイメージされていたのかは調べてもわからないのですが、もうこの2つの単語は死ぬまで忘れないだろうなと思いました。

今この記事を書いていて、ディズニーのティンカーベルには双子の姉妹、ペリウィンクルというのがいるのを知ったんですが、periwinkle といえば glee シーズン1第2話冒頭の "Look, we much, periwinkle" (青紫でお揃いだわ) → "Get a room" (部屋とったら?/いちゃついてんじゃねーよ) です。

f:id:koseki2:20160705040738p:plain

2つの単語の位置関係を憶えているので、tinker から periwinkle に向かって伸びるアーチを思い描くことができます。ある単語を空間内の位置とセットで思い出せる、というのは、いままで自分の脳には無かった機能で、不思議な気はしますが、特に無理をしている感じはしません。

上で書いた blunt - curt -blurt の話もそうですが、単にマインクラフトが学習にも使えますね、という話ではなく、ゲームとして有機的に結合している感覚があって、それが、今やっていることの一番の面白さだと思います。

ストラクチャーブロックによるパターンの共有

自分で作る方が楽しいし効果的な気はしますが、量をこなすことを考えると、人が作ったパターンを取り込めたほうがいい気もします。

マインクラフトは最近のリリースで、範囲を指定して構造物をファイルに書き出せるようになりました。ストラクチャーブロックという機能です。

ストラクチャーブロックについては、こちらの記事がわかりやすかったです。

試しにサークルをストラクチャーファイルに書き出して、ダウンロードできるようにしてみようかなと思います。(準備できたら、ここにリンクをはります)

おまけの Tips

以下蛇足です。

エンダーマンに運ばれてしまうので、土に額縁を貼るのはやめた方がいいです。 いつの間にか希少なアイテムが消えていたことがあって、土をサンドストーンに切り替えました。

雷雨で火が付くので、TNT を地面に直に置くのも避けた方が良さそうです。額縁に入れています。 まあ大抵大丈夫だと思いますが、、、雷が鳴ったらさっさと寝てしまえばいいのかもしれません。

テレポートコマンドブロックを作るために、チートコマンドをオンにしています。テレポートは、世界をブックマークする機能として必要です。ゲームをスポイルするつもりはないんですが、無意味に長距離を移動するのはさすがに苦痛なので……。

すっきりまとまった記事が見つからなかったので、以下、テレポートコマンドブロックの作り方。

  1. F3 キーでワープしたい場所の座標を調べる。間違えるとしぬので慎重に
  2. /give @p minecraft:command_block でコマンドブロックを得る
  3. コマンドブロックを適当な場所に設置
  4. /gamemode 1 でクリエイティブモードに切り替え
  5. コマンドブロックにテレポートコマンド設定 /tp @p 123 -234 345
  6. /gamemode 0 でサバイバルモードに戻る
  7. コマンドブロックにレバー等を付ける

バグだと思うんですが、手元の Mac 版では、テレポート後にベッドに入ると、起きたときに金縛り状態になってゲームを一旦終了させるしかなくなる、という現象が起きています。目が覚めているのに何もできない、というのは、なかなかぞっとする体験で面白いんですが直してほしい。

自分はマルチプレイをやったことないんですが、サーバ上で各人の記憶の宮殿を混ぜるのも、きっと面白い体験になると思います。いずれ、1つの VR 空間に全世界の人々の記憶の宮殿を構築するサービスなんかが生まれて、死語になりつつある「サイバースペース」の復興が起こるかも、などと想像しています。

Rails, Django, Laravel 開発サーバをダブルクリックで起動する vagrant-layout プラグインというのを作った

vagrant-layout というのを作りました。

この記事で書いていた環境を生成する Vagrant プラグイン&設定サンプル集です。

layout

インストール

$ vagrant plugin install vagrant-layout

プラグインをインストールします。

Quickstart

$ mkdir my-project
$ cd my-project
$ vagrant layout init php

で各種ファイルを生成します。生成された、

  • sandbox/osx/start.command (Mac)
  • sandbox/win/start.bat (Windows)

をダブルクリックしてしばらく待つと、CentOS

が入った VMhttp://localhost:8080/ で起動します。

開発は手元のファイルを編集することで行います。ローカルのフォルダが /vagrant にマウントされており、 VM に即反映されます。

開発プロジェクトが一段落したら、

  • sandbox/osx/manage/uninstall.command (Mac)
  • sandbox/win/manage/uninstall.bat (Windows)

で環境をアンインストールできます。

vagrant layout init の後ろは vagrant-layout リポジトリのブランチ名です。

を指定できます。

Gist からインストールする

Rails

$ mkdir my-project
$ cd my-project
$ vagrant layout init https://gist.github.com/koseki/37f61d9a02b9a48e6651

で生成された start.{command|bat} をダブルクリックしてしばらく待つと、 http://localhost:8080/Rails が起動します。

Rails

Django

$ mkdir my-project
$ cd my-project
$ vagrant layout init https://gist.github.com/koseki/b7670fc1576a3129e4f7

で生成された start.{command|bat} をダブルクリックしてしばらく待つと、 http://localhost:8080/Django が起動します。

Django

Laravel

$ mkdir my-project
$ cd my-project
$ vagrant layout init https://gist.github.com/koseki/efbd631472c932ff2153

で生成された start.{command|bat} をダブルクリックしてしばらく待つと、 http://localhost:8080/Laravel が起動します。

Laravel

Gist の中身

Gist は、

  • ベースになるコミットの URL が書かれた README.md
  • パッチファイル patch

でできています。

vagrant-layout プラグインは、Gist に書かれたコミットをダウンロードし、git apply でパッチをあてます。(実行には Git が必要になります)

Gist の作り方はこの記事の最後で説明します。

ファイル構成

プロジェクトのルートディレクトリに sandbox を作成し、そこに開発環境用の全設定をいれます。

.
├── sandbox/
│   ├── Procfile
│   ├── Vagrantfile
│   ├── bin/
│   │   ├── boot.sh               …… サーバ起動スクリプト本体
│   │   ├── nginx.sh              …… Nginx 起動スクリプト
│   │   ├── provision.sh          …… プロビジョニング
│   │   ├── shoreman              …… foreman の shell 版
│   │   └── start.sh              …… サーバ起動スクリプト boot.sh を呼ぶだけ
│   ├── config/
│   │   ├── mime.types            …… Ngixn MIME-Type 設定
│   │   └── nginx.conf.src        …… Nginx 設定
│   ├── logs/                     …… ログディレクトリ
│   ├── osx/                      …… OSX 用スクリプト
│   │   ├── manage/
│   │   │   ├── init              …… 設定
│   │   │   ├── provision.command …… プロビジョニング実行
│   │   │   ├── ssh.command       …… SSH ログイン
│   │   │   ├── status.command    …… VM ステータス確認
│   │   │   ├── uninstall.command …… アンインストール
│   │   │   ├── up.command        …… VM 起動のみ実行
│   │   │   └── use-proxy.command …… プロキシ使用開始
│   │   ├── start.command         …… 起動
│   │   └── stop.command          …… 終了
│   └── win/                      …… Windows 用スクリプト
│       ├── manage/               …… OSX と同様なので省略
│       ├── start.bat             …… 起動
│       └── stop.bat              …… 終了
└── static/
    └── htdocs/
        └── index.html            …… ドキュメントルートサンプル

sandbox は開発環境専用のディレクトリです。プロジェクトは sandbox を消しても動くようにします。

sandbox とステージング・本番環境の設定について

ステージング・本番環境の設定と、ローカル開発環境の設定は分けるようにしています。認証、外部サービスとの連携、デバッグログ、SSL、ロードバランサや CDN などなど、多くの場合、環境に差がありすぎるので。

プロジェクトのルートディレクトリに config のようなディレクトリを作って、そこにステージング・本番用の Chef レシピを入れたりしています。

開発環境では開発者の作業しやすさを優先し、ステージング・本番はコストが許す限り同じ構成にします。

プロビジョニング

サーバ設定を変えたときは、チームメンバーに

  • sandbox/osx/provision.command (Mac)
  • sandbox/win/provision.bat (Windows)

をダブルクリックしてもらうように依頼します。このスクリプトは、vagrant provision を実行します。

Vagrantfile でプロビジョニングに sandbox/bin/provisioning.sh を使うように設定されており、これは、VM 上のファイル /root/.provisioning_version に書かれたバージョンよりも新しい処理を実行します。

VERSION=`expr $VERSION + 1`
if [ $CURRENT_VERSION -lt $VERSION ]; then
  echo "--- $VERSION: nginx ---"

  yum install -y -v nginx --enablerepo=epel

  # To stop 'could not open error log file' alert when starting nginx.
  chmod 777 /var/log/nginx

  echo $VERSION > $VERSION_FILE
fi

provisioning.sh に、上記のような if ブロックを追加していくことで、追加の構成をメンバー全員の VM に導入します。

シェルスクリプトを使ったプロビジョニングは冪等とは言えませんが、開発環境用としては、大概これで十分なのではと思います。プロビジョニングのために RubyPython が必須になると、導入のハードルが上がってしまいます。

開発メンバー向け環境構築手順

以下は開発メンバー (特に非エンジニア) 向けのセットアップ手順です。sandbox 入りのプロジェクトリポジトリを手元に clone してある前提です。開発メンバーが vagrant-layout プラグインを入れる必要はありません。

Windows の日本語ユーザディレクトリの問題

Windows を普通にセットアップすると、日本語のホームディレクトリができてしまいます。

Vagrant は日本語のホームディレクトリでは動きません。また ASCII でも特殊な文字が文字がパスに入っていると動きません。以下の issue を参照してください。

以下の手順で問題を回避します。

  • VAGRANT_HOMEC:\vagrant に設定する
    • start.bat から呼ばれる init.bat が設定するので気にする必要はありません。ディスク容量が足りなくなった時などに思い出してください
  • プロジェクトは C:\projects\my-project-name など、安全なパスに clone する
  • VirtualBoxmachinefolderC:\vms など、安全なパスに変更する

最後のは、VirtualBox のインストール後、VirtualBox マネージャを起動して、設定 > 一般デフォルトの仮想マシンフォルダー を書き換えます。

vbox

この設定は VirtualBox 上の全 VM で共通ですが、既にインストール済みの VM があっても特に影響しない(はず)です。古い VM は、そのまま同じディレクトリを参照し続けます。参考まで、最新の VirtualBox がどこに何を保存するかについてのメモが以下にあります。

Vagrant / VirtualBox / Git のインストール

Mac

Macインストーラを実行するだけです。何か聞かれたらデフォルトのままにします。

Windows

Windows は上の2つに加えて、Git をインストールしてください。Git パッケージに含まれる ssh.exe が必要です。

Windows で Git を通常と異なるパスにインストールしたい場合は (おすすめしませんが)、init.bat を修正するとかしてください。

Windows の Git はコンテキストメニューに大量の項目を追加します。普通いらないと思うので、

こちらの記事を参考に削除してください。

Windows の改行コード設定

VM にマウントされたシェルスクリプトを実行するには、改行コードに LF を使う必要があります。

全ファイルがコミットされた状態でsandbox/win/convert-eol.bat をダブルクリックします。

このスクリプトは、

git config core.eol lf
git config core.autocrlf input

を設定し、作業ディレクトリをリセットします。

git rm --cached -r .
git reset --hard

ちなみに、*.bat は CRLF でないと動きません。.gitattributes で改行コードを設定しています。

*.bat text eol=crlf

start.command で起動する

上記が終わったら、

  • sandbox/osx/start.command (Mac)
  • sandbox/win/start.bat (Windows)

VM を起動します。成功すると、

にアクセスできます。途中、

default: Warning: Connection timeout. Retrying...

みたいな表示が出ますが、これで正常です。VM が起動し終わるまで待っている状態です。この後、プロビジョニングと呼ばれるセットアップ処理が実行されます。

プロビジョニング開始前に止まってしまう場合

    default: Warning: Connection timeout. Retrying...
    default: Warning: Connection timeout. Retrying...
    default: Warning: Connection timeout. Retrying...
Timed out while waiting for the machine to boot. This means that
Vagrant was unable to communicate with the guest machine within
the configured ("config.vm.boot_timeout" value) time period.

のように止まってしまう場合は、VM が起動していません。以下をお試し下さい。

1. 一度アンインストールしてもう一回実行してみる

sanbox/osx/manage/uninstall.command または uninstall.bat でアンインストールしてから start.command を実行してみてください。

2. DHCP のバグ対応を試す

VirtualBox の初期設定が原因で、VagrantDHCP 設定がうまく動かない問題がありました。(最新版は自動で修正するようです)

この問題を回避するには、以下の手順を試します。

  1. VirtualBox マネージャーで、全仮想環境の電源が切れているのを確認。
  2. ターミナルから次のコマンドを実行し、表示される内容をどこかにメモする。(念のため)
$ VBoxManage list hostonlyifs
$ VBoxManage list dhcpservers
  1. ターミナルから次のコマンドを実行。
$ VBoxManage dhcpserver remove --netname HostInterfaceNetworking-vboxnet0

3. GUI で OS が起動しているか確認する

Vagrantfile を開いて、vb.gui = trueコメントアウトを外すと、GUI ウィンドウが開くようになります。 これで OS が起動していない場合は、何か根本的に問題があります。VagrantVirtualBox が正常にインストールできているかどうかを確認してください。

4. プロキシ設定が必要な場合

プロキシを設定しないと LAN から外に出られない場合。

Macsandbox/osx/manage/use-proxy.command をダブルクリックで実行してください。プロキシ設定ファイルのサンプルが sandbox/config/vagrant_proxy.conf に生成されます。

export http_proxy="http://user@passwd:www.example.com:8080"
export https_proxy="http://user@passwd:www.example.com:8080"
export no_proxy="localhost,127.0.0.1"

Windows は、use-proxy.bat を実行すると sandbox/config/vagrant_proxy.bat に同様のファイルが生成されます。

生成された vagrant_proxy.{conf|bat} をエディタで開き、各行を編集してください。

  • http_proxy …… HTTP 用プロキシ設定
  • https_proxy …… HTTPS 用プロキシ設定
  • no_proxy …… プロキシを使用しないドメイン

編集後、Mac は、最初に実行した use-proxy.command を再度ダブルクリックで実行します。設定がうまくいっていれば、プロキシ経由で vagrant-proxyconf プラグインがインストールされます。

Windows は、最初のウィンドウで何かキーを押すと、プラグインのインストールを開始します。ウィンドウを閉じた場合は、もう一度 use-proxy.bat をダブルクリックしてください。

プラグインのインストールが終わったら、 start.{command|bat} でセットアップを開始します。VM に自動的に

  • http_proxy
  • https_proxy
  • no_proxy

環境変数が設定されます。また、/etc/yum.conf 等にプロキシ設定が書き込まれます。

5. DHCP ではなく固定 IP を使いたい場合

sandbox/config/vagrant_ip.conf ファイルを作成し、1行目に

192.168.33.10

のように IP を書いてください。デフォルトでは DHCP を使います。

通常は DHCP がおすすめです。同じネットワーク内で複数のメンバーが作業する場合に面倒なことになります。

プロビジョニングが途中で止まってしまう場合

--- 1: nginx --- のようなインストール処理中に止まってしまった場合は、以下を試します。

  1. 一度 stop.command または stop.batVM を止める
  2. manage/provision.{command|bat} をダブルクリックで実行
  3. 無事終了したら start.{command|bat} をダブルクリックで実行

レイアウトのカスタマイズ方法

vagrant-layout リポジトリCentOS や Nginx、MySQL を固定で使っています。

設定を変えるには、 vagrant layout init コマンド実行後に、生成したファイルを編集すれば良いのですが、変更した環境を再利用したり共有したい場合もあるかもしれません。(あればいいなと思っています)

独自のレイアウトを共有するには、Gist を使う方法とリポジトリを fork する方法があります。

Gist を使う方法

ちょっとした変更であれば Gist を使うのが手軽です。

1. patch を作る

以下の手順で patch ファイルを作成します。

  1. vagrant-layout リポジトリを clone する
  2. ベースになるブランチをチェックアウトして、新しいブランチを作る
  3. 好きなだけ編集してコミット
  4. ベースになるコミットとの diffpatch ファイルに書き出す

例えば、Rails だと以下のような手順になります。

$ git clone https://github.com/koseki/vagrant-layout.git
$ cd vagrant-layout
$ git checkout ruby
$ git checkout -b rails

: ファイルを編集

$ git add -A .
$ git commit -m 'Rails 用レイアウト'
$ git diff 'HEAD^' > ../patch

更にコミットを加える場合は、'HEAD^' が正しいコミットを指すように調整してください。git commit --amend するなどして、ベースからの変更を1つのコミットにまとめておくのも良いと思います。

ベースコミットを変更したい時は、リポジトリから pull したあと変更分のコミットを rebase してください。

2. README.md を作る

README.md の末尾に、

---
base: ${ベースとなるコミットのURL}

を書きます。patch を生成するときに指定した比較元のコミットの URL を base に指定します。この URL をダウンロードしてパッチをあてます。

README.md は、サンプルの Gist をコピーするか fork してください。

パッチのベースになるのは特定のコミットなので、元のブランチが更新されたせいでパッチに失敗したり、動作が変わってしまうようなことはありません。

3. Gist に登録して確認

README.mdpatch を Gist に登録して、

$ vagrant layout init ${Gist の URL}

でうまく動くか確認します。

vagrant-layout リポジトリを fork する方法

大きな修正を行いたい場合や、URL ではなく名前で init できるようにしたい場合は、vagrant-layout リポジトリをフォークしてください。(どうぞお気軽に)

フォークしたレイアウトは、

$ vagrant layout init ${ユーザ名}/${ブランチ名}
$ vagrant layout init ${ユーザ名}/${リポジトリ名}/${ブランチ名}

でインストールできます。例えば、

$ vagrant layout init koseki/python

のようにします。また、特定のコミットを URL で指定できます。

$ vagrant layout init https://github.com/koseki/vagrant-layout/commit/80c34e5

といった具合です。確実に同じバージョンを使いたい場合などに、こちらをご利用ください。

Git の仕組み (2) - コミット・ブランチ・タグ

Git の仕組みシリーズの2回目です。目次がここにあります。

前回の記事では、Git オブジェクトとリファレンスが大きなツリー構造になっていることを説明しました。

f:id:koseki2:20140420183120p:plain

また、Git オブジェクトがどのように記録されているか、

f:id:koseki2:20140420183844p:plain

ファイルツリーの変更がルート tree オブジェクトの ID に反映される仕組みなどを見てきました。

f:id:koseki2:20140420185211p:plain

今回は commit オブジェクト、ブランチ、タグ、stash の仕組みについて説明します。

実際のデータが見たいときは、Git Object Browser にアクセスしてみてください。

f:id:koseki2:20140421034721g:plain

5. commit オブジェクト

先に説明した通り、Git オブジェクトデータベースには、複数のファイルツリーを保存できます。

個々のファイルツリーは、最上位 (ルート) にある tree オブジェクトの ID で区別することができます。ファイルツリーは、大抵の場合、過去のファイルツリーと tree blob オブジェクトを共有しています。

けれども、それらのファイルツリーがどんな順番で作られたかは、ファイルツリーを見てもわかりません。

例えば A と B の2つのファイルツリーがあったとして、A にファイルを追加して B になったのか、B からファイルを削除して A になったのかは、ファイルツリーからでは判別が付きません。

「いつ、だれが、なぜファイルツリーを変えたか」を管理するのは、 commit オブジェクトの役割です。commit オブジェクトは、次のようにテキストで書かれています。

tree 3db13f863b338b2dbaa12f564a2a63841396e59c
parent 44c68b1afc3ae211b248a9385f89b5061cbf2f94
author KOSEKI Kengo <koseki@gmail.com> 1370766628 +0900
committer KOSEKI Kengo <koseki@gmail.com> 1370766628 +0900

ビスケットが増える重大なバグを修正した

commit オブジェクトには、

  • tree …… 1つの tree オブジェクトの ID
  • parent …… 0個以上の親 commit オブジェクトの ID
  • author, committer …… 変更・コミットしたユーザの名前、メールアドレス、変更日時
  • コミットメッセージ

などが書いてあります。

commit オブジェクトは、parent に書かれた ID でコミットツリーを形成します。

f:id:koseki2:20140611092500p:plain

tree オブジェクトとは逆に、commit オブジェクトは子が親を参照します。親は自分の子を知りません。

Git は、新しいコミットから古いコミットへとさかのぼり易く、その逆は難しい仕組みになっています。子が親を見つけるのは簡単ですが、全オブジェクトを読まない限り、親が全ての子を見つけることはできません。

Github を見ると、「前のコミット (parent)」にはリンクしていても、「次のコミット」へのリンクは見当たらないことがわかります。

f:id:koseki2:20140427181247p:plain

6. リファレンスとブランチ

Git を理解する上で、commit オブジェクトリファレンスを区別することは、とても大切です。

commit オブジェクトは、コミットツリーを形成し、一度作られたら決して変化せず、そう簡単には消えません。

一方、リファレンスは、コミットツリーの上を動き回る動的なデータで、頻繁に書き換えられ、不要になったら削除されます。

ブランチ

Git のブランチは、ブランチの先頭を指すリファレンスと、そのリファレンスから辿れる Git オブジェクトの集合で出来ています。

f:id:koseki2:20140611090743p:plain

リファレンスが指すのは、必ずしもコミットツリーの枝の先端とは限りません。リファレンスは、コミットツリーのどこか特定の位置を指します。ツリーの途中や根元が枝分かれしていることもあります。

f:id:koseki2:20140611090853p:plain

ブランチ先頭を指すリファレンス

ブランチの先頭を指すリファレンスは .git/refs/heads ディレクトリに保存されます。

たとえば master ブランチの先頭を指すリファレンスは、.git/refs/heads/master ファイルに保存されます。リファレンスには commit オブジェクトの ID が、テキストで書いてあります。

20baf26a647734601c3a02e20ee128bc26600c29

ブランチ先頭のリファレンスは、commitpull コマンドで書きかわります。また、reset コマンドを使って、自分で好きな位置に書き換えることができます。

f:id:koseki2:20140611101138p:plain

リファレンスからたどって、

  • リファレンス → commit
  • commit → 親 commit
  • commit → ルート tree
  • ルート treetree blob

といった具合に、芋づる式にオブジェクトを引き出せます。

f:id:koseki2:20140420172746p:plain

リファレンスがないと、糸口になる commit を見つけるのは難しくなってしまいます。Git オブジェクトは、外見からは区別が付かないからです。

リファレンスや他のオブジェクトから参照されなくなったオブジェクトは、2週間程度で削除候補になります。*1

参照されなくなったオブジェクトを削除したり、オブジェクトを `Pack` 形式にまとめ直す処理を gcガベージコレクション (ゴミ回収) と呼びます。`gc` は、リポジトリが乱雑になってくると自動で実行されます。また、$ git gc コマンドを使って、手動で実行することもできます。

HEAD リファレンス

今チェックアウトしているブランチは .git/HEAD という特別なリファレンスに記録されています。HEAD リファレンスは git checkout コマンドで書き換わります。

f:id:koseki2:20140611093204p:plain

HEAD は、通常は、リファレンスを参照するリファレンスです。

ref: refs/heads/master

ただし例外もあります。HEAD が、他のリファレンスではなく、直接 commit オブジェクトを参照している状態です。これを、detached HEAD (分離ヘッド) と呼びます。

detached HEAD

detached HEAD は、git checkout 時に commit オブジェクトの ID を指定したりすると発生します。例えば、

$ git checkout 20baf26

とすると、.git/HEAD ファイルには

20baf26a647734601c3a02e20ee128bc26600c29

と ID が書かれた状態になります。これが detached HEAD です。detached HEAD に入ると、

Note: checking out '20baf26'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

  git checkout -b new_branch_name

といった警告がでます。

detached HEAD 状態でもコミットはできます。ただし、HEAD 以外にそのコミットを参照するリファレンスが無いので、別のブランチをチェックアウトして HEAD を書き換えると、コミットはどこからも参照されなくなります。

f:id:koseki2:20140611093248p:plain

どこからも参照されないオブジェクトは、放っておくと、いずれガベージコレクションで消されてしまいます。

detached HEAD 中のコミットを残すには、他のブランチにマージしたり、新たにブランチを作成する必要があります。

7. 2種類のタグ

Git には2種類のタグがあります。軽量タグとアノテートタグです。

軽量タグは、コミットを直接参照するリファレンスです。ブランチと同じ、ただのリファレンスなので、名前以外の情報を持ちません。

アノテートタグは、tag オブジェクトを指すリファレンスです。tag オブジェクトは commit オブジェクトを参照します。tag オブジェクトには、tag を作った日付やユーザ名、コメントなどを含めることができます。

f:id:koseki2:20140611100731p:plain

8. 一時待避 (stash)

Git には、まだコミットしていないファイルを一時的に退避させる git stash というコマンドがあります。

git stash は毎回 2つの commit オブジェクトを作成します。

  • 現在の index を保存する commit オブジェクト (親は HEAD)
  • 作業ディレクトリを保存する commit オブジェクト (親は上のコミット)

の2つです。

stash を index に反映するには git stash pop --index のように --index オプションを指定します。

2つ作られた commit オブジェクトの内、後者を指すリファレンスが .git/refs/stash ファイルに保存されます。
.git/refs/stash に記録されるのは、一番最近の stash だけです。昔の stash は、ログ .git/logs/refs/stash に記録されています。

(続きます)

*1:記事公開当初、参照されなくなったオブジェクトは、2週間程度経つと Git が自動的に削除する、という風に説明していました。これは正しくありませんでした。一度作ったオブジェクトは、大抵はもっと長く残るようです。

GitHub for Mac と Git コマンドで .gitignore の挙動が異なる件について (聞いてみた)

Git は、1.8.4 から .gitignore に /**/ のようなパターンを書けるようになっています (正式にサポートされたようです)。

Uses of the platform fnmatch(3) function (many places in the code,
matching pathspec, .gitignore and .gitattributes to name a few)
have been replaced with wildmatch, allowing "foo/**/bar" that would
match foo/bar, foo/a/bar, foo/a/b/bar, etc.

https://github.com/git/git/blob/maint/Documentation/RelNotes/1.8.4.txt

foo/**/bar は、

  • foo/bar (←注目)
  • foo/a/bar
  • foo/a/b/bar

にマッチします。
/**/ の部分が無い場合 (foo 直下に bar がある場合) にもマッチする仕様です。

ところが GitHub for Mac (2014/04/22 リリース版 Your Hogward Eyes) だと、

  • foo/a/bar
  • foo/a/b/bar

にしかマッチしません。GitHub for Mac にバンドルされている git コマンドのバージョンは、

$ /Applications/GitHub.app/Contents/Resources/git/bin/git --version
git version 1.8.4

で 1.8.4 と互換性がありそうなのですが…。

issue トラッカーがないので、メールフォームで聞いてみたところ、内部で使っている libgit2 が未対応とのことでした。

そのうち直りそう。

Git の仕組み (1)

目次

  1. はじめに
  2. Git の全体像
  3. Git オブジェクトの ID と 中身
  4. tree と blob オブジェクト
  5. commit オブジェクト
  6. リファレンスとブランチ
  7. 2種類のタグ
  8. 一時待避 (stash)
  9. インデックス
    • キャッシュとしての役割
  10. マージ
    • Fast-Forward マージ
    • non Fast-Forward マージ
    • rebase
    • reset
  11. 2種類のブランチ
    • リポジトリが自分のブランチを持っている
    • 「リモートブランチ」について
    • 追跡ブランチ
    • ブランチの大三角形
  12. リモートと関係するコマンド、しないコマンド
  13. リモートリポジトリと refspec
    • リモートリポジトリ指定
    • refspec
    • fetch の refspec は remotes がローカル
  14. コミットツリーの森、孤児のコミット
  15. 関係ない2つのリポジトリを混ぜる
  16. bareリポジトリ
  17. 分散リポジトリ

1. はじめに

Git の難しさは独特です。

Git は、洗練されたオブジェクトモデルを、雑多なコマンド群が包み込む構造になっています。

オブジェクトモデルというのは、「Git オブジェクト」を単位としたデータ構造のことです。どのようにデータを記録してあるかが、 Git の最も魅力的な部分です。Git の内部を見ていくと、実にシンプルな仕組みで動いていることに驚きます。

一方、Git コマンドには現実の複雑さをそのまま反映しているような部分もあって、学んでいてもあまり楽しくありません。

Git は、核となるオブジェクトモデルに、これこれの操作を加えたい、という順序で発展してきており、コマンドから学び始めるのは、そもそも方向が逆なのです。

オブジェクトモデルがスタート地点で、ゴールとして雑多なコマンドがあるのに、使い始めるときはどうしてもコマンドから入門せざるを得ない。コマンドは泥臭い部分もあって、あまり楽しくない。

そこで、基本的な使い方がある程度わかったら、内部のオブジェクトモデルから入門し直すことをおすすめします。オブジェクトモデルがわかると、自然な流れでコマンドを理解できるようになります。

内部動作と実装を学ぶことは、Git がどうしてこんなに便利で有効なのかを根本的に理解するのに重要です。しかし初心者にとっては不必要に複雑で混乱を招いてしまうという人もいました。そのため、遅かれ早かれ学習の仕方に合わせて読めるように、この話題を最後の章に配置しました。いつ読むかって? それは読者の判断にお任せします。

Git - Gitの内側

私が Git を理解しようと調査した時、高級なコマンドの視点から眺めるよりボトムアップ式に理解することが役立った。そしてボトムアップ視点で見る Git がこんなにも美しくシンプルであるなら、私が調べたことを他の人も興味を持って読んでくれるのではないか、そうして私が経験した苦労を避けられるのではと考えた。

見えないチカラ: 【翻訳】Gitをボトムアップから理解する


これらの優れた記事を頼りに、この記事もまた、ボトムアップの入り口から行けるところまで行くことを目標としています。

技術的な前提知識が必要な話はほとんどありません。エンジニアでなくても十分に理解できる内容だと思います (難しかったら自分の文章力のせいです)。

生のデータが見たい方へ

エンジニアの方の中には、日本語の説明よりデータを見た方が早い、という方もいるかもしれません。

この記事とは別に Git Object Browser という Git の内部をブラウズするアプリケーションを作成しました。さらにそのアプリから書き出したデータに注釈を加えたサイトを以下で公開しています。

Part1 は、どの Git コマンドで .git ディレクトリのどこが変わるかについて、30のステップで説明しました。Part2 は複数のリポジトリを行き来しながら、リポジトリがお互いにオブジェクトをやり取りする様子を観察できるようにしてあります。

f:id:koseki2:20140421034721g:plain

このサイトは、本記事とは独立した内容です。記事を読んでいてよくわからなくなったら、実際のデータを見るとイメージがわくかもしれません。

2. Git の全体像

Git リポジトリは、大きく分けると、

  • 作業ディレクトリ
  • .git ディレクトリ

の 2 つのディレクトリでできています。

作業ディレクトリには、Git で管理されるファイルが入っています。私たちが直接編集するのは、作業ディレクトリのファイルです。

作業ディレクトリの一番上の階層に .git ディレクトリ があります。.git ディレクトリがリポジトリの本体です。また、.git ディレクトリは、そこから下が Git で管理されていることの目印にもなっています。

リポジトリから別のリポジトリを作るとき、普通は git clone コマンドを使いますが、単に .git ディレクトリを別のディレクトリにコピーするだけでも、新しいリポジトリを作ることができます。

.git の中身

.git に含まれる要素で大切なのは、

  • Git オブジェクト
  • リファレンス
  • インデックス

の3つです。ほかに補助的な要素として、

  • ログ
  • 設定

があります。

f:id:koseki2:20140420170528p:plain

  • Gitオブジェクトは .git/objects ディレクトリに、
  • リファレンスは .git/refs ディレクトリに、
  • インデックスは .git/index ファイルに、

それぞれ格納されています。

Git オブジェクトデータベース

Git オブジェクトの集合を Git オブジェクトデータベースと呼びます。

最初のうち、1 個の Git オブジェクトは 1 個のファイルに書いてあります。たとえば、

.git/objects/30/3ff981c488b812b6215f7db7920dedb3b59d9a

というファイルに Git オブジェクトが格納されています。

オブジェクトの数が増えてくると、複数のオブジェクトが pack 形式と呼ばれるファイルにまとめられることもあります。

1 オブジェクト 1 ファイルの形式でも pack 形式でも、 Git オブジェクトの中身は一緒です。記録方式が違うだけです。

4種類のオブジェクト

Git オブジェクトは 4 種類あります。treeblobcommittag の4種類です。

f:id:koseki2:20140420170220p:plain

tree はディレクトリ、blob はファイルに相当します。commit はコミット、tag はタグです。

Git オブジェクトは、ID (オブジェクト名) で区別できます。ID は 40 文字の 16 進数 (0〜9 と a〜f) で表現されます。例えば、

0b48b1325567ae55dbfaefb250e7460e4fbb7d6d

といった具合です。

ID だけ見ても、オブジェクトの種類 (treeblobcommittag のどれか) は区別がつきません。4種類のオブジェクトがごちゃ混ぜに .git/objects ディレクトリに入っています。

リファレンス

オブジェクトの他に、リファレンスと呼ばれる重要なファイルがあります。

リファレンスは、特定の commit オブジェクト (または tag オブジェクト) を指し示すブックマークのようなものです。

f:id:koseki2:20140420180706p:plain

リファレンスが指し示す commit オブジェクトからたどって、芋づる式に他のオブジェクトを引き出すことができます。

f:id:koseki2:20140420172746p:plain

リファレンスは .git/refs ディレクトリ以下に保存されています。

リファレンスには Git オブジェクトの ID がテキストで書いてあります。たとえば .git/refs/heads/master というリファレンスに、

0b48b1325567ae55dbfaefb250e7460e4fbb7d6d

と書いてあったとします。この 40 文字が commit オブジェクトの ID です。この ID を持つオブジェクトは、

.git/objects/0b/48b1325567ae55dbfaefb250e7460e4fbb7d6d

というファイルに保存されています。ID の先頭 2 文字分のディレクトリがあって、のこり 38 文字がファイル名です。

リファレンスのリファレンス

また、リファレンスには、もう一つ別の形式があります。他のリファレンスを指し示すリファレンスです。

たとえば、.git/HEAD リファレンスには、

ref: refs/heads/master

のように書いてあります。この形式のリファレンスは、 commit オブジェクトを指し示すのではなく、別のリファレンス .git/refs/heads/master を指し示しています。

f:id:koseki2:20140420180623p:plain

HEAD は特別なリファレンスファイルで、いまチェックアウトしているブランチを示します。HEAD に ref: refs/heads/master と書いてある場合、 master ブランチがチェックアウトされています。

別の名前のブランチ (例えば example) をチェックアウトすると、HEAD は ref: refs/heads/example と書き換わります。

大きなツリー

リファレンスと Git オブジェクトの参照関係は、全体で大きなツリー構造になっています。

この大きなツリー構造は、4層に整理することができます (各層の名前は勝手につけたものです。公式な呼び方ではありません)。

f:id:koseki2:20140420183120p:plain

Git にコミットしたファイルやディレクトリは、① ファイルツリーに保存されます。ファイルツリーには tree オブジェクトと blob オブジェクトでできています。

コミットのたび、ファイルツリーを1つぶら下げた commit オブジェクトが ② コミットツリー に追加されます。commit オブジェクトも、ファイルツリーと同様にツリー構造を形成します。

コミットツリーの特定の位置を ③ リファレンスが指し示します。また、④ リファレンスのリファレンス が特定のリファレンスを指し示します。

MacOSX のタイムマシンの UI がイメージしやすいかもしれません。


Mac OS X 10.5 Leopard feature video - Time ...

ファイルツリーが垂直方向に成長するとしたら、コミットツリーは奥から手前に成長します。コミットツリーのある地点を指すのがリファレンスです。

f:id:koseki2:20140420183642p:plain

歴史が一直線にならぶ Mac のタイムマシンとは異なり、Git のコミットツリーは枝分かれしたり合流しながら成長していきます。

3. Git オブジェクトの ID と 中身

Git オブジェクトの ID は、SHA1 というハッシュ関数で生成されます。

Git オブジェクトの中身は、 Zlib という圧縮ライブラリで生成されます。

下図のように、オブジェクトの種類 + (スペース) + データのサイズ + (\0) + データSHA1 関数に入力した結果が ID になり、Zlib に入力した結果が中身になります。

f:id:koseki2:20140420183844p:plain

ハッシュ関数は、同じ入力には同じハッシュ値、違う入力には違うハッシュ値を返します。Git オブジェクトの中身が同じなら ID も同じ、中身が違えば ID も異なります。

世界中、誰が作ったどのリポジトリでも、同じオブジェクトには同じ ID が付いています。

たとえば、空の blob の ID を Google で検索すると、こんな風に たくさんみつかります。

空の .gitignore ファイルが入った tree オブジェクトの ID を検索すると、こんな感じです

Git オブジェクトの ID と中身は不可分です。ある ID が付いたオブジェクトの中身だけ書き換える、というような操作はあり得ません。中身を変えると、別の ID が付いた別のオブジェクトになります。Git オブジェクトには「変更」に当たる操作は無く、「新規作成」と「削除」だけが可能です。

ハッシュ関数 SHA1 の簡単な説明

Git オブジェクトの ID を作るハッシュ関数について、ごく簡単に見ておきましょう。

Git で使われるハッシュ関数 SHA1 は、入力されたデータを 40 文字にして返します。 *1

この 40 文字をハッシュ値と呼びます。

ハッシュ関数は、同じ入力に対して常に同じハッシュ値を返します。例えば、

入力 SHA1 ハッシュ値
(空文字列) da39a3ee5e6b4b0d3255bfef95601890afd80709
a 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8
b e9d71f5ee7c92d6dc9e92ffdad17b8bd49418f98
c 84a516841ba77a5b4648de2cd0dfcb30ea46dbb4
git 46f1a0bd5592a2f9244ca321b129902a06b53e03
夏目漱石 坊ちゃん (青空文庫 XHTML版) adf63acaf3cff383174e95477b9a37b7296afccb

といった具合です。何百テラもあるような巨大なデータでも、SHA1ハッシュ値は 40 文字になります。 *2

そして、このハッシュ値は、非常に重複しにくくなっています。あなたと私が同姓同名でなければ、あなたの氏名の SHA1 と、私の氏名の SHA1 は確実に異なります。

仮に、 2 つの異なる blob オブジェクトに同じ SHA1 ハッシュ値が割り当てられた場合、2つのファイルの内 1 つが消失してしまいます。けれども、そのようなことを心配する必要はありません。

SHA1 ハッシュの重複 (衝突) しにくさについて、『Pro Git』は次のように説明しています。

たった一つの衝突が 50% の確率で発生するために必要なオブジェクトの数は約 2^80 となります (中略)。つまり一兆二千億のそのまた一兆倍です。これは、地球上にあるすべての砂粒の数の千二百倍にあたります。

地球上の人類 65 億人が全員プログラムを書いていたとします。そしてその全員が、Linux カーネルのこれまでの開発履歴 (100 万の Git オブジェクト) と同等のコードを一秒で書き上げ、馬鹿でかい単一の Git リポジトリにプッシュしていくとします。これを五年間続けたとして、SHA-1 オブジェクトの衝突がひとつでも発生する可能性がやっと 50% になります。それよりも「あなたの所属する開発チームの全メンバーが、同じ夜にそれぞれまったく無関係の事件で全員オオカミに殺されてしまう」可能性のほうがよっぽど高いことでしょう。

Git - リビジョンの選択

SHA1 ハッシュ関数については、

という 3 点だけ覚えておいてください。

4. tree と blob オブジェクト

Git に追加したディレクトリやファイルは、tree オブジェクトや blob オブジェクトとして、オブジェクトデータベースに保存されます。

tree はディレクトリ、blob はファイルに相当します。

ただし、tree は自分のディレクトリ名を知りません。blob は自分のファイル名を知りません。

f:id:koseki2:20140420184051p:plain

blob オブジェクトには、ファイルの中身がそのまま書いてあります。

tree オブジェクトには、次のように、あるディレクトリに含まれるファイル名・サブディレクトリ名と、そのオブジェクト ID が一覧で書かれています。

100644 aaa.txt \0 72943a16fb2c8f38f9dde202b7a70ccc19c52f34
100644 bbb.txt \0 f761ec192d9f0dca3329044b96ebdb12839dbff6
100644 ccc.txt \0 b2a7546679fdf79ca0eb7bfbee1e1bb342487380
40000 ddd \0 0d546c66a969f7b205bf2fd450ef5e4b9efb70bf
100755 eee.sh \0 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
120000 fff-symlink \0 35a6b12323dd0e2f9a2a3e5abe291920151ede4a

先頭の数字は、

  • ファイルの種類
  • 実行可能ファイルかどうか
    • 644 実行不可
    • 755 実行可

を示しています。

空白の後にファイル名があり、ヌルバイト(\0)が挟まった後、最後に blob または tree オブジェクトの ID が (本当はバイナリで) 書いてあります。

tree に記録されている情報はこれだけです。ファイルのタイムスタンプや所有者のような情報は、blob にも tree にも書かれていません。あるファイルを誰がいつ作ったかは、treeblob を見てもわかりません。

blob はファイルを git add したときに作成されます。一方、treegit commit した時に作成されます。このタイミングの違いについては、後でインデックスについて説明する時に詳しく見ます。

tree と blob の参照関係

あるディレクトリに同じ中身のファイル A.txt と B.txt があった場合、blob オブジェクトは1個だけ作成されます。

f:id:koseki2:20140420184333p:plain

tree オブジェクトは以下のようなデータになります。ファイルの中身が同じなので blob は同じ 72943a1... を参照しています。

100644 A.txt \0 72943a16fb2c8f38f9dde202b7a70ccc19c52f34
100644 B.txt \0 72943a16fb2c8f38f9dde202b7a70ccc19c52f34

ファイル B.txt を別のディレクトリに移動すると、移動元と移動先の tree が更新されます。blob は元のままです。

f:id:koseki2:20140420184346p:plain

tree 1

100644 A.txt \0 72943a16fb2c8f38f9dde202b7a70ccc19c52f34

tree 2

100644 B.txt \0 72943a16fb2c8f38f9dde202b7a70ccc19c52f34

ルートツリーの ID でツリー全体を識別する

treeblob を変更すると、新しいオブジェクトが作られます。

例えば、あるファイルを書き換えて、git add すると、新しい blob オブジェクトが、Git オブジェクトデータベースに追加されます。

さらに、blob が更新されると、そのオブジェクトを含む親 tree オブジェクトも更新されます。tree オブジェクトには、子の ID が書き込まれているからです。

例えば、次のような tree オブジェクトがあったとして、

100644 A.txt \0 72943a16fb2c8f38f9dde202b7a70ccc19c52f34
100644 B.txt \0 72943a16fb2c8f38f9dde202b7a70ccc19c52f34

A.txt の内容が書かれた blob が入れ替わると、

100644 A.txt \0 f761ec192d9f0dca3329044b96ebdb12839dbff6
100644 B.txt \0 72943a16fb2c8f38f9dde202b7a70ccc19c52f34

tree が変化します。同様に、子の tree が変わると、親 tree も変わります。

f:id:koseki2:20140420185211p:plain

変化は親へ親へと伝播して、最終的に、一番上のルート tree オブジェクトが入れ替わります。

f:id:koseki2:20140420185225p:plain

どれほど巨大なファイルツリーであっても、末端のどこか一箇所ファイルを書き換えると、変化は親をさかのぼってルートの tree ID に反映されます。つまり、ルート tree ID は、ファイルツリー全体を一意に識別する ID になっています。

ルート tree オブジェクトの ID さえあれば、他のファイルツリーと混同することなくファイルツリー全体を取り出すことができます。

新しいファイルツリーによって、古いファイルツリーが影響を受けることはありません。新しいファイルツリーが追加された後でも、古いルート tree オブジェクトからたどって、古いファイルツリーを取り出すことができます。

ファイルツリーは、過去のファイルツリーと tree blob オブジェクトの大半を共有しつつ、差分だけを新しいオブジェクトで置き換えて、増えていく仕組みになっています。

(続きます)

*1:正確には 16 進数文字列で40文字、160ビットの値です。

*2: SHA1 は最大で 2 ^ 64 - 1 = 約 16 エクサバイトまでの入力を計算できるそうです。 http://ja.wikipedia.org/wiki/Secure_Hash_Algorithm

Vagrant を使って Mac からダブルクリックで操作できる仮想環境を作る

この記事を公開する直前に Vagrant Cloudはてブに上がってるのを見て、今あわてて追記しています。すごい楽しいことになってますねー。

ブログ記事をざっと読んだ感じだと、Vagrant 1.5 の新機能は、

  • VagrantShare - まるで公開されてるサイトみたいに VM の URL をチームメンバーに見せる
  • Box を koseki/centos みたいな短い名前で指定できるように。バージョン管理
  • rsync と SMB でフォルダ同期
  • Hyper-V - MS の仮想環境に対応
  • insecure-private-key の代わりに SSH でパスワードが使えるように
  • プラグイン管理
  • Funtoo, NetBSD, TinyCore Linux のゲストに対応。ネットワーク設定などができるように

特にウェブ開発をやっていると、VagrantShare が大きそう。使ってみるのが楽しみです。

VagrantCloud は、Box のリポジトリと VagrantShare の仲介をやってくれるみたい。コミュニティ向けには無料、ということなので、パブリックな Box のホスティングは無料ということなんでしょうか。すばらしい。

というわけで本題。メジャーリリースの 1.5 でも後方互換は保たれているということなので、書いてた記事をそのまま公開です。ぽちり。

マウス操作で仮想環境

サーバ上で実行したい各種タスクを Mac からダブルクリックで起動できる、仮想環境のサンプルを作りました。VirtualBox + Vagrant を使います。

デザイナやフロントエンドエンジニアが、本番と同様のレスポンスを確認しながら開発できるように、手軽な仮想環境のセットアップ方法がほしい、というのがそもそもの動機です。

Vagrant で、やりたい事はほとんど実現できていましたが、ターミナルでコマンドを打たずに済むように、いくつか工夫しています。

デモ

  1. VirtualBoxVagrant をインストール
    (インストーラで簡単に入ります。Ruby を入れたりする必要はありません)
  2. リポジトリgit clone またはダウンロード
  3. start.command をダブルクリックしてしばらく待つと、VirtualBoxCentOS 上で Nginx, Rails, MySQL が起動します
  4. http://192.168.33.10:8080/ にアクセスして Rails が起動しているのを確認します

VM が、app/sample 以下に Rails アプリケーションを生成します。Mac でファイルを書き換えて、VM で確認できます。DB は VM 上で動いています。

構成

こんな感じになっています。

f:id:koseki2:20140309164347p:plain

サーバの詳細を知る必要がない開発メンバーには、以下のように見えてほしい。VM が、プロジェクトのファイルを読んでブラウザに応答する一個のアプリケーションとして振る舞うようにします。

f:id:koseki2:20140309164346p:plain

プロジェクトの動作環境を VMカプセル化すると、別のプロジェクトに干渉する心配がなくなります。開発が終わったら VM を捨てれば元通りです。

また、VM と同じ設定でローカルでもサーバが動くようにします。エンジニアは Mac で直接サーバを動かした方が便利そうです。

f:id:koseki2:20140309164345p:plain

エンジニアが自分が使っていない設定はメンテされなくなってしまうので、VM でもローカルでもなるべく同じ設定を使います。

ディレクトリ構成

.
├── LICENSE
├── README.md
├── app
│   └── sample …… VM が Rails を生成する空のディレクトリ
├── htdocs
│   ├── index.html
│   └── static
│       └── index.html
└── sandbox …… 設定の本体。プロジェクトのルートに、この sandbox を足して使うイメージです
    ├── Procfile ……foreman の設定。Nginx と Rails をまとめて起動
    ├── bin
    │   ├── halt.command …… VM をシャットダウンします
    │   ├── reset_db.command …… VM に SSH して local/reset_db.command を起動
    │   ├── sample.command …… VM に SSH して local/sample.command を起動
    │   ├── ssh.command …… VM に SSH します
    │   ├── start.command …… VM に SSH して local/start.command を起動
    │   ├── status.command …… VM の起動状態を表示します
    │   ├── up.command …… VM を起動します
    │   ├── local …… VM またはローカルで実行するシェルスクリプト
    │   │   ├── reset_db.command …… DB をリセットします。rake db:setup を実行
    │   │   ├── sample.command …… rails server を実行
    │   │   └── start.command …… foreman start を実行
    │   └── provision.command …… プロビジョニングを実行 
    ├── conf …… サーバ設定。このサンプルでは Nginx の設定が入っています
    │   ├── mime.types
    │   └── nginx.conf
    ├── logs
    ├── tmp
    └── vm
        ├── Vagrantfile  …… Vagrant 設定
        ├── provision.sh …… プロビジョニングスクリプト。Mac 上で実行されることはないので .sh です
        └── uninstall.command …… VM をアンインストールします

コマンドファイル

OSX で、シェルスクリプト拡張子.command にして実行権限を付けると、ダブルクリックで実行できるようになります。

*.command ファイルを使って、vagrant コマンドを実行します。たとえば VM を起動する up.commandは、

#! /bin/sh

DIR=`dirname $0`

cd $DIR/../vm
vagrant up

のようになっています。

VMSSH 接続してシェルスクリプトを起動するコマンド

vagrant ssh -c 'コマンド' で、VMSSH してコマンドを実行することができます。Vagrant Box がパスフレーズ無しでログインできる sshd を起動してくれるおかげで、SSH 経由で簡単に VM を操作できます。*1

この仕組みを使って、ダブルクリックで VM 上のシェルスクリプトを起動するコマンドを作ります。

たとえば、

  • Rails や nginx をまとめて起動する start.command
  • Rails を単体で起動する sample.command
  • DB をセットアップしなおす reset_db.command

は、全て同じ中身になっています。

#! /bin/sh

DIR=`dirname $0`
NAME=`basename $0`

echo "--- VM ---"
cd $DIR/../vm
vagrant up
vagrant ssh -c "/vagrant/sandbox/bin/local/$NAME" 

vagrant upVM を(起動していなかったら)起動し、local ディレクトリ以下の同名のスクリプトを実行します。

最後の行で SSH してシェルスクリプトを実行しています。プロジェクトのルートを /vagrant にマウントしているので、/vagrant/sandbox/bin/local は、このディレクトリが見えています。

local 以下の *.command ファイルがスクリプトの本体です。たとえば local/start.command は、

#! /bin/sh

set -e
DIR=`dirname $0`

cd $DIR/../..
foreman start 

のようになっています。

Foreman でサーバをまとめて起動・終了する

Foreman は最近教えてもらって知ったのですが、簡潔ですごくいいアプリケーションでした。OSに標準でついててほしい。

Foreman が Procfile を読んで、Nginx と Rails を起動します。

nginx: nginx -p . -c conf/nginx.conf
rails_sample: bin/local/sample.command 

start.command をダブルクリックすると、Nginx と Rails が起動し、ターミナルにログが流れます。Ctrl-C で起動したサーバをまとめて停止します。

サーバの設定を読み込み直すときは、Ctrl-C で止めてダブルクリックで再起動です。

Procfile は、Mac 上でサーバを起動する時にも使えます。

$ cd sandbox
$ foreman start

プロビジョニング

サーバの構成は、開発が進むにつれて変化していきます。環境が変わるたびに VM をセットアップし直しになっては困ります。

このサンプルでは、シェルスクリプトでプロビジョニングする Vagrant の機能を使っています。

  config.vm.provision :shell, :path => 'provision.sh'

これで、初回の起動時と、provisioning.command をダブルクリックした際に、VMprovision.sh が実行されます。

Vagrant は path で指定されたファイルを VM にアップロードして実行するだけのようです。

プロビジョニングのたびに毎回同じスクリプトが実行されるのでは使いにくいので、自前で簡単なバージョン管理を実装します。

# バージョン番号を記録するファイル
VERSION_FILE=/root/.provisioning_version

# バージョンファイルがあったら CURRENT_VERSION に読み込む。なかったら 0 
if [ -f $VERSION_FILE ]; then
  CURRENT_VERSION=`cat $VERSION_FILE`
else
  CURRENT_VERSION='0'
fi

のようにして、最後に実行したプロビジョニングのバージョンを読み込みます。あとは以下のようにして、バージョン番号が付いたブロックを足していきます。

VERSION=1
if [ $CURRENT_VERSION -lt $VERSION ]; then
  echo "--- $VERSION ---"

  # 何かする。

  # バージョンファイルに $VERSION を書き込み
  echo $VERSION > $VERSION_FILE
fi

ファイルに記録された $CURRENT_VERSION より $VERSION の方が数字が大きかったら処理を実行します。

原始的ですが、開発サーバ向けにはこの程度でも良いのかなという気がしました。セキュリティの要件などが全然違うので、Chef などを使って本番と同様のプロビジョニングを実行するのは大変そうです。

あるいは、一般的な構成なら Vagrant Box を公開してもいいかもしれません。プロジェクトを /vagrant にマウントして使うため、VM にはプロジェクトに関わるファイルを一切含めないことが可能です。

Nginx

Nginx は sandbox/conf/nginx.conf を直接読んで起動します。

nginx -p . -c conf/nginx.conf 
daemon off;

でデーモン化せず、ターミナルにログを書きながら実行します。

sendfile off; 

を設定しないと、共有フォルダのファイルが壊れて見える場合があるようです。


Nginx に限らず、サーバはできるだけプロジェクトに入っている設定ファイルを直接読んで起動した方がいいと思います。/etc にコピーなどしてしまうと、設定をかえるたびに毎回プロビジョニングの実行が必要になってしまいます。設定ファイルがプロジェクトに入っていれば、再起動するだけで済みます。

VM を使わずに Mac 上でサーバを起動する

VM で foreman を起動する代わりに、Mac 上で

$ cd sandbox
$ foreman start

でサーバ一式を起動することができます。また、sandbox/bin/local/*command ファイルをダブルクリックしてローカルで実行できます。

Docker

この構成でやる前は Docker を試していました。0.8 から Mac がサポートされていますが、まだファイル共有がスムーズにいかないようです。

VirtualBox (boot2docker) + Docker の2階建ては、まだちょっとローカルから直に触れていない印象でした。将来的には Docker に切り替えるのもありかと思います。

Windows

ローカルと VM で同じスクリプトが動く利点は失われてしまいますが、 Windows でもバッチファイルを足せば同じように操作できるだろうと思います。

*1:ちなみに、ssh秘密鍵このファイルです。Vagrant をインストールすると、~/.vagrant/insecure-private-key にインストールされます。