Git の内部構造を知っていたら、意外な状況で実務に役立った話です。
※ 実話です。
私について
git challenge という、Git を使った学生向け競技イベントの作問を担当しています。この git challenge は 2017/01/28(土)に第4回目が開催されますので、ご興味ある方は是非ご応募ください。
前提
あるサーバーAがあり、このサーバー上であるアプリケーションが稼働しています。このアプリケーションのコードは、Git で管理されています。
ただし、remote が謎の Git サーバーを指しています:
$ git remote --verbose
origin git@192.0.2.1:example/repo (fetch)
origin git@192.0.2.1:example/repo (push)
この Git サーバーの詳細は不明で、なるべく利用したくありません。また、アプリケーションが稼働しているサーバーのネットワーク設定変更はめんどくさいという状況です(他部署への依頼が必要)。
また、あなたの手元のマシンからこのアプリケーションサーバーへの接続には SSH が利用できます。
整理すると、登場するマシンは以下の3つです:
マシン | 説明 |
---|---|
アプリケーションの稼働しているサーバー | 謎の remote を参照している。セキュリティ設定をいじれない。 |
謎の Git サーバー | 詳細不明なので触りたくない。 |
手元のマシン | 送信したい差分が含まれたローカルリポジトリがこのマシンにある。また、アプリケーションサーバーへ SSH 接続ができる。 |
さて、あなたは手元のマシンのローカルリポジトリから、このサーバーへ差分を送信しなければなりません。
計画
手段は2つ考えられます:
- 手段1: 適当な Git サーバーを利用して、アプリケーションサーバーから pull する
- 手段2: SSH 経由で Git オブジェクトを送り込む
- 手段3(追記): (もっと簡単な方法があります。コメントをご参照ください)
手段1では、アプリケーションサーバーからのアウトバウンド接続が許可されていなければなりません。面倒なので、今回は SSH が繋がるならどんな状況でも対応可能な手段2をとることにします。
なお、手段2にある「Git オブジェクト」について、軽く説明しておきましょう。Git が管理するリソースは、主に次の4種類のオブジェクトとして保存されています1:
- Commit: 1つのコミットを表すオブジェクト。コミットメッセージや日時、直前のコミットへの参照などが記録されている。
- Tree: 1つのディレクトリを表すオブジェクト。ディレクトリが含むファイルやディレクトリの一覧(名前、SHA1、パーミッション)などが記録されている。
- Blob: ファイルの内容だけを表すオブジェクト。
- Tag2: Git オブジェクトにひもづく署名などを記録するオブジェクト。
これらの Git オブジェクトは、.git/objects/
以下に Git オブジェクトのハッシュ値(SHA1)の名前で保存されます(例: .git/objects/00/0042d79352b4a7bb789d60e5324af92f29871a
)。意外に思うかもしれませんが、Commit や Tree などのオブジェクトは、すべて同じディレクトリに格納されているのです。なお、容量削減のために .git/objects/pack/
以下にまとめて圧縮されていることもあります(なお、git clone
直後は圧縮された状態です)。
さて、普段私たちが git push
や git pull
で送受信しているのは、この Git オブジェクトたちです。したがって、SSH を介してこの Git オブジェクトをアプリケーションサーバーへ届けることができれば、アプリケーションサーバー上に差分を適用できるのです!
手順
まず、手元の Git オブジェクトをバラバラに送るのは面倒なので、ひとまとめにしてしまいましょう。ひとまとめにするには、git gc
や git pack-objects
コマンドが便利です。今回は、お手軽な git gc
を使います。
この git gc
は、Git リポジトリのお掃除と整理をおこなうコマンドです。
もともとは参照されていない Git オブジェクトのお掃除(Garbage Collection)を意味していますが、実際は Git オブジェクトの圧縮などもおこないます。この過程で、すべての Git オブジェクトが .git/objects/pack/pack-????.pack
というファイルにひとまとめにして圧縮されます3。これは利用できますね!
$ # バラバラだった Git オブジェクトを…
$ find .git/objects -type f
.git/objects/00/0042d79352b4a7bb789d60e5324af92f29871a
.git/objects/00/0079a2eaef17b7eae70e1f0f635557ea67b644
.git/objects/00/01140536704ecf3f0f86686fb6586cbc185ece
.git/objects/00/0147bbe4a00525d68efb1358c013812e10dcca
...
$ # 一つのファイルへまとめる(*.pack が圧縮されたファイル本体)
$ git gc
$ find .git/objects -type f | head
.git/objects/info/packs
.git/objects/pack/pack-65b96fd56b3f0c01ddd61cdb0c2b85890d7daa47.idx
.git/objects/pack/pack-65b96fd56b3f0c01ddd61cdb0c2b85890d7daa47.pack
$ # SSH 経由でアプリケーションサーバーへ送り込む
$ scp .git/objects/pack/pack-4ed9eb82b68c28bf2726c818a183b5adc096ba8d.pack app-server:/home/me/
Git オブジェクトをまとめたファイルをアプリケーションサーバーへ送り込めたので、サーバー上で展開します:
$ # アプリケーションサーバー上のローカルリポジトリへ移動
$ cd /path/to/local-repo
$ # まとめた Git オブジェクトを展開
$ git unpack-objects < /home/me/4ed9eb82b68c28bf2726c818a183b5adc096ba8d.pack
現在の状態で、手元の Git オブジェクトは全てアプリケーションサーバー上でも存在するようになりました。ただし、まだ HEAD が指しているブランチが古いままなので、HEAD を最新のブランチに向きなおす必要があります:
$ # 現在の HEAD が指しているブランチを確認しておく
$ git branch
* master
...
$ # ブランチの SHA1 を控えておく
$ git rev-parse master
1d1bdafd64266e5ee3bd46c6965228f32e4022ea
$ # 現在の HEAD が指しているブランチを確認しておく
$ git branch
* master
...
$ # master を転送してきた最新の HEAD へ移動する
$ git reset --hard 1d1bdafd64266e5ee3bd46c6965228f32e4022ea
以上で、作業は完了です!
まとめ
実は、この話にはオチがあります。この不明な Git サーバーは、なんとアプリケーションサーバー自身を向いていたのでした。つまり、アプリケーションサーバーに Git サーバーが同居していたということになりますね(経緯不明かつ意味不明)!
こんな状況に陥らないように、なるべく謎の野良 Git サーバーを立てるのは避けたほうがよいでしょう。