Kentaro Kuribayashi's blog

Software Engineering, Management, Books, and Daily Journal.

ghqを使ったローカルリポジトリの統一的・効率的な管理について

GitなどのVCSからcloneしたローカルリポジトリをどう管理するのがいい感じなのか、よくわからない。なんとなく自己流でやっているが、もっといい方法を知りたい。

tl;dr - ディレクトリレイアウトをgolangの作法に合わせ、すべてのリモートリポジトリをghqを使ってcloneし、percolを使って簡単に検索できるようにしましょう。

追記: いまならpercolの代わりにpecoというツールを使うのもよいでしょう。というか、僕はそうしています。設定方法はこのエントリとほぼ同様の内容でいけると思います。

背景

そんな課題を抱えつつも、特になにかをするわけでもなく日々暮らしていた折、Rebuild: 42: When in Golang, Do as the Gophers Do (lestrrat)@lestrratさんが、Goのお作法に、他の言語のリポジトリも含め、すべてあわせるようにしたとおっしゃっていて、なるほど!と思ったのであった。

Goのお作法

以下、「Goのお作法」を知らない人向けの簡単な解説。

Goのディレクトリレイアウト

Goには、外部のコードを以下のようにして取得するユーティリティが付属している。

$ go get github.com/motemen/ghq

この結果、ローカルリポジトリが、$GOPATH/srcというディレクトリ以下へ、mainパッケージがあればビルドされたコマンドとして$GOPATH/bin以下へ、次の通りに配置される。

$GOPATH
|-- src/
|   `-- github.com/
|       `-- motemen/
|           `-- ghq/
|-- bin/
     `-- ghq

これが、Goのディレクトリレイアウトに関するお作法である。簡単ですね。

パッケージのインポート

このお作法は、Goのコードを書く際にも意識する必要がある。ライブラリを使う場合、以下のように書く。

import "github.com/kentaro/delta"

この指定により、$GOPATH/src/github.com/kentaro/delta直下にあるパッケージがインポートされる。

また、この指定は、サブディレクトリ以下に配置されたパッケージをインポートする場合にも使われる。たとえば以下のような配置のプロジェクトがあり、

$GOPATH
|-- src/
    `-- github.com/
        `-- motemen/
            `-- ghq/
                |-- main.go
                `-- utils/
                    `-- log.go

いま、main.goを書いているとする。utils/log.goを使おうとする場合、単にimport utilsとするのはダメで、正しくは

import "github.com/motemen/ghq/utils"

とする必要がある。そして、その際はもちろん、$GOPATH以下からファイル名が解決される。

「Goのお作法」のインプリケーション

一般に、外部ライブラリやツールとして入れたソースコードと、自分のワークスペースは分けるものだ。たとえばCPANやrubygemsなどでも、普通に運用する分には、そうなるだろう。

Goにおいては、上記のような事情により、外部ライブラリと自分の開発用ワークスペースとを区別しない方が便利である。ある意味、大胆な選択といえるが、いったん慣れてしまえば、最初からこれでよかったのでは?というか、言語にかかわりなく、これでいいのでは?という気になる。

解決

では、Goのお作法に合わせようということになったところで、そのお作法通りのディレクトリ構造を、その都度手動で作るのは面倒だ。そこで、id:motemen作のghqを使う。使い方については、リンク先を読んでもらえばよいだろう。

さらに、我々としては上記のGoのお作法に合わせたいのだから、どこにcloneするかについてもきちんと設定する必要がある。これはどこでもいいのだけど、シンプルさを追求するため、以下のように、$GOPATH$HOMEとを同じものにすることにした。

export GOPATH=$HOME

その上で、先述の通り、Goはgo getした結果を$GOPATH/src以下に配置するので、ghqの設定を、以下の通り.gitconfigに追加する。

[ghq]
  root = ~/src

これで、ghq getしたものも、go getしたものと同様のディレクトリに配置されることになる。また、ghqをGitHub Enterpriseに対応させる私の送ったプルリクエストも採用されたので、プライベート/パブリックなもの、github.com/GH:Eとを問わず、すべてのリポジトリを同じようにghqを用いて扱うことができる。

gem-src

ところで、gem-srcという、@amatsudaさん作成のgemがある。これは、gem installをフックして、そのライブラリのgitリポジトリを自動的にcloneしてくれるというもの。

これに関しても、上記と同じようにghqを使って、同じディレクトリ構造でcloneしておくようにしたら便利ではないか。そういうわけで、gem-srcにプルリクエストを送ったり、amatsudaさんによる改良を経て、以下のような設定を.gemrcに入れておくと、ghqを使ってcloneするようになった。

.gemrc:

gemsrc_use_ghq: true

その後、ライブラリをgem installすると、以下のようになる。

$ gem i glint
Fetching: glint-0.0.2.gem (100%)
/Users/usr0600239/bin/ghq
     clone http://github.com/kentaro/glint -> /Users/usr0600239/src/github.com/kentaro/glint
       git clone http://github.com/kentaro/glint /Users/usr0600239/src/github.com/kentaro/glint
Cloning into '/Users/usr0600239/src/github.com/kentaro/glint'...
remote: Reusing existing pack: 185, done.
remote: Total 185 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (185/185), 20.76 KiB, done.
Resolving deltas: 100% (61/61), done.
Successfully installed glint-0.0.2
1 gem installed

clone先の管理

このようにして、なんでもかんでもこの方法に従ってcloneしていくと、$GOPATH/src以下に、大量のディレクトリが格納されることになるだろう。そして、なにがそこにあるのかがどんどんwかりにくくなっていく。

これに対処するにはいろんな方法があるが、ここではpercolを使う。percolはまさにUNIXライクなツールで、標準入力から入ってきた行区切りの入力に対するフィルタUIを提供する。これを使って、ghqで管理しているリポジトリのディレクトリに、簡単にcdできるようにする。

zshの場合、このような関数を用意しておく。

function percol-src () {
    local selected_dir=$(ghq list --full-path | percol --query "$LBUFFER")
    if [ -n "$selected_dir" ]; then
        BUFFER="cd ${selected_dir}"
        zle accept-line
    fi
    zle clear-screen
}
zle -N percol-src

んでもって、たとえば^sでこの関数が起動するようにしておく。

bindkey '^S' percol-src

動作の様子は以下。

応用

@lestrratさんに、以下のようなのも便利と教えてもらいました。

$ godoc $(ghq list --full-path | percol) | $PAGER

実行すると、ghqで管理されてるディレクトリからフィルターして絞り込んだ結果をgodocにわたすことで、効率よくGoのライブラリのAPIドキュメントをひけます。

結論

gitでもhgでも、プライベートでもパブリックでも、もはや同じ、統一されたディレクトリレイアウトを採用できる。そして、以上のように、そのためのツールも整った。これで私たちは、迷うことなく、コードを書いていけるだろう。