詩と創作・思索のひろば

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

Fork me on GitHub

Git config から struct に読みこむ Go パッケージを書いた

Git や GitHub にまつわるツールは gitconfig に値を格納することにしておくのがユーザにも便利で、よく使っているし広く使われているとおもう。いっぽう YAML とか JSON とか設定ファイルによく使われるフォーマットは構造体に値をまとめて読みこむのが普通でもある。そういうわけで表題のようなものを書いた。以下の例を見れば分かるとおり gitconfig ファイルを自力で解析してすべての値を読みこむようなものではないです。

README より GoDoc のほうが詳しいです。

使い方

gitconfig フィールドタグを使います。

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

type C struct {
    UserEmail  string `gitconfig:"user.email"`
    PullRebase bool   `gitconfig:"pull.rebase"`
}

var v Config
err := gitconfig.Load(&v)
...

gitconfig.Load メソッドで、タグによって指定されたキーに対応する値が構造体のフィールドに設定されます。対応している型は string[]stringbool、あと int 系。

ソースを指定する

パッケージに定義されている関数を呼び出すと内部ではオプションなしの git config が呼び出されるので、リポジトリローカルの設定もグローバルな設定も参照されることになります。これが都合わるい場合には、以下の変数や関数を使うことでソースを限定できます。

  • gitconfig.Global … グローバル(~/.gitconfig)な設定のみ
  • gitconfig.Local … ローカル(.git/config`)
  • gitconfig.File(file) … 特定のパスのファイル(.gitmodules とか)
url, err := gitconfig.Local.GetString("remote.origin.url")

単一のキーを取得する

上の例にも出ているけれど、GetXXX メソッドで単一のキーを取得することもできる。行きがかり上実装されたメソッドたちです。よく使用するキーを取得したいだけなら tcnksm/go-gitconfig を使うほうがいいと思う(名前一緒になったけど他にやりようがなかったっすサーセン……)。

オレ流 Pull Request 作業フロー

チームで作業する同じリポジトリの中で Pull Request を送り合うのではなく、オープンソースプロジェクトに外部から PR がやってくる場合の話です。

最近のフロー

送られてきた PR に対しては、大まかには仕様の話、実装方針の話、具体的な実装の話を詰めながらマージできるように持っていくわけだけれど、それがほとんど満足いく状態になっていてマージしたいと思うタイミングになっても、変数の名前付けだとか、ちょっとした処理の書き方だとかで、相手にお願いするよりは自分で手を加えてからマージした方が手っ取り早いことがある。そういう時は PR 元のブランチを手元にチェックアウトして、そのブランチを自分の変更で進めた上で master にマージするようにすると、push 時に PR も閉じられて便利です。

motemen/lgtm.sh#1 の例。分かりにくいれど、PR にさらに 1 コミット足してからマージしてる。

この際コードは fork された外部のリポジトリにあるから、たんに git checkout すればいいわけではなく予め git fetch なりする必要がある。

で、最近いろいろ試行錯誤した結果、以下のようにするとよさそうだった。

1. フォークされたリポジトリを、そのオーナー名で remote に登録する

git remote add -f jwerle https://github.com/jwerle/lgtm.sh.git

-f オプションで、登録後 git fetch する。

2. PR 元のリモートブランチを、ローカルにチェックアウトする

git checkout -b '#1-jwerle/master' --track remotes/jwerle/master

この際 #xxx をブランチ名に入れておくと、コミットメッセージなどにブランチ名が入った時に自動的にリンクになって便利(ありがたいことに # はブランチ名に使える)。また、このブランチで git pull すると、PR が更新されたときに最新のコードを取得できる。

3. ローカルにチェックアウトされたブランチの push 先を自分のリポジトリに変更

git config --local 'branch.#1-jwerle/master.pushremote' origin

これで、このブランチを変更したときに、自分のリポジトリに push することができる。この設定後も依然として git pull は相手のリポジトリから行われる。

hub-pr

一度以上のようにしてしまうと結構便利なわけだけど、セットアップに少し面倒がある。最近 PR を受け取ることが重なったこともあり、このへんの作業はもうフローが決まってるのでコマンド化することにした。ホントはちょっとしたシェルスクリプトに収めるつもりだったのだけど、なぜか色気を出してしまった……。yak shaving だけが人生だ。

motemen/hub-pr · GitHub

go get github.com/motemen/hub-pr で入手できます。

使い方

hub-pr checkout PULL_REQUEST_NUMBER

PR 番号を与えると、以上に書いたフローでブランチを作ってくれる。zsh 用の補完を用意してるので以外と簡単なのです。

f:id:motemen:20150324003457g:plain

あとは普通にマージしてやればいいのだけど、手元でマージするとあの GitHub おなじみのマージメッセージにならないので、それと同じものを生成するコマンドも用意しておいた。

hub-pr merge BRANCH

これであたかもマージボタンを押したかのようなコミットメッセージでもってマージされます。なんか便利ですね。ほかにも hub-pr browse っていう便利なのもありますが、オマケです。コンソールから PR やイシューを扱うなら、ghi ってのが圧倒的に便利。


このフローで作業したいと思う人がいるかどうかは分かりませんが……、どうぞご利用下さい。verbose output 的なオプションがないのでわりとブラックボックス感が高い。

Go で GitHub 用のツールを書く時は github.com/github/hub 以下のパッケージを使うとなかなか快適に行えるようでした。まあそりゃそうか。

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

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

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 にも対応してるのが便利そうですね。

はてなで一緒に働きませんか?