詩と創作・思索のひろば

ドキドキギュンギュンダイアリーです!!

Git の特定リビジョンのツリーをファイルシステムのように扱う Go の実装

要件

以下のようなことができればよいものとします。

パスを与えられたとき、

  • そのパスが示すファイルの内容 (open, read)
  • そのパスが示すディレクトリの内容 (opendir, readdir)
  • そのパスが示すファイルまたはディレクトリの情報 (stat)
    • パーミッション
    • 最終更新時刻
    • ファイルサイズ

を得られる。

Git のファイルツリー構造を読む

Git に登場し、われわれが対象とすることができる概念は「オブジェクト」という名前のもとに抽象化されています。通常、直接操作することがあるのはコミットとタグだけですが、残りのツリー(tree)とブロブ(blob)をここでは取り扱うことになります。

ここでは詳しく解説できませんが、簡単に言えばツリーがディレクトリに対応し、ブロブがファイル(の内容)に対応します。コミットオブジェクトは時刻やコミットメッセージの他に、ひとつのツリーオブジェクトへの参照を持っています。適当なリポジトリで git cat-file で様子をうかがってみます。

% git cat-file -p HEAD
tree cfe3cb534d9e626f3cff23583e6cb666b0ebb997 # コミット "HEAD" が指すツリーID
parent 0f1a7c509da2ed307f3a9fcb9ea0907fb8fe3601
author motemen <motemen@gmail.com> 1418313657 +0900
committer motemen <motemen@gmail.com> 1418313657 +0900

init

HEAD が指すツリーが見つかったので、これをさらにたどってみます。

% git cat-file -p cfe3cb534d9e626f3cff23583e6cb666b0ebb997
100644 blob daf913b1b347aae6de6f48d599bc89ef8c8693d6    .gitignore
100644 blob ba5d35222be3983257723ddcd0bf8fcf3ee90cdf    LICENSE
100644 blob 0613648f84ecb1ab9ca6182b46f74539f227ec72    README.md
040000 tree e90529ca501f5001c0706065aebe735bede754be    git

ルートディレクトリの中身らしきものが見えてきました。LICENSE と git に対応するオブジェクトをそれぞれ覗いてみると:

# LICENSE
% git cat-file -p ba5d35222be3983257723ddcd0bf8fcf3ee90cdf
The MIT License (MIT)
…


# git/
% git cat-file -p e90529ca501f5001c0706065aebe735bede754be
100644 blob a9ecb54af0fcb66c64cb9ca814d22887c6dfe115    git.go
100644 blob 1516b582b5010469d02f0d7995d426489ce0cc1d    git_test.go

blob がファイルの内容であり、tree がディレクトリに対応することがわかると思います。古いコミットに対しておなじ操作を行ってみると別のツリーオブジェクトをたどることになり、古いファイルの内容にアクセスできます。ワークスペースの内容が変更されコミットされるたびに、新しいツリーオブジェクトが生成され、新しいコミットはそのツリーオブジェクトを指します。

これで、以下のようにすれば特定リビジョン(コミット)におけるリポジトリの内容を取得できそうだと想像できます:

  • 当該のコミットが指すツリーを得る。
  • ツリーをたどり、深い階層のブロブやツリーを得る。

Git を用いた具体的な実装

git コマンドを通じて、ツリーとブロブによる擬似ファイルシステムを実装することにします。その前にこのオブジェクトの指定方法を知っておくと便利です: <rev>:<path>git cat-file -p HEAD:README.md のようにして使います。特定のリビジョン rev におけるファイルまたはディレクトリ path に対応するブロブ、ツリーを指す記法といったところです。リポジトリルートを指したいときは HEAD: のように指定します。./ などとすると現在のディレクトリからの相対パスになることに注意。

1. ファイルの内容を取得する

ファイルに対応するブロブの SHA1 がわかっていれば、以下のコマンドで内容を取得できます。

% git cat-file blob ba5d35222be3983257723ddcd0bf8fcf3ee90cdf

先に登場した git cat-file -p でもいいですが、上のコマンドであればブロブでないオブジェクトを表示しようとすると失敗してくれます。

2. ディレクトリの内容を取得する および 3. ファイルやディレクトリの情報を取得する

git ls-tree を使います。上で使ったように git cat-file でも同じような出力は得られますが、ツリー的オブジェクトでないものを引数に与えたときエラーになってくれるので、ls-tree のほうがよいでしょう。

ls-tree はそのままだと現在のディレクトリを使用するので、--full-tree で無視させます。また、-l オプションでブロブのサイズも取得しておきます。プログラムから使用するときは -z オプションで NUL 区切りにしておくのがよいです。

% git ls-tree --full-tree -l HEAD:git
100644 blob a9ecb54af0fcb66c64cb9ca814d22887c6dfe115    6385    git.go
100644 blob 1516b582b5010469d02f0d7995d426489ce0cc1d     867    git_test.go

モードの読み方

ls-tree の出力の最初の列がオブジェクトのモードを表しています。これに関して直接的なドキュメントはないみたいだけど、git-fast-importなんかを見るとなんとなく分かると思います。

実際のところ、以下の5パターンしかありません。

  • 100644: 標準ファイル
  • 100755: 標準ファイル(実行可能)
  • 040000: ディレクトリ
  • 120000: シンボリックリンク
  • 160000: Gitlink。git-submodule で使います

Gitはファイルのパーミッションに関しては、実行ビットしか意識していません。

最終更新時刻

これには git log を使えば良さそうです。rev を指定することで、当該リビジョン以前の最後の変更を探し出してきます。

% git log -1 --pretty=format:%aD <rev> git/git.go
Fri, 12 Dec 2014 01:00:57 +0900

Go による実装

以上を踏まえた実装が motemen/go-vcs-fs です。

godoc/vfs.FileSystem という便利なインターフェースがあり、これを実装するようなコードになっています。じつは冒頭にあげた要件もこれがベースになっています。

sourcegraph/go-vcs

どうぞご利用ください……と言おうと思ってたんですが、sourcegraph/go-vcs というほとんど同じ先行実装がありました……機能が豊富であり、インターフェースが同じなのはもちろん、シンボリックリンクの扱いが TODO になってる点も一緒だったので、こちらを使うのがよさそうです。

こんな風に使います:

import (
    "sourcegraph.com/sourcegraph/go-vcs/vcs"
    _ "sourcegraph.com/sourcegraph/go-vcs/vcs/gitcmd"
)

...

repo, err := vcs.Open("git", ".")

commit, err := repo.ResolveRevision(rev)

fs, err := repo.FileSystem(commit)

sourcegraph.com/sourcegraph/go-vcs/vcs/gitcmd をインポートすることで、vcs.Open("git", ...) が使えるようになり、sourcegraph.com/sourcegraph/go-vcs/vcs/git のほうだと libgit2 を使った実装になるみたいです。同様に Mercurial にも対応してるのが便利そうですね。

2014年買ってよかった! 本を読む人におすすめの文房具3点

f:id:motemen:20141210004127j:plain

今週のお題「今年買ってよかったもの」〈2014年をふりかえる 2〉

小説を読むときにもおすすめできるけど、どちらかというと実用的な本を読むときに。

透明のポストイット

読書のとき、気になるところに印をつけながら読むというのはみんなやることだと思うけど(このエントリで紹介するのは全部その前提なので、そうじゃない人はちょっと……)、その際もっとも敷居が低いのは付箋だろう。本を傷つけないし、貼り直せるから、自由にぺたぺたと貼れる。敷居が低いのはいいことで、この部分が気になるけれど、わざわざ書きこむほどでもないんだよな……などと躊躇して書かなかった結果、後からあれなんだったっけ、と迷うことがない。

そういう付箋のなかでもこれが優れているのは、本体が透明なので、文字に重ねて貼りつけられるところ。不透明な紙の付箋だと、行を避けて貼ったり、本文を隠してしまったりして微妙にストレスが溜まってしまうところだ。難があるとすればホコリを貼りこんじゃうとちょっと見た目がわるいところか。

ポスト・イット ジョーブ 透明見出し 44x6mm 20枚x9色 680MSH

ポスト・イット ジョーブ 透明見出し 44x6mm 20枚x9色 680MSH

蛍光マーカー

これが最もライフチェンジングで、最高だった。

いくら付箋が良いものだといってもやはり直接の書きこみに勝るものではなくて、付箋が行単位のハイライトしかできないのに対し、書きこみなら文、単語単位で強調できる。貧乏性の自分も文庫や新書など、安い本はあえて汚していこうという気持ちで鉛筆やボールペンで線を引いてみたりもしたのだけれど、どうしても線がへろへろになってしまって、きちょうめんな方ではないとはいえ、後から見返したときに心地よいものではない。蛍光ペンでのハイライトなんてもってのほかだった。

ステッドラーのこの蛍光マーカーはよくある平たいペン先の蛍光ペンと違い、クレヨンのような書き心地なのが特徴。これがすごい! 線が歪んでしまっても、そのままペン先をぐりぐりと動かしてしまえば外したところを塗りつぶすようにハイライトできる。水性のペンだと線を重ねるたびに色が濃く、紙がふやけてしまうので一発書きの趣があるところ、これはやり直しが効くので気楽だ。ぜんぜん感覚が違う。使ってみればわかります。

ステッドラー 固形蛍光マーカー テキストサーファー ゲル 264 PB3  3色セット

ステッドラー 固形蛍光マーカー テキストサーファー ゲル 264 PB3 3色セット

ブックダーツ

これは……一見おしゃれアイテムのようだけど、意外と馬鹿にできないやつ。

上で紹介した文房具たちも、かように便利千万なものであるのだけれど、ひとつ弱点があって、それは本とともに持ってくるのを忘れてしまうと無力な点だ。いつもとちがうカバンで出かけて本を取り出すと付箋やペンが手元になく、印がつけられなければ読めないと怖じ気づいて、スマホで電子書籍を読みはじめてしまうのはよくある話。

ブックダーツはページをクリップのように挟むので、しおりのようにも、行を示すようにも使え、こういったときにも役立つ。ブックダーツを挟んでおいて、あとで線を引くなり付箋を貼るなりすればいい。紙を傷つけそうなのと意外と重みがあるので常用はしないけれど。

いやいやこれも忘れたら意味ないでしょ、と思うかもしれないが、ブックダーツの便利なところは、本そのものに収納が可能なところだ。読みはじめるときに、裏表紙にでも4、5個挟んでおけば、本を持ちだすときにはかならずこのブックダーツもついてくるわけだ。かくして安らかな読書の時間を楽しめる。

【BOOK DARTS】ブックダーツ チョコラベル75個ミックス

【BOOK DARTS】ブックダーツ チョコラベル75個ミックス

紹介したうちのひとつだけでも便利なので、どうぞご活用ください。

ソースに一行追加するだけですべての HTTP 通信をロギングするモジュールを書いた #golang

Go

こちらです。Perl でいうと Devel::KYTProf に性質がちかい。

motemen/go-loghttp · GitHub (GoDoc)

使用例

たとえばこういうコードに…

package main

import (
    "io"
    "log"
    "net/http"
    "os"
)

func main() {
    resp, err := http.Get(os.Args[1])
    if err != nil {
        log.Fatal(err)
    }

    io.Copy(os.Stdout, resp.Body)
}
% go run main.go http://example.com/
<!doctype html>
...

一行追加すると:

package main

import (
    "io"
    "log"
    "net/http"
    "os"

    _ "github.com/motemen/go-loghttp/global" // ← この行
)

func main() {
    resp, err := http.Get(os.Args[1])
    if err != nil {
        log.Fatal(err)
    }

    io.Copy(os.Stdout, resp.Body)
}
% go run main.go http://example.com/
2014/12/02 13:36:27 ---> GET http://example.com/
2014/12/02 13:36:27 <--- 200 http://example.com/
<!doctype html>
...

こんな風に(log パッケージを使った)ログが自動的に吐かれるようになります。

特定のクライアントだけログを吐きたいのなら:

import "github.com/motemen/go-loghttp"

...

client := http.Client{
        Transport: &loghttp.Transport{},
}

loghttp.Transport の LogRequset、LogResponse フィールドを設定することで、どのようにログを出力するかをカスタマイズできます。(デフォルトでは上記のように標準の log パッケージを使用してます)

仕組み

loghttp は loghttp.Transport という struct を提供していて、これが http.RoundTripper を実装しているので http.Client の Transport フィールドとして設定できます。http.Cilent が HTTP リクエストを実行する際にはこの Transport を介して行いますが、loghttp.Transport は通常の HTTP 送受信に加えてロギングを行うような実装になっているので、このように HTTP 通信に引っかけてリクエストとレスポンスのロギングを行うことができるわけです。

さらに github.com/motemen/go-loghttp/global パッケージでは http.DefaultTransportloghttp.DefaultTransport に書き換え(!)しているので、インポートするだけですべての HTTP 通信が(標準の http パッケージを利用していれば)標準エラー出力に表示されるようになります。

The Way to Go: A Thorough Introduction to the Go Programming Language (English Edition)

The Way to Go: A Thorough Introduction to the Go Programming Language (English Edition)

#github tips: hub pull-request の向き先がリポジトリのデフォルトブランチにならないとき

引数なしの hub pull-request を実行すると、リポジトリのデフォルトブランチに向けたプルリクエストを作ってくれます。このデフォルトブランチはふつう master を向いてますが、変更可能なので Git Flow で言う develop 的なブランチをデフォルトブランチにしているプロジェクトもあると思います。

さて、このデフォルトブランチをプロジェクトの歴史の中のどこかの時点で変えると、新しく clone したリポジトリで hub pull-request すると当然 develop に向けてプルリクエストを作りますが、デフォルトブランチの変更以前にこのリポジトリを clone していたリポジトリで hub pull-request すると master に向けたプルリクエストを作ろうとしてしまいます。-b develop すれば develop を向くのだけど、これは事故の元……。

原因

hub pull-request-b {base} が与えられなかったとき refs/remotes/origin/HEAD という ref (GitHub のデフォルトブランチはこれに対応しています)を参照し、これが向いている先をデフォルトブランチとみなして、そのブランチに向けたプルリクエストを作成します。この origin/HEAD という symbolic ref は git clone 時に作成されるのですが、それ以外のタイミングでは更新されることがない。そのため、途中からデフォルトブランチを変更しても古いリポジトリでは追従できないわけです。

対策

以下のコマンドを実行する。

% git remote set-head --auto origin
origin/HEAD set to develop

リモートの Git リポジトリの HEAD を、手元に反映させます。めでたし。手元の HEAD がどうなっているかは、以下のコマンドで確認できます。

% git symbolic-ref refs/remotes/origin/HEAD

GitHub実践入門 ~Pull Requestによる開発の変革 (WEB+DB PRESS plus)

GitHub実践入門 ~Pull Requestによる開発の変革 (WEB+DB PRESS plus)

今すぐオススメしたい!ノベルっぽいゲーム3点

なるほどですね! そんな同僚に個人的にオススメしたいノベル系ゲー(ADV)を選んでみました!

セカンドノベル 〜彼女の夏、15分の記憶〜(PSP)

セカンドノベル ~彼女の夏、15分の記憶~

セカンドノベル ~彼女の夏、15分の記憶~

数年ぶりに戻ってきた故郷で再会したヒロインは、高校時代の「事故」以来、15分しか記憶が保てなくなっている。主人公はその彼女がある物語を思い出しながら語る手伝いをすることなる。主人公は物語を「フラグメント」にまとめながら、ヒロインの「ストーリー」づくりをサポートするんだけど、その切り替えは現在と過去との行き来にもなっていて、しだいに「事故」の真相が明らかになっていく……という趣向。物語を語るって物語。

ひとこと: 主人公の声ありでやるのがオススメです。

公式サイト: セカンドノベル ~彼女の夏、15分の記憶~

銃声とダイヤモンド(PSP)

銃声とダイヤモンド

銃声とダイヤモンド

これはノベルゲーってわけではない。プレイヤーは交渉人の一人となって、いくつかの事件をその手腕で解決していく。タンゴのメロディにのせてリアル調の画で交渉を進めていくんだけど、選択肢を間違えるとクールで頼れるはずの主人公の言動がどんどん変な方向にずれていくのが面白い。交渉はリアルタイムに進み、選択肢で立ち止まるということができないんでトライアル&エラーがちょっと面倒。

ひとこと: 夜は雨になる。

公式サイト: 銃声とダイヤモンド | ソフトウェアカタログ | プレイステーション® オフィシャルサイト

あの、素晴らしい をもう一度/再装版(PC、iOS)

あの、素晴らしい  をもう一度/再装版

あの、素晴らしい をもう一度/再装版

記憶を失くした主人公が、時間が経つとある時点までの記憶が消え去ってしまう(「くりかえし病」)ヒロインに出う、剣と魔法の世界。ふたりは旅をしながら情報をあつめ、この状況の元凶に挑むのだけれど、失敗すればその力により記憶を消され、スタート地点まで時間を巻き戻されてしまう。特徴的なのは前の周回で得た経験が次の周回の新たな分岐を作り出すシステムで、これがストーリーにも組み込まれているので、はっきりと繰り返し世界の物語です。面白いし、気づいたら iOS 版が出てたのでやるべき。

ひとこと: ググらずに頑張ってください。

公式サイト: 『あの、素晴らしい をもう一度/再装版』


いかがでしたか? 他にもおすすめあるだろって方はコメントで教えてくださいね! ではでは。

追記: こんなゲームが好きな人におすすめです ⊂(^ω^)⊃

Spike The Best 428 ~封鎖された渋谷で~

Spike The Best 428 ~封鎖された渋谷で~

BEST HIT セレクション EVER17  ~the out of infinity~ Premium Edition

BEST HIT セレクション EVER17 ~the out of infinity~ Premium Edition

Steins;Gate (シュタインズ・ゲート) (通常版)

Steins;Gate (シュタインズ・ゲート) (通常版)

ゴースト トリック NEW Best Price! 2000

ゴースト トリック NEW Best Price! 2000

ダンガンロンパ 希望の学園と絶望の高校生 PSP the Best

ダンガンロンパ 希望の学園と絶望の高校生 PSP the Best

ISUCON4 ダメでした #isucon

こないだの ISUCON で平凡な結果を取ってしまったのでお知らせします。

チーム名は「マカレラーズ」、メンバーは songmu & y_uuki でした。

予選

予選のエントリを書いてなかったので、いっしょに書いておく。具体的なポイントについてはもう覚えてないので、日記です。

予選はオンラインだったのでチーム全員がバラバラの場所で参加であったけど、Sqwiggle と Slack で十分だった。むしろ普段どおりという気もした。あらかじめ作戦を立てていたわけではなかったけど、自然と分担ができていて、アプリの細かい改修にくわえ、デプロイ周りやプロファイングは自分がやった。これがいい感じに回ったので、本戦でも自分が進んでやることになった。普通に愚直に実装して予選枠を取ったという印象だけれど、いい順位ではなかったし、おそらく次の ISUCON ではもっと参加チームも増えているだろうので、こんな感じではいかんのだろうと思う。

本戦

予選では完全リモートチームだったので三人顔を突き合わせてやるのは珍しい感じ。二人とも同じTシャツ着てるなーと思ったら Mackerel Tシャツで、自分のやつがどっかいってるのを思い出した。GitHub Tでしたすみません……。

さて、本戦はじまってからコードを読んでみるとどこから手をつけようかなーと考えあぐねてしまった。ログをファイルに書き出してるところは明らかに改善の余地があったので、そこを Songmu さんがやるということになって、そのあいだ自分はデプロイの設定を書いたりしていた。最初に8000点取って暫定一位を取ったのはうちのチームだったと記憶しています(ぼくは何もしてない。共通の Redis を使う、くらいはやったけど)。

しかしそこから早くも伸び悩んでしまった。公平性ボーナスが意外とうまそうだったので配信アルゴリズムを変える必要は感じなかったし、レポートの部分もそれほど数はなかったはず。アプリのプロファイルを取ってみるとやはりファイルアップロードと配信のあたりが重かったのでそこをどうするかというのでいろいろ試行錯誤したが、結局大した効果はあがらなかった。ベンチマークが安定した得点を叩き出してくれなかったのにもつまづいてしまって、今思うとアプリケーションレベルの最適化に逃げてしまっていたと反省。結局動画も Redis から配信のままであり、ファイルで配信するくらい試してみればよかった。

ふり返ってみると、アプリのプロファイングと高速化という視点に拘りすぎて、ウェブサービス全体としての最適化という視点に欠けていたなーと思う。翌朝用事があって結果発表までいられなかったのだけど、新幹線の中でチームメイトが 304 案件、と発言したログを見てあーあーとなったのであった。nginx のプロキシ設定くらいなら書けるがより深いところになると勘でしか分からんのでほかの二人の話についていけなかったりする。

最後までいられなかったのは残念だった。けどやっぱり今年も楽しかったです! 運営の皆様おつかれさまでした&ありがとうございます!

ごはん写真

渋谷といえば吉野家

去年に引き続き今年もビビンバ弁当を頂きました✌

突然寿司があらわれてうまかったです

イシュー管理にお役立ち。GitHubのIssue/PRの状態が一目で分かるバッヂ

f:id:motemen:20141026214110p:plain

こんな感じに使えますっていうウェブアプリ+Chrome 拡張のはなしです。(ちなみに例はこれ

GitHub ではイシューへのリンクが "#829" という風に番号になって表示されますが、イシューをバリバリ使っていてこれらの間の参照が多く発生するような場合に、これって誰がやってるんだっけ、もう終わったんだっけ、と分からなくなることが多々ありますよね。こういう時、文中に画像で埋め込まれているとパッと見て分かりやすい。

デモ

http://github-issue-badge.herokuapp.com/

にデプロイしてあります(トップページは GitHub Pages にリダイレクトされますが)。

次の画像: motemen/test-repository#1http://github-issue-badge.herokuapp.com/badge/motemen/test-repository/5)が正しく表示されればオッケー。403 と出る人は /auth を訪れて OAuth 認証してください。プライベートリポジトリにまで権限を与えたくない場合は /auth?only=public へどうぞ。

http://github-issue-badge.herokuapp.com/badge/:owner/:repo/:number という URL で、SVG のバッヂ画像を配信しています。バッヂのパーツは左からイシュー番号、ステータス(open/closed/merged)、作成者もしくは担当者、それからラベル(の色)となってます。

以下に書くように、自前でデプロイするのも簡単です。その場合は GitHub:Enterprise 用に立てることもできます。

自前でデプロイする

リポジトリはこちら: https://github.com/motemen/github-issue-badge

お好きな方法を選んでください:

  • 一番簡単なのは Heroku にデプロイする: Deploy
  • もしくは Fig を使って: fig up
  • または普通に rack アプリとしてデプロイ。

設定

環境変数として以下が指定できます。(app.json に詳しいです)

GITHUB_OAUTH_CLIENT_ID, GITHUB_OAUTH_CLIENT_SECRET

画像にアクセスするクライアントに OAuth 認証させて API を使う場合はこのふたつを設定してください。

GITHUB_OAUTH_ACCESS_TOKEN

アクセストークンを予め生成しておき、これに設定しておくと、クライアントはいちいち OAuth 認証しなくてもバッヂを見ることができます。GHE や小さな組織で利用する場合はこれで十分です。

GITHUB_API_ENDPOINT

GHE を使う場合は指定してください。http://ghe.example.com/api/v3/ とか。

REDIS_URL

Heroku か Fig を利用する場合は不要。

Chrome 拡張

で、冒頭のスクリーンショットのようにリンクを画像に差し替えるためには GitHub の Content-Security-Policy を書き換えてやる必要があり、拡張にしてます。GitHub イシュー内でイシューへのリンクを画像に差し替える拡張。

https://github.com/motemen/chrome-Embed-GitHub-Issue-Badges

GHE ユーザや個人でデプロイした人もこの拡張を使えるように、GitHub および github-issue-badge の URL をカスタマイズした拡張をビルドできるようになってます。README にも書いてますが、src/ts/config.ts を

export var badgeOrigin  = 'http://github-issue-badge.yourcompany.example.com';
export var githubOrigin = 'https://github.yourcompany.example.com';

という風に書き換えて、

npm install
npm run build

すると build/ ディレクトリ以下にその設定に応じた拡張が生成されます。これを chrome://extensions から追加してください。

Fig

Fig 使ってみたけど便利だった……。というか Redis とか Ruby とかの公式イメージが配布されてるのが便利で、とくに Ruby は FROM ruby:2.0-onbuild などとするだけで bundle 済みのイメージが簡単に作れてしまう。ちょっとしたアプリの配布とデモにはとても役立ちそう。